]> git.ipfire.org Git - thirdparty/bugzilla.git/commitdiff
no bug - reformat all the code using the new perltidy rules
authorPerl Tidy <perltidy@bugzilla.org>
Wed, 5 Dec 2018 21:00:32 +0000 (16:00 -0500)
committerDylan William Hardison <dylan@hardison.net>
Wed, 5 Dec 2018 21:01:25 +0000 (16:01 -0500)
363 files changed:
.checksetup_lib/lib/perl5/CPAN/Meta.pm
.checksetup_lib/lib/perl5/CPAN/Meta/Converter.pm
.checksetup_lib/lib/perl5/CPAN/Meta/Feature.pm
.checksetup_lib/lib/perl5/CPAN/Meta/History.pm
.checksetup_lib/lib/perl5/CPAN/Meta/Merge.pm
.checksetup_lib/lib/perl5/CPAN/Meta/Prereqs.pm
.checksetup_lib/lib/perl5/CPAN/Meta/Requirements.pm
.checksetup_lib/lib/perl5/CPAN/Meta/Spec.pm
.checksetup_lib/lib/perl5/CPAN/Meta/Validator.pm
.checksetup_lib/lib/perl5/CPAN/Meta/YAML.pm
.checksetup_lib/lib/perl5/JSON/PP.pm
.checksetup_lib/lib/perl5/JSON/PP/Boolean.pm
.checksetup_lib/lib/perl5/Module/Metadata.pm
.checksetup_lib/lib/perl5/Parse/CPAN/Meta.pm
Bugzilla.pm
Bugzilla/API/1_0/Constants.pm
Bugzilla/API/1_0/Resource.pm
Bugzilla/API/1_0/Resource/Bug.pm
Bugzilla/API/1_0/Resource/BugUserLastVisit.pm
Bugzilla/API/1_0/Resource/Bugzilla.pm
Bugzilla/API/1_0/Resource/Classification.pm
Bugzilla/API/1_0/Resource/Component.pm
Bugzilla/API/1_0/Resource/FlagType.pm
Bugzilla/API/1_0/Resource/Group.pm
Bugzilla/API/1_0/Resource/Product.pm
Bugzilla/API/1_0/Resource/User.pm
Bugzilla/API/1_0/Server.pm
Bugzilla/API/1_0/Util.pm
Bugzilla/API/Server.pm
Bugzilla/Attachment.pm
Bugzilla/Attachment/PatchReader.pm
Bugzilla/Auth.pm
Bugzilla/Auth/Login.pm
Bugzilla/Auth/Login/APIKey.pm
Bugzilla/Auth/Login/CGI.pm
Bugzilla/Auth/Login/Cookie.pm
Bugzilla/Auth/Login/Env.pm
Bugzilla/Auth/Login/Stack.pm
Bugzilla/Auth/Persist/Cookie.pm
Bugzilla/Auth/Verify.pm
Bugzilla/Auth/Verify/DB.pm
Bugzilla/Auth/Verify/LDAP.pm
Bugzilla/Auth/Verify/RADIUS.pm
Bugzilla/Auth/Verify/Stack.pm
Bugzilla/Bug.pm
Bugzilla/BugMail.pm
Bugzilla/BugUrl.pm
Bugzilla/BugUrl/Bugzilla.pm
Bugzilla/BugUrl/Bugzilla/Local.pm
Bugzilla/BugUrl/Debian.pm
Bugzilla/BugUrl/GitHub.pm
Bugzilla/BugUrl/Google.pm
Bugzilla/BugUrl/JIRA.pm
Bugzilla/BugUrl/Launchpad.pm
Bugzilla/BugUrl/MantisBT.pm
Bugzilla/BugUrl/SourceForge.pm
Bugzilla/BugUrl/Trac.pm
Bugzilla/BugUserLastVisit.pm
Bugzilla/CGI.pm
Bugzilla/CGI/ContentSecurityPolicy.pm
Bugzilla/Chart.pm
Bugzilla/Classification.pm
Bugzilla/Comment.pm
Bugzilla/Comment/TagWeights.pm
Bugzilla/Component.pm
Bugzilla/Config.pm
Bugzilla/Config/Admin.pm
Bugzilla/Config/Advanced.pm
Bugzilla/Config/Attachment.pm
Bugzilla/Config/Auth.pm
Bugzilla/Config/BugChange.pm
Bugzilla/Config/BugFields.pm
Bugzilla/Config/Common.pm
Bugzilla/Config/Core.pm
Bugzilla/Config/General.pm
Bugzilla/Config/GroupSecurity.pm
Bugzilla/Config/LDAP.pm
Bugzilla/Config/MTA.pm
Bugzilla/Config/Memcached.pm
Bugzilla/Config/Query.pm
Bugzilla/Config/RADIUS.pm
Bugzilla/Config/ShadowDB.pm
Bugzilla/Config/UserMatch.pm
Bugzilla/Constants.pm
Bugzilla/DB.pm
Bugzilla/DB/Mysql.pm
Bugzilla/DB/Oracle.pm
Bugzilla/DB/Pg.pm
Bugzilla/DB/Schema.pm
Bugzilla/DB/Schema/Mysql.pm
Bugzilla/DB/Schema/Oracle.pm
Bugzilla/DB/Schema/Pg.pm
Bugzilla/DB/Schema/Sqlite.pm
Bugzilla/DB/Sqlite.pm
Bugzilla/Error.pm
Bugzilla/Extension.pm
Bugzilla/Field.pm
Bugzilla/Field/Choice.pm
Bugzilla/Field/ChoiceInterface.pm
Bugzilla/Flag.pm
Bugzilla/FlagType.pm
Bugzilla/Group.pm
Bugzilla/Hook.pm
Bugzilla/Install.pm
Bugzilla/Install/DB.pm
Bugzilla/Install/Filesystem.pm
Bugzilla/Install/Localconfig.pm
Bugzilla/Install/Requirements.pm
Bugzilla/Install/Util.pm
Bugzilla/Job/BugMail.pm
Bugzilla/Job/Mailer.pm
Bugzilla/JobQueue.pm
Bugzilla/JobQueue/Runner.pm
Bugzilla/Keyword.pm
Bugzilla/MIME.pm
Bugzilla/Mailer.pm
Bugzilla/Markdown.pm
Bugzilla/Memcached.pm
Bugzilla/Migrate.pm
Bugzilla/Migrate/Gnats.pm
Bugzilla/Milestone.pm
Bugzilla/Object.pm
Bugzilla/Product.pm
Bugzilla/RNG.pm
Bugzilla/Report.pm
Bugzilla/Search.pm
Bugzilla/Search/Clause.pm
Bugzilla/Search/ClauseGroup.pm
Bugzilla/Search/Condition.pm
Bugzilla/Search/Quicksearch.pm
Bugzilla/Search/Recent.pm
Bugzilla/Search/Saved.pm
Bugzilla/Sender/Transport/Sendmail.pm
Bugzilla/Series.pm
Bugzilla/Status.pm
Bugzilla/Template.pm
Bugzilla/Template/Context.pm
Bugzilla/Template/Plugin/Bugzilla.pm
Bugzilla/Template/Plugin/Hook.pm
Bugzilla/Token.pm
Bugzilla/Update.pm
Bugzilla/User.pm
Bugzilla/User/APIKey.pm
Bugzilla/User/Setting.pm
Bugzilla/User/Setting/Lang.pm
Bugzilla/User/Setting/Skin.pm
Bugzilla/User/Setting/Timezone.pm
Bugzilla/UserAgent.pm
Bugzilla/Util.pm
Bugzilla/Version.pm
Bugzilla/WebService.pm
Bugzilla/WebService/Bug.pm
Bugzilla/WebService/BugUserLastVisit.pm
Bugzilla/WebService/Bugzilla.pm
Bugzilla/WebService/Classification.pm
Bugzilla/WebService/Component.pm
Bugzilla/WebService/Constants.pm
Bugzilla/WebService/FlagType.pm
Bugzilla/WebService/Group.pm
Bugzilla/WebService/Product.pm
Bugzilla/WebService/Server.pm
Bugzilla/WebService/Server/JSONRPC.pm
Bugzilla/WebService/Server/XMLRPC.pm
Bugzilla/WebService/User.pm
Bugzilla/WebService/Util.pm
Bugzilla/Whine.pm
Bugzilla/Whine/Query.pm
Bugzilla/Whine/Schedule.pm
admin.cgi
attachment.cgi
auth.cgi
buglist.cgi
chart.cgi
checksetup.pl
clean-bug-user-last-visit.pl
colchange.cgi
collectstats.pl
config.cgi
contrib/Bugzilla.pm
contrib/bz_webservice_demo.pl
contrib/bzdbcopy.pl
contrib/console.pl
contrib/convert-workflow.pl
contrib/extension-convert.pl
contrib/merge-users.pl
contrib/mysqld-watcher.pl
contrib/new-locale.pl
contrib/recode.pl
contrib/sendbugmail.pl
contrib/sendunsentbugmail.pl
contrib/syncLDAP.pl
createaccount.cgi
describecomponents.cgi
describekeywords.cgi
docs/lib/Pod/Simple/HTML/Bugzilla.pm
docs/lib/Pod/Simple/HTMLBatch/Bugzilla.pm
docs/makedocs.pl
duplicates.cgi
editclassifications.cgi
editcomponents.cgi
editfields.cgi
editflagtypes.cgi
editgroups.cgi
editkeywords.cgi
editmilestones.cgi
editparams.cgi
editproducts.cgi
editsettings.cgi
editusers.cgi
editvalues.cgi
editversions.cgi
editwhines.cgi
editworkflow.cgi
email_in.pl
enter_bug.cgi
extensions/BmpConvert/Config.pm
extensions/BmpConvert/Extension.pm
extensions/Example/API/1_0/Resource/Example.pm
extensions/Example/Config.pm
extensions/Example/Extension.pm
extensions/Example/lib/Auth/Login.pm
extensions/Example/lib/Auth/Verify.pm
extensions/Example/lib/Config.pm
extensions/Example/lib/Migrate/ImportBugs.pm
extensions/Example/lib/WebService.pm
extensions/Example/template/en/default/setup/strings.txt.pl
extensions/MoreBugUrl/Config.pm
extensions/MoreBugUrl/Extension.pm
extensions/MoreBugUrl/lib/BitBucket.pm
extensions/MoreBugUrl/lib/GetSatisfaction.pm
extensions/MoreBugUrl/lib/PHP.pm
extensions/MoreBugUrl/lib/Phabricator.pm
extensions/MoreBugUrl/lib/RT.pm
extensions/MoreBugUrl/lib/Redmine.pm
extensions/MoreBugUrl/lib/ReviewBoard.pm
extensions/MoreBugUrl/lib/Rietveld.pm
extensions/MoreBugUrl/lib/Savane.pm
extensions/Voting/Config.pm
extensions/Voting/Extension.pm
extensions/create.pl
gen-cpanfile.pl
importxml.pl
index.cgi
jobqueue.pl
jsonrpc.cgi
migrate.pl
mod_perl.pl
page.cgi
post_bug.cgi
process_bug.cgi
query.cgi
quips.cgi
relogin.cgi
report.cgi
reports.cgi
request.cgi
sanitycheck.cgi
sanitycheck.pl
search_plugin.cgi
show_activity.cgi
show_bug.cgi
showdependencygraph.cgi
showdependencytree.cgi
summarize_time.cgi
t/001compile.t
t/002goodperl.t
t/004template.t
t/005whitespace.t
t/006spellcheck.t
t/007util.t
t/008filter.t
t/009bugwords.t
t/010dependencies.t
t/011pod.t
t/012throwables.t
t/013dbschema.t
t/014critic-core.t
t/015remote_ip.t
t/016csp.t
t/100markdown.t
t/Support/Files.pm
t/Support/Templates.pm
template/en/default/filterexceptions.pl
template/en/default/setup/strings.txt.pl
testserver.pl
token.cgi
userprefs.cgi
view_job_queue.cgi
votes.cgi
whine.pl
whineatnews.pl
xmlrpc.cgi
xt/config/generate_test_data.pl
xt/extensions/QA/Config.pm
xt/extensions/QA/Extension.pm
xt/extensions/QA/lib/Util.pm
xt/lib/QA/REST.pm
xt/lib/QA/RPC.pm
xt/lib/QA/RPC/JSONRPC.pm
xt/lib/QA/RPC/XMLRPC.pm
xt/lib/QA/Tests.pm
xt/lib/QA/Util.pm
xt/rest/bugzilla.t
xt/rest/classification.t
xt/selenium/bug_edit.t
xt/selenium/choose_priority.t
xt/selenium/classifications.t
xt/selenium/config.t
xt/selenium/create_user_accounts.t
xt/selenium/custom_fields.t
xt/selenium/custom_fields_admin.t
xt/selenium/dependencies.t
xt/selenium/edit_products_properties.t
xt/selenium/email_preferences.t
xt/selenium/enter_new_bug.t
xt/selenium/flags.t
xt/selenium/flags2.t
xt/selenium/groups.t
xt/selenium/keywords.t
xt/selenium/login.t
xt/selenium/milestones.t
xt/selenium/password_complexity.t
xt/selenium/private_attachments.t
xt/selenium/qa_contact.t
xt/selenium/require_login.t
xt/selenium/sanity_check.t
xt/selenium/saved_searches.t
xt/selenium/search.t
xt/selenium/security.t
xt/selenium/shared_searches.t
xt/selenium/show_all_products.t
xt/selenium/shutdown.t
xt/selenium/status_whiteboard.t
xt/selenium/strict_isolation.t
xt/selenium/sudo_sessions.t
xt/selenium/target_milestones.t
xt/selenium/time_summary.t
xt/selenium/user_groups.t
xt/selenium/user_matching.t
xt/selenium/user_preferences.t
xt/selenium/user_privs.t
xt/selenium/votes.t
xt/webservice/bug_add_attachment.t
xt/webservice/bug_add_comment.t
xt/webservice/bug_attachments.t
xt/webservice/bug_comments.t
xt/webservice/bug_create.t
xt/webservice/bug_fields.t
xt/webservice/bug_get.t
xt/webservice/bug_history.t
xt/webservice/bug_legal_values.t
xt/webservice/bug_search.t
xt/webservice/bug_update.t
xt/webservice/bug_update_see_also.t
xt/webservice/bugzilla.t
xt/webservice/group_create.t
xt/webservice/jsonp.t
xt/webservice/product_create.t
xt/webservice/product_get.t
xt/webservice/user_create.t
xt/webservice/user_get.t
xt/webservice/user_login_logout.t
xt/webservice/user_offer_account_by_email.t

index afbb221855adf8f344e4770892521ffdff251432..d9bc720a1342de663cd374db20e72ad1ee9e934d 100644 (file)
@@ -1,6 +1,7 @@
 use 5.006;
 use strict;
 use warnings;
+
 package CPAN::Meta;
 
 our $VERSION = '2.150005';
@@ -92,7 +93,7 @@ BEGIN {
 
   no strict 'refs';
   for my $attr (@STRING_READERS) {
-    *$attr = sub { $_[0]{ $attr } };
+    *$attr = sub { $_[0]{$attr} };
   }
 }
 
@@ -121,10 +122,9 @@ BEGIN {
   no strict 'refs';
   for my $attr (@LIST_READERS) {
     *$attr = sub {
-      my $value = $_[0]{ $attr };
-      croak "$attr must be called in list context"
-        unless wantarray;
-      return @{ _dclone($value) } if ref $value;
+      my $value = $_[0]{$attr};
+      croak "$attr must be called in list context" unless wantarray;
+      return @{_dclone($value)} if ref $value;
       return $value;
     };
   }
@@ -163,7 +163,7 @@ BEGIN {
   for my $attr (@MAP_READERS) {
     (my $subname = $attr) =~ s/-/_/;
     *$subname = sub {
-      my $value = $_[0]{ $attr };
+      my $value = $_[0]{$attr};
       return _dclone($value) if $value;
       return {};
     };
@@ -182,7 +182,7 @@ BEGIN {
 #pod =cut
 
 sub custom_keys {
-  return grep { /^x_/i } keys %{$_[0]};
+  return grep {/^x_/i} keys %{$_[0]};
 }
 
 sub custom {
@@ -220,29 +220,29 @@ sub _new {
   my ($class, $struct, $options) = @_;
   my $self;
 
-  if ( $options->{lazy_validation} ) {
+  if ($options->{lazy_validation}) {
+
     # try to convert to a valid structure; if succeeds, then return it
-    my $cmc = CPAN::Meta::Converter->new( $struct );
-    $self = $cmc->convert( version => 2 ); # valid or dies
+    my $cmc = CPAN::Meta::Converter->new($struct);
+    $self = $cmc->convert(version => 2);    # valid or dies
     return bless $self, $class;
   }
   else {
     # validate original struct
-    my $cmv = CPAN::Meta::Validator->new( $struct );
-    unless ( $cmv->is_valid) {
-      die "Invalid metadata structure. Errors: "
-        . join(", ", $cmv->errors) . "\n";
+    my $cmv = CPAN::Meta::Validator->new($struct);
+    unless ($cmv->is_valid) {
+      die "Invalid metadata structure. Errors: " . join(", ", $cmv->errors) . "\n";
     }
   }
 
   # up-convert older spec versions
   my $version = $struct->{'meta-spec'}{version} || '1.0';
-  if ( $version == 2 ) {
+  if ($version == 2) {
     $self = $struct;
   }
   else {
-    my $cmc = CPAN::Meta::Converter->new( $struct );
-    $self = $cmc->convert( version => 2 );
+    my $cmc = CPAN::Meta::Converter->new($struct);
+    $self = $cmc->convert(version => 2);
   }
 
   return bless $self, $class;
@@ -268,10 +268,10 @@ sub new {
 sub create {
   my ($class, $struct, $options) = @_;
   my $version = __PACKAGE__->VERSION || 2;
-  $struct->{generated_by} ||= __PACKAGE__ . " version $version" ;
+  $struct->{generated_by} ||= __PACKAGE__ . " version $version";
   $struct->{'meta-spec'}{version} ||= int($version);
   my $self = eval { $class->_new($struct, $options) };
-  croak ($@) if $@;
+  croak($@) if $@;
   return $self;
 }
 
@@ -293,12 +293,11 @@ sub load_file {
   my ($class, $file, $options) = @_;
   $options->{lazy_validation} = 1 unless exists $options->{lazy_validation};
 
-  croak "load_file() requires a valid, readable filename"
-    unless -r $file;
+  croak "load_file() requires a valid, readable filename" unless -r $file;
 
   my $self;
   eval {
-    my $struct = Parse::CPAN::Meta->load_file( $file );
+    my $struct = Parse::CPAN::Meta->load_file($file);
     $self = $class->_new($struct, $options);
   };
   croak($@) if $@;
@@ -320,7 +319,7 @@ sub load_yaml_string {
 
   my $self;
   eval {
-    my ($struct) = Parse::CPAN::Meta->load_yaml_string( $yaml );
+    my ($struct) = Parse::CPAN::Meta->load_yaml_string($yaml);
     $self = $class->_new($struct, $options);
   };
   croak($@) if $@;
@@ -342,7 +341,7 @@ sub load_json_string {
 
   my $self;
   eval {
-    my $struct = Parse::CPAN::Meta->load_json_string( $json );
+    my $struct = Parse::CPAN::Meta->load_json_string($json);
     $self = $class->_new($struct, $options);
   };
   croak($@) if $@;
@@ -365,7 +364,7 @@ sub load_string {
 
   my $self;
   eval {
-    my $struct = Parse::CPAN::Meta->load_string( $string );
+    my $struct = Parse::CPAN::Meta->load_string($string);
     $self = $class->_new($struct, $options);
   };
   croak($@) if $@;
@@ -400,22 +399,18 @@ sub save {
   my $version = $options->{version} || '2';
   my $layer = $] ge '5.008001' ? ':utf8' : '';
 
-  if ( $version ge '2' ) {
-    carp "'$file' should end in '.json'"
-      unless $file =~ m{\.json$};
+  if ($version ge '2') {
+    carp "'$file' should end in '.json'" unless $file =~ m{\.json$};
   }
   else {
-    carp "'$file' should end in '.yml'"
-      unless $file =~ m{\.yml$};
+    carp "'$file' should end in '.yml'" unless $file =~ m{\.yml$};
   }
 
-  my $data = $self->as_string( $options );
-  open my $fh, ">$layer", $file
-    or die "Error opening '$file' for writing: $!\n";
+  my $data = $self->as_string($options);
+  open my $fh, ">$layer", $file or die "Error opening '$file' for writing: $!\n";
 
   print {$fh} $data;
-  close $fh
-    or die "Error closing '$file': $!\n";
+  close $fh or die "Error closing '$file': $!\n";
 
   return 1;
 }
@@ -455,7 +450,7 @@ sub effective_prereqs {
 
   return $prereq unless @$features;
 
-  my @other = map {; $self->feature($_)->prereqs } @$features;
+  my @other = map { ; $self->feature($_)->prereqs } @$features;
 
   return $prereq->with_merged_prereqs(\@other);
 }
@@ -476,11 +471,11 @@ sub effective_prereqs {
 sub should_index_file {
   my ($self, $filename) = @_;
 
-  for my $no_index_file (@{ $self->no_index->{file} || [] }) {
+  for my $no_index_file (@{$self->no_index->{file} || []}) {
     return if $filename eq $no_index_file;
   }
 
-  for my $no_index_dir (@{ $self->no_index->{directory} }) {
+  for my $no_index_dir (@{$self->no_index->{directory}}) {
     $no_index_dir =~ s{$}{/} unless $no_index_dir =~ m{/\z};
     return if index($filename, $no_index_dir) == 0;
   }
@@ -502,11 +497,11 @@ sub should_index_file {
 sub should_index_package {
   my ($self, $package) = @_;
 
-  for my $no_index_pkg (@{ $self->no_index->{package} || [] }) {
+  for my $no_index_pkg (@{$self->no_index->{package} || []}) {
     return if $package eq $no_index_pkg;
   }
 
-  for my $no_index_ns (@{ $self->no_index->{namespace} }) {
+  for my $no_index_ns (@{$self->no_index->{namespace}}) {
     return if index($package, "${no_index_ns}::") == 0;
   }
 
@@ -526,8 +521,8 @@ sub features {
   my ($self) = @_;
 
   my $opt_f = $self->optional_features;
-  my @features = map {; CPAN::Meta::Feature->new($_ => $opt_f->{ $_ }) }
-                 keys %$opt_f;
+  my @features
+    = map { ; CPAN::Meta::Feature->new($_ => $opt_f->{$_}) } keys %$opt_f;
 
   return @features;
 }
@@ -546,7 +541,7 @@ sub feature {
   my ($self, $ident) = @_;
 
   croak "no feature named $ident"
-    unless my $f = $self->optional_features->{ $ident };
+    unless my $f = $self->optional_features->{$ident};
 
   return CPAN::Meta::Feature->new($ident, $f);
 }
@@ -567,9 +562,9 @@ sub feature {
 sub as_struct {
   my ($self, $options) = @_;
   my $struct = _dclone($self);
-  if ( $options->{version} ) {
-    my $cmc = CPAN::Meta::Converter->new( $struct );
-    $struct = $cmc->convert( version => $options->{version} );
+  if ($options->{version}) {
+    my $cmc = CPAN::Meta::Converter->new($struct);
+    $struct = $cmc->convert(version => $options->{version});
   }
   return $struct;
 }
@@ -603,28 +598,28 @@ sub as_string {
   my $version = $options->{version} || '2';
 
   my $struct;
-  if ( $self->meta_spec_version ne $version ) {
-    my $cmc = CPAN::Meta::Converter->new( $self->as_struct );
-    $struct = $cmc->convert( version => $version );
+  if ($self->meta_spec_version ne $version) {
+    my $cmc = CPAN::Meta::Converter->new($self->as_struct);
+    $struct = $cmc->convert(version => $version);
   }
   else {
     $struct = $self->as_struct;
   }
 
   my ($data, $backend);
-  if ( $version ge '2' ) {
+  if ($version ge '2') {
     $backend = Parse::CPAN::Meta->json_backend();
-    local $struct->{x_serialization_backend} = sprintf '%s version %s',
-      $backend, $backend->VERSION;
+    local $struct->{x_serialization_backend} = sprintf '%s version %s', $backend,
+      $backend->VERSION;
     $data = $backend->new->pretty->canonical->encode($struct);
   }
   else {
     $backend = Parse::CPAN::Meta->yaml_backend();
-    local $struct->{x_serialization_backend} = sprintf '%s version %s',
-      $backend, $backend->VERSION;
+    local $struct->{x_serialization_backend} = sprintf '%s version %s', $backend,
+      $backend->VERSION;
     $data = eval { no strict 'refs'; &{"$backend\::Dump"}($struct) };
-    if ( $@ ) {
-      croak $backend->can('errstr') ? $backend->errstr : $@
+    if ($@) {
+      croak $backend->can('errstr') ? $backend->errstr : $@;
     }
   }
 
@@ -633,7 +628,7 @@ sub as_string {
 
 # Used by JSON::PP, etc. for "convert_blessed"
 sub TO_JSON {
-  return { %{ $_[0] } };
+  return {%{$_[0]}};
 }
 
 1;
index 03806bc82bf095b3acd014191905bb6befc753d0..c6a4531e20f2316a30666ee98343b87fa11b58ee 100644 (file)
@@ -1,6 +1,7 @@
 use 5.006;
 use strict;
 use warnings;
+
 package CPAN::Meta::Converter;
 
 our $VERSION = '2.150005';
@@ -34,14 +35,15 @@ use Parse::CPAN::Meta 1.4400 ();
 # prereqs and be available at runtime.
 
 BEGIN {
-  eval "use version ()"; ## no critic
-  if ( my $err = $@ ) {
-    eval "use ExtUtils::MakeMaker::version" or die $err; ## no critic
+  eval "use version ()";    ## no critic
+  if (my $err = $@) {
+    eval "use ExtUtils::MakeMaker::version" or die $err;    ## no critic
   }
 }
 
 # Perl 5.10.0 didn't have "is_qv" in version.pm
-*_is_qv = version->can('is_qv') ? sub { $_[0]->is_qv } : sub { exists $_[0]->{qv} };
+*_is_qv
+  = version->can('is_qv') ? sub { $_[0]->is_qv } : sub { exists $_[0]->{qv} };
 
 sub _dclone {
   my $ref = shift;
@@ -52,26 +54,24 @@ sub _dclone {
   # Path::Class objects, etc.
   no warnings 'once';
   no warnings 'redefine';
-  local *UNIVERSAL::TO_JSON = sub { "$_[0]" };
+  local *UNIVERSAL::TO_JSON = sub {"$_[0]"};
 
-  my $json = Parse::CPAN::Meta->json_backend()->new
-      ->utf8
-      ->allow_blessed
-      ->convert_blessed;
-  $json->decode($json->encode($ref))
+  my $json = Parse::CPAN::Meta->json_backend()
+    ->new->utf8->allow_blessed->convert_blessed;
+  $json->decode($json->encode($ref));
 }
 
 my %known_specs = (
-    '2'   => 'http://search.cpan.org/perldoc?CPAN::Meta::Spec',
-    '1.4' => 'http://module-build.sourceforge.net/META-spec-v1.4.html',
-    '1.3' => 'http://module-build.sourceforge.net/META-spec-v1.3.html',
-    '1.2' => 'http://module-build.sourceforge.net/META-spec-v1.2.html',
-    '1.1' => 'http://module-build.sourceforge.net/META-spec-v1.1.html',
-    '1.0' => 'http://module-build.sourceforge.net/META-spec-v1.0.html'
+  '2'   => 'http://search.cpan.org/perldoc?CPAN::Meta::Spec',
+  '1.4' => 'http://module-build.sourceforge.net/META-spec-v1.4.html',
+  '1.3' => 'http://module-build.sourceforge.net/META-spec-v1.3.html',
+  '1.2' => 'http://module-build.sourceforge.net/META-spec-v1.2.html',
+  '1.1' => 'http://module-build.sourceforge.net/META-spec-v1.1.html',
+  '1.0' => 'http://module-build.sourceforge.net/META-spec-v1.0.html'
 );
 
 my @spec_list = sort { $a <=> $b } keys %known_specs;
-my ($LOWEST, $HIGHEST) = @spec_list[0,-1];
+my ($LOWEST, $HIGHEST) = @spec_list[0, -1];
 
 #--------------------------------------------------------------------------#
 # converters
@@ -99,7 +99,7 @@ sub _generated_by {
   return "$gen, $sig";
 }
 
-sub _listify { ! defined $_[0] ? undef : ref $_[0] eq 'ARRAY' ? $_[0] : [$_[0]] }
+sub _listify { !defined $_[0] ? undef : ref $_[0] eq 'ARRAY' ? $_[0] : [$_[0]] }
 
 sub _prefix_custom {
   my $key = shift;
@@ -123,44 +123,27 @@ sub _no_prefix_ucfirst_custom {
 
 sub _change_meta_spec {
   my ($element, undef, undef, $version) = @_;
-  return {
-    version => $version,
-    url => $known_specs{$version},
-  };
+  return {version => $version, url => $known_specs{$version},};
 }
 
 my @open_source = (
-  'perl',
-  'gpl',
-  'apache',
-  'artistic',
-  'artistic_2',
-  'lgpl',
-  'bsd',
-  'gpl',
-  'mit',
-  'mozilla',
-  'open_source',
+  'perl',       'gpl',     'apache', 'artistic',
+  'artistic_2', 'lgpl',    'bsd',    'gpl',
+  'mit',        'mozilla', 'open_source',
 );
 
-my %is_open_source = map {; $_ => 1 } @open_source;
+my %is_open_source = map { ; $_ => 1 } @open_source;
 
-my @valid_licenses_1 = (
-  @open_source,
-  'unrestricted',
-  'restrictive',
-  'unknown',
-);
+my @valid_licenses_1
+  = (@open_source, 'unrestricted', 'restrictive', 'unknown',);
 
-my %license_map_1 = (
-  ( map { $_ => $_ } @valid_licenses_1 ),
-  artistic2 => 'artistic_2',
-);
+my %license_map_1
+  = ((map { $_ => $_ } @valid_licenses_1), artistic2 => 'artistic_2',);
 
 sub _license_1 {
   my ($element) = @_;
   return 'unknown' unless defined $element;
-  if ( $license_map_1{lc $element} ) {
+  if ($license_map_1{lc $element}) {
     return $license_map_1{lc $element};
   }
   else {
@@ -203,28 +186,28 @@ my @valid_licenses_2 = qw(
 # it specifies the version of the license.
 my %license_map_2 = (
   (map { $_ => $_ } @valid_licenses_2),
-  apache      => 'apache_2_0',  # clearly stated as 2.0
-  artistic    => 'artistic_1',  # clearly stated as 1
-  artistic2   => 'artistic_2',  # clearly stated as 2
-  gpl         => 'open_source', # we don't know which GPL; punt
-  lgpl        => 'open_source', # we don't know which LGPL; punt
-  mozilla     => 'open_source', # we don't know which MPL; punt
-  perl        => 'perl_5',      # clearly Perl 5
+  apache      => 'apache_2_0',     # clearly stated as 2.0
+  artistic    => 'artistic_1',     # clearly stated as 1
+  artistic2   => 'artistic_2',     # clearly stated as 2
+  gpl         => 'open_source',    # we don't know which GPL; punt
+  lgpl        => 'open_source',    # we don't know which LGPL; punt
+  mozilla     => 'open_source',    # we don't know which MPL; punt
+  perl        => 'perl_5',         # clearly Perl 5
   restrictive => 'restricted',
 );
 
 sub _license_2 {
   my ($element) = @_;
-  return [ 'unknown' ] unless defined $element;
-  $element = [ $element ] unless ref $element eq 'ARRAY';
+  return ['unknown'] unless defined $element;
+  $element = [$element] unless ref $element eq 'ARRAY';
   my @new_list;
-  for my $lic ( @$element ) {
+  for my $lic (@$element) {
     next unless defined $lic;
-    if ( my $new = $license_map_2{lc $lic} ) {
+    if (my $new = $license_map_2{lc $lic}) {
       push @new_list, $new;
     }
   }
-  return @new_list ? \@new_list : [ 'unknown' ];
+  return @new_list ? \@new_list : ['unknown'];
 }
 
 my %license_downgrade_map = qw(
@@ -259,48 +242,50 @@ my %license_downgrade_map = qw(
 
 sub _downgrade_license {
   my ($element) = @_;
-  if ( ! defined $element ) {
+  if (!defined $element) {
     return "unknown";
   }
-  elsif( ref $element eq 'ARRAY' ) {
-    if ( @$element > 1) {
-      if (grep { !$is_open_source{ $license_downgrade_map{lc $_} || 'unknown' } } @$element) {
+  elsif (ref $element eq 'ARRAY') {
+    if (@$element > 1) {
+      if (grep { !$is_open_source{$license_downgrade_map{lc $_} || 'unknown'} }
+        @$element)
+      {
         return 'unknown';
       }
       else {
         return 'open_source';
       }
     }
-    elsif ( @$element == 1 ) {
+    elsif (@$element == 1) {
       return $license_downgrade_map{lc $element->[0]} || "unknown";
     }
   }
-  elsif ( ! ref $element ) {
+  elsif (!ref $element) {
     return $license_downgrade_map{lc $element} || "unknown";
   }
   return "unknown";
 }
 
 my $no_index_spec_1_2 = {
-  'file' => \&_listify,
-  'dir' => \&_listify,
-  'package' => \&_listify,
+  'file'      => \&_listify,
+  'dir'       => \&_listify,
+  'package'   => \&_listify,
   'namespace' => \&_listify,
 };
 
 my $no_index_spec_1_3 = {
-  'file' => \&_listify,
+  'file'      => \&_listify,
   'directory' => \&_listify,
-  'package' => \&_listify,
+  'package'   => \&_listify,
   'namespace' => \&_listify,
 };
 
 my $no_index_spec_2 = {
-  'file' => \&_listify,
+  'file'      => \&_listify,
   'directory' => \&_listify,
-  'package' => \&_listify,
+  'package'   => \&_listify,
   'namespace' => \&_listify,
-  ':custom'  => \&_prefix_custom,
+  ':custom'   => \&_prefix_custom,
 };
 
 sub _no_index_1_2 {
@@ -309,21 +294,22 @@ sub _no_index_1_2 {
   return unless $no_index;
 
   # cleanup wrong format
-  if ( ! ref $no_index ) {
+  if (!ref $no_index) {
     my $item = $no_index;
-    $no_index = { dir => [ $item ], file => [ $item ] };
+    $no_index = {dir => [$item], file => [$item]};
   }
-  elsif ( ref $no_index eq 'ARRAY' ) {
+  elsif (ref $no_index eq 'ARRAY') {
     my $list = $no_index;
-    $no_index = { dir => [ @$list ], file => [ @$list ] };
+    $no_index = {dir => [@$list], file => [@$list]};
   }
 
   # common mistake: files -> file
-  if ( exists $no_index->{files} ) {
+  if (exists $no_index->{files}) {
     $no_index->{file} = delete $no_index->{files};
   }
+
   # common mistake: modules -> module
-  if ( exists $no_index->{modules} ) {
+  if (exists $no_index->{modules}) {
     $no_index->{module} = delete $no_index->{modules};
   }
   return _convert($no_index, $no_index_spec_1_2);
@@ -334,24 +320,26 @@ sub _no_index_directory {
   return unless $element;
 
   # cleanup wrong format
-  if ( ! ref $element ) {
+  if (!ref $element) {
     my $item = $element;
-    $element = { directory => [ $item ], file => [ $item ] };
+    $element = {directory => [$item], file => [$item]};
   }
-  elsif ( ref $element eq 'ARRAY' ) {
+  elsif (ref $element eq 'ARRAY') {
     my $list = $element;
-    $element = { directory => [ @$list ], file => [ @$list ] };
+    $element = {directory => [@$list], file => [@$list]};
   }
 
-  if ( exists $element->{dir} ) {
+  if (exists $element->{dir}) {
     $element->{directory} = delete $element->{dir};
   }
+
   # common mistake: files -> file
-  if ( exists $element->{files} ) {
+  if (exists $element->{files}) {
     $element->{file} = delete $element->{files};
   }
+
   # common mistake: modules -> module
-  if ( exists $element->{modules} ) {
+  if (exists $element->{modules}) {
     $element->{module} = delete $element->{modules};
   }
   my $spec = $version == 2 ? $no_index_spec_2 : $no_index_spec_1_3;
@@ -366,19 +354,20 @@ sub _is_module_name {
 
 sub _clean_version {
   my ($element) = @_;
-  return 0 if ! defined $element;
+  return 0 if !defined $element;
 
   $element =~ s{^\s*}{};
   $element =~ s{\s*$}{};
   $element =~ s{^\.}{0.};
 
-  return 0 if ! length $element;
-  return 0 if ( $element eq 'undef' || $element eq '<undef>' );
+  return 0 if !length $element;
+  return 0 if ($element eq 'undef' || $element eq '<undef>');
 
   my $v = eval { version->new($element) };
+
   # XXX check defined $v and not just $v because version objects leak memory
   # in boolean context -- dagolden, 2012-02-03
-  if ( defined $v ) {
+  if (defined $v) {
     return _is_qv($v) ? $v->normal : $element;
   }
   else {
@@ -390,28 +379,30 @@ sub _bad_version_hook {
   my ($v) = @_;
   $v =~ s{^\s*}{};
   $v =~ s{\s*$}{};
-  $v =~ s{[a-z]+$}{}; # strip trailing alphabetics
+  $v =~ s{[a-z]+$}{};    # strip trailing alphabetics
   my $vobj = eval { version->new($v) };
-  return defined($vobj) ? $vobj : version->new(0); # or give up
+  return defined($vobj) ? $vobj : version->new(0);    # or give up
 }
 
 sub _version_map {
   my ($element) = @_;
   return unless defined $element;
-  if ( ref $element eq 'HASH' ) {
+  if (ref $element eq 'HASH') {
+
     # XXX turn this into CPAN::Meta::Requirements with bad version hook
     # and then turn it back into a hash
     my $new_map = CPAN::Meta::Requirements->new(
-      { bad_version_hook => \&_bad_version_hook } # punt
+      {bad_version_hook => \&_bad_version_hook}    # punt
     );
-    while ( my ($k,$v) = each %$element ) {
+    while (my ($k, $v) = each %$element) {
       next unless _is_module_name($k);
-      if ( !defined($v) || !length($v) || $v eq 'undef' || $v eq '<undef>'  ) {
+      if (!defined($v) || !length($v) || $v eq 'undef' || $v eq '<undef>') {
         $v = 0;
       }
+
       # some weird, old META have bad yml with module => module
       # so check if value is like a module name and not like a version
-      if ( _is_module_name($v) && ! version::is_lax($v) ) {
+      if (_is_module_name($v) && !version::is_lax($v)) {
         $new_map->add_minimum($k => 0);
         $new_map->add_minimum($v => 0);
       }
@@ -419,12 +410,12 @@ sub _version_map {
     }
     return $new_map->as_string_hash;
   }
-  elsif ( ref $element eq 'ARRAY' ) {
-    my $hashref = { map { $_ => 0 } @$element };
-    return _version_map($hashref); # cleanup any weird stuff
+  elsif (ref $element eq 'ARRAY') {
+    my $hashref = {map { $_ => 0 } @$element};
+    return _version_map($hashref);    # cleanup any weird stuff
   }
-  elsif ( ref $element eq '' && length $element ) {
-    return { $element => 0 }
+  elsif (ref $element eq '' && length $element) {
+    return {$element => 0};
   }
   return;
 }
@@ -432,14 +423,12 @@ sub _version_map {
 sub _prereqs_from_1 {
   my (undef, undef, $meta) = @_;
   my $prereqs = {};
-  for my $phase ( qw/build configure/ ) {
+  for my $phase (qw/build configure/) {
     my $key = "${phase}_requires";
-    $prereqs->{$phase}{requires} = _version_map($meta->{$key})
-      if $meta->{$key};
+    $prereqs->{$phase}{requires} = _version_map($meta->{$key}) if $meta->{$key};
   }
-  for my $rel ( qw/requires recommends conflicts/ ) {
-    $prereqs->{runtime}{$rel} = _version_map($meta->{$rel})
-      if $meta->{$rel};
+  for my $rel (qw/requires recommends conflicts/) {
+    $prereqs->{runtime}{$rel} = _version_map($meta->{$rel}) if $meta->{$rel};
   }
   return $prereqs;
 }
@@ -450,7 +439,7 @@ my $prereqs_spec = {
   test      => \&_prereqs_rel,
   runtime   => \&_prereqs_rel,
   develop   => \&_prereqs_rel,
-  ':custom'  => \&_prefix_custom,
+  ':custom' => \&_prefix_custom,
 };
 
 my $relation_spec = {
@@ -464,13 +453,13 @@ my $relation_spec = {
 sub _cleanup_prereqs {
   my ($prereqs, $key, $meta, $to_version) = @_;
   return unless $prereqs && ref $prereqs eq 'HASH';
-  return _convert( $prereqs, $prereqs_spec, $to_version );
+  return _convert($prereqs, $prereqs_spec, $to_version);
 }
 
 sub _prereqs_rel {
   my ($relation, $key, $meta, $to_version) = @_;
   return unless $relation && ref $relation eq 'HASH';
-  return _convert( $relation, $relation_spec, $to_version );
+  return _convert($relation, $relation_spec, $to_version);
 }
 
 
@@ -482,15 +471,15 @@ BEGIN {
     conflicts
   );
 
-  for ( @old_prereqs ) {
+  for (@old_prereqs) {
     my $sub = "_get_$_";
-    my ($phase,$type) = split qr/_/, $_;
-    if ( ! defined $type ) {
-      $type = $phase;
+    my ($phase, $type) = split qr/_/, $_;
+    if (!defined $type) {
+      $type  = $phase;
       $phase = 'runtime';
     }
     no strict 'refs';
-    *{$sub} = sub { _extract_prereqs($_[2]->{prereqs},$phase,$type) };
+    *{$sub} = sub { _extract_prereqs($_[2]->{prereqs}, $phase, $type) };
   }
 }
 
@@ -515,18 +504,22 @@ sub _extract_prereqs {
 sub _downgrade_optional_features {
   my (undef, undef, $meta) = @_;
   return unless exists $meta->{optional_features};
-  my $origin = $meta->{optional_features};
+  my $origin   = $meta->{optional_features};
   my $features = {};
-  for my $name ( keys %$origin ) {
+  for my $name (keys %$origin) {
     $features->{$name} = {
       description => $origin->{$name}{description},
-      requires => _extract_prereqs($origin->{$name}{prereqs},'runtime','requires'),
-      configure_requires => _extract_prereqs($origin->{$name}{prereqs},'runtime','configure_requires'),
-      build_requires => _extract_prereqs($origin->{$name}{prereqs},'runtime','build_requires'),
-      recommends => _extract_prereqs($origin->{$name}{prereqs},'runtime','recommends'),
-      conflicts => _extract_prereqs($origin->{$name}{prereqs},'runtime','conflicts'),
+      requires => _extract_prereqs($origin->{$name}{prereqs}, 'runtime', 'requires'),
+      configure_requires =>
+        _extract_prereqs($origin->{$name}{prereqs}, 'runtime', 'configure_requires'),
+      build_requires =>
+        _extract_prereqs($origin->{$name}{prereqs}, 'runtime', 'build_requires'),
+      recommends =>
+        _extract_prereqs($origin->{$name}{prereqs}, 'runtime', 'recommends'),
+      conflicts =>
+        _extract_prereqs($origin->{$name}{prereqs}, 'runtime', 'conflicts'),
     };
-    for my $k (keys %{$features->{$name}} ) {
+    for my $k (keys %{$features->{$name}}) {
       delete $features->{$name}{$k} unless defined $features->{$name}{$k};
     }
   }
@@ -536,12 +529,12 @@ sub _downgrade_optional_features {
 sub _upgrade_optional_features {
   my (undef, undef, $meta) = @_;
   return unless exists $meta->{optional_features};
-  my $origin = $meta->{optional_features};
+  my $origin   = $meta->{optional_features};
   my $features = {};
-  for my $name ( keys %$origin ) {
+  for my $name (keys %$origin) {
     $features->{$name} = {
       description => $origin->{$name}{description},
-      prereqs => _prereqs_from_1(undef, undef, $origin->{$name}),
+      prereqs     => _prereqs_from_1(undef, undef, $origin->{$name}),
     };
     delete $features->{$name}{prereqs}{configure};
   }
@@ -550,22 +543,22 @@ sub _upgrade_optional_features {
 
 my $optional_features_2_spec = {
   description => \&_keep,
-  prereqs => \&_cleanup_prereqs,
-  ':custom'  => \&_prefix_custom,
+  prereqs     => \&_cleanup_prereqs,
+  ':custom'   => \&_prefix_custom,
 };
 
 sub _feature_2 {
   my ($element, $key, $meta, $to_version) = @_;
   return unless $element && ref $element eq 'HASH';
-  _convert( $element, $optional_features_2_spec, $to_version );
+  _convert($element, $optional_features_2_spec, $to_version);
 }
 
 sub _cleanup_optional_features_2 {
   my ($element, $key, $meta, $to_version) = @_;
   return unless $element && ref $element eq 'HASH';
   my $new_data = {};
-  for my $k ( keys %$element ) {
-    $new_data->{$k} = _feature_2( $element->{$k}, $k, $meta, $to_version );
+  for my $k (keys %$element) {
+    $new_data->{$k} = _feature_2($element->{$k}, $k, $meta, $to_version);
   }
   return unless keys %$new_data;
   return $new_data;
@@ -575,8 +568,8 @@ sub _optional_features_1_4 {
   my ($element) = @_;
   return unless $element;
   $element = _optional_features_as_map($element);
-  for my $name ( keys %$element ) {
-    for my $drop ( qw/requires_packages requires_os excluded_os/ ) {
+  for my $name (keys %$element) {
+    for my $drop (qw/requires_packages requires_os excluded_os/) {
       delete $element->{$name}{$drop};
     }
   }
@@ -586,9 +579,9 @@ sub _optional_features_1_4 {
 sub _optional_features_as_map {
   my ($element) = @_;
   return unless $element;
-  if ( ref $element eq 'ARRAY' ) {
+  if (ref $element eq 'ARRAY') {
     my %map;
-    for my $feature ( @$element ) {
+    for my $feature (@$element) {
       my (@parts) = %$feature;
       $map{$parts[0]} = $parts[1];
     }
@@ -608,33 +601,33 @@ sub _url_or_drop {
 sub _url_list {
   my ($element) = @_;
   return unless $element;
-  $element = _listify( $element );
-  $element = [ grep { _is_urlish($_) } @$element ];
+  $element = _listify($element);
+  $element = [grep { _is_urlish($_) } @$element];
   return unless @$element;
   return $element;
 }
 
 sub _author_list {
   my ($element) = @_;
-  return [ 'unknown' ] unless $element;
-  $element = _listify( $element );
-  $element = [ map { defined $_ && length $_ ? $_ : 'unknown' } @$element ];
-  return [ 'unknown' ] unless @$element;
+  return ['unknown'] unless $element;
+  $element = _listify($element);
+  $element = [map { defined $_ && length $_ ? $_ : 'unknown' } @$element];
+  return ['unknown'] unless @$element;
   return $element;
 }
 
 my $resource2_upgrade = {
-  license    => sub { return _is_urlish($_[0]) ? _listify( $_[0] ) : undef },
+  license    => sub { return _is_urlish($_[0]) ? _listify($_[0]) : undef },
   homepage   => \&_url_or_drop,
   bugtracker => sub {
     my ($item) = @_;
     return unless $item;
-    if ( $item =~ m{^mailto:(.*)$} ) { return { mailto => $1 } }
-    elsif( _is_urlish($item) ) { return { web => $item } }
-    else { return }
+    if    ($item =~ m{^mailto:(.*)$}) { return {mailto => $1} }
+    elsif (_is_urlish($item))         { return {web    => $item} }
+    else                              {return}
   },
-  repository => sub { return _is_urlish($_[0]) ? { url => $_[0] } : undef },
-  ':custom'  => \&_prefix_custom,
+  repository => sub { return _is_urlish($_[0]) ? {url => $_[0]} : undef },
+  ':custom' => \&_prefix_custom,
 };
 
 sub _upgrade_resources_2 {
@@ -643,36 +636,36 @@ sub _upgrade_resources_2 {
   return _convert($meta->{resources}, $resource2_upgrade);
 }
 
-my $bugtracker2_spec = {
-  web => \&_url_or_drop,
-  mailto => \&_keep,
-  ':custom'  => \&_prefix_custom,
-};
+my $bugtracker2_spec
+  = {web => \&_url_or_drop, mailto => \&_keep, ':custom' => \&_prefix_custom,};
 
 sub _repo_type {
   my ($element, $key, $meta, $to_version) = @_;
   return $element if defined $element;
   return unless exists $meta->{url};
   my $repo_url = $meta->{url};
-  for my $type ( qw/git svn/ ) {
+  for my $type (qw/git svn/) {
     return $type if $repo_url =~ m{\A$type};
   }
   return;
 }
 
 my $repository2_spec = {
-  web => \&_url_or_drop,
-  url => \&_url_or_drop,
-  type => \&_repo_type,
-  ':custom'  => \&_prefix_custom,
+  web       => \&_url_or_drop,
+  url       => \&_url_or_drop,
+  type      => \&_repo_type,
+  ':custom' => \&_prefix_custom,
 };
 
 my $resources2_cleanup = {
   license    => \&_url_list,
   homepage   => \&_url_or_drop,
-  bugtracker => sub { ref $_[0] ? _convert( $_[0], $bugtracker2_spec ) : undef },
-  repository => sub { my $data = shift; ref $data ? _convert( $data, $repository2_spec ) : undef },
-  ':custom'  => \&_prefix_custom,
+  bugtracker => sub { ref $_[0] ? _convert($_[0], $bugtracker2_spec) : undef },
+  repository => sub {
+    my $data = shift;
+    ref $data ? _convert($data, $repository2_spec) : undef;
+  },
+  ':custom' => \&_prefix_custom,
 };
 
 sub _cleanup_resources_2 {
@@ -700,7 +693,7 @@ sub _resources_1_3 {
 sub _resources_1_2 {
   my (undef, undef, $meta) = @_;
   my $resources = $meta->{resources} || {};
-  if ( $meta->{license_url} && ! $resources->{license} ) {
+  if ($meta->{license_url} && !$resources->{license}) {
     $resources->{license} = $meta->{license_url}
       if _is_urlish($meta->{license_url});
   }
@@ -731,26 +724,20 @@ sub _release_status {
 sub _release_status_from_version {
   my (undef, undef, $meta) = @_;
   my $version = $meta->{version} || '';
-  return ( $version =~ /_/ ) ? 'testing' : 'stable';
+  return ($version =~ /_/) ? 'testing' : 'stable';
 }
 
-my $provides_spec = {
-  file => \&_keep,
-  version => \&_keep,
-};
+my $provides_spec = {file => \&_keep, version => \&_keep,};
 
-my $provides_spec_2 = {
-  file => \&_keep,
-  version => \&_keep,
-  ':custom'  => \&_prefix_custom,
-};
+my $provides_spec_2
+  = {file => \&_keep, version => \&_keep, ':custom' => \&_prefix_custom,};
 
 sub _provides {
   my ($element, $key, $meta, $to_version) = @_;
   return unless defined $element && ref $element eq 'HASH';
   my $spec = $to_version == 2 ? $provides_spec_2 : $provides_spec;
   my $new_data = {};
-  for my $k ( keys %$element ) {
+  for my $k (keys %$element) {
     $new_data->{$k} = _convert($element->{$k}, $spec, $to_version);
     $new_data->{$k}{version} = _clean_version($element->{$k}{version})
       if exists $element->{$k}{version};
@@ -762,25 +749,24 @@ sub _convert {
   my ($data, $spec, $to_version, $is_fragment) = @_;
 
   my $new_data = {};
-  for my $key ( keys %$spec ) {
+  for my $key (keys %$spec) {
     next if $key eq ':custom' || $key eq ':drop';
     next unless my $fcn = $spec->{$key};
-    if ( $is_fragment && $key eq 'generated_by' ) {
+    if ($is_fragment && $key eq 'generated_by') {
       $fcn = \&_keep;
     }
-    die "spec for '$key' is not a coderef"
-      unless ref $fcn && ref $fcn eq 'CODE';
+    die "spec for '$key' is not a coderef" unless ref $fcn && ref $fcn eq 'CODE';
     my $new_value = $fcn->($data->{$key}, $key, $data, $to_version);
     $new_data->{$key} = $new_value if defined $new_value;
   }
 
-  my $drop_list   = $spec->{':drop'};
-  my $customizer  = $spec->{':custom'} || \&_keep;
+  my $drop_list = $spec->{':drop'};
+  my $customizer = $spec->{':custom'} || \&_keep;
 
-  for my $key ( keys %$data ) {
+  for my $key (keys %$data) {
     next if $drop_list && grep { $key eq $_ } @$drop_list;
-    next if exists $spec->{$key}; # we handled it
-    $new_data->{ $customizer->($key) } = $data->{$key};
+    next if exists $spec->{$key};    # we handled it
+    $new_data->{$customizer->($key)} = $data->{$key};
   }
 
   return $new_data;
@@ -794,30 +780,36 @@ sub _convert {
 # special ":custom" field is used for keys not recognized in spec
 my %up_convert = (
   '2-from-1.4' => {
+
     # PRIOR MANDATORY
-    'abstract'            => \&_keep_or_unknown,
-    'author'              => \&_author_list,
-    'generated_by'        => \&_generated_by,
-    'license'             => \&_license_2,
-    'meta-spec'           => \&_change_meta_spec,
-    'name'                => \&_keep,
-    'version'             => \&_keep,
+    'abstract'     => \&_keep_or_unknown,
+    'author'       => \&_author_list,
+    'generated_by' => \&_generated_by,
+    'license'      => \&_license_2,
+    'meta-spec'    => \&_change_meta_spec,
+    'name'         => \&_keep,
+    'version'      => \&_keep,
+
     # CHANGED TO MANDATORY
-    'dynamic_config'      => \&_keep_or_one,
+    'dynamic_config' => \&_keep_or_one,
+
     # ADDED MANDATORY
-    'release_status'      => \&_release_status,
+    'release_status' => \&_release_status,
+
     # PRIOR OPTIONAL
-    'keywords'            => \&_keep,
-    'no_index'            => \&_no_index_directory,
-    'optional_features'   => \&_upgrade_optional_features,
-    'provides'            => \&_provides,
-    'resources'           => \&_upgrade_resources_2,
+    'keywords'          => \&_keep,
+    'no_index'          => \&_no_index_directory,
+    'optional_features' => \&_upgrade_optional_features,
+    'provides'          => \&_provides,
+    'resources'         => \&_upgrade_resources_2,
+
     # ADDED OPTIONAL
-    'description'         => \&_keep,
-    'prereqs'             => \&_prereqs_from_1,
+    'description' => \&_keep,
+    'prereqs'     => \&_prereqs_from_1,
 
     # drop these deprecated fields, but only after we convert
-    ':drop' => [ qw(
+    ':drop' => [
+      qw(
         build_requires
         configure_requires
         conflicts
@@ -826,297 +818,341 @@ my %up_convert = (
         private
         recommends
         requires
-    ) ],
+        )
+    ],
 
     # other random keys need x_ prefixing
-    ':custom'              => \&_prefix_custom,
+    ':custom' => \&_prefix_custom,
   },
   '1.4-from-1.3' => {
+
     # PRIOR MANDATORY
-    'abstract'            => \&_keep_or_unknown,
-    'author'              => \&_author_list,
-    'generated_by'        => \&_generated_by,
-    'license'             => \&_license_1,
-    'meta-spec'           => \&_change_meta_spec,
-    'name'                => \&_keep,
-    'version'             => \&_keep,
+    'abstract'     => \&_keep_or_unknown,
+    'author'       => \&_author_list,
+    'generated_by' => \&_generated_by,
+    'license'      => \&_license_1,
+    'meta-spec'    => \&_change_meta_spec,
+    'name'         => \&_keep,
+    'version'      => \&_keep,
+
     # PRIOR OPTIONAL
-    'build_requires'      => \&_version_map,
-    'conflicts'           => \&_version_map,
-    'distribution_type'   => \&_keep,
-    'dynamic_config'      => \&_keep_or_one,
-    'keywords'            => \&_keep,
-    'no_index'            => \&_no_index_directory,
-    'optional_features'   => \&_optional_features_1_4,
-    'provides'            => \&_provides,
-    'recommends'          => \&_version_map,
-    'requires'            => \&_version_map,
-    'resources'           => \&_resources_1_4,
+    'build_requires'    => \&_version_map,
+    'conflicts'         => \&_version_map,
+    'distribution_type' => \&_keep,
+    'dynamic_config'    => \&_keep_or_one,
+    'keywords'          => \&_keep,
+    'no_index'          => \&_no_index_directory,
+    'optional_features' => \&_optional_features_1_4,
+    'provides'          => \&_provides,
+    'recommends'        => \&_version_map,
+    'requires'          => \&_version_map,
+    'resources'         => \&_resources_1_4,
+
     # ADDED OPTIONAL
-    'configure_requires'  => \&_keep,
+    'configure_requires' => \&_keep,
 
     # drop these deprecated fields, but only after we convert
-    ':drop' => [ qw(
-      license_url
-      private
-    )],
+    ':drop' => [
+      qw(
+        license_url
+        private
+        )
+    ],
 
     # other random keys are OK if already valid
-    ':custom'              => \&_keep
+    ':custom' => \&_keep
   },
   '1.3-from-1.2' => {
+
     # PRIOR MANDATORY
-    'abstract'            => \&_keep_or_unknown,
-    'author'              => \&_author_list,
-    'generated_by'        => \&_generated_by,
-    'license'             => \&_license_1,
-    'meta-spec'           => \&_change_meta_spec,
-    'name'                => \&_keep,
-    'version'             => \&_keep,
+    'abstract'     => \&_keep_or_unknown,
+    'author'       => \&_author_list,
+    'generated_by' => \&_generated_by,
+    'license'      => \&_license_1,
+    'meta-spec'    => \&_change_meta_spec,
+    'name'         => \&_keep,
+    'version'      => \&_keep,
+
     # PRIOR OPTIONAL
-    'build_requires'      => \&_version_map,
-    'conflicts'           => \&_version_map,
-    'distribution_type'   => \&_keep,
-    'dynamic_config'      => \&_keep_or_one,
-    'keywords'            => \&_keep,
-    'no_index'            => \&_no_index_directory,
-    'optional_features'   => \&_optional_features_as_map,
-    'provides'            => \&_provides,
-    'recommends'          => \&_version_map,
-    'requires'            => \&_version_map,
-    'resources'           => \&_resources_1_3,
+    'build_requires'    => \&_version_map,
+    'conflicts'         => \&_version_map,
+    'distribution_type' => \&_keep,
+    'dynamic_config'    => \&_keep_or_one,
+    'keywords'          => \&_keep,
+    'no_index'          => \&_no_index_directory,
+    'optional_features' => \&_optional_features_as_map,
+    'provides'          => \&_provides,
+    'recommends'        => \&_version_map,
+    'requires'          => \&_version_map,
+    'resources'         => \&_resources_1_3,
 
     # drop these deprecated fields, but only after we convert
-    ':drop' => [ qw(
-      license_url
-      private
-    )],
+    ':drop' => [
+      qw(
+        license_url
+        private
+        )
+    ],
 
     # other random keys are OK if already valid
-    ':custom'              => \&_keep
+    ':custom' => \&_keep
   },
   '1.2-from-1.1' => {
+
     # PRIOR MANDATORY
-    'version'             => \&_keep,
+    'version' => \&_keep,
+
     # CHANGED TO MANDATORY
-    'license'             => \&_license_1,
-    'name'                => \&_keep,
-    'generated_by'        => \&_generated_by,
+    'license'      => \&_license_1,
+    'name'         => \&_keep,
+    'generated_by' => \&_generated_by,
+
     # ADDED MANDATORY
-    'abstract'            => \&_keep_or_unknown,
-    'author'              => \&_author_list,
-    'meta-spec'           => \&_change_meta_spec,
+    'abstract'  => \&_keep_or_unknown,
+    'author'    => \&_author_list,
+    'meta-spec' => \&_change_meta_spec,
+
     # PRIOR OPTIONAL
-    'build_requires'      => \&_version_map,
-    'conflicts'           => \&_version_map,
-    'distribution_type'   => \&_keep,
-    'dynamic_config'      => \&_keep_or_one,
-    'recommends'          => \&_version_map,
-    'requires'            => \&_version_map,
+    'build_requires'    => \&_version_map,
+    'conflicts'         => \&_version_map,
+    'distribution_type' => \&_keep,
+    'dynamic_config'    => \&_keep_or_one,
+    'recommends'        => \&_version_map,
+    'requires'          => \&_version_map,
+
     # ADDED OPTIONAL
-    'keywords'            => \&_keep,
-    'no_index'            => \&_no_index_1_2,
-    'optional_features'   => \&_optional_features_as_map,
-    'provides'            => \&_provides,
-    'resources'           => \&_resources_1_2,
+    'keywords'          => \&_keep,
+    'no_index'          => \&_no_index_1_2,
+    'optional_features' => \&_optional_features_as_map,
+    'provides'          => \&_provides,
+    'resources'         => \&_resources_1_2,
 
     # drop these deprecated fields, but only after we convert
-    ':drop' => [ qw(
-      license_url
-      private
-    )],
+    ':drop' => [
+      qw(
+        license_url
+        private
+        )
+    ],
 
     # other random keys are OK if already valid
-    ':custom'              => \&_keep
+    ':custom' => \&_keep
   },
   '1.1-from-1.0' => {
+
     # CHANGED TO MANDATORY
-    'version'             => \&_keep,
+    'version' => \&_keep,
+
     # IMPLIED MANDATORY
-    'name'                => \&_keep,
+    'name' => \&_keep,
+
     # PRIOR OPTIONAL
-    'build_requires'      => \&_version_map,
-    'conflicts'           => \&_version_map,
-    'distribution_type'   => \&_keep,
-    'dynamic_config'      => \&_keep_or_one,
-    'generated_by'        => \&_generated_by,
-    'license'             => \&_license_1,
-    'recommends'          => \&_version_map,
-    'requires'            => \&_version_map,
+    'build_requires'    => \&_version_map,
+    'conflicts'         => \&_version_map,
+    'distribution_type' => \&_keep,
+    'dynamic_config'    => \&_keep_or_one,
+    'generated_by'      => \&_generated_by,
+    'license'           => \&_license_1,
+    'recommends'        => \&_version_map,
+    'requires'          => \&_version_map,
+
     # ADDED OPTIONAL
-    'license_url'         => \&_url_or_drop,
-    'private'             => \&_keep,
+    'license_url' => \&_url_or_drop,
+    'private'     => \&_keep,
 
     # other random keys are OK if already valid
-    ':custom'              => \&_keep
+    ':custom' => \&_keep
   },
 );
 
 my %down_convert = (
   '1.4-from-2' => {
+
     # MANDATORY
-    'abstract'            => \&_keep_or_unknown,
-    'author'              => \&_author_list,
-    'generated_by'        => \&_generated_by,
-    'license'             => \&_downgrade_license,
-    'meta-spec'           => \&_change_meta_spec,
-    'name'                => \&_keep,
-    'version'             => \&_keep,
+    'abstract'     => \&_keep_or_unknown,
+    'author'       => \&_author_list,
+    'generated_by' => \&_generated_by,
+    'license'      => \&_downgrade_license,
+    'meta-spec'    => \&_change_meta_spec,
+    'name'         => \&_keep,
+    'version'      => \&_keep,
+
     # OPTIONAL
-    'build_requires'      => \&_get_build_requires,
-    'configure_requires'  => \&_get_configure_requires,
-    'conflicts'           => \&_get_conflicts,
-    'distribution_type'   => \&_keep,
-    'dynamic_config'      => \&_keep_or_one,
-    'keywords'            => \&_keep,
-    'no_index'            => \&_no_index_directory,
-    'optional_features'   => \&_downgrade_optional_features,
-    'provides'            => \&_provides,
-    'recommends'          => \&_get_recommends,
-    'requires'            => \&_get_requires,
-    'resources'           => \&_downgrade_resources,
+    'build_requires'     => \&_get_build_requires,
+    'configure_requires' => \&_get_configure_requires,
+    'conflicts'          => \&_get_conflicts,
+    'distribution_type'  => \&_keep,
+    'dynamic_config'     => \&_keep_or_one,
+    'keywords'           => \&_keep,
+    'no_index'           => \&_no_index_directory,
+    'optional_features'  => \&_downgrade_optional_features,
+    'provides'           => \&_provides,
+    'recommends'         => \&_get_recommends,
+    'requires'           => \&_get_requires,
+    'resources'          => \&_downgrade_resources,
 
     # drop these unsupported fields (after conversion)
-    ':drop' => [ qw(
-      description
-      prereqs
-      release_status
-    )],
+    ':drop' => [
+      qw(
+        description
+        prereqs
+        release_status
+        )
+    ],
 
     # custom keys will be left unchanged
-    ':custom'              => \&_keep
+    ':custom' => \&_keep
   },
   '1.3-from-1.4' => {
+
     # MANDATORY
-    'abstract'            => \&_keep_or_unknown,
-    'author'              => \&_author_list,
-    'generated_by'        => \&_generated_by,
-    'license'             => \&_license_1,
-    'meta-spec'           => \&_change_meta_spec,
-    'name'                => \&_keep,
-    'version'             => \&_keep,
+    'abstract'     => \&_keep_or_unknown,
+    'author'       => \&_author_list,
+    'generated_by' => \&_generated_by,
+    'license'      => \&_license_1,
+    'meta-spec'    => \&_change_meta_spec,
+    'name'         => \&_keep,
+    'version'      => \&_keep,
+
     # OPTIONAL
-    'build_requires'      => \&_version_map,
-    'conflicts'           => \&_version_map,
-    'distribution_type'   => \&_keep,
-    'dynamic_config'      => \&_keep_or_one,
-    'keywords'            => \&_keep,
-    'no_index'            => \&_no_index_directory,
-    'optional_features'   => \&_optional_features_as_map,
-    'provides'            => \&_provides,
-    'recommends'          => \&_version_map,
-    'requires'            => \&_version_map,
-    'resources'           => \&_resources_1_3,
+    'build_requires'    => \&_version_map,
+    'conflicts'         => \&_version_map,
+    'distribution_type' => \&_keep,
+    'dynamic_config'    => \&_keep_or_one,
+    'keywords'          => \&_keep,
+    'no_index'          => \&_no_index_directory,
+    'optional_features' => \&_optional_features_as_map,
+    'provides'          => \&_provides,
+    'recommends'        => \&_version_map,
+    'requires'          => \&_version_map,
+    'resources'         => \&_resources_1_3,
 
     # drop these unsupported fields, but only after we convert
-    ':drop' => [ qw(
-      configure_requires
-    )],
+    ':drop' => [
+      qw(
+        configure_requires
+        )
+    ],
 
     # other random keys are OK if already valid
-    ':custom'              => \&_keep,
+    ':custom' => \&_keep,
   },
   '1.2-from-1.3' => {
+
     # MANDATORY
-    'abstract'            => \&_keep_or_unknown,
-    'author'              => \&_author_list,
-    'generated_by'        => \&_generated_by,
-    'license'             => \&_license_1,
-    'meta-spec'           => \&_change_meta_spec,
-    'name'                => \&_keep,
-    'version'             => \&_keep,
+    'abstract'     => \&_keep_or_unknown,
+    'author'       => \&_author_list,
+    'generated_by' => \&_generated_by,
+    'license'      => \&_license_1,
+    'meta-spec'    => \&_change_meta_spec,
+    'name'         => \&_keep,
+    'version'      => \&_keep,
+
     # OPTIONAL
-    'build_requires'      => \&_version_map,
-    'conflicts'           => \&_version_map,
-    'distribution_type'   => \&_keep,
-    'dynamic_config'      => \&_keep_or_one,
-    'keywords'            => \&_keep,
-    'no_index'            => \&_no_index_1_2,
-    'optional_features'   => \&_optional_features_as_map,
-    'provides'            => \&_provides,
-    'recommends'          => \&_version_map,
-    'requires'            => \&_version_map,
-    'resources'           => \&_resources_1_3,
+    'build_requires'    => \&_version_map,
+    'conflicts'         => \&_version_map,
+    'distribution_type' => \&_keep,
+    'dynamic_config'    => \&_keep_or_one,
+    'keywords'          => \&_keep,
+    'no_index'          => \&_no_index_1_2,
+    'optional_features' => \&_optional_features_as_map,
+    'provides'          => \&_provides,
+    'recommends'        => \&_version_map,
+    'requires'          => \&_version_map,
+    'resources'         => \&_resources_1_3,
 
     # other random keys are OK if already valid
-    ':custom'              => \&_keep,
+    ':custom' => \&_keep,
   },
   '1.1-from-1.2' => {
+
     # MANDATORY
-    'version'             => \&_keep,
+    'version' => \&_keep,
+
     # IMPLIED MANDATORY
-    'name'                => \&_keep,
-    'meta-spec'           => \&_change_meta_spec,
+    'name'      => \&_keep,
+    'meta-spec' => \&_change_meta_spec,
+
     # OPTIONAL
-    'build_requires'      => \&_version_map,
-    'conflicts'           => \&_version_map,
-    'distribution_type'   => \&_keep,
-    'dynamic_config'      => \&_keep_or_one,
-    'generated_by'        => \&_generated_by,
-    'license'             => \&_license_1,
-    'private'             => \&_keep,
-    'recommends'          => \&_version_map,
-    'requires'            => \&_version_map,
+    'build_requires'    => \&_version_map,
+    'conflicts'         => \&_version_map,
+    'distribution_type' => \&_keep,
+    'dynamic_config'    => \&_keep_or_one,
+    'generated_by'      => \&_generated_by,
+    'license'           => \&_license_1,
+    'private'           => \&_keep,
+    'recommends'        => \&_version_map,
+    'requires'          => \&_version_map,
 
     # drop unsupported fields
-    ':drop' => [ qw(
-      abstract
-      author
-      provides
-      no_index
-      keywords
-      resources
-    )],
+    ':drop' => [
+      qw(
+        abstract
+        author
+        provides
+        no_index
+        keywords
+        resources
+        )
+    ],
 
     # other random keys are OK if already valid
-    ':custom'              => \&_keep,
+    ':custom' => \&_keep,
   },
   '1.0-from-1.1' => {
+
     # IMPLIED MANDATORY
-    'name'                => \&_keep,
-    'meta-spec'           => \&_change_meta_spec,
-    'version'             => \&_keep,
+    'name'      => \&_keep,
+    'meta-spec' => \&_change_meta_spec,
+    'version'   => \&_keep,
+
     # PRIOR OPTIONAL
-    'build_requires'      => \&_version_map,
-    'conflicts'           => \&_version_map,
-    'distribution_type'   => \&_keep,
-    'dynamic_config'      => \&_keep_or_one,
-    'generated_by'        => \&_generated_by,
-    'license'             => \&_license_1,
-    'recommends'          => \&_version_map,
-    'requires'            => \&_version_map,
+    'build_requires'    => \&_version_map,
+    'conflicts'         => \&_version_map,
+    'distribution_type' => \&_keep,
+    'dynamic_config'    => \&_keep_or_one,
+    'generated_by'      => \&_generated_by,
+    'license'           => \&_license_1,
+    'recommends'        => \&_version_map,
+    'requires'          => \&_version_map,
 
     # other random keys are OK if already valid
-    ':custom'              => \&_keep,
+    ':custom' => \&_keep,
   },
 );
 
 my %cleanup = (
   '2' => {
+
     # PRIOR MANDATORY
-    'abstract'            => \&_keep_or_unknown,
-    'author'              => \&_author_list,
-    'generated_by'        => \&_generated_by,
-    'license'             => \&_license_2,
-    'meta-spec'           => \&_change_meta_spec,
-    'name'                => \&_keep,
-    'version'             => \&_keep,
+    'abstract'     => \&_keep_or_unknown,
+    'author'       => \&_author_list,
+    'generated_by' => \&_generated_by,
+    'license'      => \&_license_2,
+    'meta-spec'    => \&_change_meta_spec,
+    'name'         => \&_keep,
+    'version'      => \&_keep,
+
     # CHANGED TO MANDATORY
-    'dynamic_config'      => \&_keep_or_one,
+    'dynamic_config' => \&_keep_or_one,
+
     # ADDED MANDATORY
-    'release_status'      => \&_release_status,
+    'release_status' => \&_release_status,
+
     # PRIOR OPTIONAL
-    'keywords'            => \&_keep,
-    'no_index'            => \&_no_index_directory,
-    'optional_features'   => \&_cleanup_optional_features_2,
-    'provides'            => \&_provides,
-    'resources'           => \&_cleanup_resources_2,
+    'keywords'          => \&_keep,
+    'no_index'          => \&_no_index_directory,
+    'optional_features' => \&_cleanup_optional_features_2,
+    'provides'          => \&_provides,
+    'resources'         => \&_cleanup_resources_2,
+
     # ADDED OPTIONAL
-    'description'         => \&_keep,
-    'prereqs'             => \&_cleanup_prereqs,
+    'description' => \&_keep,
+    'prereqs'     => \&_cleanup_prereqs,
 
     # drop these deprecated fields, but only after we convert
-    ':drop' => [ qw(
+    ':drop' => [
+      qw(
         build_requires
         configure_requires
         conflicts
@@ -1125,130 +1161,147 @@ my %cleanup = (
         private
         recommends
         requires
-    ) ],
+        )
+    ],
 
     # other random keys need x_ prefixing
-    ':custom'              => \&_prefix_custom,
+    ':custom' => \&_prefix_custom,
   },
   '1.4' => {
+
     # PRIOR MANDATORY
-    'abstract'            => \&_keep_or_unknown,
-    'author'              => \&_author_list,
-    'generated_by'        => \&_generated_by,
-    'license'             => \&_license_1,
-    'meta-spec'           => \&_change_meta_spec,
-    'name'                => \&_keep,
-    'version'             => \&_keep,
+    'abstract'     => \&_keep_or_unknown,
+    'author'       => \&_author_list,
+    'generated_by' => \&_generated_by,
+    'license'      => \&_license_1,
+    'meta-spec'    => \&_change_meta_spec,
+    'name'         => \&_keep,
+    'version'      => \&_keep,
+
     # PRIOR OPTIONAL
-    'build_requires'      => \&_version_map,
-    'conflicts'           => \&_version_map,
-    'distribution_type'   => \&_keep,
-    'dynamic_config'      => \&_keep_or_one,
-    'keywords'            => \&_keep,
-    'no_index'            => \&_no_index_directory,
-    'optional_features'   => \&_optional_features_1_4,
-    'provides'            => \&_provides,
-    'recommends'          => \&_version_map,
-    'requires'            => \&_version_map,
-    'resources'           => \&_resources_1_4,
+    'build_requires'    => \&_version_map,
+    'conflicts'         => \&_version_map,
+    'distribution_type' => \&_keep,
+    'dynamic_config'    => \&_keep_or_one,
+    'keywords'          => \&_keep,
+    'no_index'          => \&_no_index_directory,
+    'optional_features' => \&_optional_features_1_4,
+    'provides'          => \&_provides,
+    'recommends'        => \&_version_map,
+    'requires'          => \&_version_map,
+    'resources'         => \&_resources_1_4,
+
     # ADDED OPTIONAL
-    'configure_requires'  => \&_keep,
+    'configure_requires' => \&_keep,
 
     # other random keys are OK if already valid
-    ':custom'             => \&_keep
+    ':custom' => \&_keep
   },
   '1.3' => {
+
     # PRIOR MANDATORY
-    'abstract'            => \&_keep_or_unknown,
-    'author'              => \&_author_list,
-    'generated_by'        => \&_generated_by,
-    'license'             => \&_license_1,
-    'meta-spec'           => \&_change_meta_spec,
-    'name'                => \&_keep,
-    'version'             => \&_keep,
+    'abstract'     => \&_keep_or_unknown,
+    'author'       => \&_author_list,
+    'generated_by' => \&_generated_by,
+    'license'      => \&_license_1,
+    'meta-spec'    => \&_change_meta_spec,
+    'name'         => \&_keep,
+    'version'      => \&_keep,
+
     # PRIOR OPTIONAL
-    'build_requires'      => \&_version_map,
-    'conflicts'           => \&_version_map,
-    'distribution_type'   => \&_keep,
-    'dynamic_config'      => \&_keep_or_one,
-    'keywords'            => \&_keep,
-    'no_index'            => \&_no_index_directory,
-    'optional_features'   => \&_optional_features_as_map,
-    'provides'            => \&_provides,
-    'recommends'          => \&_version_map,
-    'requires'            => \&_version_map,
-    'resources'           => \&_resources_1_3,
+    'build_requires'    => \&_version_map,
+    'conflicts'         => \&_version_map,
+    'distribution_type' => \&_keep,
+    'dynamic_config'    => \&_keep_or_one,
+    'keywords'          => \&_keep,
+    'no_index'          => \&_no_index_directory,
+    'optional_features' => \&_optional_features_as_map,
+    'provides'          => \&_provides,
+    'recommends'        => \&_version_map,
+    'requires'          => \&_version_map,
+    'resources'         => \&_resources_1_3,
 
     # other random keys are OK if already valid
-    ':custom'             => \&_keep
+    ':custom' => \&_keep
   },
   '1.2' => {
+
     # PRIOR MANDATORY
-    'version'             => \&_keep,
+    'version' => \&_keep,
+
     # CHANGED TO MANDATORY
-    'license'             => \&_license_1,
-    'name'                => \&_keep,
-    'generated_by'        => \&_generated_by,
+    'license'      => \&_license_1,
+    'name'         => \&_keep,
+    'generated_by' => \&_generated_by,
+
     # ADDED MANDATORY
-    'abstract'            => \&_keep_or_unknown,
-    'author'              => \&_author_list,
-    'meta-spec'           => \&_change_meta_spec,
+    'abstract'  => \&_keep_or_unknown,
+    'author'    => \&_author_list,
+    'meta-spec' => \&_change_meta_spec,
+
     # PRIOR OPTIONAL
-    'build_requires'      => \&_version_map,
-    'conflicts'           => \&_version_map,
-    'distribution_type'   => \&_keep,
-    'dynamic_config'      => \&_keep_or_one,
-    'recommends'          => \&_version_map,
-    'requires'            => \&_version_map,
+    'build_requires'    => \&_version_map,
+    'conflicts'         => \&_version_map,
+    'distribution_type' => \&_keep,
+    'dynamic_config'    => \&_keep_or_one,
+    'recommends'        => \&_version_map,
+    'requires'          => \&_version_map,
+
     # ADDED OPTIONAL
-    'keywords'            => \&_keep,
-    'no_index'            => \&_no_index_1_2,
-    'optional_features'   => \&_optional_features_as_map,
-    'provides'            => \&_provides,
-    'resources'           => \&_resources_1_2,
+    'keywords'          => \&_keep,
+    'no_index'          => \&_no_index_1_2,
+    'optional_features' => \&_optional_features_as_map,
+    'provides'          => \&_provides,
+    'resources'         => \&_resources_1_2,
 
     # other random keys are OK if already valid
-    ':custom'             => \&_keep
+    ':custom' => \&_keep
   },
   '1.1' => {
+
     # CHANGED TO MANDATORY
-    'version'             => \&_keep,
+    'version' => \&_keep,
+
     # IMPLIED MANDATORY
-    'name'                => \&_keep,
-    'meta-spec'           => \&_change_meta_spec,
+    'name'      => \&_keep,
+    'meta-spec' => \&_change_meta_spec,
+
     # PRIOR OPTIONAL
-    'build_requires'      => \&_version_map,
-    'conflicts'           => \&_version_map,
-    'distribution_type'   => \&_keep,
-    'dynamic_config'      => \&_keep_or_one,
-    'generated_by'        => \&_generated_by,
-    'license'             => \&_license_1,
-    'recommends'          => \&_version_map,
-    'requires'            => \&_version_map,
+    'build_requires'    => \&_version_map,
+    'conflicts'         => \&_version_map,
+    'distribution_type' => \&_keep,
+    'dynamic_config'    => \&_keep_or_one,
+    'generated_by'      => \&_generated_by,
+    'license'           => \&_license_1,
+    'recommends'        => \&_version_map,
+    'requires'          => \&_version_map,
+
     # ADDED OPTIONAL
-    'license_url'         => \&_url_or_drop,
-    'private'             => \&_keep,
+    'license_url' => \&_url_or_drop,
+    'private'     => \&_keep,
 
     # other random keys are OK if already valid
-    ':custom'             => \&_keep
+    ':custom' => \&_keep
   },
   '1.0' => {
+
     # IMPLIED MANDATORY
-    'name'                => \&_keep,
-    'meta-spec'           => \&_change_meta_spec,
-    'version'             => \&_keep,
+    'name'      => \&_keep,
+    'meta-spec' => \&_change_meta_spec,
+    'version'   => \&_keep,
+
     # IMPLIED OPTIONAL
-    'build_requires'      => \&_version_map,
-    'conflicts'           => \&_version_map,
-    'distribution_type'   => \&_keep,
-    'dynamic_config'      => \&_keep_or_one,
-    'generated_by'        => \&_generated_by,
-    'license'             => \&_license_1,
-    'recommends'          => \&_version_map,
-    'requires'            => \&_version_map,
+    'build_requires'    => \&_version_map,
+    'conflicts'         => \&_version_map,
+    'distribution_type' => \&_keep,
+    'dynamic_config'    => \&_keep_or_one,
+    'generated_by'      => \&_generated_by,
+    'license'           => \&_license_1,
+    'recommends'        => \&_version_map,
+    'requires'          => \&_version_map,
 
     # other random keys are OK if already valid
-    ':custom'             => \&_keep,
+    ':custom' => \&_keep,
   },
 );
 
@@ -1257,43 +1310,44 @@ my %cleanup = (
 # we always expect a meta-spec to be generated
 my %fragments_generate = (
   '2' => {
-    'abstract'            =>   'abstract',
-    'author'              =>   'author',
-    'generated_by'        =>   'generated_by',
-    'license'             =>   'license',
-    'name'                =>   'name',
-    'version'             =>   'version',
-    'dynamic_config'      =>   'dynamic_config',
-    'release_status'      =>   'release_status',
-    'keywords'            =>   'keywords',
-    'no_index'            =>   'no_index',
-    'optional_features'   =>   'optional_features',
-    'provides'            =>   'provides',
-    'resources'           =>   'resources',
-    'description'         =>   'description',
-    'prereqs'             =>   'prereqs',
+    'abstract'          => 'abstract',
+    'author'            => 'author',
+    'generated_by'      => 'generated_by',
+    'license'           => 'license',
+    'name'              => 'name',
+    'version'           => 'version',
+    'dynamic_config'    => 'dynamic_config',
+    'release_status'    => 'release_status',
+    'keywords'          => 'keywords',
+    'no_index'          => 'no_index',
+    'optional_features' => 'optional_features',
+    'provides'          => 'provides',
+    'resources'         => 'resources',
+    'description'       => 'description',
+    'prereqs'           => 'prereqs',
   },
   '1.4' => {
-    'abstract'            => 'abstract',
-    'author'              => 'author',
-    'generated_by'        => 'generated_by',
-    'license'             => 'license',
-    'name'                => 'name',
-    'version'             => 'version',
-    'build_requires'      => 'prereqs',
-    'conflicts'           => 'prereqs',
-    'distribution_type'   => 'distribution_type',
-    'dynamic_config'      => 'dynamic_config',
-    'keywords'            => 'keywords',
-    'no_index'            => 'no_index',
-    'optional_features'   => 'optional_features',
-    'provides'            => 'provides',
-    'recommends'          => 'prereqs',
-    'requires'            => 'prereqs',
-    'resources'           => 'resources',
-    'configure_requires'  => 'prereqs',
+    'abstract'           => 'abstract',
+    'author'             => 'author',
+    'generated_by'       => 'generated_by',
+    'license'            => 'license',
+    'name'               => 'name',
+    'version'            => 'version',
+    'build_requires'     => 'prereqs',
+    'conflicts'          => 'prereqs',
+    'distribution_type'  => 'distribution_type',
+    'dynamic_config'     => 'dynamic_config',
+    'keywords'           => 'keywords',
+    'no_index'           => 'no_index',
+    'optional_features'  => 'optional_features',
+    'provides'           => 'provides',
+    'recommends'         => 'prereqs',
+    'requires'           => 'prereqs',
+    'resources'          => 'resources',
+    'configure_requires' => 'prereqs',
   },
 );
+
 # this is not quite true but will work well enough
 # as 1.4 is a superset of earlier ones
 $fragments_generate{$_} = $fragments_generate{'1.4'} for qw/1.3 1.2 1.1 1.0/;
@@ -1320,12 +1374,12 @@ $fragments_generate{$_} = $fragments_generate{'1.4'} for qw/1.3 1.2 1.1 1.0/;
 #pod =cut
 
 sub new {
-  my ($class,$data,%args) = @_;
+  my ($class, $data, %args) = @_;
 
   # create an attributes hash
   my $self = {
-    'data'    => $data,
-    'spec'    => _extract_spec_version($data, $args{default_version}),
+    'data' => $data,
+    'spec' => _extract_spec_version($data, $args{default_version}),
   };
 
   # create the object
@@ -1333,23 +1387,23 @@ sub new {
 }
 
 sub _extract_spec_version {
-    my ($data, $default) = @_;
-    my $spec = $data->{'meta-spec'};
+  my ($data, $default) = @_;
+  my $spec = $data->{'meta-spec'};
 
-    # is meta-spec there and valid?
-    return( $default || "1.0" ) unless defined $spec && ref $spec eq 'HASH'; # before meta-spec?
+  # is meta-spec there and valid?
+  return ($default || "1.0") unless defined $spec && ref $spec eq 'HASH'; # before meta-spec?
 
-    # does the version key look like a valid version?
-    my $v = $spec->{version};
-    if ( defined $v && $v =~ /^\d+(?:\.\d+)?$/ ) {
-        return $v if defined $v && grep { $v eq $_ } keys %known_specs; # known spec
-        return $v+0 if defined $v && grep { $v == $_ } keys %known_specs; # 2.0 => 2
-    }
+  # does the version key look like a valid version?
+  my $v = $spec->{version};
+  if (defined $v && $v =~ /^\d+(?:\.\d+)?$/) {
+    return $v     if defined $v && grep { $v eq $_ } keys %known_specs; # known spec
+    return $v + 0 if defined $v && grep { $v == $_ } keys %known_specs; # 2.0 => 2
+  }
 
-    # otherwise, use heuristics: look for 1.x vs 2.0 fields
-    return "2" if exists $data->{prereqs};
-    return "1.4" if exists $data->{configure_requires};
-    return( $default || "1.2" ); # when meta-spec was first defined
+  # otherwise, use heuristics: look for 1.x vs 2.0 fields
+  return "2"   if exists $data->{prereqs};
+  return "1.4" if exists $data->{configure_requires};
+  return ($default || "1.2");    # when meta-spec was first defined
 }
 
 #pod =method convert
@@ -1402,7 +1456,7 @@ sub _extract_spec_version {
 
 sub convert {
   my ($self, %args) = @_;
-  my $args = { %args };
+  my $args = {%args};
 
   my $new_version = $args->{version} || $HIGHEST;
   my $is_fragment = $args->{is_fragment};
@@ -1410,27 +1464,29 @@ sub convert {
   my ($old_version) = $self->{spec};
   my $converted = _dclone($self->{data});
 
-  if ( $old_version == $new_version ) {
-    $converted = _convert( $converted, $cleanup{$old_version}, $old_version, $is_fragment );
-    unless ( $args->{is_fragment} ) {
-      my $cmv = CPAN::Meta::Validator->new( $converted );
-      unless ( $cmv->is_valid ) {
+  if ($old_version == $new_version) {
+    $converted
+      = _convert($converted, $cleanup{$old_version}, $old_version, $is_fragment);
+    unless ($args->{is_fragment}) {
+      my $cmv = CPAN::Meta::Validator->new($converted);
+      unless ($cmv->is_valid) {
         my $errs = join("\n", $cmv->errors);
         die "Failed to clean-up $old_version metadata. Errors:\n$errs\n";
       }
     }
     return $converted;
   }
-  elsif ( $old_version > $new_version )  {
+  elsif ($old_version > $new_version) {
     my @vers = sort { $b <=> $a } keys %known_specs;
-    for my $i ( 0 .. $#vers-1 ) {
+    for my $i (0 .. $#vers - 1) {
       next if $vers[$i] > $old_version;
-      last if $vers[$i+1] < $new_version;
+      last if $vers[$i + 1] < $new_version;
       my $spec_string = "$vers[$i+1]-from-$vers[$i]";
-      $converted = _convert( $converted, $down_convert{$spec_string}, $vers[$i+1], $is_fragment );
-      unless ( $args->{is_fragment} ) {
-        my $cmv = CPAN::Meta::Validator->new( $converted );
-        unless ( $cmv->is_valid ) {
+      $converted = _convert($converted, $down_convert{$spec_string}, $vers[$i + 1],
+        $is_fragment);
+      unless ($args->{is_fragment}) {
+        my $cmv = CPAN::Meta::Validator->new($converted);
+        unless ($cmv->is_valid) {
           my $errs = join("\n", $cmv->errors);
           die "Failed to downconvert metadata to $vers[$i+1]. Errors:\n$errs\n";
         }
@@ -1440,14 +1496,15 @@ sub convert {
   }
   else {
     my @vers = sort { $a <=> $b } keys %known_specs;
-    for my $i ( 0 .. $#vers-1 ) {
+    for my $i (0 .. $#vers - 1) {
       next if $vers[$i] < $old_version;
-      last if $vers[$i+1] > $new_version;
+      last if $vers[$i + 1] > $new_version;
       my $spec_string = "$vers[$i+1]-from-$vers[$i]";
-      $converted = _convert( $converted, $up_convert{$spec_string}, $vers[$i+1], $is_fragment );
-      unless ( $args->{is_fragment} ) {
-        my $cmv = CPAN::Meta::Validator->new( $converted );
-        unless ( $cmv->is_valid ) {
+      $converted = _convert($converted, $up_convert{$spec_string}, $vers[$i + 1],
+        $is_fragment);
+      unless ($args->{is_fragment}) {
+        my $cmv = CPAN::Meta::Validator->new($converted);
+        unless ($cmv->is_valid) {
           my $errs = join("\n", $cmv->errors);
           die "Failed to upconvert metadata to $vers[$i+1]. Errors:\n$errs\n";
         }
@@ -1470,15 +1527,14 @@ sub convert {
 #pod =cut
 
 sub upgrade_fragment {
-  my ($self) = @_;
+  my ($self)        = @_;
   my ($old_version) = $self->{spec};
-  my %expected =
-    map {; $_ => 1 }
-    grep { defined }
-    map { $fragments_generate{$old_version}{$_} }
-    keys %{ $self->{data} };
-  my $converted = $self->convert( version => $HIGHEST, is_fragment => 1 );
-  for my $key ( keys %$converted ) {
+  my %expected
+    = map { ; $_ => 1 }
+    grep  {defined}
+    map   { $fragments_generate{$old_version}{$_} } keys %{$self->{data}};
+  my $converted = $self->convert(version => $HIGHEST, is_fragment => 1);
+  for my $key (keys %$converted) {
     next if $key =~ /^x_/i || $key eq 'meta-spec';
     delete $converted->{$key} unless $expected{$key};
   }
index 9dac4f421852e6674917eb9142f2ebe99ad6a382..479b9816b2eeecea56d98fbc7b6c3b6976e20cd6 100644 (file)
@@ -1,6 +1,7 @@
 use 5.006;
 use strict;
 use warnings;
+
 package CPAN::Meta::Feature;
 
 our $VERSION = '2.150005';
@@ -44,7 +45,7 @@ sub new {
 #pod
 #pod =cut
 
-sub identifier  { $_[0]{identifier}  }
+sub identifier { $_[0]{identifier} }
 
 #pod =method description
 #pod
@@ -61,7 +62,7 @@ sub description { $_[0]{description} }
 #pod
 #pod =cut
 
-sub prereqs     { $_[0]{prereqs} }
+sub prereqs { $_[0]{prereqs} }
 
 1;
 
index f4cac5e59a9d66440a057f4de033fef6c290d046..e6db84af5b22ee0505b658c617044dccc7521e55 100644 (file)
@@ -2,6 +2,7 @@
 use 5.006;
 use strict;
 use warnings;
+
 package CPAN::Meta::History;
 
 our $VERSION = '2.150005';
index 05a18ea97327529f3b51cc0756559012e35d9776..75d13a77b8face6b423c5fa183109ff8ae276197 100644 (file)
@@ -11,15 +11,16 @@ use CPAN::Meta::Converter 2.141170;
 
 sub _is_identical {
   my ($left, $right) = @_;
-  return
-    (not defined $left and not defined $right)
+  return (not defined $left and not defined $right)
+
     # if either of these are references, we compare the serialized value
     || (defined $left and defined $right and $left eq $right);
 }
 
 sub _identical {
   my ($left, $right, $path) = @_;
-  croak sprintf "Can't merge attribute %s: '%s' does not equal '%s'", join('.', @{$path}), $left, $right
+  croak sprintf "Can't merge attribute %s: '%s' does not equal '%s'",
+    join('.', @{$path}), $left, $right
     unless _is_identical($left, $right);
   return $left;
 }
@@ -31,10 +32,10 @@ sub _merge {
       $current->{$key} = $next->{$key};
     }
     elsif (my $merger = $mergers->{$key}) {
-      $current->{$key} = $merger->($current->{$key}, $next->{$key}, [ @{$path}, $key ]);
+      $current->{$key} = $merger->($current->{$key}, $next->{$key}, [@{$path}, $key]);
     }
     elsif ($merger = $mergers->{':default'}) {
-      $current->{$key} = $merger->($current->{$key}, $next->{$key}, [ @{$path}, $key ]);
+      $current->{$key} = $merger->($current->{$key}, $next->{$key}, [@{$path}, $key]);
     }
     else {
       croak sprintf "Can't merge unknown attribute '%s'", join '.', @{$path}, $key;
@@ -50,7 +51,7 @@ sub _uniq {
 
 sub _set_addition {
   my ($left, $right) = @_;
-  return [ +_uniq(@{$left}, @{$right}) ];
+  return [+_uniq(@{$left}, @{$right})];
 }
 
 sub _uniq_map {
@@ -59,12 +60,13 @@ sub _uniq_map {
     if (not exists $left->{$key}) {
       $left->{$key} = $right->{$key};
     }
+
     # identical strings or references are merged identically
     elsif (_is_identical($left->{$key}, $right->{$key})) {
-      1; # do nothing - keep left
+      1;    # do nothing - keep left
     }
     elsif (ref $left->{$key} eq 'HASH' and ref $right->{$key} eq 'HASH') {
-      $left->{$key} = _uniq_map($left->{$key}, $right->{$key}, [ @{$path}, $key ]);
+      $left->{$key} = _uniq_map($left->{$key}, $right->{$key}, [@{$path}, $key]);
     }
     else {
       croak 'Duplication of element ' . join '.', @{$path}, $key;
@@ -98,22 +100,26 @@ sub _optional_features {
       $left->{$key} = $right->{$key};
     }
     else {
-      for my $subkey (keys %{ $right->{$key} }) {
+      for my $subkey (keys %{$right->{$key}}) {
         next if $subkey eq 'prereqs';
         if (not exists $left->{$key}{$subkey}) {
           $left->{$key}{$subkey} = $right->{$key}{$subkey};
         }
         else {
-          Carp::croak "Cannot merge two optional_features named '$key' with different '$subkey' values"
-            if do { no warnings 'uninitialized'; $left->{$key}{$subkey} ne $right->{$key}{$subkey} };
+          Carp::croak
+            "Cannot merge two optional_features named '$key' with different '$subkey' values"
+            if do {
+            no warnings 'uninitialized';
+            $left->{$key}{$subkey} ne $right->{$key}{$subkey};
+            };
         }
       }
 
       require CPAN::Meta::Prereqs;
-      $left->{$key}{prereqs} =
-        CPAN::Meta::Prereqs->new($left->{$key}{prereqs})
-          ->with_merged_prereqs(CPAN::Meta::Prereqs->new($right->{$key}{prereqs}))
-          ->as_string_hash;
+      $left->{$key}{prereqs}
+        CPAN::Meta::Prereqs->new($left->{$key}{prereqs})
+        ->with_merged_prereqs(CPAN::Meta::Prereqs->new($right->{$key}{prereqs}))
+        ->as_string_hash;
     }
   }
   return $left;
@@ -131,21 +137,19 @@ my %default = (
     my ($left, $right) = @_;
     return join ', ', _uniq(split(/, /, $left), split(/, /, $right));
   },
-  license     => \&_set_addition,
-  'meta-spec' => {
-    version => \&_identical,
-    url     => \&_identical
-  },
-  name              => \&_identical,
-  release_status    => \&_identical,
-  version           => \&_identical,
-  description       => \&_identical,
-  keywords          => \&_set_addition,
-  no_index          => { map { ($_ => \&_set_addition) } qw/file directory package namespace/ },
+  license        => \&_set_addition,
+  'meta-spec'    => {version => \&_identical, url => \&_identical},
+  name           => \&_identical,
+  release_status => \&_identical,
+  version        => \&_identical,
+  description    => \&_identical,
+  keywords       => \&_set_addition,
+  no_index =>
+    {map { ($_ => \&_set_addition) } qw/file directory package namespace/},
   optional_features => \&_optional_features,
   prereqs           => sub {
     require CPAN::Meta::Prereqs;
-    my ($left, $right) = map { CPAN::Meta::Prereqs->new($_) } @_[0,1];
+    my ($left, $right) = map { CPAN::Meta::Prereqs->new($_) } @_[0, 1];
     return $left->with_merged_prereqs($right)->as_string_hash;
   },
   provides  => \&_uniq_map,
@@ -163,10 +167,10 @@ sub new {
   my ($class, %arguments) = @_;
   croak 'default version required' if not exists $arguments{default_version};
   my %mapping = %default;
-  my %extra = %{ $arguments{extra_mappings} || {} };
+  my %extra = %{$arguments{extra_mappings} || {}};
   for my $key (keys %extra) {
     if (ref($mapping{$key}) eq 'HASH') {
-      $mapping{$key} = { %{ $mapping{$key} }, %{ $extra{$key} } };
+      $mapping{$key} = {%{$mapping{$key}}, %{$extra{$key}}};
     }
     else {
       $mapping{$key} = $extra{$key};
@@ -174,7 +178,7 @@ sub new {
   }
   return bless {
     default_version => $arguments{default_version},
-    mapping => _coerce_mapping(\%mapping, []),
+    mapping         => _coerce_mapping(\%mapping, []),
   }, $class;
 }
 
@@ -194,10 +198,10 @@ sub _coerce_mapping {
       $ret{$key} = $value;
     }
     elsif (ref($value) eq 'HASH') {
-      my $mapping = _coerce_mapping($value, [ @{$map_path}, $key ]);
+      my $mapping = _coerce_mapping($value, [@{$map_path}, $key]);
       $ret{$key} = sub {
         my ($left, $right, $path) = @_;
-        return _merge($left, $right, $mapping, [ @{$path} ]);
+        return _merge($left, $right, $mapping, [@{$path}]);
       };
     }
     elsif ($coderef_for{$value}) {
@@ -214,13 +218,12 @@ sub merge {
   my ($self, @items) = @_;
   my $current = {};
   for my $next (@items) {
-    if ( blessed($next) && $next->isa('CPAN::Meta') ) {
+    if (blessed($next) && $next->isa('CPAN::Meta')) {
       $next = $next->as_struct;
     }
-    elsif ( ref($next) eq 'HASH' ) {
-      my $cmc = CPAN::Meta::Converter->new(
-        $next, default_version => $self->{default_version}
-      );
+    elsif (ref($next) eq 'HASH') {
+      my $cmc = CPAN::Meta::Converter->new($next,
+        default_version => $self->{default_version});
       $next = $cmc->upgrade_fragment;
     }
     else {
index 8a13eb13c81f4c08b3aa612f605765829d8bf488..1bfb92ce3afde38cd71975b8ecab7d87facf5978 100644 (file)
@@ -1,6 +1,7 @@
 use 5.006;
 use strict;
 use warnings;
+
 package CPAN::Meta::Prereqs;
 
 our $VERSION = '2.150005';
@@ -45,34 +46,33 @@ use CPAN::Meta::Requirements 2.121;
 #pod
 #pod =cut
 
-sub __legal_phases { qw(configure build test runtime develop)   }
-sub __legal_types  { qw(requires recommends suggests conflicts) }
+sub __legal_phases {qw(configure build test runtime develop)}
+sub __legal_types  {qw(requires recommends suggests conflicts)}
 
 # expect a prereq spec from META.json -- rjbs, 2010-04-11
 sub new {
   my ($class, $prereq_spec) = @_;
   $prereq_spec ||= {};
 
-  my %is_legal_phase = map {; $_ => 1 } $class->__legal_phases;
-  my %is_legal_type  = map {; $_ => 1 } $class->__legal_types;
+  my %is_legal_phase = map { ; $_ => 1 } $class->__legal_phases;
+  my %is_legal_type  = map { ; $_ => 1 } $class->__legal_types;
 
   my %guts;
-  PHASE: for my $phase (keys %$prereq_spec) {
+PHASE: for my $phase (keys %$prereq_spec) {
     next PHASE unless $phase =~ /\Ax_/i or $is_legal_phase{$phase};
 
-    my $phase_spec = $prereq_spec->{ $phase };
+    my $phase_spec = $prereq_spec->{$phase};
     next PHASE unless keys %$phase_spec;
 
-    TYPE: for my $type (keys %$phase_spec) {
+  TYPE: for my $type (keys %$phase_spec) {
       next TYPE unless $type =~ /\Ax_/i or $is_legal_type{$type};
 
-      my $spec = $phase_spec->{ $type };
+      my $spec = $phase_spec->{$type};
 
       next TYPE unless keys %$spec;
 
-      $guts{prereqs}{$phase}{$type} = CPAN::Meta::Requirements->from_string_hash(
-        $spec
-      );
+      $guts{prereqs}{$phase}{$type}
+        = CPAN::Meta::Requirements->from_string_hash($spec);
     }
   }
 
@@ -152,7 +152,7 @@ sub with_merged_prereqs {
 
       next unless $req->required_modules;
 
-      $new_arg{ $phase }{ $type } = $req->as_string_hash;
+      $new_arg{$phase}{$type} = $req->as_string_hash;
     }
   }
 
@@ -174,8 +174,8 @@ sub with_merged_prereqs {
 
 sub merged_requirements {
   my ($self, $phases, $types) = @_;
-  $phases = [qw/runtime build test/] unless defined $phases;
-  $types = [qw/requires recommends/] unless defined $types;
+  $phases = [qw/runtime build test/]  unless defined $phases;
+  $types  = [qw/requires recommends/] unless defined $types;
 
   confess "merged_requirements phases argument must be an arrayref"
     unless ref $phases eq 'ARRAY';
@@ -184,15 +184,15 @@ sub merged_requirements {
 
   my $req = CPAN::Meta::Requirements->new;
 
-  for my $phase ( @$phases ) {
+  for my $phase (@$phases) {
     unless ($phase =~ /\Ax_/i or grep { $phase eq $_ } $self->__legal_phases) {
-        confess "requested requirements for unknown phase: $phase";
+      confess "requested requirements for unknown phase: $phase";
     }
-    for my $type ( @$types ) {
+    for my $type (@$types) {
       unless ($type =~ /\Ax_/i or grep { $type eq $_ } $self->__legal_types) {
-          confess "requested requirements for unknown type: $type";
+        confess "requested requirements for unknown type: $type";
       }
-      $req->add_requirements( $self->requirements_for($phase, $type) );
+      $req->add_requirements($self->requirements_for($phase, $type));
     }
   }
 
@@ -220,7 +220,7 @@ sub as_string_hash {
       my $req = $self->requirements_for($phase, $type);
       next unless $req->required_modules;
 
-      $hash{ $phase }{ $type } = $req->as_string_hash;
+      $hash{$phase}{$type} = $req->as_string_hash;
     }
   }
 
@@ -249,8 +249,8 @@ sub finalize {
 
   $self->{finalized} = 1;
 
-  for my $phase (keys %{ $self->{prereqs} }) {
-    $_->finalize for values %{ $self->{prereqs}{$phase} };
+  for my $phase (keys %{$self->{prereqs}}) {
+    $_->finalize for values %{$self->{prereqs}{$phase}};
   }
 }
 
@@ -268,7 +268,7 @@ sub finalize {
 sub clone {
   my ($self) = @_;
 
-  my $clone = (ref $self)->new( $self->as_string_hash );
+  my $clone = (ref $self)->new($self->as_string_hash);
 }
 
 1;
index b0e83b0d2d288b47f96581c808b49c6dbd0fb896..f16870f4a95a45c8dd264582153d2629754cc310 100644 (file)
@@ -1,7 +1,9 @@
-use 5.006; # keep at v5.6 for CPAN.pm
+use 5.006;    # keep at v5.6 for CPAN.pm
 use strict;
 use warnings;
+
 package CPAN::Meta::Requirements;
+
 # ABSTRACT: a set of version requirements for a CPAN dist
 
 our $VERSION = '2.140';
@@ -42,14 +44,15 @@ use Carp ();
 # prereqs and be available at runtime.
 
 BEGIN {
-  eval "use version ()"; ## no critic
-  if ( my $err = $@ ) {
-    eval "use ExtUtils::MakeMaker::version" or die $err; ## no critic
+  eval "use version ()";    ## no critic
+  if (my $err = $@) {
+    eval "use ExtUtils::MakeMaker::version" or die $err;    ## no critic
   }
 }
 
 # Perl 5.10.0 didn't have "is_qv" in version.pm
-*_is_qv = version->can('is_qv') ? sub { $_[0]->is_qv } : sub { exists $_[0]->{qv} };
+*_is_qv
+  = version->can('is_qv') ? sub { $_[0]->is_qv } : sub { exists $_[0]->{qv} };
 
 # construct once, reuse many times
 my $V0 = version->new(0);
@@ -78,20 +81,20 @@ sub new {
   $options ||= {};
   Carp::croak "Argument to $class\->new() must be a hash reference"
     unless ref $options eq 'HASH';
-  my %self = map {; $_ => $options->{$_}} @valid_options;
+  my %self = map { ; $_ => $options->{$_} } @valid_options;
 
   return bless \%self => $class;
 }
 
 # from version::vpp
 sub _find_magic_vstring {
-  my $value = shift;
+  my $value  = shift;
   my $tvalue = '';
   require B;
   my $sv = B::svref_2object(\$value);
   my $magic = ref($sv) eq 'B::PVMG' ? $sv->MAGIC : undef;
-  while ( $magic ) {
-    if ( $magic->TYPE eq 'V' ) {
+  while ($magic) {
+    if ($magic->TYPE eq 'V') {
       $tvalue = $magic->PTR;
       $tvalue =~ s/^v?(.+)$/v$1/;
       last;
@@ -105,7 +108,7 @@ sub _find_magic_vstring {
 
 # safe if given an unblessed reference
 sub _isa_version {
-  UNIVERSAL::isa( $_[0], 'UNIVERSAL' ) && $_[0]->isa('version')
+  UNIVERSAL::isa($_[0], 'UNIVERSAL') && $_[0]->isa('version');
 }
 
 sub _version_object {
@@ -116,29 +119,35 @@ sub _version_object {
   if (not defined $version or (!ref($version) && $version eq '0')) {
     return $V0;
   }
-  elsif ( ref($version) eq 'version' || ( ref($version) && _isa_version($version) ) ) {
+  elsif (ref($version) eq 'version' || (ref($version) && _isa_version($version)))
+  {
     $vobj = $version;
   }
   else {
     # hack around version::vpp not handling <3 character vstring literals
-    if ( $INC{'version/vpp.pm'} || $INC{'ExtUtils/MakeMaker/version/vpp.pm'} ) {
-      my $magic = _find_magic_vstring( $version );
+    if ($INC{'version/vpp.pm'} || $INC{'ExtUtils/MakeMaker/version/vpp.pm'}) {
+      my $magic = _find_magic_vstring($version);
       $version = $magic if length $magic;
     }
+
     # pad to 3 characters if before 5.8.1 and appears to be a v-string
-    if ( $] < 5.008001 && $version !~ /\A[0-9]/ && substr($version,0,1) ne 'v' && length($version) < 3 ) {
+    if ( $] < 5.008001
+      && $version !~ /\A[0-9]/
+      && substr($version, 0, 1) ne 'v'
+      && length($version) < 3)
+    {
       $version .= "\0" x (3 - length($version));
     }
     eval {
       local $SIG{__WARN__} = sub { die "Invalid version: $_[0]" };
+
       # avoid specific segfault on some older version.pm versions
       die "Invalid version: $version" if $version eq 'version';
       $vobj = version->new($version);
     };
-    if ( my $err = $@ ) {
+    if (my $err = $@) {
       my $hook = $self->{bad_version_hook};
-      $vobj = eval { $hook->($version, $module) }
-        if ref $hook eq 'CODE';
+      $vobj = eval { $hook->($version, $module) } if ref $hook eq 'CODE';
       unless (eval { $vobj->isa("version") }) {
         $err =~ s{ at .* line \d+.*$}{};
         die "Can't convert '$version': $err";
@@ -147,12 +156,12 @@ sub _version_object {
   }
 
   # ensure no leading '.'
-  if ( $vobj =~ m{\A\.} ) {
+  if ($vobj =~ m{\A\.}) {
     $vobj = version->new("0$vobj");
   }
 
   # ensure normal v-string form
-  if ( _is_qv($vobj) ) {
+  if (_is_qv($vobj)) {
     $vobj = version->new($vobj->normal);
   }
 
@@ -219,7 +228,7 @@ BEGIN {
     my $code = sub {
       my ($self, $name, $version) = @_;
 
-      $version = $self->_version_object( $name, $version );
+      $version = $self->_version_object($name, $version);
 
       $self->__modify_entry_for($name, $method, $version);
 
@@ -243,11 +252,11 @@ sub add_minimum {
     Carp::confess("can't add new requirements to finalized requirements")
       if $self->is_finalized;
 
-    $self->{requirements}{ $name } =
-      CPAN::Meta::Requirements::_Range::Range->with_minimum($V0, $name);
+    $self->{requirements}{$name}
+      CPAN::Meta::Requirements::_Range::Range->with_minimum($V0, $name);
   }
   else {
-    $version = $self->_version_object( $name, $version );
+    $version = $self->_version_object($name, $version);
 
     $self->__modify_entry_for($name, 'with_minimum', $version);
   }
@@ -274,7 +283,7 @@ sub add_requirements {
     for my $modifier (@$modifiers) {
       my ($method, @args) = @$modifier;
       $self->$method($module => @args);
-    };
+    }
   }
 
   return $self;
@@ -300,7 +309,7 @@ sub add_requirements {
 sub accepts_module {
   my ($self, $module, $version) = @_;
 
-  $version = $self->_version_object( $module, $version );
+  $version = $self->_version_object($module, $version);
 
   return 1 unless my $range = $self->__entry_for($module);
   return $range->_accepts($version);
@@ -324,7 +333,7 @@ sub clear_requirement {
   Carp::confess("can't clear requirements on finalized requirements")
     if $self->is_finalized;
 
-  delete $self->{requirements}{ $module };
+  delete $self->{requirements}{$module};
 
   return $self;
 }
@@ -374,7 +383,7 @@ sub structured_requirements_for_module {
 #pod
 #pod =cut
 
-sub required_modules { keys %{ $_[0]{requirements} } }
+sub required_modules { keys %{$_[0]{requirements}} }
 
 #pod =method clone
 #pod
@@ -392,7 +401,7 @@ sub clone {
   return $new->add_requirements($self);
 }
 
-sub __entry_for     { $_[0]{requirements}{ $_[1] } }
+sub __entry_for { $_[0]{requirements}{$_[1]} }
 
 sub __modify_entry_for {
   my ($self, $name, $method, $version) = @_;
@@ -404,12 +413,12 @@ sub __modify_entry_for {
     if $fin and not $old;
 
   my $new = ($old || 'CPAN::Meta::Requirements::_Range::Range')
-          ->$method($version, $name);
+    ->$method($version, $name);
 
   Carp::confess("can't modify finalized requirements")
     if $fin and $old->as_string ne $new->as_string;
 
-  $self->{requirements}{ $name } = $new;
+  $self->{requirements}{$name} = $new;
 }
 
 #pod =method is_simple
@@ -422,6 +431,7 @@ sub __modify_entry_for {
 sub is_simple {
   my ($self) = @_;
   for my $module ($self->required_modules) {
+
     # XXX: This is a complete hack, but also entirely correct.
     return if $self->__entry_for($module)->as_string =~ /\s/;
   }
@@ -488,8 +498,8 @@ sub finalize { $_[0]{finalized} = 1 }
 sub as_string_hash {
   my ($self) = @_;
 
-  my %hash = map {; $_ => $self->{requirements}{$_}->as_string }
-             $self->required_modules;
+  my %hash = map { ; $_ => $self->{requirements}{$_}->as_string }
+    $self->required_modules;
 
   return \%hash;
 }
@@ -530,23 +540,23 @@ sub as_string_hash {
 #pod =cut
 
 my %methods_for_op = (
-  '==' => [ qw(exact_version) ],
-  '!=' => [ qw(add_exclusion) ],
-  '>=' => [ qw(add_minimum)   ],
-  '<=' => [ qw(add_maximum)   ],
-  '>'  => [ qw(add_minimum add_exclusion) ],
-  '<'  => [ qw(add_maximum add_exclusion) ],
+  '==' => [qw(exact_version)],
+  '!=' => [qw(add_exclusion)],
+  '>=' => [qw(add_minimum)],
+  '<=' => [qw(add_maximum)],
+  '>'  => [qw(add_minimum add_exclusion)],
+  '<'  => [qw(add_maximum add_exclusion)],
 );
 
 sub add_string_requirement {
   my ($self, $module, $req) = @_;
 
-  unless ( defined $req && length $req ) {
+  unless (defined $req && length $req) {
     $req = 0;
     $self->_blank_carp($module);
   }
 
-  my $magic = _find_magic_vstring( $req );
+  my $magic = _find_magic_vstring($req);
   if (length $magic) {
     $self->add_minimum($module => $magic);
     return;
@@ -557,11 +567,12 @@ sub add_string_requirement {
   for my $part (@parts) {
     my ($op, $ver) = $part =~ m{\A\s*(==|>=|>|<=|<|!=)\s*(.*)\z};
 
-    if (! defined $op) {
+    if (!defined $op) {
       $self->add_minimum($module => $part);
-    } else {
+    }
+    else {
       Carp::confess("illegal requirement string: $req")
-        unless my $methods = $methods_for_op{ $op };
+        unless my $methods = $methods_for_op{$op};
 
       $self->$_($module => $ver) for @$methods;
     }
@@ -594,7 +605,7 @@ sub from_string_hash {
 
   for my $module (keys %$hash) {
     my $req = $hash->{$module};
-    unless ( defined $req && length $req ) {
+    unless (defined $req && length $req) {
       $req = 0;
       $class->_blank_carp($module);
     }
@@ -607,25 +618,25 @@ sub from_string_hash {
 ##############################################################
 
 {
-  package
-    CPAN::Meta::Requirements::_Range::Exact;
-  sub _new     { bless { version => $_[1] } => $_[0] }
+
+  package CPAN::Meta::Requirements::_Range::Exact;
+  sub _new { bless {version => $_[1]} => $_[0] }
 
   sub _accepts { return $_[0]{version} == $_[1] }
 
   sub as_string { return "== $_[0]{version}" }
 
-  sub as_struct { return [ [ '==', "$_[0]{version}" ] ] }
+  sub as_struct { return [['==', "$_[0]{version}"]] }
 
-  sub as_modifiers { return [ [ exact_version => $_[0]{version} ] ] }
+  sub as_modifiers { return [[exact_version => $_[0]{version}]] }
 
   sub _reject_requirements {
     my ($self, $module, $error) = @_;
-    Carp::confess("illegal requirements for $module: $error")
+    Carp::confess("illegal requirements for $module: $error");
   }
 
   sub _clone {
-    (ref $_[0])->_new( version->new( $_[0]{version} ) )
+    (ref $_[0])->_new(version->new($_[0]{version}));
   }
 
   sub with_exact_version {
@@ -645,8 +656,7 @@ sub from_string_hash {
     $module = 'module' unless defined $module;
 
     return $self->_clone if $self->{version} >= $minimum;
-    $self->_reject_requirements(
-      $module,
+    $self->_reject_requirements($module,
       "minimum $minimum exceeds exact specification $self->{version}",
     );
   }
@@ -656,8 +666,7 @@ sub from_string_hash {
     $module = 'module' unless defined $module;
 
     return $self->_clone if $self->{version} <= $maximum;
-    $self->_reject_requirements(
-      $module,
+    $self->_reject_requirements($module,
       "maximum $maximum below exact specification $self->{version}",
     );
   }
@@ -667,8 +676,7 @@ sub from_string_hash {
     $module = 'module' unless defined $module;
 
     return $self->_clone unless $exclusion == $self->{version};
-    $self->_reject_requirements(
-      $module,
+    $self->_reject_requirements($module,
       "tried to exclude $exclusion, which is already exactly specified",
     );
   }
@@ -677,22 +685,24 @@ sub from_string_hash {
 ##############################################################
 
 {
-  package
-    CPAN::Meta::Requirements::_Range::Range;
 
-  sub _self { ref($_[0]) ? $_[0] : (bless { } => $_[0]) }
+  package CPAN::Meta::Requirements::_Range::Range;
+
+  sub _self { ref($_[0]) ? $_[0] : (bless {} => $_[0]) }
 
   sub _clone {
-    return (bless { } => $_[0]) unless ref $_[0];
+    return (bless {} => $_[0]) unless ref $_[0];
 
     my ($s) = @_;
     my %guts = (
       (exists $s->{minimum} ? (minimum => version->new($s->{minimum})) : ()),
       (exists $s->{maximum} ? (maximum => version->new($s->{maximum})) : ()),
 
-      (exists $s->{exclusions}
-        ? (exclusions => [ map { version->new($_) } @{ $s->{exclusions} } ])
-        : ()),
+      (
+        exists $s->{exclusions}
+        ? (exclusions => [map { version->new($_) } @{$s->{exclusions}}])
+        : ()
+      ),
     );
 
     bless \%guts => ref($s);
@@ -701,38 +711,36 @@ sub from_string_hash {
   sub as_modifiers {
     my ($self) = @_;
     my @mods;
-    push @mods, [ add_minimum => $self->{minimum} ] if exists $self->{minimum};
-    push @mods, [ add_maximum => $self->{maximum} ] if exists $self->{maximum};
-    push @mods, map {; [ add_exclusion => $_ ] } @{$self->{exclusions} || []};
+    push @mods, [add_minimum => $self->{minimum}] if exists $self->{minimum};
+    push @mods, [add_maximum => $self->{maximum}] if exists $self->{maximum};
+    push @mods, map { ; [add_exclusion => $_] } @{$self->{exclusions} || []};
     return \@mods;
   }
 
   sub as_struct {
     my ($self) = @_;
 
-    return 0 if ! keys %$self;
+    return 0 if !keys %$self;
 
-    my @exclusions = @{ $self->{exclusions} || [] };
+    my @exclusions = @{$self->{exclusions} || []};
 
     my @parts;
 
-    for my $tuple (
-      [ qw( >= > minimum ) ],
-      [ qw( <= < maximum ) ],
-    ) {
+    for my $tuple ([qw( >= > minimum )], [qw( <= < maximum )],) {
       my ($op, $e_op, $k) = @$tuple;
       if (exists $self->{$k}) {
-        my @new_exclusions = grep { $_ != $self->{ $k } } @exclusions;
+        my @new_exclusions = grep { $_ != $self->{$k} } @exclusions;
         if (@new_exclusions == @exclusions) {
-          push @parts, [ $op, "$self->{ $k }" ];
-        } else {
-          push @parts, [ $e_op, "$self->{ $k }" ];
+          push @parts, [$op, "$self->{ $k }"];
+        }
+        else {
+          push @parts, [$e_op, "$self->{ $k }"];
           @exclusions = @new_exclusions;
         }
       }
     }
 
-    push @parts, map {; [ "!=", "$_" ] } @exclusions;
+    push @parts, map { ; ["!=", "$_"] } @exclusions;
 
     return \@parts;
   }
@@ -740,16 +748,16 @@ sub from_string_hash {
   sub as_string {
     my ($self) = @_;
 
-    my @parts = @{ $self->as_struct };
+    my @parts = @{$self->as_struct};
 
     return $parts[0][1] if @parts == 1 and $parts[0][0] eq '>=';
 
-    return join q{, }, map {; join q{ }, @$_ } @parts;
+    return join q{, }, map { ; join q{ }, @$_ } @parts;
   }
 
   sub _reject_requirements {
     my ($self, $module, $error) = @_;
-    Carp::confess("illegal requirements for $module: $error")
+    Carp::confess("illegal requirements for $module: $error");
   }
 
   sub with_exact_version {
@@ -758,10 +766,8 @@ sub from_string_hash {
     $self = $self->_clone;
 
     unless ($self->_accepts($version)) {
-      $self->_reject_requirements(
-        $module,
-        "exact specification $version outside of range " . $self->as_string
-      );
+      $self->_reject_requirements($module,
+        "exact specification $version outside of range " . $self->as_string);
     }
 
     return CPAN::Meta::Requirements::_Range::Exact->_new($version);
@@ -772,19 +778,17 @@ sub from_string_hash {
 
     if (defined $self->{minimum} and defined $self->{maximum}) {
       if ($self->{minimum} == $self->{maximum}) {
-        if (grep { $_ == $self->{minimum} } @{ $self->{exclusions} || [] }) {
-          $self->_reject_requirements(
-            $module,
+        if (grep { $_ == $self->{minimum} } @{$self->{exclusions} || []}) {
+          $self->_reject_requirements($module,
             "minimum and maximum are both $self->{minimum}, which is excluded",
           );
         }
 
-        return CPAN::Meta::Requirements::_Range::Exact->_new($self->{minimum})
+        return CPAN::Meta::Requirements::_Range::Exact->_new($self->{minimum});
       }
 
       if ($self->{minimum} > $self->{maximum}) {
-        $self->_reject_requirements(
-          $module,
+        $self->_reject_requirements($module,
           "minimum $self->{minimum} exceeds maximum $self->{maximum}",
         );
       }
@@ -793,13 +797,11 @@ sub from_string_hash {
     # eliminate irrelevant exclusions
     if ($self->{exclusions}) {
       my %seen;
-      @{ $self->{exclusions} } = grep {
-        (! defined $self->{minimum} or $_ >= $self->{minimum})
-        and
-        (! defined $self->{maximum} or $_ <= $self->{maximum})
-        and
-        ! $seen{$_}++
-      } @{ $self->{exclusions} };
+      @{$self->{exclusions}} = grep {
+              (!defined $self->{minimum} or $_ >= $self->{minimum})
+          and (!defined $self->{maximum} or $_ <= $self->{maximum})
+          and !$seen{$_}++
+      } @{$self->{exclusions}};
     }
 
     return $self;
@@ -810,9 +812,10 @@ sub from_string_hash {
     $module = 'module' unless defined $module;
     $self = $self->_clone;
 
-    if (defined (my $old_min = $self->{minimum})) {
+    if (defined(my $old_min = $self->{minimum})) {
       $self->{minimum} = (sort { $b cmp $a } ($minimum, $old_min))[0];
-    } else {
+    }
+    else {
       $self->{minimum} = $minimum;
     }
 
@@ -824,9 +827,10 @@ sub from_string_hash {
     $module = 'module' unless defined $module;
     $self = $self->_clone;
 
-    if (defined (my $old_max = $self->{maximum})) {
+    if (defined(my $old_max = $self->{maximum})) {
       $self->{maximum} = (sort { $a cmp $b } ($maximum, $old_max))[0];
-    } else {
+    }
+    else {
       $self->{maximum} = $maximum;
     }
 
@@ -838,7 +842,7 @@ sub from_string_hash {
     $module = 'module' unless defined $module;
     $self = $self->_clone;
 
-    push @{ $self->{exclusions} ||= [] }, $exclusion;
+    push @{$self->{exclusions} ||= []}, $exclusion;
 
     return $self->_simplify($module);
   }
@@ -848,14 +852,16 @@ sub from_string_hash {
 
     return if defined $self->{minimum} and $version < $self->{minimum};
     return if defined $self->{maximum} and $version > $self->{maximum};
-    return if defined $self->{exclusions}
-          and grep { $version == $_ } @{ $self->{exclusions} };
+    return
+      if defined $self->{exclusions}
+      and grep { $version == $_ } @{$self->{exclusions}};
 
     return 1;
   }
 }
 
 1;
+
 # vim: ts=2 sts=2 sw=2 et:
 
 __END__
index 9056940b51a26a9d91cb5537726d08b0bb4ec140..a069fb3e959fcc79d428e0ea1f8c42a5d840d18e 100644 (file)
@@ -6,6 +6,7 @@
 use 5.006;
 use strict;
 use warnings;
+
 package CPAN::Meta::Spec;
 
 our $VERSION = '2.150005';
index eddaa1073025ed5170f32fb100f4c3340a483ed2..db0ab513622d7cb0f16e2931af1536e6cdec06a1 100644 (file)
@@ -1,6 +1,7 @@
 use 5.006;
 use strict;
 use warnings;
+
 package CPAN::Meta::Validator;
 
 our $VERSION = '2.150005';
@@ -35,403 +36,413 @@ our $VERSION = '2.150005';
 #--------------------------------------------------------------------------#
 
 my %known_specs = (
-    '1.4' => 'http://module-build.sourceforge.net/META-spec-v1.4.html',
-    '1.3' => 'http://module-build.sourceforge.net/META-spec-v1.3.html',
-    '1.2' => 'http://module-build.sourceforge.net/META-spec-v1.2.html',
-    '1.1' => 'http://module-build.sourceforge.net/META-spec-v1.1.html',
-    '1.0' => 'http://module-build.sourceforge.net/META-spec-v1.0.html'
+  '1.4' => 'http://module-build.sourceforge.net/META-spec-v1.4.html',
+  '1.3' => 'http://module-build.sourceforge.net/META-spec-v1.3.html',
+  '1.2' => 'http://module-build.sourceforge.net/META-spec-v1.2.html',
+  '1.1' => 'http://module-build.sourceforge.net/META-spec-v1.1.html',
+  '1.0' => 'http://module-build.sourceforge.net/META-spec-v1.0.html'
 );
-my %known_urls = map {$known_specs{$_} => $_} keys %known_specs;
+my %known_urls = map { $known_specs{$_} => $_ } keys %known_specs;
 
-my $module_map1 = { 'map' => { ':key' => { name => \&module, value => \&exversion } } };
+my $module_map1
+  = {'map' => {':key' => {name => \&module, value => \&exversion}}};
 
-my $module_map2 = { 'map' => { ':key' => { name => \&module, value => \&version   } } };
+my $module_map2 = {'map' => {':key' => {name => \&module, value => \&version}}};
 
 my $no_index_2 = {
-    'map'       => { file       => { list => { value => \&string } },
-                     directory  => { list => { value => \&string } },
-                     'package'  => { list => { value => \&string } },
-                     namespace  => { list => { value => \&string } },
-                    ':key'      => { name => \&custom_2, value => \&anything },
-    }
+  'map' => {
+    file      => {list => {value => \&string}},
+    directory => {list => {value => \&string}},
+    'package' => {list => {value => \&string}},
+    namespace => {list => {value => \&string}},
+    ':key' => {name => \&custom_2, value => \&anything},
+  }
 };
 
 my $no_index_1_3 = {
-    'map'       => { file       => { list => { value => \&string } },
-                     directory  => { list => { value => \&string } },
-                     'package'  => { list => { value => \&string } },
-                     namespace  => { list => { value => \&string } },
-                     ':key'     => { name => \&string, value => \&anything },
-    }
+  'map' => {
+    file      => {list => {value => \&string}},
+    directory => {list => {value => \&string}},
+    'package' => {list => {value => \&string}},
+    namespace => {list => {value => \&string}},
+    ':key' => {name => \&string, value => \&anything},
+  }
 };
 
 my $no_index_1_2 = {
-    'map'       => { file       => { list => { value => \&string } },
-                     dir        => { list => { value => \&string } },
-                     'package'  => { list => { value => \&string } },
-                     namespace  => { list => { value => \&string } },
-                     ':key'     => { name => \&string, value => \&anything },
-    }
+  'map' => {
+    file      => {list => {value => \&string}},
+    dir       => {list => {value => \&string}},
+    'package' => {list => {value => \&string}},
+    namespace => {list => {value => \&string}},
+    ':key' => {name => \&string, value => \&anything},
+  }
 };
 
-my $no_index_1_1 = {
-    'map'       => { ':key'     => { name => \&string, list => { value => \&string } },
-    }
-};
+my $no_index_1_1
+  = {'map' => {':key' => {name => \&string, list => {value => \&string}},}};
 
 my $prereq_map = {
   map => {
     ':key' => {
-      name => \&phase,
-      'map' => {
-        ':key'  => {
-          name => \&relation,
-          %$module_map1,
-        },
-      },
+      name  => \&phase,
+      'map' => {':key' => {name => \&relation, %$module_map1,},},
     }
   },
 };
 
 my %definitions = (
   '2' => {
+
     # REQUIRED
-    'abstract'            => { mandatory => 1, value => \&string  },
-    'author'              => { mandatory => 1, list => { value => \&string } },
-    'dynamic_config'      => { mandatory => 1, value => \&boolean },
-    'generated_by'        => { mandatory => 1, value => \&string  },
-    'license'             => { mandatory => 1, list => { value => \&license } },
+    'abstract'       => {mandatory => 1, value => \&string},
+    'author'         => {mandatory => 1, list  => {value => \&string}},
+    'dynamic_config' => {mandatory => 1, value => \&boolean},
+    'generated_by'   => {mandatory => 1, value => \&string},
+    'license'        => {mandatory => 1, list  => {value => \&license}},
     'meta-spec' => {
       mandatory => 1,
-      'map' => {
-        version => { mandatory => 1, value => \&version},
-        url     => { value => \&url },
-        ':key' => { name => \&custom_2, value => \&anything },
+      'map'     => {
+        version => {mandatory => 1, value => \&version},
+        url     => {value     => \&url},
+        ':key'  => {name      => \&custom_2, value => \&anything},
       }
     },
-    'name'                => { mandatory => 1, value => \&string  },
-    'release_status'      => { mandatory => 1, value => \&release_status },
-    'version'             => { mandatory => 1, value => \&version },
+    'name'           => {mandatory => 1, value => \&string},
+    'release_status' => {mandatory => 1, value => \&release_status},
+    'version'        => {mandatory => 1, value => \&version},
 
     # OPTIONAL
-    'description' => { value => \&string },
-    'keywords'    => { list => { value => \&string } },
-    'no_index'    => $no_index_2,
-    'optional_features'   => {
-      'map'       => {
-        ':key'  => {
-          name => \&string,
-          'map'   => {
-            description        => { value => \&string },
-            prereqs => $prereq_map,
-            ':key' => { name => \&custom_2, value => \&anything },
+    'description' => {value => \&string},
+    'keywords'    => {list  => {value => \&string}},
+    'no_index'          => $no_index_2,
+    'optional_features' => {
+      'map' => {
+        ':key' => {
+          name  => \&string,
+          'map' => {
+            description => {value => \&string},
+            prereqs     => $prereq_map,
+            ':key'      => {name => \&custom_2, value => \&anything},
           }
         }
       }
     },
-    'prereqs' => $prereq_map,
-    'provides'    => {
-      'map'       => {
+    'prereqs'  => $prereq_map,
+    'provides' => {
+      'map' => {
         ':key' => {
           name  => \&module,
           'map' => {
-            file    => { mandatory => 1, value => \&file },
-            version => { value => \&version },
-            ':key' => { name => \&custom_2, value => \&anything },
+            file    => {mandatory => 1, value => \&file},
+            version => {value     => \&version},
+            ':key'  => {name      => \&custom_2, value => \&anything},
           }
         }
       }
     },
-    'resources'   => {
-      'map'       => {
-        license    => { list => { value => \&url } },
-        homepage   => { value => \&url },
+    'resources' => {
+      'map' => {
+        license    => {list  => {value => \&url}},
+        homepage   => {value => \&url},
         bugtracker => {
           'map' => {
-            web => { value => \&url },
-            mailto => { value => \&string},
-            ':key' => { name => \&custom_2, value => \&anything },
+            web    => {value => \&url},
+            mailto => {value => \&string},
+            ':key' => {name  => \&custom_2, value => \&anything},
           }
         },
         repository => {
           'map' => {
-            web => { value => \&url },
-            url => { value => \&url },
-            type => { value => \&string },
-            ':key' => { name => \&custom_2, value => \&anything },
+            web    => {value => \&url},
+            url    => {value => \&url},
+            type   => {value => \&string},
+            ':key' => {name  => \&custom_2, value => \&anything},
           }
         },
-        ':key'     => { value => \&string, name => \&custom_2 },
+        ':key' => {value => \&string, name => \&custom_2},
       }
     },
 
     # CUSTOM -- additional user defined key/value pairs
     # note we can only validate the key name, as the structure is user defined
-    ':key'        => { name => \&custom_2, value => \&anything },
+    ':key' => {name => \&custom_2, value => \&anything},
   },
 
-'1.4' => {
-  'meta-spec'           => {
-    mandatory => 1,
-    'map' => {
-      version => { mandatory => 1, value => \&version},
-      url     => { mandatory => 1, value => \&urlspec },
-      ':key'  => { name => \&string, value => \&anything },
+  '1.4' => {
+    'meta-spec' => {
+      mandatory => 1,
+      'map'     => {
+        version => {mandatory => 1,        value => \&version},
+        url     => {mandatory => 1,        value => \&urlspec},
+        ':key'  => {name      => \&string, value => \&anything},
+      },
     },
-  },
 
-  'name'                => { mandatory => 1, value => \&string  },
-  'version'             => { mandatory => 1, value => \&version },
-  'abstract'            => { mandatory => 1, value => \&string  },
-  'author'              => { mandatory => 1, list  => { value => \&string } },
-  'license'             => { mandatory => 1, value => \&license },
-  'generated_by'        => { mandatory => 1, value => \&string  },
-
-  'distribution_type'   => { value => \&string  },
-  'dynamic_config'      => { value => \&boolean },
-
-  'requires'            => $module_map1,
-  'recommends'          => $module_map1,
-  'build_requires'      => $module_map1,
-  'configure_requires'  => $module_map1,
-  'conflicts'           => $module_map2,
-
-  'optional_features'   => {
-    'map'       => {
-        ':key'  => { name => \&string,
-            'map'   => { description        => { value => \&string },
-                         requires           => $module_map1,
-                         recommends         => $module_map1,
-                         build_requires     => $module_map1,
-                         conflicts          => $module_map2,
-                         ':key'  => { name => \&string, value => \&anything },
-            }
+    'name'         => {mandatory => 1, value => \&string},
+    'version'      => {mandatory => 1, value => \&version},
+    'abstract'     => {mandatory => 1, value => \&string},
+    'author'       => {mandatory => 1, list  => {value => \&string}},
+    'license'      => {mandatory => 1, value => \&license},
+    'generated_by' => {mandatory => 1, value => \&string},
+
+    'distribution_type' => {value => \&string},
+    'dynamic_config'    => {value => \&boolean},
+
+    'requires'           => $module_map1,
+    'recommends'         => $module_map1,
+    'build_requires'     => $module_map1,
+    'configure_requires' => $module_map1,
+    'conflicts'          => $module_map2,
+
+    'optional_features' => {
+      'map' => {
+        ':key' => {
+          name  => \&string,
+          'map' => {
+            description    => {value => \&string},
+            requires       => $module_map1,
+            recommends     => $module_map1,
+            build_requires => $module_map1,
+            conflicts      => $module_map2,
+            ':key'         => {name => \&string, value => \&anything},
+          }
         }
-     }
-  },
+      }
+    },
 
-  'provides'    => {
-    'map'       => {
-      ':key' => { name  => \&module,
-        'map' => {
-          file    => { mandatory => 1, value => \&file },
-          version => { value => \&version },
-          ':key'  => { name => \&string, value => \&anything },
+    'provides' => {
+      'map' => {
+        ':key' => {
+          name  => \&module,
+          'map' => {
+            file    => {mandatory => 1, value => \&file},
+            version => {value     => \&version},
+            ':key'  => {name      => \&string, value => \&anything},
+          }
         }
       }
-    }
-  },
+    },
 
-  'no_index'    => $no_index_1_3,
-  'private'     => $no_index_1_3,
+    'no_index' => $no_index_1_3,
+    'private'  => $no_index_1_3,
 
-  'keywords'    => { list => { value => \&string } },
+    'keywords' => {list => {value => \&string}},
 
-  'resources'   => {
-    'map'       => { license    => { value => \&url },
-                     homepage   => { value => \&url },
-                     bugtracker => { value => \&url },
-                     repository => { value => \&url },
-                     ':key'     => { value => \&string, name => \&custom_1 },
-    }
+    'resources' => {
+      'map' => {
+        license    => {value => \&url},
+        homepage   => {value => \&url},
+        bugtracker => {value => \&url},
+        repository => {value => \&url},
+        ':key'     => {value => \&string, name => \&custom_1},
+      }
+    },
+
+    # additional user defined key/value pairs
+    # note we can only validate the key name, as the structure is user defined
+    ':key' => {name => \&string, value => \&anything},
   },
 
-  # additional user defined key/value pairs
-  # note we can only validate the key name, as the structure is user defined
-  ':key'        => { name => \&string, value => \&anything },
-},
-
-'1.3' => {
-  'meta-spec'           => {
-    mandatory => 1,
-    'map' => {
-      version => { mandatory => 1, value => \&version},
-      url     => { mandatory => 1, value => \&urlspec },
-      ':key'  => { name => \&string, value => \&anything },
+  '1.3' => {
+    'meta-spec' => {
+      mandatory => 1,
+      'map'     => {
+        version => {mandatory => 1,        value => \&version},
+        url     => {mandatory => 1,        value => \&urlspec},
+        ':key'  => {name      => \&string, value => \&anything},
+      },
     },
-  },
 
-  'name'                => { mandatory => 1, value => \&string  },
-  'version'             => { mandatory => 1, value => \&version },
-  'abstract'            => { mandatory => 1, value => \&string  },
-  'author'              => { mandatory => 1, list  => { value => \&string } },
-  'license'             => { mandatory => 1, value => \&license },
-  'generated_by'        => { mandatory => 1, value => \&string  },
-
-  'distribution_type'   => { value => \&string  },
-  'dynamic_config'      => { value => \&boolean },
-
-  'requires'            => $module_map1,
-  'recommends'          => $module_map1,
-  'build_requires'      => $module_map1,
-  'conflicts'           => $module_map2,
-
-  'optional_features'   => {
-    'map'       => {
-        ':key'  => { name => \&string,
-            'map'   => { description        => { value => \&string },
-                         requires           => $module_map1,
-                         recommends         => $module_map1,
-                         build_requires     => $module_map1,
-                         conflicts          => $module_map2,
-                         ':key'  => { name => \&string, value => \&anything },
-            }
+    'name'         => {mandatory => 1, value => \&string},
+    'version'      => {mandatory => 1, value => \&version},
+    'abstract'     => {mandatory => 1, value => \&string},
+    'author'       => {mandatory => 1, list  => {value => \&string}},
+    'license'      => {mandatory => 1, value => \&license},
+    'generated_by' => {mandatory => 1, value => \&string},
+
+    'distribution_type' => {value => \&string},
+    'dynamic_config'    => {value => \&boolean},
+
+    'requires'       => $module_map1,
+    'recommends'     => $module_map1,
+    'build_requires' => $module_map1,
+    'conflicts'      => $module_map2,
+
+    'optional_features' => {
+      'map' => {
+        ':key' => {
+          name  => \&string,
+          'map' => {
+            description    => {value => \&string},
+            requires       => $module_map1,
+            recommends     => $module_map1,
+            build_requires => $module_map1,
+            conflicts      => $module_map2,
+            ':key'         => {name => \&string, value => \&anything},
+          }
         }
-     }
-  },
+      }
+    },
 
-  'provides'    => {
-    'map'       => {
-      ':key' => { name  => \&module,
-        'map' => {
-          file    => { mandatory => 1, value => \&file },
-          version => { value => \&version },
-          ':key'  => { name => \&string, value => \&anything },
+    'provides' => {
+      'map' => {
+        ':key' => {
+          name  => \&module,
+          'map' => {
+            file    => {mandatory => 1, value => \&file},
+            version => {value     => \&version},
+            ':key'  => {name      => \&string, value => \&anything},
+          }
         }
       }
-    }
-  },
+    },
 
 
-  'no_index'    => $no_index_1_3,
-  'private'     => $no_index_1_3,
+    'no_index' => $no_index_1_3,
+    'private'  => $no_index_1_3,
 
-  'keywords'    => { list => { value => \&string } },
+    'keywords' => {list => {value => \&string}},
 
-  'resources'   => {
-    'map'       => { license    => { value => \&url },
-                     homepage   => { value => \&url },
-                     bugtracker => { value => \&url },
-                     repository => { value => \&url },
-                     ':key'     => { value => \&string, name => \&custom_1 },
-    }
-  },
+    'resources' => {
+      'map' => {
+        license    => {value => \&url},
+        homepage   => {value => \&url},
+        bugtracker => {value => \&url},
+        repository => {value => \&url},
+        ':key'     => {value => \&string, name => \&custom_1},
+      }
+    },
 
-  # additional user defined key/value pairs
-  # note we can only validate the key name, as the structure is user defined
-  ':key'        => { name => \&string, value => \&anything },
-},
+    # additional user defined key/value pairs
+    # note we can only validate the key name, as the structure is user defined
+    ':key' => {name => \&string, value => \&anything},
+  },
 
 # v1.2 is misleading, it seems to assume that a number of fields where created
 # within v1.1, when they were created within v1.2. This may have been an
 # original mistake, and that a v1.1 was retro fitted into the timeline, when
 # v1.2 was originally slated as v1.1. But I could be wrong ;)
-'1.2' => {
-  'meta-spec'           => {
-    mandatory => 1,
-    'map' => {
-      version => { mandatory => 1, value => \&version},
-      url     => { mandatory => 1, value => \&urlspec },
-      ':key'  => { name => \&string, value => \&anything },
+  '1.2' => {
+    'meta-spec' => {
+      mandatory => 1,
+      'map'     => {
+        version => {mandatory => 1,        value => \&version},
+        url     => {mandatory => 1,        value => \&urlspec},
+        ':key'  => {name      => \&string, value => \&anything},
+      },
     },
-  },
 
 
-  'name'                => { mandatory => 1, value => \&string  },
-  'version'             => { mandatory => 1, value => \&version },
-  'license'             => { mandatory => 1, value => \&license },
-  'generated_by'        => { mandatory => 1, value => \&string  },
-  'author'              => { mandatory => 1, list => { value => \&string } },
-  'abstract'            => { mandatory => 1, value => \&string  },
-
-  'distribution_type'   => { value => \&string  },
-  'dynamic_config'      => { value => \&boolean },
-
-  'keywords'            => { list => { value => \&string } },
-
-  'private'             => $no_index_1_2,
-  '$no_index'           => $no_index_1_2,
-
-  'requires'            => $module_map1,
-  'recommends'          => $module_map1,
-  'build_requires'      => $module_map1,
-  'conflicts'           => $module_map2,
-
-  'optional_features'   => {
-    'map'       => {
-        ':key'  => { name => \&string,
-            'map'   => { description        => { value => \&string },
-                         requires           => $module_map1,
-                         recommends         => $module_map1,
-                         build_requires     => $module_map1,
-                         conflicts          => $module_map2,
-                         ':key'  => { name => \&string, value => \&anything },
-            }
+    'name'         => {mandatory => 1, value => \&string},
+    'version'      => {mandatory => 1, value => \&version},
+    'license'      => {mandatory => 1, value => \&license},
+    'generated_by' => {mandatory => 1, value => \&string},
+    'author'       => {mandatory => 1, list  => {value => \&string}},
+    'abstract'     => {mandatory => 1, value => \&string},
+
+    'distribution_type' => {value => \&string},
+    'dynamic_config'    => {value => \&boolean},
+
+    'keywords' => {list => {value => \&string}},
+
+    'private'   => $no_index_1_2,
+    '$no_index' => $no_index_1_2,
+
+    'requires'       => $module_map1,
+    'recommends'     => $module_map1,
+    'build_requires' => $module_map1,
+    'conflicts'      => $module_map2,
+
+    'optional_features' => {
+      'map' => {
+        ':key' => {
+          name  => \&string,
+          'map' => {
+            description    => {value => \&string},
+            requires       => $module_map1,
+            recommends     => $module_map1,
+            build_requires => $module_map1,
+            conflicts      => $module_map2,
+            ':key'         => {name => \&string, value => \&anything},
+          }
         }
-     }
-  },
+      }
+    },
 
-  'provides'    => {
-    'map'       => {
-      ':key' => { name  => \&module,
-        'map' => {
-          file    => { mandatory => 1, value => \&file },
-          version => { value => \&version },
-          ':key'  => { name => \&string, value => \&anything },
+    'provides' => {
+      'map' => {
+        ':key' => {
+          name  => \&module,
+          'map' => {
+            file    => {mandatory => 1, value => \&file},
+            version => {value     => \&version},
+            ':key'  => {name      => \&string, value => \&anything},
+          }
         }
       }
-    }
-  },
+    },
 
-  'resources'   => {
-    'map'       => { license    => { value => \&url },
-                     homepage   => { value => \&url },
-                     bugtracker => { value => \&url },
-                     repository => { value => \&url },
-                     ':key'     => { value => \&string, name => \&custom_1 },
-    }
-  },
+    'resources' => {
+      'map' => {
+        license    => {value => \&url},
+        homepage   => {value => \&url},
+        bugtracker => {value => \&url},
+        repository => {value => \&url},
+        ':key'     => {value => \&string, name => \&custom_1},
+      }
+    },
 
-  # additional user defined key/value pairs
-  # note we can only validate the key name, as the structure is user defined
-  ':key'        => { name => \&string, value => \&anything },
-},
+    # additional user defined key/value pairs
+    # note we can only validate the key name, as the structure is user defined
+    ':key' => {name => \&string, value => \&anything},
+  },
 
 # note that the 1.1 spec only specifies 'version' as mandatory
-'1.1' => {
-  'name'                => { value => \&string  },
-  'version'             => { mandatory => 1, value => \&version },
-  'license'             => { value => \&license },
-  'generated_by'        => { value => \&string  },
+  '1.1' => {
+    'name'         => {value     => \&string},
+    'version'      => {mandatory => 1, value => \&version},
+    'license'      => {value     => \&license},
+    'generated_by' => {value     => \&string},
 
-  'license_uri'         => { value => \&url },
-  'distribution_type'   => { value => \&string  },
-  'dynamic_config'      => { value => \&boolean },
+    'license_uri'       => {value => \&url},
+    'distribution_type' => {value => \&string},
+    'dynamic_config'    => {value => \&boolean},
 
-  'private'             => $no_index_1_1,
+    'private' => $no_index_1_1,
 
-  'requires'            => $module_map1,
-  'recommends'          => $module_map1,
-  'build_requires'      => $module_map1,
-  'conflicts'           => $module_map2,
+    'requires'       => $module_map1,
+    'recommends'     => $module_map1,
+    'build_requires' => $module_map1,
+    'conflicts'      => $module_map2,
 
-  # additional user defined key/value pairs
-  # note we can only validate the key name, as the structure is user defined
-  ':key'        => { name => \&string, value => \&anything },
-},
+    # additional user defined key/value pairs
+    # note we can only validate the key name, as the structure is user defined
+    ':key' => {name => \&string, value => \&anything},
+  },
 
 # note that the 1.0 spec doesn't specify optional or mandatory fields
 # but we will treat version as mandatory since otherwise META 1.0 is
 # completely arbitrary and pointless
-'1.0' => {
-  'name'                => { value => \&string  },
-  'version'             => { mandatory => 1, value => \&version },
-  'license'             => { value => \&license },
-  'generated_by'        => { value => \&string  },
-
-  'license_uri'         => { value => \&url },
-  'distribution_type'   => { value => \&string  },
-  'dynamic_config'      => { value => \&boolean },
-
-  'requires'            => $module_map1,
-  'recommends'          => $module_map1,
-  'build_requires'      => $module_map1,
-  'conflicts'           => $module_map2,
-
-  # additional user defined key/value pairs
-  # note we can only validate the key name, as the structure is user defined
-  ':key'        => { name => \&string, value => \&anything },
-},
+  '1.0' => {
+    'name'         => {value     => \&string},
+    'version'      => {mandatory => 1, value => \&version},
+    'license'      => {value     => \&license},
+    'generated_by' => {value     => \&string},
+
+    'license_uri'       => {value => \&url},
+    'distribution_type' => {value => \&string},
+    'dynamic_config'    => {value => \&boolean},
+
+    'requires'       => $module_map1,
+    'recommends'     => $module_map1,
+    'build_requires' => $module_map1,
+    'conflicts'      => $module_map2,
+
+    # additional user defined key/value pairs
+    # note we can only validate the key name, as the structure is user defined
+    ':key' => {name => \&string, value => \&anything},
+  },
 );
 
 #--------------------------------------------------------------------------#
@@ -447,13 +458,13 @@ my %definitions = (
 #pod =cut
 
 sub new {
-  my ($class,$data) = @_;
+  my ($class, $data) = @_;
 
   # create an attributes hash
   my $self = {
-    'data'    => $data,
-    'spec'    => eval { $data->{'meta-spec'}{'version'} } || "1.0",
-    'errors'  => undef,
+    'data'   => $data,
+    'spec'   => eval { $data->{'meta-spec'}{'version'} } || "1.0",
+    'errors' => undef,
   };
 
   # create the object
@@ -472,11 +483,11 @@ sub new {
 #pod =cut
 
 sub is_valid {
-    my $self = shift;
-    my $data = $self->{data};
-    my $spec_version = $self->{spec};
-    $self->check_map($definitions{$spec_version},$data);
-    return ! $self->errors;
+  my $self         = shift;
+  my $data         = $self->{data};
+  my $spec_version = $self->{spec};
+  $self->check_map($definitions{$spec_version}, $data);
+  return !$self->errors;
 }
 
 #pod =method errors
@@ -488,9 +499,9 @@ sub is_valid {
 #pod =cut
 
 sub errors {
-    my $self = shift;
-    return ()   unless(defined $self->{errors});
-    return @{$self->{errors}};
+  my $self = shift;
+  return () unless (defined $self->{errors});
+  return @{$self->{errors}};
 }
 
 #pod =begin :internals
@@ -523,88 +534,100 @@ my $spec_error = "Missing validation action in specification. "
   . "Must be one of 'map', 'list', or 'value'";
 
 sub check_map {
-    my ($self,$spec,$data) = @_;
+  my ($self, $spec, $data) = @_;
 
-    if(ref($spec) ne 'HASH') {
-        $self->_error( "Unknown META specification, cannot validate." );
-        return;
-    }
+  if (ref($spec) ne 'HASH') {
+    $self->_error("Unknown META specification, cannot validate.");
+    return;
+  }
 
-    if(ref($data) ne 'HASH') {
-        $self->_error( "Expected a map structure from string or file." );
-        return;
-    }
+  if (ref($data) ne 'HASH') {
+    $self->_error("Expected a map structure from string or file.");
+    return;
+  }
+
+  for my $key (keys %$spec) {
+    next unless ($spec->{$key}->{mandatory});
+    next if (defined $data->{$key});
+    push @{$self->{stack}}, $key;
+    $self->_error("Missing mandatory field, '$key'");
+    pop @{$self->{stack}};
+  }
+
+  for my $key (keys %$data) {
+    push @{$self->{stack}}, $key;
+    if ($spec->{$key}) {
+      if ($spec->{$key}{value}) {
+        $spec->{$key}{value}->($self, $key, $data->{$key});
+      }
+      elsif ($spec->{$key}{'map'}) {
+        $self->check_map($spec->{$key}{'map'}, $data->{$key});
+      }
+      elsif ($spec->{$key}{'list'}) {
+        $self->check_list($spec->{$key}{'list'}, $data->{$key});
+      }
+      else {
+        $self->_error("$spec_error for '$key'");
+      }
 
-    for my $key (keys %$spec) {
-        next    unless($spec->{$key}->{mandatory});
-        next    if(defined $data->{$key});
-        push @{$self->{stack}}, $key;
-        $self->_error( "Missing mandatory field, '$key'" );
-        pop @{$self->{stack}};
     }
+    elsif ($spec->{':key'}) {
+      $spec->{':key'}{name}->($self, $key, $key);
+      if ($spec->{':key'}{value}) {
+        $spec->{':key'}{value}->($self, $key, $data->{$key});
+      }
+      elsif ($spec->{':key'}{'map'}) {
+        $self->check_map($spec->{':key'}{'map'}, $data->{$key});
+      }
+      elsif ($spec->{':key'}{'list'}) {
+        $self->check_list($spec->{':key'}{'list'}, $data->{$key});
+      }
+      else {
+        $self->_error("$spec_error for ':key'");
+      }
+
 
-    for my $key (keys %$data) {
-        push @{$self->{stack}}, $key;
-        if($spec->{$key}) {
-            if($spec->{$key}{value}) {
-                $spec->{$key}{value}->($self,$key,$data->{$key});
-            } elsif($spec->{$key}{'map'}) {
-                $self->check_map($spec->{$key}{'map'},$data->{$key});
-            } elsif($spec->{$key}{'list'}) {
-                $self->check_list($spec->{$key}{'list'},$data->{$key});
-            } else {
-                $self->_error( "$spec_error for '$key'" );
-            }
-
-        } elsif ($spec->{':key'}) {
-            $spec->{':key'}{name}->($self,$key,$key);
-            if($spec->{':key'}{value}) {
-                $spec->{':key'}{value}->($self,$key,$data->{$key});
-            } elsif($spec->{':key'}{'map'}) {
-                $self->check_map($spec->{':key'}{'map'},$data->{$key});
-            } elsif($spec->{':key'}{'list'}) {
-                $self->check_list($spec->{':key'}{'list'},$data->{$key});
-            } else {
-                $self->_error( "$spec_error for ':key'" );
-            }
-
-
-        } else {
-            $self->_error( "Unknown key, '$key', found in map structure" );
-        }
-        pop @{$self->{stack}};
     }
+    else {
+      $self->_error("Unknown key, '$key', found in map structure");
+    }
+    pop @{$self->{stack}};
+  }
 }
 
 sub check_list {
-    my ($self,$spec,$data) = @_;
+  my ($self, $spec, $data) = @_;
 
-    if(ref($data) ne 'ARRAY') {
-        $self->_error( "Expected a list structure" );
-        return;
-    }
+  if (ref($data) ne 'ARRAY') {
+    $self->_error("Expected a list structure");
+    return;
+  }
 
-    if(defined $spec->{mandatory}) {
-        if(!defined $data->[0]) {
-            $self->_error( "Missing entries from mandatory list" );
-        }
+  if (defined $spec->{mandatory}) {
+    if (!defined $data->[0]) {
+      $self->_error("Missing entries from mandatory list");
     }
+  }
 
-    for my $value (@$data) {
-        push @{$self->{stack}}, $value || "<undef>";
-        if(defined $spec->{value}) {
-            $spec->{value}->($self,'list',$value);
-        } elsif(defined $spec->{'map'}) {
-            $self->check_map($spec->{'map'},$value);
-        } elsif(defined $spec->{'list'}) {
-            $self->check_list($spec->{'list'},$value);
-        } elsif ($spec->{':key'}) {
-            $self->check_map($spec,$value);
-        } else {
-          $self->_error( "$spec_error associated with '$self->{stack}[-2]'" );
-        }
-        pop @{$self->{stack}};
+  for my $value (@$data) {
+    push @{$self->{stack}}, $value || "<undef>";
+    if (defined $spec->{value}) {
+      $spec->{value}->($self, 'list', $value);
+    }
+    elsif (defined $spec->{'map'}) {
+      $self->check_map($spec->{'map'}, $value);
+    }
+    elsif (defined $spec->{'list'}) {
+      $self->check_list($spec->{'list'}, $value);
     }
+    elsif ($spec->{':key'}) {
+      $self->check_map($spec, $value);
+    }
+    else {
+      $self->_error("$spec_error associated with '$self->{stack}[-2]'");
+    }
+    pop @{$self->{stack}};
+  }
 }
 
 #pod =head2 Validator Methods
@@ -716,146 +739,150 @@ sub check_list {
 #pod =cut
 
 sub header {
-    my ($self,$key,$value) = @_;
-    if(defined $value) {
-        return 1    if($value && $value =~ /^--- #YAML:1.0/);
-    }
-    $self->_error( "file does not have a valid YAML header." );
-    return 0;
+  my ($self, $key, $value) = @_;
+  if (defined $value) {
+    return 1 if ($value && $value =~ /^--- #YAML:1.0/);
+  }
+  $self->_error("file does not have a valid YAML header.");
+  return 0;
 }
 
 sub release_status {
-  my ($self,$key,$value) = @_;
-  if(defined $value) {
+  my ($self, $key, $value) = @_;
+  if (defined $value) {
     my $version = $self->{data}{version} || '';
-    if ( $version =~ /_/ ) {
-      return 1 if ( $value =~ /\A(?:testing|unstable)\z/ );
-      $self->_error( "'$value' for '$key' is invalid for version '$version'" );
+    if ($version =~ /_/) {
+      return 1 if ($value =~ /\A(?:testing|unstable)\z/);
+      $self->_error("'$value' for '$key' is invalid for version '$version'");
     }
     else {
-      return 1 if ( $value =~ /\A(?:stable|testing|unstable)\z/ );
-      $self->_error( "'$value' for '$key' is invalid" );
+      return 1 if ($value =~ /\A(?:stable|testing|unstable)\z/);
+      $self->_error("'$value' for '$key' is invalid");
     }
   }
   else {
-    $self->_error( "'$key' is not defined" );
+    $self->_error("'$key' is not defined");
   }
   return 0;
 }
 
 # _uri_split taken from URI::Split by Gisle Aas, Copyright 2003
 sub _uri_split {
-     return $_[0] =~ m,(?:([^:/?#]+):)?(?://([^/?#]*))?([^?#]*)(?:\?([^#]*))?(?:#(.*))?,;
+  return $_[0]
+    =~ m,(?:([^:/?#]+):)?(?://([^/?#]*))?([^?#]*)(?:\?([^#]*))?(?:#(.*))?,;
 }
 
 sub url {
-    my ($self,$key,$value) = @_;
-    if(defined $value) {
-      my ($scheme, $auth, $path, $query, $frag) = _uri_split($value);
-      unless ( defined $scheme && length $scheme ) {
-        $self->_error( "'$value' for '$key' does not have a URL scheme" );
-        return 0;
-      }
-      unless ( defined $auth && length $auth ) {
-        $self->_error( "'$value' for '$key' does not have a URL authority" );
-        return 0;
-      }
-      return 1;
+  my ($self, $key, $value) = @_;
+  if (defined $value) {
+    my ($scheme, $auth, $path, $query, $frag) = _uri_split($value);
+    unless (defined $scheme && length $scheme) {
+      $self->_error("'$value' for '$key' does not have a URL scheme");
+      return 0;
+    }
+    unless (defined $auth && length $auth) {
+      $self->_error("'$value' for '$key' does not have a URL authority");
+      return 0;
     }
-    $value ||= '';
-    $self->_error( "'$value' for '$key' is not a valid URL." );
-    return 0;
+    return 1;
+  }
+  $value ||= '';
+  $self->_error("'$value' for '$key' is not a valid URL.");
+  return 0;
 }
 
 sub urlspec {
-    my ($self,$key,$value) = @_;
-    if(defined $value) {
-        return 1    if($value && $known_specs{$self->{spec}} eq $value);
-        if($value && $known_urls{$value}) {
-            $self->_error( 'META specification URL does not match version' );
-            return 0;
-        }
+  my ($self, $key, $value) = @_;
+  if (defined $value) {
+    return 1 if ($value && $known_specs{$self->{spec}} eq $value);
+    if ($value && $known_urls{$value}) {
+      $self->_error('META specification URL does not match version');
+      return 0;
     }
-    $self->_error( 'Unknown META specification' );
-    return 0;
+  }
+  $self->_error('Unknown META specification');
+  return 0;
 }
 
 sub anything { return 1 }
 
 sub string {
-    my ($self,$key,$value) = @_;
-    if(defined $value) {
-        return 1    if($value || $value =~ /^0$/);
-    }
-    $self->_error( "value is an undefined string" );
-    return 0;
+  my ($self, $key, $value) = @_;
+  if (defined $value) {
+    return 1 if ($value || $value =~ /^0$/);
+  }
+  $self->_error("value is an undefined string");
+  return 0;
 }
 
 sub string_or_undef {
-    my ($self,$key,$value) = @_;
-    return 1    unless(defined $value);
-    return 1    if($value || $value =~ /^0$/);
-    $self->_error( "No string defined for '$key'" );
-    return 0;
+  my ($self, $key, $value) = @_;
+  return 1 unless (defined $value);
+  return 1 if ($value || $value =~ /^0$/);
+  $self->_error("No string defined for '$key'");
+  return 0;
 }
 
 sub file {
-    my ($self,$key,$value) = @_;
-    return 1    if(defined $value);
-    $self->_error( "No file defined for '$key'" );
-    return 0;
+  my ($self, $key, $value) = @_;
+  return 1 if (defined $value);
+  $self->_error("No file defined for '$key'");
+  return 0;
 }
 
 sub exversion {
-    my ($self,$key,$value) = @_;
-    if(defined $value && ($value || $value =~ /0/)) {
-        my $pass = 1;
-        for(split(",",$value)) { $self->version($key,$_) or ($pass = 0); }
-        return $pass;
-    }
-    $value = '<undef>'  unless(defined $value);
-    $self->_error( "'$value' for '$key' is not a valid version." );
-    return 0;
+  my ($self, $key, $value) = @_;
+  if (defined $value && ($value || $value =~ /0/)) {
+    my $pass = 1;
+    for (split(",", $value)) { $self->version($key, $_) or ($pass = 0); }
+    return $pass;
+  }
+  $value = '<undef>' unless (defined $value);
+  $self->_error("'$value' for '$key' is not a valid version.");
+  return 0;
 }
 
 sub version {
-    my ($self,$key,$value) = @_;
-    if(defined $value) {
-        return 0    unless($value || $value =~ /0/);
-        return 1    if($value =~ /^\s*((<|<=|>=|>|!=|==)\s*)?v?\d+((\.\d+((_|\.)\d+)?)?)/);
-    } else {
-        $value = '<undef>';
-    }
-    $self->_error( "'$value' for '$key' is not a valid version." );
-    return 0;
+  my ($self, $key, $value) = @_;
+  if (defined $value) {
+    return 0 unless ($value || $value =~ /0/);
+    return 1
+      if ($value =~ /^\s*((<|<=|>=|>|!=|==)\s*)?v?\d+((\.\d+((_|\.)\d+)?)?)/);
+  }
+  else {
+    $value = '<undef>';
+  }
+  $self->_error("'$value' for '$key' is not a valid version.");
+  return 0;
 }
 
 sub boolean {
-    my ($self,$key,$value) = @_;
-    if(defined $value) {
-        return 1    if($value =~ /^(0|1|true|false)$/);
-    } else {
-        $value = '<undef>';
-    }
-    $self->_error( "'$value' for '$key' is not a boolean value." );
-    return 0;
+  my ($self, $key, $value) = @_;
+  if (defined $value) {
+    return 1 if ($value =~ /^(0|1|true|false)$/);
+  }
+  else {
+    $value = '<undef>';
+  }
+  $self->_error("'$value' for '$key' is not a boolean value.");
+  return 0;
 }
 
 my %v1_licenses = (
-    'perl'         => 'http://dev.perl.org/licenses/',
-    'gpl'          => 'http://www.opensource.org/licenses/gpl-license.php',
-    'apache'       => 'http://apache.org/licenses/LICENSE-2.0',
-    'artistic'     => 'http://opensource.org/licenses/artistic-license.php',
-    'artistic_2'   => 'http://opensource.org/licenses/artistic-license-2.0.php',
-    'lgpl'         => 'http://www.opensource.org/licenses/lgpl-license.php',
-    'bsd'          => 'http://www.opensource.org/licenses/bsd-license.php',
-    'gpl'          => 'http://www.opensource.org/licenses/gpl-license.php',
-    'mit'          => 'http://opensource.org/licenses/mit-license.php',
-    'mozilla'      => 'http://opensource.org/licenses/mozilla1.1.php',
-    'open_source'  => undef,
-    'unrestricted' => undef,
-    'restrictive'  => undef,
-    'unknown'      => undef,
+  'perl'         => 'http://dev.perl.org/licenses/',
+  'gpl'          => 'http://www.opensource.org/licenses/gpl-license.php',
+  'apache'       => 'http://apache.org/licenses/LICENSE-2.0',
+  'artistic'     => 'http://opensource.org/licenses/artistic-license.php',
+  'artistic_2'   => 'http://opensource.org/licenses/artistic-license-2.0.php',
+  'lgpl'         => 'http://www.opensource.org/licenses/lgpl-license.php',
+  'bsd'          => 'http://www.opensource.org/licenses/bsd-license.php',
+  'gpl'          => 'http://www.opensource.org/licenses/gpl-license.php',
+  'mit'          => 'http://opensource.org/licenses/mit-license.php',
+  'mozilla'      => 'http://opensource.org/licenses/mozilla1.1.php',
+  'open_source'  => undef,
+  'unrestricted' => undef,
+  'restrictive'  => undef,
+  'unknown'      => undef,
 );
 
 my %v2_licenses = map { $_ => 1 } qw(
@@ -889,97 +916,107 @@ my %v2_licenses = map { $_ => 1 } qw(
 );
 
 sub license {
-    my ($self,$key,$value) = @_;
-    my $licenses = $self->{spec} < 2 ? \%v1_licenses : \%v2_licenses;
-    if(defined $value) {
-        return 1    if($value && exists $licenses->{$value});
-    } else {
-        $value = '<undef>';
-    }
-    $self->_error( "License '$value' is invalid" );
-    return 0;
+  my ($self, $key, $value) = @_;
+  my $licenses = $self->{spec} < 2 ? \%v1_licenses : \%v2_licenses;
+  if (defined $value) {
+    return 1 if ($value && exists $licenses->{$value});
+  }
+  else {
+    $value = '<undef>';
+  }
+  $self->_error("License '$value' is invalid");
+  return 0;
 }
 
 sub custom_1 {
-    my ($self,$key) = @_;
-    if(defined $key) {
-        # a valid user defined key should be alphabetic
-        # and contain at least one capital case letter.
-        return 1    if($key && $key =~ /^[_a-z]+$/i && $key =~ /[A-Z]/);
-    } else {
-        $key = '<undef>';
-    }
-    $self->_error( "Custom resource '$key' must be in CamelCase." );
-    return 0;
+  my ($self, $key) = @_;
+  if (defined $key) {
+
+    # a valid user defined key should be alphabetic
+    # and contain at least one capital case letter.
+    return 1 if ($key && $key =~ /^[_a-z]+$/i && $key =~ /[A-Z]/);
+  }
+  else {
+    $key = '<undef>';
+  }
+  $self->_error("Custom resource '$key' must be in CamelCase.");
+  return 0;
 }
 
 sub custom_2 {
-    my ($self,$key) = @_;
-    if(defined $key) {
-        return 1    if($key && $key =~ /^x_/i);  # user defined
-    } else {
-        $key = '<undef>';
-    }
-    $self->_error( "Custom key '$key' must begin with 'x_' or 'X_'." );
-    return 0;
+  my ($self, $key) = @_;
+  if (defined $key) {
+    return 1 if ($key && $key =~ /^x_/i);    # user defined
+  }
+  else {
+    $key = '<undef>';
+  }
+  $self->_error("Custom key '$key' must begin with 'x_' or 'X_'.");
+  return 0;
 }
 
 sub identifier {
-    my ($self,$key) = @_;
-    if(defined $key) {
-        return 1    if($key && $key =~ /^([a-z][_a-z]+)$/i);    # spec 2.0 defined
-    } else {
-        $key = '<undef>';
-    }
-    $self->_error( "Key '$key' is not a legal identifier." );
-    return 0;
+  my ($self, $key) = @_;
+  if (defined $key) {
+    return 1 if ($key && $key =~ /^([a-z][_a-z]+)$/i);    # spec 2.0 defined
+  }
+  else {
+    $key = '<undef>';
+  }
+  $self->_error("Key '$key' is not a legal identifier.");
+  return 0;
 }
 
 sub module {
-    my ($self,$key) = @_;
-    if(defined $key) {
-        return 1    if($key && $key =~ /^[A-Za-z0-9_]+(::[A-Za-z0-9_]+)*$/);
-    } else {
-        $key = '<undef>';
-    }
-    $self->_error( "Key '$key' is not a legal module name." );
-    return 0;
+  my ($self, $key) = @_;
+  if (defined $key) {
+    return 1 if ($key && $key =~ /^[A-Za-z0-9_]+(::[A-Za-z0-9_]+)*$/);
+  }
+  else {
+    $key = '<undef>';
+  }
+  $self->_error("Key '$key' is not a legal module name.");
+  return 0;
 }
 
 my @valid_phases = qw/ configure build test runtime develop /;
+
 sub phase {
-    my ($self,$key) = @_;
-    if(defined $key) {
-        return 1 if( length $key && grep { $key eq $_ } @valid_phases );
-        return 1 if $key =~ /x_/i;
-    } else {
-        $key = '<undef>';
-    }
-    $self->_error( "Key '$key' is not a legal phase." );
-    return 0;
+  my ($self, $key) = @_;
+  if (defined $key) {
+    return 1 if (length $key && grep { $key eq $_ } @valid_phases);
+    return 1 if $key =~ /x_/i;
+  }
+  else {
+    $key = '<undef>';
+  }
+  $self->_error("Key '$key' is not a legal phase.");
+  return 0;
 }
 
 my @valid_relations = qw/ requires recommends suggests conflicts /;
+
 sub relation {
-    my ($self,$key) = @_;
-    if(defined $key) {
-        return 1 if( length $key && grep { $key eq $_ } @valid_relations );
-        return 1 if $key =~ /x_/i;
-    } else {
-        $key = '<undef>';
-    }
-    $self->_error( "Key '$key' is not a legal prereq relationship." );
-    return 0;
+  my ($self, $key) = @_;
+  if (defined $key) {
+    return 1 if (length $key && grep { $key eq $_ } @valid_relations);
+    return 1 if $key =~ /x_/i;
+  }
+  else {
+    $key = '<undef>';
+  }
+  $self->_error("Key '$key' is not a legal prereq relationship.");
+  return 0;
 }
 
 sub _error {
-    my $self = shift;
-    my $mess = shift;
+  my $self = shift;
+  my $mess = shift;
 
-    $mess .= ' ('.join(' -> ',@{$self->{stack}}).')'  if($self->{stack});
-    $mess .= " [Validation: $self->{spec}]";
+  $mess .= ' (' . join(' -> ', @{$self->{stack}}) . ')' if ($self->{stack});
+  $mess .= " [Validation: $self->{spec}]";
 
-    push @{$self->{errors}}, $mess;
+  push @{$self->{errors}}, $mess;
 }
 
 1;
index 746abd63bc45c6baddbc9263f032b484ea71d838..8a298f0c55e04521008c9516d95b166786823816 100644 (file)
@@ -1,12 +1,14 @@
-use 5.008001; # sane UTF-8 support
+use 5.008001;    # sane UTF-8 support
 use strict;
 use warnings;
-package CPAN::Meta::YAML; # git description: v1.68-2-gcc5324e
+
+package CPAN::Meta::YAML;    # git description: v1.68-2-gcc5324e
+
 # XXX-INGY is 5.8.1 too old/broken for utf8?
 # XXX-XDG Lancaster consensus was that it was sufficient until
 # proven otherwise
 $CPAN::Meta::YAML::VERSION = '0.018';
-; # original $VERSION removed by Doppelgaenger
+;                            # original $VERSION removed by Doppelgaenger
 
 #####################################################################
 # The CPAN::Meta::YAML API.
@@ -23,43 +25,45 @@ our @EXPORT_OK = qw{ LoadFile DumpFile freeze thaw };
 # Functional/Export API:
 
 sub Dump {
-    return CPAN::Meta::YAML->new(@_)->_dump_string;
+  return CPAN::Meta::YAML->new(@_)->_dump_string;
 }
 
 # XXX-INGY Returning last document seems a bad behavior.
 # XXX-XDG I think first would seem more natural, but I don't know
 # that it's worth changing now
 sub Load {
-    my $self = CPAN::Meta::YAML->_load_string(@_);
-    if ( wantarray ) {
-        return @$self;
-    } else {
-        # To match YAML.pm, return the last document
-        return $self->[-1];
-    }
+  my $self = CPAN::Meta::YAML->_load_string(@_);
+  if (wantarray) {
+    return @$self;
+  }
+  else {
+    # To match YAML.pm, return the last document
+    return $self->[-1];
+  }
 }
 
 # XXX-INGY Do we really need freeze and thaw?
 # XXX-XDG I don't think so.  I'd support deprecating them.
 BEGIN {
-    *freeze = \&Dump;
-    *thaw   = \&Load;
+  *freeze = \&Dump;
+  *thaw   = \&Load;
 }
 
 sub DumpFile {
-    my $file = shift;
-    return CPAN::Meta::YAML->new(@_)->_dump_file($file);
+  my $file = shift;
+  return CPAN::Meta::YAML->new(@_)->_dump_file($file);
 }
 
 sub LoadFile {
-    my $file = shift;
-    my $self = CPAN::Meta::YAML->_load_file($file);
-    if ( wantarray ) {
-        return @$self;
-    } else {
-        # Return only the last document to match YAML.pm,
-        return $self->[-1];
-    }
+  my $file = shift;
+  my $self = CPAN::Meta::YAML->_load_file($file);
+  if (wantarray) {
+    return @$self;
+  }
+  else {
+    # Return only the last document to match YAML.pm,
+    return $self->[-1];
+  }
 }
 
 
@@ -76,8 +80,8 @@ sub LoadFile {
 # we not change it until YAML.pm's own OO API is established so that
 # users only have one API change to digest, not two
 sub new {
-    my $class = shift;
-    bless [ @_ ], $class;
+  my $class = shift;
+  bless [@_], $class;
 }
 
 # XXX-INGY It probably doesn't matter, and it's probably too late to
@@ -89,46 +93,52 @@ sub new {
 # to add new {read,write}_* methods to this API.
 
 sub read_string {
-    my $self = shift;
-    $self->_load_string(@_);
+  my $self = shift;
+  $self->_load_string(@_);
 }
 
 sub write_string {
-    my $self = shift;
-    $self->_dump_string(@_);
+  my $self = shift;
+  $self->_dump_string(@_);
 }
 
 sub read {
-    my $self = shift;
-    $self->_load_file(@_);
+  my $self = shift;
+  $self->_load_file(@_);
 }
 
 sub write {
-    my $self = shift;
-    $self->_dump_file(@_);
+  my $self = shift;
+  $self->_dump_file(@_);
 }
 
 
-
-
 #####################################################################
 # Constants
 
 # Printed form of the unprintable characters in the lowest range
 # of ASCII characters, listed by ASCII ordinal position.
 my @UNPRINTABLE = qw(
-    0    x01  x02  x03  x04  x05  x06  a
-    b    t    n    v    f    r    x0E  x0F
-    x10  x11  x12  x13  x14  x15  x16  x17
-    x18  x19  x1A  e    x1C  x1D  x1E  x1F
+  0    x01  x02  x03  x04  x05  x06  a
+  b    t    n    v    f    r    x0E  x0F
+  x10  x11  x12  x13  x14  x15  x16  x17
+  x18  x19  x1A  e    x1C  x1D  x1E  x1F
 );
 
 # Printable characters for escapes
 my %UNESCAPES = (
-    0 => "\x00", z => "\x00", N    => "\x85",
-    a => "\x07", b => "\x08", t    => "\x09",
-    n => "\x0a", v => "\x0b", f    => "\x0c",
-    r => "\x0d", e => "\x1b", '\\' => '\\',
+  0    => "\x00",
+  z    => "\x00",
+  N    => "\x85",
+  a    => "\x07",
+  b    => "\x08",
+  t    => "\x09",
+  n    => "\x0a",
+  v    => "\x0b",
+  f    => "\x0c",
+  r    => "\x0d",
+  e    => "\x1b",
+  '\\' => '\\',
 );
 
 # XXX-INGY
@@ -138,7 +148,7 @@ my %UNESCAPES = (
 # These 3 values have special meaning when unquoted and using the
 # default YAML schema. They need quotes if they are strings.
 my %QUOTE = map { $_ => 1 } qw{
-    null true false
+  null true false
 };
 
 # The commented out form is simpler, but overloaded the Perl regex
@@ -147,13 +157,12 @@ my %QUOTE = map { $_ => 1 } qw{
 # qr/\"((?:\\.|[^\"])*)\"/
 my $re_capture_double_quoted = qr/\"([^\\"]*(?:\\.[^\\"]*)*)\"/;
 my $re_capture_single_quoted = qr/\'([^\']*(?:\'\'[^\']*)*)\'/;
-# unquoted re gets trailing space that needs to be stripped
-my $re_capture_unquoted_key  = qr/([^:]+(?::+\S(?:[^:]*|.*?(?=:)))*)(?=\s*\:(?:\s+|$))/;
-my $re_trailing_comment      = qr/(?:\s+\#.*)?/;
-my $re_key_value_separator   = qr/\s*:(?:\s+(?:\#.*)?|$)/;
-
-
 
+# unquoted re gets trailing space that needs to be stripped
+my $re_capture_unquoted_key
+  = qr/([^:]+(?::+\S(?:[^:]*|.*?(?=:)))*)(?=\s*\:(?:\s+|$))/;
+my $re_trailing_comment    = qr/(?:\s+\#.*)?/;
+my $re_key_value_separator = qr/\s*:(?:\s+(?:\#.*)?|$)/;
 
 
 #####################################################################
@@ -168,387 +177,394 @@ my $re_key_value_separator   = qr/\s*:(?:\s+(?:\#.*)?|$)/;
 
 # Create an object from a file
 sub _load_file {
-    my $class = ref $_[0] ? ref shift : shift;
-
-    # Check the file
-    my $file = shift or $class->_error( 'You did not specify a file name' );
-    $class->_error( "File '$file' does not exist" )
-        unless -e $file;
-    $class->_error( "'$file' is a directory, not a file" )
-        unless -f _;
-    $class->_error( "Insufficient permissions to read '$file'" )
-        unless -r _;
+  my $class = ref $_[0] ? ref shift : shift;
+
+  # Check the file
+  my $file = shift or $class->_error('You did not specify a file name');
+  $class->_error("File '$file' does not exist")              unless -e $file;
+  $class->_error("'$file' is a directory, not a file")       unless -f _;
+  $class->_error("Insufficient permissions to read '$file'") unless -r _;
+
+  # Open unbuffered with strict UTF-8 decoding and no translation layers
+  open(my $fh, "<:unix:encoding(UTF-8)", $file);
+  unless ($fh) {
+    $class->_error("Failed to open file '$file': $!");
+  }
+
+  # flock if available (or warn if not possible for OS-specific reasons)
+  if (_can_flock()) {
+    flock($fh, Fcntl::LOCK_SH()) or warn "Couldn't lock '$file' for reading: $!";
+  }
+
+  # slurp the contents
+  my $contents = eval {
+    use warnings FATAL => 'utf8';
+    local $/;
+    <$fh>;
+  };
+  if (my $err = $@) {
+    $class->_error("Error reading from file '$file': $err");
+  }
+
+  # close the file (release the lock)
+  unless (close $fh) {
+    $class->_error("Failed to close file '$file': $!");
+  }
+
+  $class->_load_string($contents);
+}
 
-    # Open unbuffered with strict UTF-8 decoding and no translation layers
-    open( my $fh, "<:unix:encoding(UTF-8)", $file );
-    unless ( $fh ) {
-        $class->_error("Failed to open file '$file': $!");
+# Create an object from a string
+sub _load_string {
+  my $class = ref $_[0] ? ref shift : shift;
+  my $self = bless [], $class;
+  my $string = $_[0];
+  eval {
+    unless (defined $string) {
+      die \"Did not provide a string to load";
     }
 
-    # flock if available (or warn if not possible for OS-specific reasons)
-    if ( _can_flock() ) {
-        flock( $fh, Fcntl::LOCK_SH() )
-            or warn "Couldn't lock '$file' for reading: $!";
+    # Check if Perl has it marked as characters, but it's internally
+    # inconsistent.  E.g. maybe latin1 got read on a :utf8 layer
+    if (utf8::is_utf8($string) && !utf8::valid($string)) {
+      die \<<'...';
+Read an invalid UTF-8 string (maybe mixed UTF-8 and 8-bit character set).
+Did you decode with lax ":utf8" instead of strict ":encoding(UTF-8)"?
+...
     }
 
-    # slurp the contents
-    my $contents = eval {
-        use warnings FATAL => 'utf8';
-        local $/;
-        <$fh>
-    };
-    if ( my $err = $@ ) {
-        $class->_error("Error reading from file '$file': $err");
-    }
+    # Ensure Unicode character semantics, even for 0x80-0xff
+    utf8::upgrade($string);
 
-    # close the file (release the lock)
-    unless ( close $fh ) {
-        $class->_error("Failed to close file '$file': $!");
-    }
+    # Check for and strip any leading UTF-8 BOM
+    $string =~ s/^\x{FEFF}//;
 
-    $class->_load_string( $contents );
-}
+    # Check for some special cases
+    return $self unless length $string;
 
-# Create an object from a string
-sub _load_string {
-    my $class  = ref $_[0] ? ref shift : shift;
-    my $self   = bless [], $class;
-    my $string = $_[0];
-    eval {
-        unless ( defined $string ) {
-            die \"Did not provide a string to load";
-        }
+    # Split the file into lines
+    my @lines = grep { !/^\s*(?:\#.*)?\z/ } split /(?:\015{1,2}\012|\015|\012)/,
+      $string;
 
-        # Check if Perl has it marked as characters, but it's internally
-        # inconsistent.  E.g. maybe latin1 got read on a :utf8 layer
-        if ( utf8::is_utf8($string) && ! utf8::valid($string) ) {
-            die \<<'...';
-Read an invalid UTF-8 string (maybe mixed UTF-8 and 8-bit character set).
-Did you decode with lax ":utf8" instead of strict ":encoding(UTF-8)"?
-...
+    # Strip the initial YAML header
+    @lines and $lines[0] =~ /^\%YAML[: ][\d\.]+.*\z/ and shift @lines;
+
+    # A nibbling parser
+    my $in_document = 0;
+    while (@lines) {
+
+      # Do we have a document header?
+      if ($lines[0] =~ /^---\s*(?:(.+)\s*)?\z/) {
+
+        # Handle scalar documents
+        shift @lines;
+        if (defined $1 and $1 !~ /^(?:\#.+|\%YAML[: ][\d\.]+)\z/) {
+          push @$self, $self->_load_scalar("$1", [undef], \@lines);
+          next;
         }
+        $in_document = 1;
+      }
 
-        # Ensure Unicode character semantics, even for 0x80-0xff
-        utf8::upgrade($string);
-
-        # Check for and strip any leading UTF-8 BOM
-        $string =~ s/^\x{FEFF}//;
-
-        # Check for some special cases
-        return $self unless length $string;
-
-        # Split the file into lines
-        my @lines = grep { ! /^\s*(?:\#.*)?\z/ }
-                split /(?:\015{1,2}\012|\015|\012)/, $string;
-
-        # Strip the initial YAML header
-        @lines and $lines[0] =~ /^\%YAML[: ][\d\.]+.*\z/ and shift @lines;
-
-        # A nibbling parser
-        my $in_document = 0;
-        while ( @lines ) {
-            # Do we have a document header?
-            if ( $lines[0] =~ /^---\s*(?:(.+)\s*)?\z/ ) {
-                # Handle scalar documents
-                shift @lines;
-                if ( defined $1 and $1 !~ /^(?:\#.+|\%YAML[: ][\d\.]+)\z/ ) {
-                    push @$self,
-                        $self->_load_scalar( "$1", [ undef ], \@lines );
-                    next;
-                }
-                $in_document = 1;
-            }
-
-            if ( ! @lines or $lines[0] =~ /^(?:---|\.\.\.)/ ) {
-                # A naked document
-                push @$self, undef;
-                while ( @lines and $lines[0] !~ /^---/ ) {
-                    shift @lines;
-                }
-                $in_document = 0;
-
-            # XXX The final '-+$' is to look for -- which ends up being an
-            # error later.
-            } elsif ( ! $in_document && @$self ) {
-                # only the first document can be explicit
-                die \"CPAN::Meta::YAML failed to classify the line '$lines[0]'";
-            } elsif ( $lines[0] =~ /^\s*\-(?:\s|$|-+$)/ ) {
-                # An array at the root
-                my $document = [ ];
-                push @$self, $document;
-                $self->_load_array( $document, [ 0 ], \@lines );
-
-            } elsif ( $lines[0] =~ /^(\s*)\S/ ) {
-                # A hash at the root
-                my $document = { };
-                push @$self, $document;
-                $self->_load_hash( $document, [ length($1) ], \@lines );
-
-            } else {
-                # Shouldn't get here.  @lines have whitespace-only lines
-                # stripped, and previous match is a line with any
-                # non-whitespace.  So this clause should only be reachable via
-                # a perlbug where \s is not symmetric with \S
-
-                # uncoverable statement
-                die \"CPAN::Meta::YAML failed to classify the line '$lines[0]'";
-            }
+      if (!@lines or $lines[0] =~ /^(?:---|\.\.\.)/) {
+
+        # A naked document
+        push @$self, undef;
+        while (@lines and $lines[0] !~ /^---/) {
+          shift @lines;
         }
-    };
-    my $err = $@;
-    if ( ref $err eq 'SCALAR' ) {
-        $self->_error(${$err});
-    } elsif ( $err ) {
-        $self->_error($err);
+        $in_document = 0;
+
+        # XXX The final '-+$' is to look for -- which ends up being an
+        # error later.
+      }
+      elsif (!$in_document && @$self) {
+
+        # only the first document can be explicit
+        die \"CPAN::Meta::YAML failed to classify the line '$lines[0]'";
+      }
+      elsif ($lines[0] =~ /^\s*\-(?:\s|$|-+$)/) {
+
+        # An array at the root
+        my $document = [];
+        push @$self, $document;
+        $self->_load_array($document, [0], \@lines);
+
+      }
+      elsif ($lines[0] =~ /^(\s*)\S/) {
+
+        # A hash at the root
+        my $document = {};
+        push @$self, $document;
+        $self->_load_hash($document, [length($1)], \@lines);
+
+      }
+      else {
+        # Shouldn't get here.  @lines have whitespace-only lines
+        # stripped, and previous match is a line with any
+        # non-whitespace.  So this clause should only be reachable via
+        # a perlbug where \s is not symmetric with \S
+
+        # uncoverable statement
+        die \"CPAN::Meta::YAML failed to classify the line '$lines[0]'";
+      }
     }
-
-    return $self;
+  };
+  my $err = $@;
+  if (ref $err eq 'SCALAR') {
+    $self->_error(${$err});
+  }
+  elsif ($err) {
+    $self->_error($err);
+  }
+
+  return $self;
 }
 
 sub _unquote_single {
-    my ($self, $string) = @_;
-    return '' unless length $string;
-    $string =~ s/\'\'/\'/g;
-    return $string;
+  my ($self, $string) = @_;
+  return '' unless length $string;
+  $string =~ s/\'\'/\'/g;
+  return $string;
 }
 
 sub _unquote_double {
-    my ($self, $string) = @_;
-    return '' unless length $string;
-    $string =~ s/\\"/"/g;
-    $string =~
-        s{\\([Nnever\\fartz0b]|x([0-9a-fA-F]{2}))}
+  my ($self, $string) = @_;
+  return '' unless length $string;
+  $string =~ s/\\"/"/g;
+  $string =~ s{\\([Nnever\\fartz0b]|x([0-9a-fA-F]{2}))}
          {(length($1)>1)?pack("H2",$2):$UNESCAPES{$1}}gex;
-    return $string;
+  return $string;
 }
 
 # Load a YAML scalar string to the actual Perl scalar
 sub _load_scalar {
-    my ($self, $string, $indent, $lines) = @_;
+  my ($self, $string, $indent, $lines) = @_;
+
+  # Trim trailing whitespace
+  $string =~ s/\s*\z//;
+
+  # Explitic null/undef
+  return undef if $string eq '~';
+
+  # Single quote
+  if ($string =~ /^$re_capture_single_quoted$re_trailing_comment\z/) {
+    return $self->_unquote_single($1);
+  }
+
+  # Double quote.
+  if ($string =~ /^$re_capture_double_quoted$re_trailing_comment\z/) {
+    return $self->_unquote_double($1);
+  }
+
+  # Special cases
+  if ($string =~ /^[\'\"!&]/) {
+    die \"CPAN::Meta::YAML does not support a feature in line '$string'";
+  }
+  return {} if $string =~ /^{}(?:\s+\#.*)?\z/;
+  return [] if $string =~ /^\[\](?:\s+\#.*)?\z/;
+
+  # Regular unquoted string
+  if ($string !~ /^[>|]/) {
+    die \"CPAN::Meta::YAML found illegal characters in plain scalar: '$string'"
+      if $string =~ /^(?:-(?:\s|$)|[\@\%\`])/ or $string =~ /:(?:\s|$)/;
+    $string =~ s/\s+#.*\z//;
+    return $string;
+  }
+
+  # Error
+  die \"CPAN::Meta::YAML failed to find multi-line scalar content" unless @$lines;
+
+  # Check the indent depth
+  $lines->[0] =~ /^(\s*)/;
+  $indent->[-1] = length("$1");
+  if (defined $indent->[-2] and $indent->[-1] <= $indent->[-2]) {
+    die \"CPAN::Meta::YAML found bad indenting in line '$lines->[0]'";
+  }
+
+  # Pull the lines
+  my @multiline = ();
+  while (@$lines) {
+    $lines->[0] =~ /^(\s*)/;
+    last unless length($1) >= $indent->[-1];
+    push @multiline, substr(shift(@$lines), length($1));
+  }
+
+  my $j = (substr($string, 0, 1) eq '>') ? ' ' : "\n";
+  my $t = (substr($string, 1, 1) eq '-') ? ''  : "\n";
+  return join($j, @multiline) . $t;
+}
 
-    # Trim trailing whitespace
-    $string =~ s/\s*\z//;
+# Load an array
+sub _load_array {
+  my ($self, $array, $indent, $lines) = @_;
 
-    # Explitic null/undef
-    return undef if $string eq '~';
+  while (@$lines) {
 
-    # Single quote
-    if ( $string =~ /^$re_capture_single_quoted$re_trailing_comment\z/ ) {
-        return $self->_unquote_single($1);
+    # Check for a new document
+    if ($lines->[0] =~ /^(?:---|\.\.\.)/) {
+      while (@$lines and $lines->[0] !~ /^---/) {
+        shift @$lines;
+      }
+      return 1;
     }
 
-    # Double quote.
-    if ( $string =~ /^$re_capture_double_quoted$re_trailing_comment\z/ ) {
-        return $self->_unquote_double($1);
+    # Check the indent level
+    $lines->[0] =~ /^(\s*)/;
+    if (length($1) < $indent->[-1]) {
+      return 1;
     }
-
-    # Special cases
-    if ( $string =~ /^[\'\"!&]/ ) {
-        die \"CPAN::Meta::YAML does not support a feature in line '$string'";
+    elsif (length($1) > $indent->[-1]) {
+      die \"CPAN::Meta::YAML found bad indenting in line '$lines->[0]'";
     }
-    return {} if $string =~ /^{}(?:\s+\#.*)?\z/;
-    return [] if $string =~ /^\[\](?:\s+\#.*)?\z/;
 
-    # Regular unquoted string
-    if ( $string !~ /^[>|]/ ) {
-        die \"CPAN::Meta::YAML found illegal characters in plain scalar: '$string'"
-            if $string =~ /^(?:-(?:\s|$)|[\@\%\`])/ or
-                $string =~ /:(?:\s|$)/;
-        $string =~ s/\s+#.*\z//;
-        return $string;
-    }
+    if ($lines->[0] =~ /^(\s*\-\s+)[^\'\"]\S*\s*:(?:\s+|$)/) {
 
-    # Error
-    die \"CPAN::Meta::YAML failed to find multi-line scalar content" unless @$lines;
+      # Inline nested hash
+      my $indent2 = length("$1");
+      $lines->[0] =~ s/-/ /;
+      push @$array, {};
+      $self->_load_hash($array->[-1], [@$indent, $indent2], $lines);
 
-    # Check the indent depth
-    $lines->[0]   =~ /^(\s*)/;
-    $indent->[-1] = length("$1");
-    if ( defined $indent->[-2] and $indent->[-1] <= $indent->[-2] ) {
-        die \"CPAN::Meta::YAML found bad indenting in line '$lines->[0]'";
     }
+    elsif ($lines->[0] =~ /^\s*\-\s*\z/) {
+      shift @$lines;
+      unless (@$lines) {
+        push @$array, undef;
+        return 1;
+      }
+      if ($lines->[0] =~ /^(\s*)\-/) {
+        my $indent2 = length("$1");
+        if ($indent->[-1] == $indent2) {
+
+          # Null array entry
+          push @$array, undef;
+        }
+        else {
+          # Naked indenter
+          push @$array, [];
+          $self->_load_array($array->[-1], [@$indent, $indent2], $lines);
+        }
+
+      }
+      elsif ($lines->[0] =~ /^(\s*)\S/) {
+        push @$array, {};
+        $self->_load_hash($array->[-1], [@$indent, length("$1")], $lines);
+
+      }
+      else {
+        die \"CPAN::Meta::YAML failed to classify line '$lines->[0]'";
+      }
 
-    # Pull the lines
-    my @multiline = ();
-    while ( @$lines ) {
-        $lines->[0] =~ /^(\s*)/;
-        last unless length($1) >= $indent->[-1];
-        push @multiline, substr(shift(@$lines), length($1));
     }
+    elsif ($lines->[0] =~ /^\s*\-(\s*)(.+?)\s*\z/) {
 
-    my $j = (substr($string, 0, 1) eq '>') ? ' ' : "\n";
-    my $t = (substr($string, 1, 1) eq '-') ? ''  : "\n";
-    return join( $j, @multiline ) . $t;
-}
+      # Array entry with a value
+      shift @$lines;
+      push @$array, $self->_load_scalar("$2", [@$indent, undef], $lines);
 
-# Load an array
-sub _load_array {
-    my ($self, $array, $indent, $lines) = @_;
-
-    while ( @$lines ) {
-        # Check for a new document
-        if ( $lines->[0] =~ /^(?:---|\.\.\.)/ ) {
-            while ( @$lines and $lines->[0] !~ /^---/ ) {
-                shift @$lines;
-            }
-            return 1;
-        }
+    }
+    elsif (defined $indent->[-2] and $indent->[-1] == $indent->[-2]) {
 
-        # Check the indent level
-        $lines->[0] =~ /^(\s*)/;
-        if ( length($1) < $indent->[-1] ) {
-            return 1;
-        } elsif ( length($1) > $indent->[-1] ) {
-            die \"CPAN::Meta::YAML found bad indenting in line '$lines->[0]'";
-        }
+      # This is probably a structure like the following...
+      # ---
+      # foo:
+      # - list
+      # bar: value
+      #
+      # ... so lets return and let the hash parser handle it
+      return 1;
 
-        if ( $lines->[0] =~ /^(\s*\-\s+)[^\'\"]\S*\s*:(?:\s+|$)/ ) {
-            # Inline nested hash
-            my $indent2 = length("$1");
-            $lines->[0] =~ s/-/ /;
-            push @$array, { };
-            $self->_load_hash( $array->[-1], [ @$indent, $indent2 ], $lines );
-
-        } elsif ( $lines->[0] =~ /^\s*\-\s*\z/ ) {
-            shift @$lines;
-            unless ( @$lines ) {
-                push @$array, undef;
-                return 1;
-            }
-            if ( $lines->[0] =~ /^(\s*)\-/ ) {
-                my $indent2 = length("$1");
-                if ( $indent->[-1] == $indent2 ) {
-                    # Null array entry
-                    push @$array, undef;
-                } else {
-                    # Naked indenter
-                    push @$array, [ ];
-                    $self->_load_array(
-                        $array->[-1], [ @$indent, $indent2 ], $lines
-                    );
-                }
-
-            } elsif ( $lines->[0] =~ /^(\s*)\S/ ) {
-                push @$array, { };
-                $self->_load_hash(
-                    $array->[-1], [ @$indent, length("$1") ], $lines
-                );
-
-            } else {
-                die \"CPAN::Meta::YAML failed to classify line '$lines->[0]'";
-            }
-
-        } elsif ( $lines->[0] =~ /^\s*\-(\s*)(.+?)\s*\z/ ) {
-            # Array entry with a value
-            shift @$lines;
-            push @$array, $self->_load_scalar(
-                "$2", [ @$indent, undef ], $lines
-            );
-
-        } elsif ( defined $indent->[-2] and $indent->[-1] == $indent->[-2] ) {
-            # This is probably a structure like the following...
-            # ---
-            # foo:
-            # - list
-            # bar: value
-            #
-            # ... so lets return and let the hash parser handle it
-            return 1;
-
-        } else {
-            die \"CPAN::Meta::YAML failed to classify line '$lines->[0]'";
-        }
     }
+    else {
+      die \"CPAN::Meta::YAML failed to classify line '$lines->[0]'";
+    }
+  }
 
-    return 1;
+  return 1;
 }
 
 # Load a hash
 sub _load_hash {
-    my ($self, $hash, $indent, $lines) = @_;
-
-    while ( @$lines ) {
-        # Check for a new document
-        if ( $lines->[0] =~ /^(?:---|\.\.\.)/ ) {
-            while ( @$lines and $lines->[0] !~ /^---/ ) {
-                shift @$lines;
-            }
-            return 1;
-        }
+  my ($self, $hash, $indent, $lines) = @_;
 
-        # Check the indent level
-        $lines->[0] =~ /^(\s*)/;
-        if ( length($1) < $indent->[-1] ) {
-            return 1;
-        } elsif ( length($1) > $indent->[-1] ) {
-            die \"CPAN::Meta::YAML found bad indenting in line '$lines->[0]'";
-        }
+  while (@$lines) {
 
-        # Find the key
-        my $key;
+    # Check for a new document
+    if ($lines->[0] =~ /^(?:---|\.\.\.)/) {
+      while (@$lines and $lines->[0] !~ /^---/) {
+        shift @$lines;
+      }
+      return 1;
+    }
 
-        # Quoted keys
-        if ( $lines->[0] =~
-            s/^\s*$re_capture_single_quoted$re_key_value_separator//
-        ) {
-            $key = $self->_unquote_single($1);
-        }
-        elsif ( $lines->[0] =~
-            s/^\s*$re_capture_double_quoted$re_key_value_separator//
-        ) {
-            $key = $self->_unquote_double($1);
-        }
-        elsif ( $lines->[0] =~
-            s/^\s*$re_capture_unquoted_key$re_key_value_separator//
-        ) {
-            $key = $1;
-            $key =~ s/\s+$//;
-        }
-        elsif ( $lines->[0] =~ /^\s*\?/ ) {
-            die \"CPAN::Meta::YAML does not support a feature in line '$lines->[0]'";
-        }
-        else {
-            die \"CPAN::Meta::YAML failed to classify line '$lines->[0]'";
-        }
+    # Check the indent level
+    $lines->[0] =~ /^(\s*)/;
+    if (length($1) < $indent->[-1]) {
+      return 1;
+    }
+    elsif (length($1) > $indent->[-1]) {
+      die \"CPAN::Meta::YAML found bad indenting in line '$lines->[0]'";
+    }
 
-        if ( exists $hash->{$key} ) {
-            warn "CPAN::Meta::YAML found a duplicate key '$key' in line '$lines->[0]'";
-        }
+    # Find the key
+    my $key;
 
-        # Do we have a value?
-        if ( length $lines->[0] ) {
-            # Yes
-            $hash->{$key} = $self->_load_scalar(
-                shift(@$lines), [ @$indent, undef ], $lines
-            );
-        } else {
-            # An indent
-            shift @$lines;
-            unless ( @$lines ) {
-                $hash->{$key} = undef;
-                return 1;
-            }
-            if ( $lines->[0] =~ /^(\s*)-/ ) {
-                $hash->{$key} = [];
-                $self->_load_array(
-                    $hash->{$key}, [ @$indent, length($1) ], $lines
-                );
-            } elsif ( $lines->[0] =~ /^(\s*)./ ) {
-                my $indent2 = length("$1");
-                if ( $indent->[-1] >= $indent2 ) {
-                    # Null hash entry
-                    $hash->{$key} = undef;
-                } else {
-                    $hash->{$key} = {};
-                    $self->_load_hash(
-                        $hash->{$key}, [ @$indent, length($1) ], $lines
-                    );
-                }
-            }
+    # Quoted keys
+    if ($lines->[0] =~ s/^\s*$re_capture_single_quoted$re_key_value_separator//) {
+      $key = $self->_unquote_single($1);
+    }
+    elsif ($lines->[0] =~ s/^\s*$re_capture_double_quoted$re_key_value_separator//)
+    {
+      $key = $self->_unquote_double($1);
+    }
+    elsif ($lines->[0] =~ s/^\s*$re_capture_unquoted_key$re_key_value_separator//) {
+      $key = $1;
+      $key =~ s/\s+$//;
+    }
+    elsif ($lines->[0] =~ /^\s*\?/) {
+      die \"CPAN::Meta::YAML does not support a feature in line '$lines->[0]'";
+    }
+    else {
+      die \"CPAN::Meta::YAML failed to classify line '$lines->[0]'";
+    }
+
+    if (exists $hash->{$key}) {
+      warn "CPAN::Meta::YAML found a duplicate key '$key' in line '$lines->[0]'";
+    }
+
+    # Do we have a value?
+    if (length $lines->[0]) {
+
+      # Yes
+      $hash->{$key} = $self->_load_scalar(shift(@$lines), [@$indent, undef], $lines);
+    }
+    else {
+      # An indent
+      shift @$lines;
+      unless (@$lines) {
+        $hash->{$key} = undef;
+        return 1;
+      }
+      if ($lines->[0] =~ /^(\s*)-/) {
+        $hash->{$key} = [];
+        $self->_load_array($hash->{$key}, [@$indent, length($1)], $lines);
+      }
+      elsif ($lines->[0] =~ /^(\s*)./) {
+        my $indent2 = length("$1");
+        if ($indent->[-1] >= $indent2) {
+
+          # Null hash entry
+          $hash->{$key} = undef;
         }
+        else {
+          $hash->{$key} = {};
+          $self->_load_hash($hash->{$key}, [@$indent, length($1)], $lines);
+        }
+      }
     }
+  }
 
-    return 1;
+  return 1;
 }
 
 
@@ -557,245 +573,261 @@ sub _load_hash {
 
 # Save an object to a file
 sub _dump_file {
-    my $self = shift;
+  my $self = shift;
 
-    require Fcntl;
+  require Fcntl;
 
-    # Check the file
-    my $file = shift or $self->_error( 'You did not specify a file name' );
-
-    my $fh;
-    # flock if available (or warn if not possible for OS-specific reasons)
-    if ( _can_flock() ) {
-        # Open without truncation (truncate comes after lock)
-        my $flags = Fcntl::O_WRONLY()|Fcntl::O_CREAT();
-        sysopen( $fh, $file, $flags );
-        unless ( $fh ) {
-            $self->_error("Failed to open file '$file' for writing: $!");
-        }
+  # Check the file
+  my $file = shift or $self->_error('You did not specify a file name');
 
-        # Use no translation and strict UTF-8
-        binmode( $fh, ":raw:encoding(UTF-8)");
+  my $fh;
 
-        flock( $fh, Fcntl::LOCK_EX() )
-            or warn "Couldn't lock '$file' for reading: $!";
+  # flock if available (or warn if not possible for OS-specific reasons)
+  if (_can_flock()) {
 
-        # truncate and spew contents
-        truncate $fh, 0;
-        seek $fh, 0, 0;
-    }
-    else {
-        open $fh, ">:unix:encoding(UTF-8)", $file;
+    # Open without truncation (truncate comes after lock)
+    my $flags = Fcntl::O_WRONLY() | Fcntl::O_CREAT();
+    sysopen($fh, $file, $flags);
+    unless ($fh) {
+      $self->_error("Failed to open file '$file' for writing: $!");
     }
 
-    # serialize and spew to the handle
-    print {$fh} $self->_dump_string;
+    # Use no translation and strict UTF-8
+    binmode($fh, ":raw:encoding(UTF-8)");
 
-    # close the file (release the lock)
-    unless ( close $fh ) {
-        $self->_error("Failed to close file '$file': $!");
-    }
+    flock($fh, Fcntl::LOCK_EX()) or warn "Couldn't lock '$file' for reading: $!";
+
+    # truncate and spew contents
+    truncate $fh, 0;
+    seek $fh, 0, 0;
+  }
+  else {
+    open $fh, ">:unix:encoding(UTF-8)", $file;
+  }
 
-    return 1;
+  # serialize and spew to the handle
+  print {$fh} $self->_dump_string;
+
+  # close the file (release the lock)
+  unless (close $fh) {
+    $self->_error("Failed to close file '$file': $!");
+  }
+
+  return 1;
 }
 
 # Save an object to a string
 sub _dump_string {
-    my $self = shift;
-    return '' unless ref $self && @$self;
-
-    # Iterate over the documents
-    my $indent = 0;
-    my @lines  = ();
-
-    eval {
-        foreach my $cursor ( @$self ) {
-            push @lines, '---';
-
-            # An empty document
-            if ( ! defined $cursor ) {
-                # Do nothing
-
-            # A scalar document
-            } elsif ( ! ref $cursor ) {
-                $lines[-1] .= ' ' . $self->_dump_scalar( $cursor );
-
-            # A list at the root
-            } elsif ( ref $cursor eq 'ARRAY' ) {
-                unless ( @$cursor ) {
-                    $lines[-1] .= ' []';
-                    next;
-                }
-                push @lines, $self->_dump_array( $cursor, $indent, {} );
-
-            # A hash at the root
-            } elsif ( ref $cursor eq 'HASH' ) {
-                unless ( %$cursor ) {
-                    $lines[-1] .= ' {}';
-                    next;
-                }
-                push @lines, $self->_dump_hash( $cursor, $indent, {} );
-
-            } else {
-                die \("Cannot serialize " . ref($cursor));
-            }
+  my $self = shift;
+  return '' unless ref $self && @$self;
+
+  # Iterate over the documents
+  my $indent = 0;
+  my @lines  = ();
+
+  eval {
+    foreach my $cursor (@$self) {
+      push @lines, '---';
+
+      # An empty document
+      if (!defined $cursor) {
+
+        # Do nothing
+
+        # A scalar document
+      }
+      elsif (!ref $cursor) {
+        $lines[-1] .= ' ' . $self->_dump_scalar($cursor);
+
+        # A list at the root
+      }
+      elsif (ref $cursor eq 'ARRAY') {
+        unless (@$cursor) {
+          $lines[-1] .= ' []';
+          next;
         }
-    };
-    if ( ref $@ eq 'SCALAR' ) {
-        $self->_error(${$@});
-    } elsif ( $@ ) {
-        $self->_error($@);
-    }
+        push @lines, $self->_dump_array($cursor, $indent, {});
+
+        # A hash at the root
+      }
+      elsif (ref $cursor eq 'HASH') {
+        unless (%$cursor) {
+          $lines[-1] .= ' {}';
+          next;
+        }
+        push @lines, $self->_dump_hash($cursor, $indent, {});
 
-    join '', map { "$_\n" } @lines;
+      }
+      else {
+        die \("Cannot serialize " . ref($cursor));
+      }
+    }
+  };
+  if (ref $@ eq 'SCALAR') {
+    $self->_error(${$@});
+  }
+  elsif ($@) {
+    $self->_error($@);
+  }
+
+  join '', map {"$_\n"} @lines;
 }
 
 sub _has_internal_string_value {
-    my $value = shift;
-    my $b_obj = B::svref_2object(\$value);  # for round trip problem
-    return $b_obj->FLAGS & B::SVf_POK();
+  my $value = shift;
+  my $b_obj = B::svref_2object(\$value);    # for round trip problem
+  return $b_obj->FLAGS & B::SVf_POK();
 }
 
 sub _dump_scalar {
-    my $string = $_[1];
-    my $is_key = $_[2];
-    # Check this before checking length or it winds up looking like a string!
-    my $has_string_flag = _has_internal_string_value($string);
-    return '~'  unless defined $string;
-    return "''" unless length  $string;
-    if (Scalar::Util::looks_like_number($string)) {
-        # keys and values that have been used as strings get quoted
-        if ( $is_key || $has_string_flag ) {
-            return qq['$string'];
-        }
-        else {
-            return $string;
-        }
-    }
-    if ( $string =~ /[\x00-\x09\x0b-\x0d\x0e-\x1f\x7f-\x9f\'\n]/ ) {
-        $string =~ s/\\/\\\\/g;
-        $string =~ s/"/\\"/g;
-        $string =~ s/\n/\\n/g;
-        $string =~ s/[\x85]/\\N/g;
-        $string =~ s/([\x00-\x1f])/\\$UNPRINTABLE[ord($1)]/g;
-        $string =~ s/([\x7f-\x9f])/'\x' . sprintf("%X",ord($1))/ge;
-        return qq|"$string"|;
+  my $string = $_[1];
+  my $is_key = $_[2];
+
+  # Check this before checking length or it winds up looking like a string!
+  my $has_string_flag = _has_internal_string_value($string);
+  return '~'  unless defined $string;
+  return "''" unless length $string;
+  if (Scalar::Util::looks_like_number($string)) {
+
+    # keys and values that have been used as strings get quoted
+    if ($is_key || $has_string_flag) {
+      return qq['$string'];
     }
-    if ( $string =~ /(?:^[~!@#%&*|>?:,'"`{}\[\]]|^-+$|\s|:\z)/ or
-        $QUOTE{$string}
-    ) {
-        return "'$string'";
+    else {
+      return $string;
     }
-    return $string;
+  }
+  if ($string =~ /[\x00-\x09\x0b-\x0d\x0e-\x1f\x7f-\x9f\'\n]/) {
+    $string =~ s/\\/\\\\/g;
+    $string =~ s/"/\\"/g;
+    $string =~ s/\n/\\n/g;
+    $string =~ s/[\x85]/\\N/g;
+    $string =~ s/([\x00-\x1f])/\\$UNPRINTABLE[ord($1)]/g;
+    $string =~ s/([\x7f-\x9f])/'\x' . sprintf("%X",ord($1))/ge;
+    return qq|"$string"|;
+  }
+  if ($string =~ /(?:^[~!@#%&*|>?:,'"`{}\[\]]|^-+$|\s|:\z)/ or $QUOTE{$string}) {
+    return "'$string'";
+  }
+  return $string;
 }
 
 sub _dump_array {
-    my ($self, $array, $indent, $seen) = @_;
-    if ( $seen->{refaddr($array)}++ ) {
-        die \"CPAN::Meta::YAML does not support circular references";
-    }
-    my @lines  = ();
-    foreach my $el ( @$array ) {
-        my $line = ('  ' x $indent) . '-';
-        my $type = ref $el;
-        if ( ! $type ) {
-            $line .= ' ' . $self->_dump_scalar( $el );
-            push @lines, $line;
-
-        } elsif ( $type eq 'ARRAY' ) {
-            if ( @$el ) {
-                push @lines, $line;
-                push @lines, $self->_dump_array( $el, $indent + 1, $seen );
-            } else {
-                $line .= ' []';
-                push @lines, $line;
-            }
-
-        } elsif ( $type eq 'HASH' ) {
-            if ( keys %$el ) {
-                push @lines, $line;
-                push @lines, $self->_dump_hash( $el, $indent + 1, $seen );
-            } else {
-                $line .= ' {}';
-                push @lines, $line;
-            }
-
-        } else {
-            die \"CPAN::Meta::YAML does not support $type references";
-        }
+  my ($self, $array, $indent, $seen) = @_;
+  if ($seen->{refaddr($array)}++) {
+    die \"CPAN::Meta::YAML does not support circular references";
+  }
+  my @lines = ();
+  foreach my $el (@$array) {
+    my $line = ('  ' x $indent) . '-';
+    my $type = ref $el;
+    if (!$type) {
+      $line .= ' ' . $self->_dump_scalar($el);
+      push @lines, $line;
+
+    }
+    elsif ($type eq 'ARRAY') {
+      if (@$el) {
+        push @lines, $line;
+        push @lines, $self->_dump_array($el, $indent + 1, $seen);
+      }
+      else {
+        $line .= ' []';
+        push @lines, $line;
+      }
+
     }
+    elsif ($type eq 'HASH') {
+      if (keys %$el) {
+        push @lines, $line;
+        push @lines, $self->_dump_hash($el, $indent + 1, $seen);
+      }
+      else {
+        $line .= ' {}';
+        push @lines, $line;
+      }
 
-    @lines;
+    }
+    else {
+      die \"CPAN::Meta::YAML does not support $type references";
+    }
+  }
+
+  @lines;
 }
 
 sub _dump_hash {
-    my ($self, $hash, $indent, $seen) = @_;
-    if ( $seen->{refaddr($hash)}++ ) {
-        die \"CPAN::Meta::YAML does not support circular references";
-    }
-    my @lines  = ();
-    foreach my $name ( sort keys %$hash ) {
-        my $el   = $hash->{$name};
-        my $line = ('  ' x $indent) . $self->_dump_scalar($name, 1) . ":";
-        my $type = ref $el;
-        if ( ! $type ) {
-            $line .= ' ' . $self->_dump_scalar( $el );
-            push @lines, $line;
-
-        } elsif ( $type eq 'ARRAY' ) {
-            if ( @$el ) {
-                push @lines, $line;
-                push @lines, $self->_dump_array( $el, $indent + 1, $seen );
-            } else {
-                $line .= ' []';
-                push @lines, $line;
-            }
-
-        } elsif ( $type eq 'HASH' ) {
-            if ( keys %$el ) {
-                push @lines, $line;
-                push @lines, $self->_dump_hash( $el, $indent + 1, $seen );
-            } else {
-                $line .= ' {}';
-                push @lines, $line;
-            }
-
-        } else {
-            die \"CPAN::Meta::YAML does not support $type references";
-        }
+  my ($self, $hash, $indent, $seen) = @_;
+  if ($seen->{refaddr($hash)}++) {
+    die \"CPAN::Meta::YAML does not support circular references";
+  }
+  my @lines = ();
+  foreach my $name (sort keys %$hash) {
+    my $el   = $hash->{$name};
+    my $line = ('  ' x $indent) . $self->_dump_scalar($name, 1) . ":";
+    my $type = ref $el;
+    if (!$type) {
+      $line .= ' ' . $self->_dump_scalar($el);
+      push @lines, $line;
+
     }
+    elsif ($type eq 'ARRAY') {
+      if (@$el) {
+        push @lines, $line;
+        push @lines, $self->_dump_array($el, $indent + 1, $seen);
+      }
+      else {
+        $line .= ' []';
+        push @lines, $line;
+      }
 
-    @lines;
-}
+    }
+    elsif ($type eq 'HASH') {
+      if (keys %$el) {
+        push @lines, $line;
+        push @lines, $self->_dump_hash($el, $indent + 1, $seen);
+      }
+      else {
+        $line .= ' {}';
+        push @lines, $line;
+      }
 
+    }
+    else {
+      die \"CPAN::Meta::YAML does not support $type references";
+    }
+  }
+
+  @lines;
+}
 
 
 #####################################################################
 # DEPRECATED API methods:
 
 # Error storage (DEPRECATED as of 1.57)
-our $errstr    = '';
+our $errstr = '';
 
 # Set error
 sub _error {
-    require Carp;
-    $errstr = $_[1];
-    $errstr =~ s/ at \S+ line \d+.*//;
-    Carp::croak( $errstr );
+  require Carp;
+  $errstr = $_[1];
+  $errstr =~ s/ at \S+ line \d+.*//;
+  Carp::croak($errstr);
 }
 
 # Retrieve error
 my $errstr_warned;
+
 sub errstr {
-    require Carp;
-    Carp::carp( "CPAN::Meta::YAML->errstr and \$CPAN::Meta::YAML::errstr is deprecated" )
-        unless $errstr_warned++;
-    $errstr;
+  require Carp;
+  Carp::carp(
+    "CPAN::Meta::YAML->errstr and \$CPAN::Meta::YAML::errstr is deprecated")
+    unless $errstr_warned++;
+  $errstr;
 }
 
 
-
-
 #####################################################################
 # Helper functions. Possibly not needed.
 
@@ -809,17 +841,18 @@ use B;
 # to be locking whenever possible.  People (foolishly) use YAML
 # files for things like session storage, which has race issues.
 my $HAS_FLOCK;
+
 sub _can_flock {
-    if ( defined $HAS_FLOCK ) {
-        return $HAS_FLOCK;
-    }
-    else {
-        require Config;
-        my $c = \%Config::Config;
-        $HAS_FLOCK = grep { $c->{$_} } qw/d_flock d_fcntl_can_lock d_lockf/;
-        require Fcntl if $HAS_FLOCK;
-        return $HAS_FLOCK;
-    }
+  if (defined $HAS_FLOCK) {
+    return $HAS_FLOCK;
+  }
+  else {
+    require Config;
+    my $c = \%Config::Config;
+    $HAS_FLOCK = grep { $c->{$_} } qw/d_flock d_fcntl_can_lock d_lockf/;
+    require Fcntl if $HAS_FLOCK;
+    return $HAS_FLOCK;
+  }
 }
 
 
@@ -829,13 +862,14 @@ sub _can_flock {
 # Use Scalar::Util if possible, otherwise emulate it
 
 use Scalar::Util ();
+
 BEGIN {
-    local $@;
-    if ( eval { Scalar::Util->VERSION(1.18); } ) {
-        *refaddr = *Scalar::Util::refaddr;
-    }
-    else {
-        eval <<'END_PERL';
+  local $@;
+  if (eval { Scalar::Util->VERSION(1.18); }) {
+    *refaddr = *Scalar::Util::refaddr;
+  }
+  else {
+    eval <<'END_PERL';
 # Scalar::Util failed to load or too old
 sub refaddr {
     my $pkg = ref($_[0]) or return undef;
@@ -850,7 +884,7 @@ sub refaddr {
     $i;
 }
 END_PERL
-    }
+  }
 }
 
 delete $CPAN::Meta::YAML::{refaddr};
index 28ea2d757e456392573e5e48ed40c252c0556b0c..342535b0b6bac88b8f8e78798efb5d5513bbf4bd 100644 (file)
@@ -8,7 +8,8 @@ use base qw(Exporter);
 use overload ();
 
 use Carp ();
-use B ();
+use B    ();
+
 #use Devel::Peek;
 
 $JSON::PP::VERSION = '2.27400';
@@ -18,52 +19,52 @@ $JSON::PP::VERSION = '2.27400';
 # instead of hash-access, i tried index-access for speed.
 # but this method is not faster than what i expected. so it will be changed.
 
-use constant P_ASCII                => 0;
-use constant P_LATIN1               => 1;
-use constant P_UTF8                 => 2;
-use constant P_INDENT               => 3;
-use constant P_CANONICAL            => 4;
-use constant P_SPACE_BEFORE         => 5;
-use constant P_SPACE_AFTER          => 6;
-use constant P_ALLOW_NONREF         => 7;
-use constant P_SHRINK               => 8;
-use constant P_ALLOW_BLESSED        => 9;
-use constant P_CONVERT_BLESSED      => 10;
-use constant P_RELAXED              => 11;
-
-use constant P_LOOSE                => 12;
-use constant P_ALLOW_BIGNUM         => 13;
-use constant P_ALLOW_BAREKEY        => 14;
-use constant P_ALLOW_SINGLEQUOTE    => 15;
-use constant P_ESCAPE_SLASH         => 16;
-use constant P_AS_NONBLESSED        => 17;
-
-use constant P_ALLOW_UNKNOWN        => 18;
+use constant P_ASCII           => 0;
+use constant P_LATIN1          => 1;
+use constant P_UTF8            => 2;
+use constant P_INDENT          => 3;
+use constant P_CANONICAL       => 4;
+use constant P_SPACE_BEFORE    => 5;
+use constant P_SPACE_AFTER     => 6;
+use constant P_ALLOW_NONREF    => 7;
+use constant P_SHRINK          => 8;
+use constant P_ALLOW_BLESSED   => 9;
+use constant P_CONVERT_BLESSED => 10;
+use constant P_RELAXED         => 11;
+
+use constant P_LOOSE             => 12;
+use constant P_ALLOW_BIGNUM      => 13;
+use constant P_ALLOW_BAREKEY     => 14;
+use constant P_ALLOW_SINGLEQUOTE => 15;
+use constant P_ESCAPE_SLASH      => 16;
+use constant P_AS_NONBLESSED     => 17;
+
+use constant P_ALLOW_UNKNOWN => 18;
 
 use constant OLD_PERL => $] < 5.008 ? 1 : 0;
 
 BEGIN {
-    my @xs_compati_bit_properties = qw(
-            latin1 ascii utf8 indent canonical space_before space_after allow_nonref shrink
-            allow_blessed convert_blessed relaxed allow_unknown
-    );
-    my @pp_bit_properties = qw(
-            allow_singlequote allow_bignum loose
-            allow_barekey escape_slash as_nonblessed
-    );
-
-    # Perl version check, Unicode handling is enabled?
-    # Helper module sets @JSON::PP::_properties.
-    if ($] < 5.008 ) {
-        my $helper = $] >= 5.006 ? 'JSON::PP::Compat5006' : 'JSON::PP::Compat5005';
-        eval qq| require $helper |;
-        if ($@) { Carp::croak $@; }
-    }
-
-    for my $name (@xs_compati_bit_properties, @pp_bit_properties) {
-        my $flag_name = 'P_' . uc($name);
-
-        eval qq/
+  my @xs_compati_bit_properties = qw(
+    latin1 ascii utf8 indent canonical space_before space_after allow_nonref shrink
+    allow_blessed convert_blessed relaxed allow_unknown
+  );
+  my @pp_bit_properties = qw(
+    allow_singlequote allow_bignum loose
+    allow_barekey escape_slash as_nonblessed
+  );
+
+  # Perl version check, Unicode handling is enabled?
+  # Helper module sets @JSON::PP::_properties.
+  if ($] < 5.008) {
+    my $helper = $] >= 5.006 ? 'JSON::PP::Compat5006' : 'JSON::PP::Compat5005';
+    eval qq| require $helper |;
+    if ($@) { Carp::croak $@; }
+  }
+
+  for my $name (@xs_compati_bit_properties, @pp_bit_properties) {
+    my $flag_name = 'P_' . uc($name);
+
+    eval qq/
             sub $name {
                 my \$enable = defined \$_[1] ? \$_[1] : 1;
 
@@ -81,76 +82,75 @@ BEGIN {
                 \$_[0]->{PROPS}->[$flag_name] ? 1 : '';
             }
         /;
-    }
+  }
 
 }
 
 
-
 # Functions
 
-my %encode_allow_method
-     = map {($_ => 1)} qw/utf8 pretty allow_nonref latin1 self_encode escape_slash
-                          allow_blessed convert_blessed indent indent_length allow_bignum
-                          as_nonblessed
-                        /;
-my %decode_allow_method
-     = map {($_ => 1)} qw/utf8 allow_nonref loose allow_singlequote allow_bignum
-                          allow_barekey max_size relaxed/;
+my %encode_allow_method = map { ($_ => 1) }
+  qw/utf8 pretty allow_nonref latin1 self_encode escape_slash
+  allow_blessed convert_blessed indent indent_length allow_bignum
+  as_nonblessed
+  /;
+my %decode_allow_method = map { ($_ => 1) }
+  qw/utf8 allow_nonref loose allow_singlequote allow_bignum
+  allow_barekey max_size relaxed/;
 
 
-my $JSON; # cache
+my $JSON;    # cache
 
-sub encode_json ($) { # encode
-    ($JSON ||= __PACKAGE__->new->utf8)->encode(@_);
+sub encode_json ($) {    # encode
+  ($JSON ||= __PACKAGE__->new->utf8)->encode(@_);
 }
 
 
-sub decode_json { # decode
-    ($JSON ||= __PACKAGE__->new->utf8)->decode(@_);
+sub decode_json {        # decode
+  ($JSON ||= __PACKAGE__->new->utf8)->decode(@_);
 }
 
 # Obsoleted
 
 sub to_json($) {
-   Carp::croak ("JSON::PP::to_json has been renamed to encode_json.");
+  Carp::croak("JSON::PP::to_json has been renamed to encode_json.");
 }
 
 
 sub from_json($) {
-   Carp::croak ("JSON::PP::from_json has been renamed to decode_json.");
+  Carp::croak("JSON::PP::from_json has been renamed to decode_json.");
 }
 
 
 # Methods
 
 sub new {
-    my $class = shift;
-    my $self  = {
-        max_depth   => 512,
-        max_size    => 0,
-        indent      => 0,
-        FLAGS       => 0,
-        fallback      => sub { encode_error('Invalid value. JSON can only reference.') },
-        indent_length => 3,
-    };
-
-    bless $self, $class;
+  my $class = shift;
+  my $self  = {
+    max_depth => 512,
+    max_size  => 0,
+    indent    => 0,
+    FLAGS     => 0,
+    fallback  => sub { encode_error('Invalid value. JSON can only reference.') },
+    indent_length => 3,
+  };
+
+  bless $self, $class;
 }
 
 
 sub encode {
-    return $_[0]->PP_encode_json($_[1]);
+  return $_[0]->PP_encode_json($_[1]);
 }
 
 
 sub decode {
-    return $_[0]->PP_decode_json($_[1], 0x00000000);
+  return $_[0]->PP_decode_json($_[1], 0x00000000);
 }
 
 
 sub decode_prefix {
-    return $_[0]->PP_decode_json($_[1], 0x00000001);
+  return $_[0]->PP_decode_json($_[1], 0x00000001);
 }
 
 
@@ -160,25 +160,25 @@ sub decode_prefix {
 # pretty printing
 
 sub pretty {
-    my ($self, $v) = @_;
-    my $enable = defined $v ? $v : 1;
+  my ($self, $v) = @_;
+  my $enable = defined $v ? $v : 1;
 
-    if ($enable) { # indent_length(3) for JSON::XS compatibility
-        $self->indent(1)->indent_length(3)->space_before(1)->space_after(1);
-    }
-    else {
-        $self->indent(0)->space_before(0)->space_after(0);
-    }
+  if ($enable) {    # indent_length(3) for JSON::XS compatibility
+    $self->indent(1)->indent_length(3)->space_before(1)->space_after(1);
+  }
+  else {
+    $self->indent(0)->space_before(0)->space_after(0);
+  }
 
-    $self;
+  $self;
 }
 
 # etc
 
 sub max_depth {
-    my $max  = defined $_[1] ? $_[1] : 0x80000000;
-    $_[0]->{max_depth} = $max;
-    $_[0];
+  my $max = defined $_[1] ? $_[1] : 0x80000000;
+  $_[0]->{max_depth} = $max;
+  $_[0];
 }
 
 
@@ -186,9 +186,9 @@ sub get_max_depth { $_[0]->{max_depth}; }
 
 
 sub max_size {
-    my $max  = defined $_[1] ? $_[1] : 0;
-    $_[0]->{max_size} = $max;
-    $_[0];
+  my $max = defined $_[1] ? $_[1] : 0;
+  $_[0]->{max_size} = $max;
+  $_[0];
 }
 
 
@@ -196,40 +196,40 @@ sub get_max_size { $_[0]->{max_size}; }
 
 
 sub filter_json_object {
-    $_[0]->{cb_object} = defined $_[1] ? $_[1] : 0;
-    $_[0]->{F_HOOK} = ($_[0]->{cb_object} or $_[0]->{cb_sk_object}) ? 1 : 0;
-    $_[0];
+  $_[0]->{cb_object} = defined $_[1] ? $_[1] : 0;
+  $_[0]->{F_HOOK} = ($_[0]->{cb_object} or $_[0]->{cb_sk_object}) ? 1 : 0;
+  $_[0];
 }
 
 sub filter_json_single_key_object {
-    if (@_ > 1) {
-        $_[0]->{cb_sk_object}->{$_[1]} = $_[2];
-    }
-    $_[0]->{F_HOOK} = ($_[0]->{cb_object} or $_[0]->{cb_sk_object}) ? 1 : 0;
-    $_[0];
+  if (@_ > 1) {
+    $_[0]->{cb_sk_object}->{$_[1]} = $_[2];
+  }
+  $_[0]->{F_HOOK} = ($_[0]->{cb_object} or $_[0]->{cb_sk_object}) ? 1 : 0;
+  $_[0];
 }
 
 sub indent_length {
-    if (!defined $_[1] or $_[1] > 15 or $_[1] < 0) {
-        Carp::carp "The acceptable range of indent_length() is 0 to 15.";
-    }
-    else {
-        $_[0]->{indent_length} = $_[1];
-    }
-    $_[0];
+  if (!defined $_[1] or $_[1] > 15 or $_[1] < 0) {
+    Carp::carp "The acceptable range of indent_length() is 0 to 15.";
+  }
+  else {
+    $_[0]->{indent_length} = $_[1];
+  }
+  $_[0];
 }
 
 sub get_indent_length {
-    $_[0]->{indent_length};
+  $_[0]->{indent_length};
 }
 
 sub sort_by {
-    $_[0]->{sort_by} = defined $_[1] ? $_[1] : 1;
-    $_[0];
+  $_[0]->{sort_by} = defined $_[1] ? $_[1] : 1;
+  $_[0];
 }
 
 sub allow_bigint {
-    Carp::carp("allow_bigint() is obsoleted. use allow_bignum() insted.");
+  Carp::carp("allow_bigint() is obsoleted. use allow_bignum() insted.");
 }
 
 ###############################
@@ -239,342 +239,360 @@ sub allow_bigint {
 ###
 
 
-{ # Convert
-
-    my $max_depth;
-    my $indent;
-    my $ascii;
-    my $latin1;
-    my $utf8;
-    my $space_before;
-    my $space_after;
-    my $canonical;
-    my $allow_blessed;
-    my $convert_blessed;
-
-    my $indent_length;
-    my $escape_slash;
-    my $bignum;
-    my $as_nonblessed;
+{    # Convert
 
-    my $depth;
-    my $indent_count;
-    my $keysort;
+  my $max_depth;
+  my $indent;
+  my $ascii;
+  my $latin1;
+  my $utf8;
+  my $space_before;
+  my $space_after;
+  my $canonical;
+  my $allow_blessed;
+  my $convert_blessed;
 
+  my $indent_length;
+  my $escape_slash;
+  my $bignum;
+  my $as_nonblessed;
 
-    sub PP_encode_json {
-        my $self = shift;
-        my $obj  = shift;
+  my $depth;
+  my $indent_count;
+  my $keysort;
 
-        $indent_count = 0;
-        $depth        = 0;
 
-        my $idx = $self->{PROPS};
+  sub PP_encode_json {
+    my $self = shift;
+    my $obj  = shift;
 
-        ($ascii, $latin1, $utf8, $indent, $canonical, $space_before, $space_after, $allow_blessed,
-            $convert_blessed, $escape_slash, $bignum, $as_nonblessed)
-         = @{$idx}[P_ASCII .. P_SPACE_AFTER, P_ALLOW_BLESSED, P_CONVERT_BLESSED,
-                    P_ESCAPE_SLASH, P_ALLOW_BIGNUM, P_AS_NONBLESSED];
+    $indent_count = 0;
+    $depth        = 0;
 
-        ($max_depth, $indent_length) = @{$self}{qw/max_depth indent_length/};
+    my $idx = $self->{PROPS};
 
-        $keysort = $canonical ? sub { $a cmp $b } : undef;
+    (
+      $ascii,           $latin1,       $utf8,        $indent,
+      $canonical,       $space_before, $space_after, $allow_blessed,
+      $convert_blessed, $escape_slash, $bignum,      $as_nonblessed
+      )
+      = @{$idx}[
+      P_ASCII .. P_SPACE_AFTER, P_ALLOW_BLESSED,
+      P_CONVERT_BLESSED,        P_ESCAPE_SLASH,
+      P_ALLOW_BIGNUM,           P_AS_NONBLESSED
+      ];
 
-        if ($self->{sort_by}) {
-            $keysort = ref($self->{sort_by}) eq 'CODE' ? $self->{sort_by}
-                     : $self->{sort_by} =~ /\D+/       ? $self->{sort_by}
-                     : sub { $a cmp $b };
-        }
+    ($max_depth, $indent_length) = @{$self}{qw/max_depth indent_length/};
 
-        encode_error("hash- or arrayref expected (not a simple scalar, use allow_nonref to allow this)")
-             if(!ref $obj and !$idx->[ P_ALLOW_NONREF ]);
+    $keysort = $canonical ? sub { $a cmp $b } : undef;
 
-        my $str  = $self->object_to_json($obj);
+    if ($self->{sort_by}) {
+      $keysort
+        = ref($self->{sort_by}) eq 'CODE' ? $self->{sort_by}
+        : $self->{sort_by} =~ /\D+/       ? $self->{sort_by}
+        :                                   sub { $a cmp $b };
+    }
 
-        $str .= "\n" if ( $indent ); # JSON::XS 2.26 compatible
+    encode_error(
+      "hash- or arrayref expected (not a simple scalar, use allow_nonref to allow this)"
+    ) if (!ref $obj and !$idx->[P_ALLOW_NONREF]);
 
-        unless ($ascii or $latin1 or $utf8) {
-            utf8::upgrade($str);
-        }
+    my $str = $self->object_to_json($obj);
 
-        if ($idx->[ P_SHRINK ]) {
-            utf8::downgrade($str, 1);
-        }
+    $str .= "\n" if ($indent);    # JSON::XS 2.26 compatible
 
-        return $str;
+    unless ($ascii or $latin1 or $utf8) {
+      utf8::upgrade($str);
     }
 
+    if ($idx->[P_SHRINK]) {
+      utf8::downgrade($str, 1);
+    }
 
-    sub object_to_json {
-        my ($self, $obj) = @_;
-        my $type = ref($obj);
+    return $str;
+  }
 
-        if($type eq 'HASH'){
-            return $self->hash_to_json($obj);
-        }
-        elsif($type eq 'ARRAY'){
-            return $self->array_to_json($obj);
-        }
-        elsif ($type) { # blessed object?
-            if (blessed($obj)) {
-
-                return $self->value_to_json($obj) if ( $obj->isa('JSON::PP::Boolean') );
-
-                if ( $convert_blessed and $obj->can('TO_JSON') ) {
-                    my $result = $obj->TO_JSON();
-                    if ( defined $result and ref( $result ) ) {
-                        if ( refaddr( $obj ) eq refaddr( $result ) ) {
-                            encode_error( sprintf(
-                                "%s::TO_JSON method returned same object as was passed instead of a new one",
-                                ref $obj
-                            ) );
-                        }
-                    }
-
-                    return $self->object_to_json( $result );
-                }
-
-                return "$obj" if ( $bignum and _is_bignum($obj) );
-                return $self->blessed_to_json($obj) if ($allow_blessed and $as_nonblessed); # will be removed.
 
-                encode_error( sprintf("encountered object '%s', but neither allow_blessed "
-                    . "nor convert_blessed settings are enabled", $obj)
-                ) unless ($allow_blessed);
+  sub object_to_json {
+    my ($self, $obj) = @_;
+    my $type = ref($obj);
 
-                return 'null';
-            }
-            else {
-                return $self->value_to_json($obj);
+    if ($type eq 'HASH') {
+      return $self->hash_to_json($obj);
+    }
+    elsif ($type eq 'ARRAY') {
+      return $self->array_to_json($obj);
+    }
+    elsif ($type) {    # blessed object?
+      if (blessed($obj)) {
+
+        return $self->value_to_json($obj) if ($obj->isa('JSON::PP::Boolean'));
+
+        if ($convert_blessed and $obj->can('TO_JSON')) {
+          my $result = $obj->TO_JSON();
+          if (defined $result and ref($result)) {
+            if (refaddr($obj) eq refaddr($result)) {
+              encode_error(sprintf(
+                "%s::TO_JSON method returned same object as was passed instead of a new one",
+                ref $obj));
             }
+          }
+
+          return $self->object_to_json($result);
         }
-        else{
-            return $self->value_to_json($obj);
-        }
-    }
 
+        return "$obj" if ($bignum and _is_bignum($obj));
+        return $self->blessed_to_json($obj) if ($allow_blessed and $as_nonblessed); # will be removed.
 
-    sub hash_to_json {
-        my ($self, $obj) = @_;
-        my @res;
+        encode_error(sprintf(
+          "encountered object '%s', but neither allow_blessed "
+            . "nor convert_blessed settings are enabled",
+          $obj
+        ))
+          unless ($allow_blessed);
 
-        encode_error("json text or perl structure exceeds maximum nesting level (max_depth set too low?)")
-                                         if (++$depth > $max_depth);
+        return 'null';
+      }
+      else {
+        return $self->value_to_json($obj);
+      }
+    }
+    else {
+      return $self->value_to_json($obj);
+    }
+  }
 
-        my ($pre, $post) = $indent ? $self->_up_indent() : ('', '');
-        my $del = ($space_before ? ' ' : '') . ':' . ($space_after ? ' ' : '');
 
-        for my $k ( _sort( $obj ) ) {
-            if ( OLD_PERL ) { utf8::decode($k) } # key for Perl 5.6 / be optimized
-            push @res, string_to_json( $self, $k )
-                          .  $del
-                          . ( $self->object_to_json( $obj->{$k} ) || $self->value_to_json( $obj->{$k} ) );
-        }
+  sub hash_to_json {
+    my ($self, $obj) = @_;
+    my @res;
+
+    encode_error(
+      "json text or perl structure exceeds maximum nesting level (max_depth set too low?)"
+    ) if (++$depth > $max_depth);
 
-        --$depth;
-        $self->_down_indent() if ($indent);
+    my ($pre, $post) = $indent ? $self->_up_indent() : ('', '');
+    my $del = ($space_before ? ' ' : '') . ':' . ($space_after ? ' ' : '');
 
-        return   '{' . ( @res ? $pre : '' ) . ( @res ? join( ",$pre", @res ) . $post : '' )  . '}';
+    for my $k (_sort($obj)) {
+      if (OLD_PERL) { utf8::decode($k) }    # key for Perl 5.6 / be optimized
+      push @res,
+          string_to_json($self, $k)
+        . $del
+        . ($self->object_to_json($obj->{$k}) || $self->value_to_json($obj->{$k}));
     }
 
+    --$depth;
+    $self->_down_indent() if ($indent);
 
-    sub array_to_json {
-        my ($self, $obj) = @_;
-        my @res;
+    return
+        '{'
+      . (@res ? $pre : '')
+      . (@res ? join(",$pre", @res) . $post : '') . '}';
+  }
 
-        encode_error("json text or perl structure exceeds maximum nesting level (max_depth set too low?)")
-                                         if (++$depth > $max_depth);
 
-        my ($pre, $post) = $indent ? $self->_up_indent() : ('', '');
+  sub array_to_json {
+    my ($self, $obj) = @_;
+    my @res;
 
-        for my $v (@$obj){
-            push @res, $self->object_to_json($v) || $self->value_to_json($v);
-        }
+    encode_error(
+      "json text or perl structure exceeds maximum nesting level (max_depth set too low?)"
+    ) if (++$depth > $max_depth);
 
-        --$depth;
-        $self->_down_indent() if ($indent);
+    my ($pre, $post) = $indent ? $self->_up_indent() : ('', '');
 
-        return '[' . ( @res ? $pre : '' ) . ( @res ? join( ",$pre", @res ) . $post : '' ) . ']';
+    for my $v (@$obj) {
+      push @res, $self->object_to_json($v) || $self->value_to_json($v);
     }
 
+    --$depth;
+    $self->_down_indent() if ($indent);
 
-    sub value_to_json {
-        my ($self, $value) = @_;
+    return
+        '['
+      . (@res ? $pre : '')
+      . (@res ? join(",$pre", @res) . $post : '') . ']';
+  }
 
-        return 'null' if(!defined $value);
 
-        my $b_obj = B::svref_2object(\$value);  # for round trip problem
-        my $flags = $b_obj->FLAGS;
+  sub value_to_json {
+    my ($self, $value) = @_;
 
-        return $value # as is 
-            if $flags & ( B::SVp_IOK | B::SVp_NOK ) and !( $flags & B::SVp_POK ); # SvTYPE is IV or NV?
+    return 'null' if (!defined $value);
 
-        my $type = ref($value);
+    my $b_obj = B::svref_2object(\$value);    # for round trip problem
+    my $flags = $b_obj->FLAGS;
 
-        if(!$type){
-            return string_to_json($self, $value);
-        }
-        elsif( blessed($value) and  $value->isa('JSON::PP::Boolean') ){
-            return $$value == 1 ? 'true' : 'false';
-        }
-        elsif ($type) {
-            if ((overload::StrVal($value) =~ /=(\w+)/)[0]) {
-                return $self->value_to_json("$value");
-            }
+    return $value                             # as is
+      if $flags & (B::SVp_IOK | B::SVp_NOK) and !($flags & B::SVp_POK); # SvTYPE is IV or NV?
 
-            if ($type eq 'SCALAR' and defined $$value) {
-                return   $$value eq '1' ? 'true'
-                       : $$value eq '0' ? 'false'
-                       : $self->{PROPS}->[ P_ALLOW_UNKNOWN ] ? 'null'
-                       : encode_error("cannot encode reference to scalar");
-            }
+    my $type = ref($value);
 
-             if ( $self->{PROPS}->[ P_ALLOW_UNKNOWN ] ) {
-                 return 'null';
-             }
-             else {
-                 if ( $type eq 'SCALAR' or $type eq 'REF' ) {
-                    encode_error("cannot encode reference to scalar");
-                 }
-                 else {
-                    encode_error("encountered $value, but JSON can only represent references to arrays or hashes");
-                 }
-             }
+    if (!$type) {
+      return string_to_json($self, $value);
+    }
+    elsif (blessed($value) and $value->isa('JSON::PP::Boolean')) {
+      return $$value == 1 ? 'true' : 'false';
+    }
+    elsif ($type) {
+      if ((overload::StrVal($value) =~ /=(\w+)/)[0]) {
+        return $self->value_to_json("$value");
+      }
 
+      if ($type eq 'SCALAR' and defined $$value) {
+        return
+            $$value eq '1'                    ? 'true'
+          : $$value eq '0'                    ? 'false'
+          : $self->{PROPS}->[P_ALLOW_UNKNOWN] ? 'null'
+          :   encode_error("cannot encode reference to scalar");
+      }
+
+      if ($self->{PROPS}->[P_ALLOW_UNKNOWN]) {
+        return 'null';
+      }
+      else {
+        if ($type eq 'SCALAR' or $type eq 'REF') {
+          encode_error("cannot encode reference to scalar");
         }
         else {
-            return $self->{fallback}->($value)
-                 if ($self->{fallback} and ref($self->{fallback}) eq 'CODE');
-            return 'null';
+          encode_error(
+            "encountered $value, but JSON can only represent references to arrays or hashes"
+          );
         }
+      }
 
     }
+    else {
+      return $self->{fallback}->($value)
+        if ($self->{fallback} and ref($self->{fallback}) eq 'CODE');
+      return 'null';
+    }
 
+  }
 
-    my %esc = (
-        "\n" => '\n',
-        "\r" => '\r',
-        "\t" => '\t',
-        "\f" => '\f',
-        "\b" => '\b',
-        "\"" => '\"',
-        "\\" => '\\\\',
-        "\'" => '\\\'',
-    );
 
+  my %esc = (
+    "\n" => '\n',
+    "\r" => '\r',
+    "\t" => '\t',
+    "\f" => '\f',
+    "\b" => '\b',
+    "\"" => '\"',
+    "\\" => '\\\\',
+    "\'" => '\\\'',
+  );
 
-    sub string_to_json {
-        my ($self, $arg) = @_;
 
-        $arg =~ s/([\x22\x5c\n\r\t\f\b])/$esc{$1}/g;
-        $arg =~ s/\//\\\//g if ($escape_slash);
-        $arg =~ s/([\x00-\x08\x0b\x0e-\x1f])/'\\u00' . unpack('H2', $1)/eg;
+  sub string_to_json {
+    my ($self, $arg) = @_;
 
-        if ($ascii) {
-            $arg = JSON_PP_encode_ascii($arg);
-        }
+    $arg =~ s/([\x22\x5c\n\r\t\f\b])/$esc{$1}/g;
+    $arg =~ s/\//\\\//g if ($escape_slash);
+    $arg =~ s/([\x00-\x08\x0b\x0e-\x1f])/'\\u00' . unpack('H2', $1)/eg;
 
-        if ($latin1) {
-            $arg = JSON_PP_encode_latin1($arg);
-        }
+    if ($ascii) {
+      $arg = JSON_PP_encode_ascii($arg);
+    }
 
-        if ($utf8) {
-            utf8::encode($arg);
-        }
+    if ($latin1) {
+      $arg = JSON_PP_encode_latin1($arg);
+    }
 
-        return '"' . $arg . '"';
+    if ($utf8) {
+      utf8::encode($arg);
     }
 
+    return '"' . $arg . '"';
+  }
 
-    sub blessed_to_json {
-        my $reftype = reftype($_[1]) || '';
-        if ($reftype eq 'HASH') {
-            return $_[0]->hash_to_json($_[1]);
-        }
-        elsif ($reftype eq 'ARRAY') {
-            return $_[0]->array_to_json($_[1]);
-        }
-        else {
-            return 'null';
-        }
+
+  sub blessed_to_json {
+    my $reftype = reftype($_[1]) || '';
+    if ($reftype eq 'HASH') {
+      return $_[0]->hash_to_json($_[1]);
     }
+    elsif ($reftype eq 'ARRAY') {
+      return $_[0]->array_to_json($_[1]);
+    }
+    else {
+      return 'null';
+    }
+  }
 
 
-    sub encode_error {
-        my $error  = shift;
-        Carp::croak "$error";
-    }
+  sub encode_error {
+    my $error = shift;
+    Carp::croak "$error";
+  }
 
 
-    sub _sort {
-        defined $keysort ? (sort $keysort (keys %{$_[0]})) : keys %{$_[0]};
-    }
+  sub _sort {
+    defined $keysort ? (sort $keysort (keys %{$_[0]})) : keys %{$_[0]};
+  }
 
 
-    sub _up_indent {
-        my $self  = shift;
-        my $space = ' ' x $indent_length;
+  sub _up_indent {
+    my $self  = shift;
+    my $space = ' ' x $indent_length;
 
-        my ($pre,$post) = ('','');
+    my ($pre, $post) = ('', '');
 
-        $post = "\n" . $space x $indent_count;
+    $post = "\n" . $space x $indent_count;
 
-        $indent_count++;
+    $indent_count++;
 
-        $pre = "\n" . $space x $indent_count;
+    $pre = "\n" . $space x $indent_count;
 
-        return ($pre,$post);
-    }
+    return ($pre, $post);
+  }
 
 
-    sub _down_indent { $indent_count--; }
+  sub _down_indent { $indent_count--; }
 
 
-    sub PP_encode_box {
-        {
-            depth        => $depth,
-            indent_count => $indent_count,
-        };
-    }
+  sub PP_encode_box {
+    {depth => $depth, indent_count => $indent_count,};
+  }
 
-} # Convert
+}    # Convert
 
 
 sub _encode_ascii {
-    join('',
-        map {
-            $_ <= 127 ?
-                chr($_) :
-            $_ <= 65535 ?
-                sprintf('\u%04x', $_) : sprintf('\u%x\u%x', _encode_surrogates($_));
-        } unpack('U*', $_[0])
-    );
+  join(
+    '',
+    map {
+          $_ <= 127 ? chr($_)
+        : $_ <= 65535 ? sprintf('\u%04x', $_)
+        :               sprintf('\u%x\u%x', _encode_surrogates($_));
+    } unpack('U*', $_[0])
+  );
 }
 
 
 sub _encode_latin1 {
-    join('',
-        map {
-            $_ <= 255 ?
-                chr($_) :
-            $_ <= 65535 ?
-                sprintf('\u%04x', $_) : sprintf('\u%x\u%x', _encode_surrogates($_));
-        } unpack('U*', $_[0])
-    );
+  join(
+    '',
+    map {
+          $_ <= 255 ? chr($_)
+        : $_ <= 65535 ? sprintf('\u%04x', $_)
+        :               sprintf('\u%x\u%x', _encode_surrogates($_));
+    } unpack('U*', $_[0])
+  );
 }
 
 
-sub _encode_surrogates { # from perlunicode
-    my $uni = $_[0] - 0x10000;
-    return ($uni / 0x400 + 0xD800, $uni % 0x400 + 0xDC00);
+sub _encode_surrogates {    # from perlunicode
+  my $uni = $_[0] - 0x10000;
+  return ($uni / 0x400 + 0xD800, $uni % 0x400 + 0xDC00);
 }
 
 
 sub _is_bignum {
-    $_[0]->isa('Math::BigInt') or $_[0]->isa('Math::BigFloat');
+  $_[0]->isa('Math::BigInt') or $_[0]->isa('Math::BigFloat');
 }
 
 
-
 #
 # JSON => Perl
 #
@@ -582,586 +600,604 @@ sub _is_bignum {
 my $max_intsize;
 
 BEGIN {
-    my $checkint = 1111;
-    for my $d (5..64) {
-        $checkint .= 1;
-        my $int   = eval qq| $checkint |;
-        if ($int =~ /[eE]/) {
-            $max_intsize = $d - 1;
-            last;
-        }
+  my $checkint = 1111;
+  for my $d (5 .. 64) {
+    $checkint .= 1;
+    my $int = eval qq| $checkint |;
+    if ($int =~ /[eE]/) {
+      $max_intsize = $d - 1;
+      last;
     }
+  }
 }
 
-{ # PARSE 
-
-    my %escapes = ( #  by Jeremy Muhlich <jmuhlich [at] bitflood.org>
-        b    => "\x8",
-        t    => "\x9",
-        n    => "\xA",
-        f    => "\xC",
-        r    => "\xD",
-        '\\' => '\\',
-        '"'  => '"',
-        '/'  => '/',
-    );
-
-    my $text; # json data
-    my $at;   # offset
-    my $ch;   # first character
-    my $len;  # text length (changed according to UTF8 or NON UTF8)
-    # INTERNAL
-    my $depth;          # nest counter
-    my $encoding;       # json text encoding
-    my $is_valid_utf8;  # temp variable
-    my $utf8_len;       # utf8 byte length
-    # FLAGS
-    my $utf8;           # must be utf8
-    my $max_depth;      # max nest number of objects and arrays
-    my $max_size;
-    my $relaxed;
-    my $cb_object;
-    my $cb_sk_object;
-
-    my $F_HOOK;
-
-    my $allow_bigint;   # using Math::BigInt
-    my $singlequote;    # loosely quoting
-    my $loose;          # 
-    my $allow_barekey;  # bareKey
-
-    # $opt flag
-    # 0x00000001 .... decode_prefix
-    # 0x10000000 .... incr_parse
-
-    sub PP_decode_json {
-        my ($self, $opt); # $opt is an effective flag during this decode_json.
-
-        ($self, $text, $opt) = @_;
-
-        ($at, $ch, $depth) = (0, '', 0);
-
-        if ( !defined $text or ref $text ) {
-            decode_error("malformed JSON string, neither array, object, number, string or atom");
-        }
+{    # PARSE
+
+  my %escapes = (    #  by Jeremy Muhlich <jmuhlich [at] bitflood.org>
+    b    => "\x8",
+    t    => "\x9",
+    n    => "\xA",
+    f    => "\xC",
+    r    => "\xD",
+    '\\' => '\\',
+    '"'  => '"',
+    '/'  => '/',
+  );
+
+  my $text;             # json data
+  my $at;               # offset
+  my $ch;               # first character
+  my $len;              # text length (changed according to UTF8 or NON UTF8)
+                        # INTERNAL
+  my $depth;            # nest counter
+  my $encoding;         # json text encoding
+  my $is_valid_utf8;    # temp variable
+  my $utf8_len;         # utf8 byte length
+                        # FLAGS
+  my $utf8;             # must be utf8
+  my $max_depth;        # max nest number of objects and arrays
+  my $max_size;
+  my $relaxed;
+  my $cb_object;
+  my $cb_sk_object;
+
+  my $F_HOOK;
+
+  my $allow_bigint;     # using Math::BigInt
+  my $singlequote;      # loosely quoting
+  my $loose;            #
+  my $allow_barekey;    # bareKey
+
+  # $opt flag
+  # 0x00000001 .... decode_prefix
+  # 0x10000000 .... incr_parse
+
+  sub PP_decode_json {
+    my ($self, $opt);    # $opt is an effective flag during this decode_json.
+
+    ($self, $text, $opt) = @_;
+
+    ($at, $ch, $depth) = (0, '', 0);
+
+    if (!defined $text or ref $text) {
+      decode_error(
+        "malformed JSON string, neither array, object, number, string or atom");
+    }
 
-        my $idx = $self->{PROPS};
+    my $idx = $self->{PROPS};
 
-        ($utf8, $relaxed, $loose, $allow_bigint, $allow_barekey, $singlequote)
-            = @{$idx}[P_UTF8, P_RELAXED, P_LOOSE .. P_ALLOW_SINGLEQUOTE];
+    ($utf8, $relaxed, $loose, $allow_bigint, $allow_barekey, $singlequote)
+      = @{$idx}[P_UTF8, P_RELAXED, P_LOOSE .. P_ALLOW_SINGLEQUOTE];
 
-        if ( $utf8 ) {
-            utf8::downgrade( $text, 1 ) or Carp::croak("Wide character in subroutine entry");
-        }
-        else {
-            utf8::upgrade( $text );
-            utf8::encode( $text );
-        }
+    if ($utf8) {
+      utf8::downgrade($text, 1) or Carp::croak("Wide character in subroutine entry");
+    }
+    else {
+      utf8::upgrade($text);
+      utf8::encode($text);
+    }
 
-        $len = length $text;
+    $len = length $text;
+
+    ($max_depth, $max_size, $cb_object, $cb_sk_object, $F_HOOK)
+      = @{$self}{qw/max_depth  max_size cb_object cb_sk_object F_HOOK/};
+
+    if ($max_size > 1) {
+      use bytes;
+      my $bytes = length $text;
+      decode_error(
+        sprintf(
+          "attempted decode of JSON text of %s bytes size, but max_size is set to %s",
+          $bytes, $max_size
+        ),
+        1
+      ) if ($bytes > $max_size);
+    }
 
-        ($max_depth, $max_size, $cb_object, $cb_sk_object, $F_HOOK)
-             = @{$self}{qw/max_depth  max_size cb_object cb_sk_object F_HOOK/};
+    # Currently no effect
+    # should use regexp
+    my @octets = unpack('C4', $text);
+    $encoding
+      = ($octets[0] and $octets[1]) ? 'UTF-8'
+      : (!$octets[0] and $octets[1])  ? 'UTF-16BE'
+      : (!$octets[0] and !$octets[1]) ? 'UTF-32BE'
+      : ($octets[2])  ? 'UTF-16LE'
+      : (!$octets[2]) ? 'UTF-32LE'
+      :                 'unknown';
 
-        if ($max_size > 1) {
-            use bytes;
-            my $bytes = length $text;
-            decode_error(
-                sprintf("attempted decode of JSON text of %s bytes size, but max_size is set to %s"
-                    , $bytes, $max_size), 1
-            ) if ($bytes > $max_size);
-        }
+    white();    # remove head white space
 
-        # Currently no effect
-        # should use regexp
-        my @octets = unpack('C4', $text);
-        $encoding =   ( $octets[0] and  $octets[1]) ? 'UTF-8'
-                    : (!$octets[0] and  $octets[1]) ? 'UTF-16BE'
-                    : (!$octets[0] and !$octets[1]) ? 'UTF-32BE'
-                    : ( $octets[2]                ) ? 'UTF-16LE'
-                    : (!$octets[2]                ) ? 'UTF-32LE'
-                    : 'unknown';
+    my $valid_start = defined $ch;  # Is there a first character for JSON structure?
 
-        white(); # remove head white space
+    my $result = value();
 
-        my $valid_start = defined $ch; # Is there a first character for JSON structure?
+    return undef if (!$result && ($opt & 0x10000000));    # for incr_parse
 
-        my $result = value();
+    decode_error(
+      "malformed JSON string, neither array, object, number, string or atom")
+      unless $valid_start;
 
-        return undef if ( !$result && ( $opt & 0x10000000 ) ); # for incr_parse
+    if (!$idx->[P_ALLOW_NONREF] and !ref $result) {
+      decode_error(
+        'JSON text must be an object or array (but found number, string, true, false or null,'
+          . ' use allow_nonref to allow this)',
+        1
+      );
+    }
 
-        decode_error("malformed JSON string, neither array, object, number, string or atom") unless $valid_start;
+    Carp::croak('something wrong.') if $len < $at;    # we won't arrive here.
 
-        if ( !$idx->[ P_ALLOW_NONREF ] and !ref $result ) {
-                decode_error(
-                'JSON text must be an object or array (but found number, string, true, false or null,'
-                       . ' use allow_nonref to allow this)', 1);
-        }
+    my $consumed = defined $ch ? $at - 1 : $at;       # consumed JSON text length
 
-        Carp::croak('something wrong.') if $len < $at; # we won't arrive here.
+    white();                                          # remove tail white space
 
-        my $consumed = defined $ch ? $at - 1 : $at; # consumed JSON text length
+    if ($ch) {
+      return ($result, $consumed) if ($opt & 0x00000001); # all right if decode_prefix
+      decode_error("garbage after JSON object");
+    }
 
-        white(); # remove tail white space
+    ($opt & 0x00000001) ? ($result, $consumed) : $result;
+  }
 
-        if ( $ch ) {
-            return ( $result, $consumed ) if ($opt & 0x00000001); # all right if decode_prefix
-            decode_error("garbage after JSON object");
-        }
 
-        ( $opt & 0x00000001 ) ? ( $result, $consumed ) : $result;
-    }
+  sub next_chr {
+    return $ch = undef if ($at >= $len);
+    $ch = substr($text, $at++, 1);
+  }
 
 
-    sub next_chr {
-        return $ch = undef if($at >= $len);
-        $ch = substr($text, $at++, 1);
-    }
+  sub value {
+    white();
+    return          if (!defined $ch);
+    return object() if ($ch eq '{');
+    return array()  if ($ch eq '[');
+    return string() if ($ch eq '"' or ($singlequote and $ch eq "'"));
+    return number() if ($ch =~ /[0-9]/ or $ch eq '-');
+    return word();
+  }
 
+  sub string {
+    my ($i, $s, $t, $u);
+    my $utf16;
+    my $is_utf8;
 
-    sub value {
-        white();
-        return          if(!defined $ch);
-        return object() if($ch eq '{');
-        return array()  if($ch eq '[');
-        return string() if($ch eq '"' or ($singlequote and $ch eq "'"));
-        return number() if($ch =~ /[0-9]/ or $ch eq '-');
-        return word();
-    }
+    ($is_valid_utf8, $utf8_len) = ('', 0);
 
-    sub string {
-        my ($i, $s, $t, $u);
-        my $utf16;
-        my $is_utf8;
+    $s = '';    # basically UTF8 flag on
 
-        ($is_valid_utf8, $utf8_len) = ('', 0);
+    if ($ch eq '"' or ($singlequote and $ch eq "'")) {
+      my $boundChar = $ch;
 
-        $s = ''; # basically UTF8 flag on
+    OUTER: while (defined(next_chr())) {
 
-        if($ch eq '"' or ($singlequote and $ch eq "'")){
-            my $boundChar = $ch;
+        if ($ch eq $boundChar) {
+          next_chr();
 
-            OUTER: while( defined(next_chr()) ){
+          if ($utf16) {
+            decode_error("missing low surrogate character in surrogate pair");
+          }
 
-                if($ch eq $boundChar){
-                    next_chr();
+          utf8::decode($s) if ($is_utf8);
 
-                    if ($utf16) {
-                        decode_error("missing low surrogate character in surrogate pair");
-                    }
+          return $s;
+        }
+        elsif ($ch eq '\\') {
+          next_chr();
+          if (exists $escapes{$ch}) {
+            $s .= $escapes{$ch};
+          }
+          elsif ($ch eq 'u') {    # UNICODE handling
+            my $u = '';
 
-                    utf8::decode($s) if($is_utf8);
+            for (1 .. 4) {
+              $ch = next_chr();
+              last OUTER if ($ch !~ /[0-9a-fA-F]/);
+              $u .= $ch;
+            }
 
-                    return $s;
-                }
-                elsif($ch eq '\\'){
-                    next_chr();
-                    if(exists $escapes{$ch}){
-                        $s .= $escapes{$ch};
-                    }
-                    elsif($ch eq 'u'){ # UNICODE handling
-                        my $u = '';
-
-                        for(1..4){
-                            $ch = next_chr();
-                            last OUTER if($ch !~ /[0-9a-fA-F]/);
-                            $u .= $ch;
-                        }
-
-                        # U+D800 - U+DBFF
-                        if ($u =~ /^[dD][89abAB][0-9a-fA-F]{2}/) { # UTF-16 high surrogate?
-                            $utf16 = $u;
-                        }
-                        # U+DC00 - U+DFFF
-                        elsif ($u =~ /^[dD][c-fC-F][0-9a-fA-F]{2}/) { # UTF-16 low surrogate?
-                            unless (defined $utf16) {
-                                decode_error("missing high surrogate character in surrogate pair");
-                            }
-                            $is_utf8 = 1;
-                            $s .= JSON_PP_decode_surrogates($utf16, $u) || next;
-                            $utf16 = undef;
-                        }
-                        else {
-                            if (defined $utf16) {
-                                decode_error("surrogate pair expected");
-                            }
-
-                            if ( ( my $hex = hex( $u ) ) > 127 ) {
-                                $is_utf8 = 1;
-                                $s .= JSON_PP_decode_unicode($u) || next;
-                            }
-                            else {
-                                $s .= chr $hex;
-                            }
-                        }
-
-                    }
-                    else{
-                        unless ($loose) {
-                            $at -= 2;
-                            decode_error('illegal backslash escape sequence in string');
-                        }
-                        $s .= $ch;
-                    }
-                }
-                else{
-
-                    if ( ord $ch  > 127 ) {
-                        unless( $ch = is_valid_utf8($ch) ) {
-                            $at -= 1;
-                            decode_error("malformed UTF-8 character in JSON string");
-                        }
-                        else {
-                            $at += $utf8_len - 1;
-                        }
-
-                        $is_utf8 = 1;
-                    }
-
-                    if (!$loose) {
-                        if ($ch =~ /[\x00-\x1f\x22\x5c]/)  { # '/' ok
-                            $at--;
-                            decode_error('invalid character encountered while parsing JSON string');
-                        }
-                    }
-
-                    $s .= $ch;
-                }
+            # U+D800 - U+DBFF
+            if ($u =~ /^[dD][89abAB][0-9a-fA-F]{2}/) {    # UTF-16 high surrogate?
+              $utf16 = $u;
             }
-        }
 
-        decode_error("unexpected end of string while parsing JSON string");
-    }
+            # U+DC00 - U+DFFF
+            elsif ($u =~ /^[dD][c-fC-F][0-9a-fA-F]{2}/) {    # UTF-16 low surrogate?
+              unless (defined $utf16) {
+                decode_error("missing high surrogate character in surrogate pair");
+              }
+              $is_utf8 = 1;
+              $s .= JSON_PP_decode_surrogates($utf16, $u) || next;
+              $utf16 = undef;
+            }
+            else {
+              if (defined $utf16) {
+                decode_error("surrogate pair expected");
+              }
+
+              if ((my $hex = hex($u)) > 127) {
+                $is_utf8 = 1;
+                $s .= JSON_PP_decode_unicode($u) || next;
+              }
+              else {
+                $s .= chr $hex;
+              }
+            }
 
+          }
+          else {
+            unless ($loose) {
+              $at -= 2;
+              decode_error('illegal backslash escape sequence in string');
+            }
+            $s .= $ch;
+          }
+        }
+        else {
 
-    sub white {
-        while( defined $ch  ){
-            if($ch le ' '){
-                next_chr();
+          if (ord $ch > 127) {
+            unless ($ch = is_valid_utf8($ch)) {
+              $at -= 1;
+              decode_error("malformed UTF-8 character in JSON string");
             }
-            elsif($ch eq '/'){
-                next_chr();
-                if(defined $ch and $ch eq '/'){
-                    1 while(defined(next_chr()) and $ch ne "\n" and $ch ne "\r");
-                }
-                elsif(defined $ch and $ch eq '*'){
-                    next_chr();
-                    while(1){
-                        if(defined $ch){
-                            if($ch eq '*'){
-                                if(defined(next_chr()) and $ch eq '/'){
-                                    next_chr();
-                                    last;
-                                }
-                            }
-                            else{
-                                next_chr();
-                            }
-                        }
-                        else{
-                            decode_error("Unterminated comment");
-                        }
-                    }
-                    next;
-                }
-                else{
-                    $at--;
-                    decode_error("malformed JSON string, neither array, object, number, string or atom");
-                }
+            else {
+              $at += $utf8_len - 1;
             }
-            else{
-                if ($relaxed and $ch eq '#') { # correctly?
-                    pos($text) = $at;
-                    $text =~ /\G([^\n]*(?:\r\n|\r|\n|$))/g;
-                    $at = pos($text);
-                    next_chr;
-                    next;
-                }
 
-                last;
+            $is_utf8 = 1;
+          }
+
+          if (!$loose) {
+            if ($ch =~ /[\x00-\x1f\x22\x5c]/) {    # '/' ok
+              $at--;
+              decode_error('invalid character encountered while parsing JSON string');
             }
+          }
+
+          $s .= $ch;
         }
+      }
     }
 
+    decode_error("unexpected end of string while parsing JSON string");
+  }
 
-    sub array {
-        my $a  = $_[0] || []; # you can use this code to use another array ref object.
-
-        decode_error('json text or perl structure exceeds maximum nesting level (max_depth set too low?)')
-                                                    if (++$depth > $max_depth);
 
+  sub white {
+    while (defined $ch) {
+      if ($ch le ' ') {
         next_chr();
-        white();
-
-        if(defined $ch and $ch eq ']'){
-            --$depth;
-            next_chr();
-            return $a;
+      }
+      elsif ($ch eq '/') {
+        next_chr();
+        if (defined $ch and $ch eq '/') {
+          1 while (defined(next_chr()) and $ch ne "\n" and $ch ne "\r");
+        }
+        elsif (defined $ch and $ch eq '*') {
+          next_chr();
+          while (1) {
+            if (defined $ch) {
+              if ($ch eq '*') {
+                if (defined(next_chr()) and $ch eq '/') {
+                  next_chr();
+                  last;
+                }
+              }
+              else {
+                next_chr();
+              }
+            }
+            else {
+              decode_error("Unterminated comment");
+            }
+          }
+          next;
         }
         else {
-            while(defined($ch)){
-                push @$a, value();
+          $at--;
+          decode_error(
+            "malformed JSON string, neither array, object, number, string or atom");
+        }
+      }
+      else {
+        if ($relaxed and $ch eq '#') {    # correctly?
+          pos($text) = $at;
+          $text =~ /\G([^\n]*(?:\r\n|\r|\n|$))/g;
+          $at = pos($text);
+          next_chr;
+          next;
+        }
 
-                white();
+        last;
+      }
+    }
+  }
 
-                if (!defined $ch) {
-                    last;
-                }
 
-                if($ch eq ']'){
-                    --$depth;
-                    next_chr();
-                    return $a;
-                }
+  sub array {
+    my $a = $_[0] || [];    # you can use this code to use another array ref object.
 
-                if($ch ne ','){
-                    last;
-                }
+    decode_error(
+      'json text or perl structure exceeds maximum nesting level (max_depth set too low?)'
+    ) if (++$depth > $max_depth);
 
-                next_chr();
-                white();
+    next_chr();
+    white();
 
-                if ($relaxed and $ch eq ']') {
-                    --$depth;
-                    next_chr();
-                    return $a;
-                }
+    if (defined $ch and $ch eq ']') {
+      --$depth;
+      next_chr();
+      return $a;
+    }
+    else {
+      while (defined($ch)) {
+        push @$a, value();
 
-            }
-        }
+        white();
 
-        decode_error(", or ] expected while parsing array");
-    }
+        if (!defined $ch) {
+          last;
+        }
 
+        if ($ch eq ']') {
+          --$depth;
+          next_chr();
+          return $a;
+        }
 
-    sub object {
-        my $o = $_[0] || {}; # you can use this code to use another hash ref object.
-        my $k;
+        if ($ch ne ',') {
+          last;
+        }
 
-        decode_error('json text or perl structure exceeds maximum nesting level (max_depth set too low?)')
-                                                if (++$depth > $max_depth);
         next_chr();
         white();
 
-        if(defined $ch and $ch eq '}'){
-            --$depth;
-            next_chr();
-            if ($F_HOOK) {
-                return _json_object_hook($o);
-            }
-            return $o;
+        if ($relaxed and $ch eq ']') {
+          --$depth;
+          next_chr();
+          return $a;
         }
-        else {
-            while (defined $ch) {
-                $k = ($allow_barekey and $ch ne '"' and $ch ne "'") ? bareKey() : string();
-                white();
 
-                if(!defined $ch or $ch ne ':'){
-                    $at--;
-                    decode_error("':' expected");
-                }
+      }
+    }
 
-                next_chr();
-                $o->{$k} = value();
-                white();
-
-                last if (!defined $ch);
-
-                if($ch eq '}'){
-                    --$depth;
-                    next_chr();
-                    if ($F_HOOK) {
-                        return _json_object_hook($o);
-                    }
-                    return $o;
-                }
+    decode_error(", or ] expected while parsing array");
+  }
 
-                if($ch ne ','){
-                    last;
-                }
 
-                next_chr();
-                white();
-
-                if ($relaxed and $ch eq '}') {
-                    --$depth;
-                    next_chr();
-                    if ($F_HOOK) {
-                        return _json_object_hook($o);
-                    }
-                    return $o;
-                }
+  sub object {
+    my $o = $_[0] || {};    # you can use this code to use another hash ref object.
+    my $k;
 
-            }
-
-        }
+    decode_error(
+      'json text or perl structure exceeds maximum nesting level (max_depth set too low?)'
+    ) if (++$depth > $max_depth);
+    next_chr();
+    white();
 
-        $at--;
-        decode_error(", or } expected while parsing object/hash");
+    if (defined $ch and $ch eq '}') {
+      --$depth;
+      next_chr();
+      if ($F_HOOK) {
+        return _json_object_hook($o);
+      }
+      return $o;
     }
+    else {
+      while (defined $ch) {
+        $k = ($allow_barekey and $ch ne '"' and $ch ne "'") ? bareKey() : string();
+        white();
 
-
-    sub bareKey { # doesn't strictly follow Standard ECMA-262 3rd Edition
-        my $key;
-        while($ch =~ /[^\x00-\x23\x25-\x2F\x3A-\x40\x5B-\x5E\x60\x7B-\x7F]/){
-            $key .= $ch;
-            next_chr();
+        if (!defined $ch or $ch ne ':') {
+          $at--;
+          decode_error("':' expected");
         }
-        return $key;
-    }
 
+        next_chr();
+        $o->{$k} = value();
+        white();
 
-    sub word {
-        my $word =  substr($text,$at-1,4);
+        last if (!defined $ch);
 
-        if($word eq 'true'){
-            $at += 3;
-            next_chr;
-            return $JSON::PP::true;
+        if ($ch eq '}') {
+          --$depth;
+          next_chr();
+          if ($F_HOOK) {
+            return _json_object_hook($o);
+          }
+          return $o;
         }
-        elsif($word eq 'null'){
-            $at += 3;
-            next_chr;
-            return undef;
+
+        if ($ch ne ',') {
+          last;
         }
-        elsif($word eq 'fals'){
-            $at += 3;
-            if(substr($text,$at,1) eq 'e'){
-                $at++;
-                next_chr;
-                return $JSON::PP::false;
-            }
+
+        next_chr();
+        white();
+
+        if ($relaxed and $ch eq '}') {
+          --$depth;
+          next_chr();
+          if ($F_HOOK) {
+            return _json_object_hook($o);
+          }
+          return $o;
         }
 
-        $at--; # for decode_error report
+      }
 
-        decode_error("'null' expected")  if ($word =~ /^n/);
-        decode_error("'true' expected")  if ($word =~ /^t/);
-        decode_error("'false' expected") if ($word =~ /^f/);
-        decode_error("malformed JSON string, neither array, object, number, string or atom");
     }
 
+    $at--;
+    decode_error(", or } expected while parsing object/hash");
+  }
 
-    sub number {
-        my $n    = '';
-        my $v;
-        my $is_dec;
 
-        # According to RFC4627, hex or oct digits are invalid.
-        if($ch eq '0'){
-            my $peek = substr($text,$at,1);
-            my $hex  = $peek =~ /[xX]/; # 0 or 1
+  sub bareKey {    # doesn't strictly follow Standard ECMA-262 3rd Edition
+    my $key;
+    while ($ch =~ /[^\x00-\x23\x25-\x2F\x3A-\x40\x5B-\x5E\x60\x7B-\x7F]/) {
+      $key .= $ch;
+      next_chr();
+    }
+    return $key;
+  }
 
-            if($hex){
-                decode_error("malformed number (leading zero must not be followed by another digit)");
-                ($n) = ( substr($text, $at+1) =~ /^([0-9a-fA-F]+)/);
-            }
-            else{ # oct
-                ($n) = ( substr($text, $at) =~ /^([0-7]+)/);
-                if (defined $n and length $n > 1) {
-                    decode_error("malformed number (leading zero must not be followed by another digit)");
-                }
-            }
 
-            if(defined $n and length($n)){
-                if (!$hex and length($n) == 1) {
-                   decode_error("malformed number (leading zero must not be followed by another digit)");
-                }
-                $at += length($n) + $hex;
-                next_chr;
-                return $hex ? hex($n) : oct($n);
-            }
-        }
+  sub word {
+    my $word = substr($text, $at - 1, 4);
 
-        if($ch eq '-'){
-            $n = '-';
-            next_chr;
-            if (!defined $ch or $ch !~ /\d/) {
-                decode_error("malformed number (no digits after initial minus)");
-            }
+    if ($word eq 'true') {
+      $at += 3;
+      next_chr;
+      return $JSON::PP::true;
+    }
+    elsif ($word eq 'null') {
+      $at += 3;
+      next_chr;
+      return undef;
+    }
+    elsif ($word eq 'fals') {
+      $at += 3;
+      if (substr($text, $at, 1) eq 'e') {
+        $at++;
+        next_chr;
+        return $JSON::PP::false;
+      }
+    }
+
+    $at--;    # for decode_error report
+
+    decode_error("'null' expected")  if ($word =~ /^n/);
+    decode_error("'true' expected")  if ($word =~ /^t/);
+    decode_error("'false' expected") if ($word =~ /^f/);
+    decode_error(
+      "malformed JSON string, neither array, object, number, string or atom");
+  }
+
+
+  sub number {
+    my $n = '';
+    my $v;
+    my $is_dec;
+
+    # According to RFC4627, hex or oct digits are invalid.
+    if ($ch eq '0') {
+      my $peek = substr($text, $at, 1);
+      my $hex = $peek =~ /[xX]/;    # 0 or 1
+
+      if ($hex) {
+        decode_error(
+          "malformed number (leading zero must not be followed by another digit)");
+        ($n) = (substr($text, $at + 1) =~ /^([0-9a-fA-F]+)/);
+      }
+      else {    # oct
+        ($n) = (substr($text, $at) =~ /^([0-7]+)/);
+        if (defined $n and length $n > 1) {
+          decode_error(
+            "malformed number (leading zero must not be followed by another digit)");
         }
+      }
 
-        while(defined $ch and $ch =~ /\d/){
-            $n .= $ch;
-            next_chr;
+      if (defined $n and length($n)) {
+        if (!$hex and length($n) == 1) {
+          decode_error(
+            "malformed number (leading zero must not be followed by another digit)");
         }
+        $at += length($n) + $hex;
+        next_chr;
+        return $hex ? hex($n) : oct($n);
+      }
+    }
 
-        if(defined $ch and $ch eq '.'){
-            $n .= '.';
-            $is_dec = 1;
+    if ($ch eq '-') {
+      $n = '-';
+      next_chr;
+      if (!defined $ch or $ch !~ /\d/) {
+        decode_error("malformed number (no digits after initial minus)");
+      }
+    }
 
-            next_chr;
-            if (!defined $ch or $ch !~ /\d/) {
-                decode_error("malformed number (no digits after decimal point)");
-            }
-            else {
-                $n .= $ch;
-            }
+    while (defined $ch and $ch =~ /\d/) {
+      $n .= $ch;
+      next_chr;
+    }
 
-            while(defined(next_chr) and $ch =~ /\d/){
-                $n .= $ch;
-            }
-        }
+    if (defined $ch and $ch eq '.') {
+      $n .= '.';
+      $is_dec = 1;
 
-        if(defined $ch and ($ch eq 'e' or $ch eq 'E')){
-            $n .= $ch;
-            next_chr;
+      next_chr;
+      if (!defined $ch or $ch !~ /\d/) {
+        decode_error("malformed number (no digits after decimal point)");
+      }
+      else {
+        $n .= $ch;
+      }
 
-            if(defined($ch) and ($ch eq '+' or $ch eq '-')){
-                $n .= $ch;
-                next_chr;
-                if (!defined $ch or $ch =~ /\D/) {
-                    decode_error("malformed number (no digits after exp sign)");
-                }
-                $n .= $ch;
-            }
-            elsif(defined($ch) and $ch =~ /\d/){
-                $n .= $ch;
-            }
-            else {
-                decode_error("malformed number (no digits after exp sign)");
-            }
+      while (defined(next_chr) and $ch =~ /\d/) {
+        $n .= $ch;
+      }
+    }
 
-            while(defined(next_chr) and $ch =~ /\d/){
-                $n .= $ch;
-            }
+    if (defined $ch and ($ch eq 'e' or $ch eq 'E')) {
+      $n .= $ch;
+      next_chr;
 
+      if (defined($ch) and ($ch eq '+' or $ch eq '-')) {
+        $n .= $ch;
+        next_chr;
+        if (!defined $ch or $ch =~ /\D/) {
+          decode_error("malformed number (no digits after exp sign)");
         }
+        $n .= $ch;
+      }
+      elsif (defined($ch) and $ch =~ /\d/) {
+        $n .= $ch;
+      }
+      else {
+        decode_error("malformed number (no digits after exp sign)");
+      }
 
-        $v .= $n;
+      while (defined(next_chr) and $ch =~ /\d/) {
+        $n .= $ch;
+      }
 
-        if ($v !~ /[.eE]/ and length $v > $max_intsize) {
-            if ($allow_bigint) { # from Adam Sussman
-                require Math::BigInt;
-                return Math::BigInt->new($v);
-            }
-            else {
-                return "$v";
-            }
-        }
-        elsif ($allow_bigint) {
-            require Math::BigFloat;
-            return Math::BigFloat->new($v);
-        }
+    }
 
-        return $is_dec ? $v/1.0 : 0+$v;
+    $v .= $n;
+
+    if ($v !~ /[.eE]/ and length $v > $max_intsize) {
+      if ($allow_bigint) {    # from Adam Sussman
+        require Math::BigInt;
+        return Math::BigInt->new($v);
+      }
+      else {
+        return "$v";
+      }
+    }
+    elsif ($allow_bigint) {
+      require Math::BigFloat;
+      return Math::BigFloat->new($v);
     }
 
+    return $is_dec ? $v / 1.0 : 0 + $v;
+  }
+
 
-    sub is_valid_utf8 {
+  sub is_valid_utf8 {
 
-        $utf8_len = $_[0] =~ /[\x00-\x7F]/  ? 1
-                  : $_[0] =~ /[\xC2-\xDF]/  ? 2
-                  : $_[0] =~ /[\xE0-\xEF]/  ? 3
-                  : $_[0] =~ /[\xF0-\xF4]/  ? 4
-                  : 0
-                  ;
+    $utf8_len
+      = $_[0] =~ /[\x00-\x7F]/ ? 1
+      : $_[0] =~ /[\xC2-\xDF]/ ? 2
+      : $_[0] =~ /[\xE0-\xEF]/ ? 3
+      : $_[0] =~ /[\xF0-\xF4]/ ? 4
+      :                          0;
 
-        return unless $utf8_len;
+    return unless $utf8_len;
 
-        my $is_valid_utf8 = substr($text, $at - 1, $utf8_len);
+    my $is_valid_utf8 = substr($text, $at - 1, $utf8_len);
 
-        return ( $is_valid_utf8 =~ /^(?:
+    return (
+      $is_valid_utf8 =~ /^(?:
              [\x00-\x7F]
             |[\xC2-\xDF][\x80-\xBF]
             |[\xE0][\xA0-\xBF][\x80-\xBF]
@@ -1171,97 +1207,98 @@ BEGIN {
             |[\xF0][\x90-\xBF][\x80-\xBF][\x80-\xBF]
             |[\xF1-\xF3][\x80-\xBF][\x80-\xBF][\x80-\xBF]
             |[\xF4][\x80-\x8F][\x80-\xBF][\x80-\xBF]
-        )$/x )  ? $is_valid_utf8 : '';
+        )$/x
+    ) ? $is_valid_utf8 : '';
+  }
+
+
+  sub decode_error {
+    my $error  = shift;
+    my $no_rep = shift;
+    my $str    = defined $text ? substr($text, $at) : '';
+    my $mess   = '';
+    my $type = $] >= 5.008 ? 'U*' : $] < 5.006 ? 'C*' : utf8::is_utf8($str) ? 'U*' # 5.6
+      : 'C*';
+
+    for my $c (unpack($type, $str)) {    # emulate pv_uni_display() ?
+      $mess
+        .= $c == 0x07 ? '\a'
+        : $c == 0x09  ? '\t'
+        : $c == 0x0a  ? '\n'
+        : $c == 0x0d  ? '\r'
+        : $c == 0x0c  ? '\f'
+        : $c < 0x20 ? sprintf('\x{%x}', $c)
+        : $c == 0x5c ? '\\\\'
+        : $c < 0x80  ? chr($c)
+        :              sprintf('\x{%x}', $c);
+      if (length $mess >= 20) {
+        $mess .= '...';
+        last;
+      }
     }
 
-
-    sub decode_error {
-        my $error  = shift;
-        my $no_rep = shift;
-        my $str    = defined $text ? substr($text, $at) : '';
-        my $mess   = '';
-        my $type   = $] >= 5.008           ? 'U*'
-                   : $] <  5.006           ? 'C*'
-                   : utf8::is_utf8( $str ) ? 'U*' # 5.6
-                   : 'C*'
-                   ;
-
-        for my $c ( unpack( $type, $str ) ) { # emulate pv_uni_display() ?
-            $mess .=  $c == 0x07 ? '\a'
-                    : $c == 0x09 ? '\t'
-                    : $c == 0x0a ? '\n'
-                    : $c == 0x0d ? '\r'
-                    : $c == 0x0c ? '\f'
-                    : $c <  0x20 ? sprintf('\x{%x}', $c)
-                    : $c == 0x5c ? '\\\\'
-                    : $c <  0x80 ? chr($c)
-                    : sprintf('\x{%x}', $c)
-                    ;
-            if ( length $mess >= 20 ) {
-                $mess .= '...';
-                last;
-            }
-        }
-
-        unless ( length $mess ) {
-            $mess = '(end of string)';
-        }
-
-        Carp::croak (
-            $no_rep ? "$error" : "$error, at character offset $at (before \"$mess\")"
-        );
-
+    unless (length $mess) {
+      $mess = '(end of string)';
     }
 
+    Carp::croak(
+      $no_rep ? "$error" : "$error, at character offset $at (before \"$mess\")");
 
-    sub _json_object_hook {
-        my $o    = $_[0];
-        my @ks = keys %{$o};
+  }
 
-        if ( $cb_sk_object and @ks == 1 and exists $cb_sk_object->{ $ks[0] } and ref $cb_sk_object->{ $ks[0] } ) {
-            my @val = $cb_sk_object->{ $ks[0] }->( $o->{$ks[0]} );
-            if (@val == 1) {
-                return $val[0];
-            }
-        }
 
-        my @val = $cb_object->($o) if ($cb_object);
-        if (@val == 0 or @val > 1) {
-            return $o;
-        }
-        else {
-            return $val[0];
-        }
-    }
+  sub _json_object_hook {
+    my $o  = $_[0];
+    my @ks = keys %{$o};
 
+    if (  $cb_sk_object
+      and @ks == 1
+      and exists $cb_sk_object->{$ks[0]}
+      and ref $cb_sk_object->{$ks[0]})
+    {
+      my @val = $cb_sk_object->{$ks[0]}->($o->{$ks[0]});
+      if (@val == 1) {
+        return $val[0];
+      }
+    }
 
-    sub PP_decode_box {
-        {
-            text    => $text,
-            at      => $at,
-            ch      => $ch,
-            len     => $len,
-            depth   => $depth,
-            encoding      => $encoding,
-            is_valid_utf8 => $is_valid_utf8,
-        };
+    my @val = $cb_object->($o) if ($cb_object);
+    if (@val == 0 or @val > 1) {
+      return $o;
+    }
+    else {
+      return $val[0];
     }
+  }
+
+
+  sub PP_decode_box {
+    {
+      text          => $text,
+      at            => $at,
+      ch            => $ch,
+      len           => $len,
+      depth         => $depth,
+      encoding      => $encoding,
+      is_valid_utf8 => $is_valid_utf8,
+    };
+  }
 
-} # PARSE
+}    # PARSE
 
 
-sub _decode_surrogates { # from perlunicode
-    my $uni = 0x10000 + (hex($_[0]) - 0xD800) * 0x400 + (hex($_[1]) - 0xDC00);
-    my $un  = pack('U*', $uni);
-    utf8::encode( $un );
-    return $un;
+sub _decode_surrogates {    # from perlunicode
+  my $uni = 0x10000 + (hex($_[0]) - 0xD800) * 0x400 + (hex($_[1]) - 0xDC00);
+  my $un = pack('U*', $uni);
+  utf8::encode($un);
+  return $un;
 }
 
 
 sub _decode_unicode {
-    my $un = pack('U', hex shift);
-    utf8::encode( $un );
-    return $un;
+  my $un = pack('U', hex shift);
+  utf8::encode($un);
+  return $un;
 }
 
 #
@@ -1270,23 +1307,24 @@ sub _decode_unicode {
 
 BEGIN {
 
-    unless ( defined &utf8::is_utf8 ) {
-       require Encode;
-       *utf8::is_utf8 = *Encode::is_utf8;
-    }
+  unless (defined &utf8::is_utf8) {
+    require Encode;
+    *utf8::is_utf8 = *Encode::is_utf8;
+  }
 
-    if ( $] >= 5.008 ) {
-        *JSON::PP::JSON_PP_encode_ascii      = \&_encode_ascii;
-        *JSON::PP::JSON_PP_encode_latin1     = \&_encode_latin1;
-        *JSON::PP::JSON_PP_decode_surrogates = \&_decode_surrogates;
-        *JSON::PP::JSON_PP_decode_unicode    = \&_decode_unicode;
-    }
+  if ($] >= 5.008) {
+    *JSON::PP::JSON_PP_encode_ascii      = \&_encode_ascii;
+    *JSON::PP::JSON_PP_encode_latin1     = \&_encode_latin1;
+    *JSON::PP::JSON_PP_decode_surrogates = \&_decode_surrogates;
+    *JSON::PP::JSON_PP_decode_unicode    = \&_decode_unicode;
+  }
+
+  if ($] >= 5.008 and $] < 5.008003) {    # join() in 5.8.0 - 5.8.2 is broken.
 
-    if ($] >= 5.008 and $] < 5.008003) { # join() in 5.8.0 - 5.8.2 is broken.
-        package JSON::PP;
-        require subs;
-        subs->import('join');
-        eval q|
+    package JSON::PP;
+    require subs;
+    subs->import('join');
+    eval q|
             sub join {
                 return '' if (@_ < 2);
                 my $j   = shift;
@@ -1295,25 +1333,25 @@ BEGIN {
                 return $str;
             }
         |;
-    }
+  }
 
 
-    sub JSON::PP::incr_parse {
-        local $Carp::CarpLevel = 1;
-        ( $_[0]->{_incr_parser} ||= JSON::PP::IncrParser->new )->incr_parse( @_ );
-    }
+  sub JSON::PP::incr_parse {
+    local $Carp::CarpLevel = 1;
+    ($_[0]->{_incr_parser} ||= JSON::PP::IncrParser->new)->incr_parse(@_);
+  }
 
 
-    sub JSON::PP::incr_skip {
-        ( $_[0]->{_incr_parser} ||= JSON::PP::IncrParser->new )->incr_skip;
-    }
+  sub JSON::PP::incr_skip {
+    ($_[0]->{_incr_parser} ||= JSON::PP::IncrParser->new)->incr_skip;
+  }
 
 
-    sub JSON::PP::incr_reset {
-        ( $_[0]->{_incr_parser} ||= JSON::PP::IncrParser->new )->incr_reset;
-    }
+  sub JSON::PP::incr_reset {
+    ($_[0]->{_incr_parser} ||= JSON::PP::IncrParser->new)->incr_reset;
+  }
 
-    eval q{
+  eval q{
         sub JSON::PP::incr_text : lvalue {
             $_[0]->{_incr_parser} ||= JSON::PP::IncrParser->new;
 
@@ -1322,9 +1360,9 @@ BEGIN {
             }
             $_[0]->{_incr_parser}->{incr_text};
         }
-    } if ( $] >= 5.006 );
+    } if ($] >= 5.006);
 
-} # Setup for various Perl versions (the code from JSON::PP58)
+}    # Setup for various Perl versions (the code from JSON::PP58)
 
 
 ###############################
@@ -1332,58 +1370,56 @@ BEGIN {
 #
 
 BEGIN {
-    eval 'require Scalar::Util';
-    unless($@){
-        *JSON::PP::blessed = \&Scalar::Util::blessed;
-        *JSON::PP::reftype = \&Scalar::Util::reftype;
-        *JSON::PP::refaddr = \&Scalar::Util::refaddr;
-    }
-    else{ # This code is from Sclar::Util.
-        # warn $@;
-        eval 'sub UNIVERSAL::a_sub_not_likely_to_be_here { ref($_[0]) }';
-        *JSON::PP::blessed = sub {
-            local($@, $SIG{__DIE__}, $SIG{__WARN__});
-            ref($_[0]) ? eval { $_[0]->a_sub_not_likely_to_be_here } : undef;
-        };
-        my %tmap = qw(
-            B::NULL   SCALAR
-            B::HV     HASH
-            B::AV     ARRAY
-            B::CV     CODE
-            B::IO     IO
-            B::GV     GLOB
-            B::REGEXP REGEXP
-        );
-        *JSON::PP::reftype = sub {
-            my $r = shift;
-
-            return undef unless length(ref($r));
-
-            my $t = ref(B::svref_2object($r));
-
-            return
-                exists $tmap{$t} ? $tmap{$t}
-              : length(ref($$r)) ? 'REF'
-              :                    'SCALAR';
-        };
-        *JSON::PP::refaddr = sub {
-          return undef unless length(ref($_[0]));
+  eval 'require Scalar::Util';
+  unless ($@) {
+    *JSON::PP::blessed = \&Scalar::Util::blessed;
+    *JSON::PP::reftype = \&Scalar::Util::reftype;
+    *JSON::PP::refaddr = \&Scalar::Util::refaddr;
+  }
+  else {    # This code is from Sclar::Util.
+            # warn $@;
+    eval 'sub UNIVERSAL::a_sub_not_likely_to_be_here { ref($_[0]) }';
+    *JSON::PP::blessed = sub {
+      local ($@, $SIG{__DIE__}, $SIG{__WARN__});
+      ref($_[0]) ? eval { $_[0]->a_sub_not_likely_to_be_here } : undef;
+    };
+    my %tmap = qw(
+      B::NULL   SCALAR
+      B::HV     HASH
+      B::AV     ARRAY
+      B::CV     CODE
+      B::IO     IO
+      B::GV     GLOB
+      B::REGEXP REGEXP
+    );
+    *JSON::PP::reftype = sub {
+      my $r = shift;
 
-          my $addr;
-          if(defined(my $pkg = blessed($_[0]))) {
-            $addr .= bless $_[0], 'Scalar::Util::Fake';
-            bless $_[0], $pkg;
-          }
-          else {
-            $addr .= $_[0]
-          }
+      return undef unless length(ref($r));
 
-          $addr =~ /0x(\w+)/;
-          local $^W;
-          #no warnings 'portable';
-          hex($1);
-        }
-    }
+      my $t = ref(B::svref_2object($r));
+
+      return exists $tmap{$t} ? $tmap{$t} : length(ref($$r)) ? 'REF' : 'SCALAR';
+    };
+    *JSON::PP::refaddr = sub {
+      return undef unless length(ref($_[0]));
+
+      my $addr;
+      if (defined(my $pkg = blessed($_[0]))) {
+        $addr .= bless $_[0], 'Scalar::Util::Fake';
+        bless $_[0], $pkg;
+      }
+      else {
+        $addr .= $_[0];
+      }
+
+      $addr =~ /0x(\w+)/;
+      local $^W;
+
+      #no warnings 'portable';
+      hex($1);
+      }
+  }
 }
 
 
@@ -1394,8 +1430,8 @@ $JSON::PP::false = do { bless \(my $dummy = 0), "JSON::PP::Boolean" };
 
 sub is_bool { defined $_[0] and UNIVERSAL::isa($_[0], "JSON::PP::Boolean"); }
 
-sub true  { $JSON::PP::true  }
-sub false { $JSON::PP::false }
+sub true  {$JSON::PP::true}
+sub false {$JSON::PP::false}
 sub null  { undef; }
 
 ###############################
@@ -1403,10 +1439,10 @@ sub null  { undef; }
 package JSON::PP::Boolean;
 
 use overload (
-   "0+"     => sub { ${$_[0]} },
-   "++"     => sub { $_[0] = ${$_[0]} + 1 },
-   "--"     => sub { $_[0] = ${$_[0]} - 1 },
-   fallback => 1,
+  "0+" => sub { ${$_[0]} },
+  "++" => sub { $_[0] = ${$_[0]} + 1 },
+  "--" => sub { $_[0] = ${$_[0]} - 1 },
+  fallback => 1,
 );
 
 
@@ -1416,10 +1452,10 @@ package JSON::PP::IncrParser;
 
 use strict;
 
-use constant INCR_M_WS   => 0; # initial whitespace skipping
-use constant INCR_M_STR  => 1; # inside string
-use constant INCR_M_BS   => 2; # inside backslash
-use constant INCR_M_JSON => 3; # outside anything, count nesting
+use constant INCR_M_WS   => 0;    # initial whitespace skipping
+use constant INCR_M_STR  => 1;    # inside string
+use constant INCR_M_BS   => 2;    # inside backslash
+use constant INCR_M_JSON => 3;    # outside anything, count nesting
 use constant INCR_M_C0   => 4;
 use constant INCR_M_C1   => 5;
 
@@ -1428,166 +1464,167 @@ $JSON::PP::IncrParser::VERSION = '1.01';
 my $unpack_format = $] < 5.006 ? 'C*' : 'U*';
 
 sub new {
-    my ( $class ) = @_;
-
-    bless {
-        incr_nest    => 0,
-        incr_text    => undef,
-        incr_parsing => 0,
-        incr_p       => 0,
-    }, $class;
+  my ($class) = @_;
+
+  bless {incr_nest => 0, incr_text => undef, incr_parsing => 0, incr_p => 0,},
+    $class;
 }
 
 
 sub incr_parse {
-    my ( $self, $coder, $text ) = @_;
+  my ($self, $coder, $text) = @_;
 
-    $self->{incr_text} = '' unless ( defined $self->{incr_text} );
+  $self->{incr_text} = '' unless (defined $self->{incr_text});
 
-    if ( defined $text ) {
-        if ( utf8::is_utf8( $text ) and !utf8::is_utf8( $self->{incr_text} ) ) {
-            utf8::upgrade( $self->{incr_text} ) ;
-            utf8::decode( $self->{incr_text} ) ;
-        }
-        $self->{incr_text} .= $text;
+  if (defined $text) {
+    if (utf8::is_utf8($text) and !utf8::is_utf8($self->{incr_text})) {
+      utf8::upgrade($self->{incr_text});
+      utf8::decode($self->{incr_text});
     }
+    $self->{incr_text} .= $text;
+  }
 
 
-    my $max_size = $coder->get_max_size;
+  my $max_size = $coder->get_max_size;
 
-    if ( defined wantarray ) {
+  if (defined wantarray) {
 
-        $self->{incr_mode} = INCR_M_WS unless defined $self->{incr_mode};
+    $self->{incr_mode} = INCR_M_WS unless defined $self->{incr_mode};
 
-        if ( wantarray ) {
-            my @ret;
+    if (wantarray) {
+      my @ret;
 
-            $self->{incr_parsing} = 1;
+      $self->{incr_parsing} = 1;
 
-            do {
-                push @ret, $self->_incr_parse( $coder, $self->{incr_text} );
+      do {
+        push @ret, $self->_incr_parse($coder, $self->{incr_text});
 
-                unless ( !$self->{incr_nest} and $self->{incr_mode} == INCR_M_JSON ) {
-                    $self->{incr_mode} = INCR_M_WS if $self->{incr_mode} != INCR_M_STR;
-                }
-
-            } until ( length $self->{incr_text} >= $self->{incr_p} );
+        unless (!$self->{incr_nest} and $self->{incr_mode} == INCR_M_JSON) {
+          $self->{incr_mode} = INCR_M_WS if $self->{incr_mode} != INCR_M_STR;
+        }
 
-            $self->{incr_parsing} = 0;
+      } until (length $self->{incr_text} >= $self->{incr_p});
 
-            return @ret;
-        }
-        else { # in scalar context
-            $self->{incr_parsing} = 1;
-            my $obj = $self->_incr_parse( $coder, $self->{incr_text} );
-            $self->{incr_parsing} = 0 if defined $obj; # pointed by Martin J. Evans
-            return $obj ? $obj : undef; # $obj is an empty string, parsing was completed.
-        }
+      $self->{incr_parsing} = 0;
 
+      return @ret;
     }
+    else {    # in scalar context
+      $self->{incr_parsing} = 1;
+      my $obj = $self->_incr_parse($coder, $self->{incr_text});
+      $self->{incr_parsing} = 0 if defined $obj;    # pointed by Martin J. Evans
+      return $obj ? $obj : undef;    # $obj is an empty string, parsing was completed.
+    }
+
+  }
 
 }
 
 
 sub _incr_parse {
-    my ( $self, $coder, $text, $skip ) = @_;
-    my $p = $self->{incr_p};
-    my $restore = $p;
-
-    my @obj;
-    my $len = length $text;
-
-    if ( $self->{incr_mode} == INCR_M_WS ) {
-        while ( $len > $p ) {
-            my $s = substr( $text, $p, 1 );
-            $p++ and next if ( 0x20 >= unpack($unpack_format, $s) );
-            $self->{incr_mode} = INCR_M_JSON;
-            last;
-       }
+  my ($self, $coder, $text, $skip) = @_;
+  my $p       = $self->{incr_p};
+  my $restore = $p;
+
+  my @obj;
+  my $len = length $text;
+
+  if ($self->{incr_mode} == INCR_M_WS) {
+    while ($len > $p) {
+      my $s = substr($text, $p, 1);
+      $p++ and next if (0x20 >= unpack($unpack_format, $s));
+      $self->{incr_mode} = INCR_M_JSON;
+      last;
     }
+  }
 
-    while ( $len > $p ) {
-        my $s = substr( $text, $p++, 1 );
+  while ($len > $p) {
+    my $s = substr($text, $p++, 1);
 
-        if ( $s eq '"' ) {
-            if (substr( $text, $p - 2, 1 ) eq '\\' ) {
-                next;
-            }
+    if ($s eq '"') {
+      if (substr($text, $p - 2, 1) eq '\\') {
+        next;
+      }
 
-            if ( $self->{incr_mode} != INCR_M_STR  ) {
-                $self->{incr_mode} = INCR_M_STR;
-            }
-            else {
-                $self->{incr_mode} = INCR_M_JSON;
-                unless ( $self->{incr_nest} ) {
-                    last;
-                }
-            }
+      if ($self->{incr_mode} != INCR_M_STR) {
+        $self->{incr_mode} = INCR_M_STR;
+      }
+      else {
+        $self->{incr_mode} = INCR_M_JSON;
+        unless ($self->{incr_nest}) {
+          last;
         }
+      }
+    }
 
-        if ( $self->{incr_mode} == INCR_M_JSON ) {
-
-            if ( $s eq '[' or $s eq '{' ) {
-                if ( ++$self->{incr_nest} > $coder->get_max_depth ) {
-                    Carp::croak('json text or perl structure exceeds maximum nesting level (max_depth set too low?)');
-                }
-            }
-            elsif ( $s eq ']' or $s eq '}' ) {
-                last if ( --$self->{incr_nest} <= 0 );
-            }
-            elsif ( $s eq '#' ) {
-                while ( $len > $p ) {
-                    last if substr( $text, $p++, 1 ) eq "\n";
-                }
-            }
+    if ($self->{incr_mode} == INCR_M_JSON) {
 
+      if ($s eq '[' or $s eq '{') {
+        if (++$self->{incr_nest} > $coder->get_max_depth) {
+          Carp::croak(
+            'json text or perl structure exceeds maximum nesting level (max_depth set too low?)'
+          );
         }
+      }
+      elsif ($s eq ']' or $s eq '}') {
+        last if (--$self->{incr_nest} <= 0);
+      }
+      elsif ($s eq '#') {
+        while ($len > $p) {
+          last if substr($text, $p++, 1) eq "\n";
+        }
+      }
 
     }
 
-    $self->{incr_p} = $p;
+  }
 
-    return if ( $self->{incr_mode} == INCR_M_STR and not $self->{incr_nest} );
-    return if ( $self->{incr_mode} == INCR_M_JSON and $self->{incr_nest} > 0 );
+  $self->{incr_p} = $p;
 
-    return '' unless ( length substr( $self->{incr_text}, 0, $p ) );
+  return if ($self->{incr_mode} == INCR_M_STR  and not $self->{incr_nest});
+  return if ($self->{incr_mode} == INCR_M_JSON and $self->{incr_nest} > 0);
 
-    local $Carp::CarpLevel = 2;
+  return '' unless (length substr($self->{incr_text}, 0, $p));
 
-    $self->{incr_p} = $restore;
-    $self->{incr_c} = $p;
+  local $Carp::CarpLevel = 2;
 
-    my ( $obj, $tail ) = $coder->PP_decode_json( substr( $self->{incr_text}, 0, $p ), 0x10000001 );
+  $self->{incr_p} = $restore;
+  $self->{incr_c} = $p;
 
-    $self->{incr_text} = substr( $self->{incr_text}, $p );
-    $self->{incr_p} = 0;
+  my ($obj, $tail)
+    = $coder->PP_decode_json(substr($self->{incr_text}, 0, $p), 0x10000001);
 
-    return $obj || '';
+  $self->{incr_text} = substr($self->{incr_text}, $p);
+  $self->{incr_p} = 0;
+
+  return $obj || '';
 }
 
 
 sub incr_text {
-    if ( $_[0]->{incr_parsing} ) {
-        Carp::croak("incr_text cannot be called when the incremental parser already started parsing");
-    }
-    $_[0]->{incr_text};
+  if ($_[0]->{incr_parsing}) {
+    Carp::croak(
+      "incr_text cannot be called when the incremental parser already started parsing"
+    );
+  }
+  $_[0]->{incr_text};
 }
 
 
 sub incr_skip {
-    my $self  = shift;
-    $self->{incr_text} = substr( $self->{incr_text}, $self->{incr_c} );
-    $self->{incr_p} = 0;
+  my $self = shift;
+  $self->{incr_text} = substr($self->{incr_text}, $self->{incr_c});
+  $self->{incr_p} = 0;
 }
 
 
 sub incr_reset {
-    my $self = shift;
-    $self->{incr_text}    = undef;
-    $self->{incr_p}       = 0;
-    $self->{incr_mode}    = 0;
-    $self->{incr_nest}    = 0;
-    $self->{incr_parsing} = 0;
+  my $self = shift;
+  $self->{incr_text}    = undef;
+  $self->{incr_p}       = 0;
+  $self->{incr_mode}    = 0;
+  $self->{incr_nest}    = 0;
+  $self->{incr_parsing} = 0;
 }
 
 ###############################
index 0b1fb19b2c29d93f9e6d5cc72339524544cfe20a..d132e7203d5021d32f4afa7b01e35ba25f9bdadc 100644 (file)
@@ -1,3 +1,4 @@
+
 =head1 NAME
 
 JSON::PP::Boolean - dummy module providing JSON::PP::Boolean
index e352d31620853c5f6d2b164645d6740143e816c6..447d96477383db1e5cef0bd24b98a7fe1896f68e 100644 (file)
@@ -19,16 +19,18 @@ use Carp qw/croak/;
 use File::Spec;
 use IO::File;
 use version 0.87;
+
 BEGIN {
   if ($INC{'Log/Contextual.pm'}) {
     Log::Contextual->import('log_info');
-  } else {
+  }
+  else {
     *log_info = sub (&) { warn $_[0]->() };
   }
 }
 use File::Find qw(find);
 
-my $V_NUM_REGEXP = qr{v?[0-9._]+};  # crudely, a v-string or decimal
+my $V_NUM_REGEXP = qr{v?[0-9._]+};    # crudely, a v-string or decimal
 
 my $PKG_FIRST_WORD_REGEXP = qr{ # the FIRST word in a package name
   [a-zA-Z_]                     # the first word CANNOT start with a digit
@@ -58,7 +60,7 @@ my $PKG_NAME_REGEXP = qr{ # match a package name
   )?
 }x;
 
-my $PKG_REGEXP  = qr{   # match a package declaration
+my $PKG_REGEXP = qr{   # match a package declaration
   ^[\s\{;]*             # intro chars on a line
   package               # the word 'package'
   \s+                   # whitespace
@@ -92,9 +94,9 @@ my $VERS_REGEXP = qr{ # match a VERSION definition
 
 sub new_from_file {
   my $class    = shift;
-  my $filename = File::Spec->rel2abs( shift );
+  my $filename = File::Spec->rel2abs(shift);
 
-  return undef unless defined( $filename ) && -f $filename;
+  return undef unless defined($filename) && -f $filename;
   return $class->_init(undef, $filename, @_);
 }
 
@@ -103,7 +105,7 @@ sub new_from_handle {
   my $handle   = shift;
   my $filename = shift;
   return undef unless defined($handle) && defined($filename);
-  $filename = File::Spec->rel2abs( $filename );
+  $filename = File::Spec->rel2abs($filename);
 
   return $class->_init(undef, $filename, @_, handle => $handle);
 
@@ -111,13 +113,13 @@ sub new_from_handle {
 
 
 sub new_from_module {
-  my $class   = shift;
-  my $module  = shift;
-  my %props   = @_;
+  my $class  = shift;
+  my $module = shift;
+  my %props  = @_;
 
   $props{inc} ||= \@INC;
-  my $filename = $class->find_module_by_name( $module, $props{inc} );
-  return undef unless defined( $filename ) && -f $filename;
+  my $filename = $class->find_module_by_name($module, $props{inc});
+  return undef unless defined($filename) && -f $filename;
   return $class->_init($module, $filename, %props);
 }
 
@@ -125,26 +127,25 @@ sub new_from_module {
 
   my $compare_versions = sub {
     my ($v1, $op, $v2) = @_;
-    $v1 = version->new($v1)
-      unless UNIVERSAL::isa($v1,'version');
+    $v1 = version->new($v1) unless UNIVERSAL::isa($v1, 'version');
 
     my $eval_str = "\$v1 $op \$v2";
     my $result   = eval $eval_str;
-    log_info { "error comparing versions: '$eval_str' $@" } if $@;
+    log_info {"error comparing versions: '$eval_str' $@"} if $@;
 
     return $result;
   };
 
   my $normalize_version = sub {
     my ($version) = @_;
-    if ( $version =~ /[=<>!,]/ ) { # logic, not just version
-      # take as is without modification
+    if ($version =~ /[=<>!,]/) {    # logic, not just version
+                                    # take as is without modification
     }
-    elsif ( ref $version eq 'version' ) { # version objects
+    elsif (ref $version eq 'version') {    # version objects
       $version = $version->is_qv ? $version->normal : $version->stringify;
     }
-    elsif ( $version =~ /^[^v][^.]*\.[^.]+\./ ) { # no leading v, multiple dots
-      # normalize string tuples without "v": "1.2.3" -> "v1.2.3"
+    elsif ($version =~ /^[^v][^.]*\.[^.]+\./) {    # no leading v, multiple dots
+          # normalize string tuples without "v": "1.2.3" -> "v1.2.3"
       $version = "v$version";
     }
     else {
@@ -158,33 +159,31 @@ sub new_from_module {
   my $resolve_module_versions = sub {
     my $packages = shift;
 
-    my( $file, $version );
+    my ($file, $version);
     my $err = '';
-      foreach my $p ( @$packages ) {
-        if ( defined( $p->{version} ) ) {
-       if ( defined( $version ) ) {
-         if ( $compare_versions->( $version, '!=', $p->{version} ) ) {
-           $err .= "  $p->{file} ($p->{version})\n";
-         } else {
-           # same version declared multiple times, ignore
-         }
-       } else {
-         $file    = $p->{file};
-         $version = $p->{version};
-       }
+    foreach my $p (@$packages) {
+      if (defined($p->{version})) {
+        if (defined($version)) {
+          if ($compare_versions->($version, '!=', $p->{version})) {
+            $err .= "  $p->{file} ($p->{version})\n";
+          }
+          else {
+            # same version declared multiple times, ignore
+          }
+        }
+        else {
+          $file    = $p->{file};
+          $version = $p->{version};
         }
-        $file ||= $p->{file} if defined( $p->{file} );
       }
+      $file ||= $p->{file} if defined($p->{file});
+    }
 
-    if ( $err ) {
+    if ($err) {
       $err = "  $file ($version)\n" . $err;
     }
 
-    my %result = (
-      file    => $file,
-      version => $version,
-      err     => $err
-    );
+    my %result = (file => $file, version => $version, err => $err);
 
     return \%result;
   };
@@ -198,16 +197,15 @@ sub new_from_module {
     croak "provides() takes only one of 'dir' or 'files'\n"
       if $args{dir} && $args{files};
 
-    croak "provides() requires a 'version' argument"
-      unless defined $args{version};
+    croak "provides() requires a 'version' argument" unless defined $args{version};
 
     croak "provides() does not support version '$args{version}' metadata"
-        unless grep { $args{version} eq $_ } qw/1.4 2/;
+      unless grep { $args{version} eq $_ } qw/1.4 2/;
 
     $args{prefix} = 'lib' unless defined $args{prefix};
 
     my $p;
-    if ( $args{dir} ) {
+    if ($args{dir}) {
       $p = $class->package_versions_from_directory($args{dir});
     }
     else {
@@ -217,63 +215,66 @@ sub new_from_module {
     }
 
     # Now, fix up files with prefix
-    if ( length $args{prefix} ) { # check in case disabled with q{}
+    if (length $args{prefix}) {    # check in case disabled with q{}
       $args{prefix} =~ s{/$}{};
-      for my $v ( values %$p ) {
+      for my $v (values %$p) {
         $v->{file} = "$args{prefix}/$v->{file}";
       }
     }
 
-    return $p
+    return $p;
   }
 
   sub package_versions_from_directory {
-    my ( $class, $dir, $files ) = @_;
+    my ($class, $dir, $files) = @_;
 
     my @files;
 
-    if ( $files ) {
+    if ($files) {
       @files = @$files;
-    } else {
-      find( {
-        wanted => sub {
-          push @files, $_ if -f $_ && /\.pm$/;
+    }
+    else {
+      find(
+        {
+          wanted => sub {
+            push @files, $_ if -f $_ && /\.pm$/;
+          },
+          no_chdir => 1,
         },
-        no_chdir => 1,
-      }, $dir );
+        $dir
+      );
     }
 
     # First, we enumerate all packages & versions,
     # separating into primary & alternative candidates
-    my( %prime, %alt );
+    my (%prime, %alt);
     foreach my $file (@files) {
-      my $mapped_filename = File::Spec::Unix->abs2rel( $file, $dir );
-      my @path = split( /\//, $mapped_filename );
-      (my $prime_package = join( '::', @path )) =~ s/\.pm$//;
+      my $mapped_filename = File::Spec::Unix->abs2rel($file, $dir);
+      my @path = split(/\//, $mapped_filename);
+      (my $prime_package = join('::', @path)) =~ s/\.pm$//;
 
-      my $pm_info = $class->new_from_file( $file );
+      my $pm_info = $class->new_from_file($file);
 
-      foreach my $package ( $pm_info->packages_inside ) {
-        next if $package eq 'main';  # main can appear numerous times, ignore
-        next if $package eq 'DB';    # special debugging package, ignore
-        next if grep /^_/, split( /::/, $package ); # private package, ignore
+      foreach my $package ($pm_info->packages_inside) {
+        next if $package eq 'main';    # main can appear numerous times, ignore
+        next if $package eq 'DB';      # special debugging package, ignore
+        next if grep /^_/, split(/::/, $package);    # private package, ignore
 
-        my $version = $pm_info->version( $package );
+        my $version = $pm_info->version($package);
 
         $prime_package = $package if lc($prime_package) eq lc($package);
-        if ( $package eq $prime_package ) {
-          if ( exists( $prime{$package} ) ) {
+        if ($package eq $prime_package) {
+          if (exists($prime{$package})) {
             croak "Unexpected conflict in '$package'; multiple versions found.\n";
-          } else {
+          }
+          else {
             $mapped_filename = "$package.pm" if lc("$package.pm") eq lc($mapped_filename);
             $prime{$package}{file} = $mapped_filename;
-            $prime{$package}{version} = $version if defined( $version );
+            $prime{$package}{version} = $version if defined($version);
           }
-        } else {
-          push( @{$alt{$package}}, {
-                                    file    => $mapped_filename,
-                                    version => $version,
-                                   } );
+        }
+        else {
+          push(@{$alt{$package}}, {file => $mapped_filename, version => $version,});
         }
       }
     }
@@ -281,74 +282,75 @@ sub new_from_module {
     # Then we iterate over all the packages found above, identifying conflicts
     # and selecting the "best" candidate for recording the file & version
     # for each package.
-    foreach my $package ( keys( %alt ) ) {
-      my $result = $resolve_module_versions->( $alt{$package} );
+    foreach my $package (keys(%alt)) {
+      my $result = $resolve_module_versions->($alt{$package});
+
+      if (exists($prime{$package})) {    # primary package selected
 
-      if ( exists( $prime{$package} ) ) { # primary package selected
+        if ($result->{err}) {
 
-        if ( $result->{err} ) {
-       # Use the selected primary package, but there are conflicting
-       # errors among multiple alternative packages that need to be
-       # reported
+          # Use the selected primary package, but there are conflicting
+          # errors among multiple alternative packages that need to be
+          # reported
           log_info {
-           "Found conflicting versions for package '$package'\n" .
-           "  $prime{$package}{file} ($prime{$package}{version})\n" .
-           $result->{err}
+            "Found conflicting versions for package '$package'\n"
+              . "  $prime{$package}{file} ($prime{$package}{version})\n"
+              . $result->{err}
           };
 
-        } elsif ( defined( $result->{version} ) ) {
-       # There is a primary package selected, and exactly one
-       # alternative package
-
-       if ( exists( $prime{$package}{version} ) &&
-            defined( $prime{$package}{version} ) ) {
-         # Unless the version of the primary package agrees with the
-         # version of the alternative package, report a conflict
-         if ( $compare_versions->(
-                 $prime{$package}{version}, '!=', $result->{version}
-               )
-             ) {
-
-            log_info {
-              "Found conflicting versions for package '$package'\n" .
-             "  $prime{$package}{file} ($prime{$package}{version})\n" .
-             "  $result->{file} ($result->{version})\n"
-            };
-         }
-
-       } else {
-         # The prime package selected has no version so, we choose to
-         # use any alternative package that does have a version
-         $prime{$package}{file}    = $result->{file};
-         $prime{$package}{version} = $result->{version};
-       }
-
-        } else {
-       # no alt package found with a version, but we have a prime
-       # package so we use it whether it has a version or not
         }
+        elsif (defined($result->{version})) {
+
+          # There is a primary package selected, and exactly one
+          # alternative package
+
+          if (exists($prime{$package}{version}) && defined($prime{$package}{version})) {
 
-      } else { # No primary package was selected, use the best alternative
+            # Unless the version of the primary package agrees with the
+            # version of the alternative package, report a conflict
+            if ($compare_versions->($prime{$package}{version}, '!=', $result->{version})) {
+
+              log_info {
+                "Found conflicting versions for package '$package'\n"
+                  . "  $prime{$package}{file} ($prime{$package}{version})\n"
+                  . "  $result->{file} ($result->{version})\n"
+              };
+            }
+
+          }
+          else {
+            # The prime package selected has no version so, we choose to
+            # use any alternative package that does have a version
+            $prime{$package}{file}    = $result->{file};
+            $prime{$package}{version} = $result->{version};
+          }
 
-        if ( $result->{err} ) {
+        }
+        else {
+          # no alt package found with a version, but we have a prime
+          # package so we use it whether it has a version or not
+        }
+
+      }
+      else {    # No primary package was selected, use the best alternative
+
+        if ($result->{err}) {
           log_info {
-            "Found conflicting versions for package '$package'\n" .
-           $result->{err}
+            "Found conflicting versions for package '$package'\n" . $result->{err}
           };
         }
 
         # Despite possible conflicting versions, we choose to record
         # something rather than nothing
-        $prime{$package}{file}    = $result->{file};
-        $prime{$package}{version} = $result->{version}
-         if defined( $result->{version} );
+        $prime{$package}{file} = $result->{file};
+        $prime{$package}{version} = $result->{version} if defined($result->{version});
       }
     }
 
     # Normalize versions.  Can't use exists() here because of bug in YAML::Node.
     # XXX "bug in YAML::Node" comment seems irrelevant -- dagolden, 2009-05-18
     for (grep defined $_->{version}, values %prime) {
-      $_->{version} = $normalize_version->( $_->{version} );
+      $_->{version} = $normalize_version->($_->{version});
     }
 
     return \%prime;
@@ -360,13 +362,13 @@ sub _init {
   my $class    = shift;
   my $module   = shift;
   my $filename = shift;
-  my %props = @_;
+  my %props    = @_;
 
   my $handle = delete $props{handle};
-  my( %valid_props, @valid_props );
+  my (%valid_props, @valid_props);
   @valid_props = qw( collect_pod inc );
-  @valid_props{@valid_props} = delete( @props{@valid_props} );
-  warn "Unknown properties: @{[keys %props]}\n" if scalar( %props );
+  @valid_props{@valid_props} = delete(@props{@valid_props});
+  warn "Unknown properties: @{[keys %props]}\n" if scalar(%props);
 
   my %data = (
     module       => $module,
@@ -383,22 +385,22 @@ sub _init {
 
   my $self = bless(\%data, $class);
 
-  if ( $handle ) {
+  if ($handle) {
     $self->_parse_fh($handle);
   }
   else {
     $self->_parse_file();
   }
 
-  unless($self->{module} and length($self->{module})) {
+  unless ($self->{module} and length($self->{module})) {
     my ($v, $d, $f) = File::Spec->splitpath($self->{filename});
-    if($f =~ /\.pm$/) {
+    if ($f =~ /\.pm$/) {
       $f =~ s/\..+$//;
       my @candidates = grep /$f$/, @{$self->{packages}};
-      $self->{module} = shift(@candidates); # punt
+      $self->{module} = shift(@candidates);    # punt
     }
     else {
-      if(grep /main/, @{$self->{packages}}) {
+      if (grep /main/, @{$self->{packages}}) {
         $self->{module} = 'main';
       }
       else {
@@ -408,24 +410,22 @@ sub _init {
   }
 
   $self->{version} = $self->{versions}{$self->{module}}
-      if defined( $self->{module} );
+    if defined($self->{module});
 
   return $self;
 }
 
 # class method
 sub _do_find_module {
-  my $class   = shift;
-  my $module  = shift || croak 'find_module_by_name() requires a package name';
-  my $dirs    = shift || \@INC;
+  my $class  = shift;
+  my $module = shift || croak 'find_module_by_name() requires a package name';
+  my $dirs   = shift || \@INC;
 
-  my $file = File::Spec->catfile(split( /::/, $module));
-  foreach my $dir ( @$dirs ) {
+  my $file = File::Spec->catfile(split(/::/, $module));
+  foreach my $dir (@$dirs) {
     my $testfile = File::Spec->catfile($dir, $file);
-    return [ File::Spec->rel2abs( $testfile ), $dir ]
-       if -e $testfile and !-d _;  # For stuff like ExtUtils::xsubpp
-    return [ File::Spec->rel2abs( "$testfile.pm" ), $dir ]
-       if -e "$testfile.pm";
+    return [File::Spec->rel2abs($testfile), $dir] if -e $testfile and !-d _; # For stuff like ExtUtils::xsubpp
+    return [File::Spec->rel2abs("$testfile.pm"), $dir] if -e "$testfile.pm";
   }
   return;
 }
@@ -449,24 +449,23 @@ sub _parse_version_expression {
   my $self = shift;
   my $line = shift;
 
-  my( $sig, $var, $pkg );
-  if ( $line =~ /$VERS_REGEXP/o ) {
-    ( $sig, $var, $pkg ) = $2 ? ( $1, $2, $3 ) : ( $4, $5, $6 );
-    if ( $pkg ) {
+  my ($sig, $var, $pkg);
+  if ($line =~ /$VERS_REGEXP/o) {
+    ($sig, $var, $pkg) = $2 ? ($1, $2, $3) : ($4, $5, $6);
+    if ($pkg) {
       $pkg = ($pkg eq '::') ? 'main' : $pkg;
       $pkg =~ s/::$//;
     }
   }
 
-  return ( $sig, $var, $pkg );
+  return ($sig, $var, $pkg);
 }
 
 sub _parse_file {
   my $self = shift;
 
   my $filename = $self->{filename};
-  my $fh = IO::File->new( $filename )
-    or croak( "Can't open '$filename': $!" );
+  my $fh = IO::File->new($filename) or croak("Can't open '$filename': $!");
 
   $self->_handle_bom($fh, $filename);
 
@@ -482,30 +481,34 @@ sub _handle_bom {
   return unless defined $pos;
 
   my $buf = ' ' x 2;
-  my $count = $fh->read( $buf, length $buf );
+  my $count = $fh->read($buf, length $buf);
   return unless defined $count and $count >= 2;
 
   my $encoding;
-  if ( $buf eq "\x{FE}\x{FF}" ) {
+  if ($buf eq "\x{FE}\x{FF}") {
     $encoding = 'UTF-16BE';
-  } elsif ( $buf eq "\x{FF}\x{FE}" ) {
+  }
+  elsif ($buf eq "\x{FF}\x{FE}") {
     $encoding = 'UTF-16LE';
-  } elsif ( $buf eq "\x{EF}\x{BB}" ) {
+  }
+  elsif ($buf eq "\x{EF}\x{BB}") {
     $buf = ' ';
-    $count = $fh->read( $buf, length $buf );
-    if ( defined $count and $count >= 1 and $buf eq "\x{BF}" ) {
+    $count = $fh->read($buf, length $buf);
+    if (defined $count and $count >= 1 and $buf eq "\x{BF}") {
       $encoding = 'UTF-8';
     }
   }
 
-  if ( defined $encoding ) {
-    if ( "$]" >= 5.008 ) {
+  if (defined $encoding) {
+    if ("$]" >= 5.008) {
+
       # $fh->binmode requires perl 5.10
-      binmode( $fh, ":encoding($encoding)" );
+      binmode($fh, ":encoding($encoding)");
     }
-  } else {
+  }
+  else {
     $fh->setpos($pos)
-      or croak( sprintf "Can't reset position to the top of '$filename'" );
+      or croak(sprintf "Can't reset position to the top of '$filename'");
   }
 
   return $encoding;
@@ -514,53 +517,57 @@ sub _handle_bom {
 sub _parse_fh {
   my ($self, $fh) = @_;
 
-  my( $in_pod, $seen_end, $need_vers ) = ( 0, 0, 0 );
-  my( @pkgs, %vers, %pod, @pod );
-  my $pkg = 'main';
+  my ($in_pod, $seen_end, $need_vers) = (0, 0, 0);
+  my (@pkgs, %vers, %pod, @pod);
+  my $pkg      = 'main';
   my $pod_sect = '';
   my $pod_data = '';
-  my $in_end = 0;
+  my $in_end   = 0;
 
-  while (defined( my $line = <$fh> )) {
+  while (defined(my $line = <$fh>)) {
     my $line_num = $.;
 
-    chomp( $line );
+    chomp($line);
 
     # From toke.c : any line that begins by "=X", where X is an alphabetic
     # character, introduces a POD segment.
     my $is_cut;
-    if ( $line =~ /^=([a-zA-Z].*)/ ) {
+    if ($line =~ /^=([a-zA-Z].*)/) {
       my $cmd = $1;
+
       # Then it goes back to Perl code for "=cutX" where X is a non-alphabetic
       # character (which includes the newline, but here we chomped it away).
       $is_cut = $cmd =~ /^cut(?:[^a-zA-Z]|$)/;
       $in_pod = !$is_cut;
     }
 
-    if ( $in_pod ) {
+    if ($in_pod) {
 
-      if ( $line =~ /^=head[1-4]\s+(.+)\s*$/ ) {
-       push( @pod, $1 );
-       if ( $self->{collect_pod} && length( $pod_data ) ) {
+      if ($line =~ /^=head[1-4]\s+(.+)\s*$/) {
+        push(@pod, $1);
+        if ($self->{collect_pod} && length($pod_data)) {
           $pod{$pod_sect} = $pod_data;
           $pod_data = '';
         }
-       $pod_sect = $1;
+        $pod_sect = $1;
 
-      } elsif ( $self->{collect_pod} ) {
-       $pod_data .= "$line\n";
+      }
+      elsif ($self->{collect_pod}) {
+        $pod_data .= "$line\n";
 
       }
 
-    } elsif ( $is_cut ) {
+    }
+    elsif ($is_cut) {
 
-      if ( $self->{collect_pod} && length( $pod_data ) ) {
+      if ($self->{collect_pod} && length($pod_data)) {
         $pod{$pod_sect} = $pod_data;
         $pod_data = '';
       }
       $pod_sect = '';
 
-    } else {
+    }
+    else {
 
       # Skip after __END__
       next if $in_end;
@@ -576,51 +583,51 @@ sub _parse_fh {
       last if $line eq '__DATA__';
 
       # parse $line to see if it's a $VERSION declaration
-      my( $vers_sig, $vers_fullname, $vers_pkg ) =
-          ($line =~ /VERSION/)
-              ? $self->_parse_version_expression( $line )
-              : ();
+      my ($vers_sig, $vers_fullname, $vers_pkg)
+        = ($line =~ /VERSION/) ? $self->_parse_version_expression($line) : ();
 
-      if ( $line =~ /$PKG_REGEXP/o ) {
+      if ($line =~ /$PKG_REGEXP/o) {
         $pkg = $1;
-        push( @pkgs, $pkg ) unless grep( $pkg eq $_, @pkgs );
-        $vers{$pkg} = $2 unless exists( $vers{$pkg} );
+        push(@pkgs, $pkg) unless grep($pkg eq $_, @pkgs);
+        $vers{$pkg} = $2 unless exists($vers{$pkg});
         $need_vers = defined $2 ? 0 : 1;
 
-      # VERSION defined with full package spec, i.e. $Module::VERSION
-      } elsif ( $vers_fullname && $vers_pkg ) {
-       push( @pkgs, $vers_pkg ) unless grep( $vers_pkg eq $_, @pkgs );
-       $need_vers = 0 if $vers_pkg eq $pkg;
-
-       unless ( defined $vers{$vers_pkg} && length $vers{$vers_pkg} ) {
-         $vers{$vers_pkg} =
-           $self->_evaluate_version_line( $vers_sig, $vers_fullname, $line );
-       }
-
-      # first non-comment line in undeclared package main is VERSION
-      } elsif ( !exists($vers{main}) && $pkg eq 'main' && $vers_fullname ) {
-       $need_vers = 0;
-       my $v =
-         $self->_evaluate_version_line( $vers_sig, $vers_fullname, $line );
-       $vers{$pkg} = $v;
-       push( @pkgs, 'main' );
-
-      # first non-comment line in undeclared package defines package main
-      } elsif ( !exists($vers{main}) && $pkg eq 'main' && $line =~ /\w+/ ) {
-       $need_vers = 1;
-       $vers{main} = '';
-       push( @pkgs, 'main' );
-
-      # only keep if this is the first $VERSION seen
-      } elsif ( $vers_fullname && $need_vers ) {
-       $need_vers = 0;
-       my $v =
-         $self->_evaluate_version_line( $vers_sig, $vers_fullname, $line );
-
-
-       unless ( defined $vers{$pkg} && length $vers{$pkg} ) {
-         $vers{$pkg} = $v;
-       }
+        # VERSION defined with full package spec, i.e. $Module::VERSION
+      }
+      elsif ($vers_fullname && $vers_pkg) {
+        push(@pkgs, $vers_pkg) unless grep($vers_pkg eq $_, @pkgs);
+        $need_vers = 0 if $vers_pkg eq $pkg;
+
+        unless (defined $vers{$vers_pkg} && length $vers{$vers_pkg}) {
+          $vers{$vers_pkg}
+            = $self->_evaluate_version_line($vers_sig, $vers_fullname, $line);
+        }
+
+        # first non-comment line in undeclared package main is VERSION
+      }
+      elsif (!exists($vers{main}) && $pkg eq 'main' && $vers_fullname) {
+        $need_vers = 0;
+        my $v = $self->_evaluate_version_line($vers_sig, $vers_fullname, $line);
+        $vers{$pkg} = $v;
+        push(@pkgs, 'main');
+
+        # first non-comment line in undeclared package defines package main
+      }
+      elsif (!exists($vers{main}) && $pkg eq 'main' && $line =~ /\w+/) {
+        $need_vers = 1;
+        $vers{main} = '';
+        push(@pkgs, 'main');
+
+        # only keep if this is the first $VERSION seen
+      }
+      elsif ($vers_fullname && $need_vers) {
+        $need_vers = 0;
+        my $v = $self->_evaluate_version_line($vers_sig, $vers_fullname, $line);
+
+
+        unless (defined $vers{$pkg} && length $vers{$pkg}) {
+          $vers{$pkg} = $v;
+        }
 
       }
 
@@ -628,29 +635,30 @@ sub _parse_fh {
 
   }
 
-  if ( $self->{collect_pod} && length($pod_data) ) {
+  if ($self->{collect_pod} && length($pod_data)) {
     $pod{$pod_sect} = $pod_data;
   }
 
-  $self->{versions} = \%vers;
-  $self->{packages} = \@pkgs;
-  $self->{pod} = \%pod;
+  $self->{versions}     = \%vers;
+  $self->{packages}     = \@pkgs;
+  $self->{pod}          = \%pod;
   $self->{pod_headings} = \@pod;
 }
 
 {
-my $pn = 0;
-sub _evaluate_version_line {
-  my $self = shift;
-  my( $sigil, $var, $line ) = @_;
+  my $pn = 0;
+
+  sub _evaluate_version_line {
+    my $self = shift;
+    my ($sigil, $var, $line) = @_;
 
-  # Some of this code came from the ExtUtils:: hierarchy.
+    # Some of this code came from the ExtUtils:: hierarchy.
 
-  # We compile into $vsub because 'use version' would cause
-  # compiletime/runtime issues with local()
-  my $vsub;
-  $pn++; # everybody gets their own package
-  my $eval = qq{BEGIN { my \$dummy = q#  Hide from _packages_inside()
+    # We compile into $vsub because 'use version' would cause
+    # compiletime/runtime issues with local()
+    my $vsub;
+    $pn++;    # everybody gets their own package
+    my $eval = qq{BEGIN { my \$dummy = q#  Hide from _packages_inside()
     #; package Module::Metadata::_version::p$pn;
     use version;
     no strict;
@@ -664,38 +672,42 @@ sub _evaluate_version_line {
       };
   }};
 
-  $eval = $1 if $eval =~ m{^(.+)}s;
+    $eval = $1 if $eval =~ m{^(.+)}s;
 
-  local $^W;
-  # Try to get the $VERSION
-  eval $eval;
-  # some modules say $VERSION = $Foo::Bar::VERSION, but Foo::Bar isn't
-  # installed, so we need to hunt in ./lib for it
-  if ( $@ =~ /Can't locate/ && -d 'lib' ) {
-    local @INC = ('lib',@INC);
+    local $^W;
+
+    # Try to get the $VERSION
     eval $eval;
-  }
-  warn "Error evaling version line '$eval' in $self->{filename}: $@\n"
-    if $@;
-  (ref($vsub) eq 'CODE') or
-    croak "failed to build version sub for $self->{filename}";
-  my $result = eval { $vsub->() };
-  croak "Could not get version from $self->{filename} by executing:\n$eval\n\nThe fatal error was: $@\n"
-    if $@;
 
-  # Upgrade it into a version object
-  my $version = eval { _dwim_version($result) };
+    # some modules say $VERSION = $Foo::Bar::VERSION, but Foo::Bar isn't
+    # installed, so we need to hunt in ./lib for it
+    if ($@ =~ /Can't locate/ && -d 'lib') {
+      local @INC = ('lib', @INC);
+      eval $eval;
+    }
+    warn "Error evaling version line '$eval' in $self->{filename}: $@\n" if $@;
+    (ref($vsub) eq 'CODE')
+      or croak "failed to build version sub for $self->{filename}";
+    my $result = eval { $vsub->() };
+    croak
+      "Could not get version from $self->{filename} by executing:\n$eval\n\nThe fatal error was: $@\n"
+      if $@;
 
-  croak "Version '$result' from $self->{filename} does not appear to be valid:\n$eval\n\nThe fatal error was: $@\n"
-    unless defined $version; # "0" is OK!
+    # Upgrade it into a version object
+    my $version = eval { _dwim_version($result) };
 
-  return $version;
-}
+    croak
+      "Version '$result' from $self->{filename} does not appear to be valid:\n$eval\n\nThe fatal error was: $@\n"
+      unless defined $version;    # "0" is OK!
+
+    return $version;
+  }
 }
 
 # Try to DWIM when things fail the lax version test in obvious ways
 {
   my @version_prep = (
+
     # Best case, it just works
     sub { return shift },
 
@@ -703,7 +715,7 @@ sub _evaluate_version_line {
     # trailing junk that is prohibited by lax rules
     sub {
       my $v = shift;
-      $v =~ s{([0-9])[a-z-].*$}{$1}i; # 1.23-alpha or 1.23b
+      $v =~ s{([0-9])[a-z-].*$}{$1}i;    # 1.23-alpha or 1.23b
       return $v;
     },
 
@@ -711,11 +723,11 @@ sub _evaluate_version_line {
     # cause version.pm to think it's an invalid alpha.  So check for that
     # and strip them
     sub {
-      my $v = shift;
-      my $num_dots = () = $v =~ m{(\.)}g;
+      my $v          = shift;
+      my $num_dots   = () = $v =~ m{(\.)}g;
       my $num_unders = () = $v =~ m{(_)}g;
-      my $leading_v = substr($v,0,1) eq 'v';
-      if ( ! $leading_v && $num_dots < 2 && $num_unders > 1 ) {
+      my $leading_v  = substr($v, 0, 1) eq 'v';
+      if (!$leading_v && $num_dots < 2 && $num_unders > 1) {
         $v =~ s{_}{}g;
         $num_unders = () = $v =~ m{(_)}g;
       }
@@ -740,7 +752,7 @@ sub _evaluate_version_line {
     for my $f (@version_prep) {
       $result = $f->($result);
       $version = eval { version->new($result) };
-      $error ||= $@ if $@; # capture first failure
+      $error ||= $@ if $@;    # capture first failure
       last if defined $version;
     }
 
@@ -753,34 +765,34 @@ sub _evaluate_version_line {
 ############################################################
 
 # accessors
-sub name            { $_[0]->{module}            }
+sub name { $_[0]->{module} }
 
-sub filename        { $_[0]->{filename}          }
-sub packages_inside { @{$_[0]->{packages}}       }
-sub pod_inside      { @{$_[0]->{pod_headings}}   }
-sub contains_pod    { 0+@{$_[0]->{pod_headings}} }
+sub filename        { $_[0]->{filename} }
+sub packages_inside { @{$_[0]->{packages}} }
+sub pod_inside      { @{$_[0]->{pod_headings}} }
+sub contains_pod    { 0 + @{$_[0]->{pod_headings}} }
 
 sub version {
-    my $self = shift;
-    my $mod  = shift || $self->{module};
-    my $vers;
-    if ( defined( $mod ) && length( $mod ) &&
-        exists( $self->{versions}{$mod} ) ) {
-       return $self->{versions}{$mod};
-    } else {
-       return undef;
-    }
+  my $self = shift;
+  my $mod = shift || $self->{module};
+  my $vers;
+  if (defined($mod) && length($mod) && exists($self->{versions}{$mod})) {
+    return $self->{versions}{$mod};
+  }
+  else {
+    return undef;
+  }
 }
 
 sub pod {
-    my $self = shift;
-    my $sect = shift;
-    if ( defined( $sect ) && length( $sect ) &&
-        exists( $self->{pod}{$sect} ) ) {
-       return $self->{pod}{$sect};
-    } else {
-       return undef;
-    }
+  my $self = shift;
+  my $sect = shift;
+  if (defined($sect) && length($sect) && exists($self->{pod}{$sect})) {
+    return $self->{pod}{$sect};
+  }
+  else {
+    return undef;
+  }
 }
 
 1;
index 77a0764772aef2506a7aa210dec2bee77ad111e3..55929c59fab2178049c85a2a7e07197bf13bac0e 100644 (file)
@@ -1,6 +1,8 @@
 use 5.008001;
 use strict;
+
 package Parse::CPAN::Meta;
+
 # ABSTRACT: Parse META.yml and META.json CPAN metadata files
 
 our $VERSION = '1.4417';
@@ -8,7 +10,7 @@ our $VERSION = '1.4417';
 use Exporter;
 use Carp 'croak';
 
-our @ISA = qw/Exporter/;
+our @ISA       = qw/Exporter/;
 our @EXPORT_OK = qw/Load LoadFile/;
 
 sub load_file {
@@ -23,19 +25,19 @@ sub load_file {
     return $class->load_json_string($meta);
   }
   else {
-    $class->load_string($meta); # try to detect yaml/json
+    $class->load_string($meta);    # try to detect yaml/json
   }
 }
 
 sub load_string {
   my ($class, $string) = @_;
-  if ( $string =~ /^---/ ) { # looks like YAML
+  if ($string =~ /^---/) {         # looks like YAML
     return $class->load_yaml_string($string);
   }
-  elsif ( $string =~ /^\s*\{/ ) { # looks like JSON
+  elsif ($string =~ /^\s*\{/) {    # looks like JSON
     return $class->load_json_string($string);
   }
-  else { # maybe doc-marker-free YAML
+  else {                           # maybe doc-marker-free YAML
     return $class->load_yaml_string($string);
   }
 }
@@ -45,7 +47,7 @@ sub load_yaml_string {
   my $backend = $class->yaml_backend();
   my $data = eval { no strict 'refs'; &{"$backend\::Load"}($string) };
   croak $@ if $@;
-  return $data || {}; # in case document was valid but empty
+  return $data || {};              # in case document was valid but empty
 }
 
 sub load_json_string {
@@ -56,15 +58,14 @@ sub load_json_string {
 }
 
 sub yaml_backend {
-  if (! defined $ENV{PERL_YAML_BACKEND} ) {
-    _can_load( 'CPAN::Meta::YAML', 0.011 )
+  if (!defined $ENV{PERL_YAML_BACKEND}) {
+    _can_load('CPAN::Meta::YAML', 0.011)
       or croak "CPAN::Meta::YAML 0.011 is not available\n";
     return "CPAN::Meta::YAML";
   }
   else {
     my $backend = $ENV{PERL_YAML_BACKEND};
-    _can_load( $backend )
-      or croak "Could not load PERL_YAML_BACKEND '$backend'\n";
+    _can_load($backend) or croak "Could not load PERL_YAML_BACKEND '$backend'\n";
     $backend->can("Load")
       or croak "PERL_YAML_BACKEND '$backend' does not implement Load()\n";
     return $backend;
@@ -72,51 +73,48 @@ sub yaml_backend {
 }
 
 sub json_backend {
-  if (! $ENV{PERL_JSON_BACKEND} or $ENV{PERL_JSON_BACKEND} eq 'JSON::PP') {
-    _can_load( 'JSON::PP' => 2.27103 )
-      or croak "JSON::PP 2.27103 is not available\n";
+  if (!$ENV{PERL_JSON_BACKEND} or $ENV{PERL_JSON_BACKEND} eq 'JSON::PP') {
+    _can_load('JSON::PP' => 2.27103) or croak "JSON::PP 2.27103 is not available\n";
     return 'JSON::PP';
   }
   else {
-    _can_load( 'JSON' => 2.5 )
-      or croak  "JSON 2.5 is required for " .
-                "\$ENV{PERL_JSON_BACKEND} = '$ENV{PERL_JSON_BACKEND}'\n";
+    _can_load('JSON' => 2.5)
+      or croak "JSON 2.5 is required for "
+      . "\$ENV{PERL_JSON_BACKEND} = '$ENV{PERL_JSON_BACKEND}'\n";
     return "JSON";
   }
 }
 
 sub _slurp {
   require Encode;
-  open my $fh, "<:raw", "$_[0]" ## no critic
+  open my $fh, "<:raw", "$_[0]"    ## no critic
     or die "can't open $_[0] for reading: $!";
   my $content = do { local $/; <$fh> };
   $content = Encode::decode('UTF-8', $content, Encode::PERLQQ());
   return $content;
 }
-  
+
 sub _can_load {
   my ($module, $version) = @_;
   (my $file = $module) =~ s{::}{/}g;
   $file .= ".pm";
   return 1 if $INC{$file};
-  return 0 if exists $INC{$file}; # prior load failed
-  eval { require $file; 1 }
-    or return 0;
-  if ( defined $version ) {
-    eval { $module->VERSION($version); 1 }
-      or return 0;
+  return 0 if exists $INC{$file};    # prior load failed
+  eval { require $file; 1 } or return 0;
+  if (defined $version) {
+    eval { $module->VERSION($version); 1 } or return 0;
   }
   return 1;
 }
 
 # Kept for backwards compatibility only
 # Create an object from a file
-sub LoadFile ($) { ## no critic
+sub LoadFile ($) {                   ## no critic
   return Load(_slurp(shift));
 }
 
 # Parse a document from a string.
-sub Load ($) { ## no critic
+sub Load ($) {                       ## no critic
   require CPAN::Meta::YAML;
   my $object = eval { CPAN::Meta::YAML::Load(shift) };
   croak $@ if $@;
index ea15aafca4ab04fc7b4b1722fd62da7c2d26c3dd..9f7a205d6e4e67393bbe6efe74896e17e580b07f 100644 (file)
@@ -13,14 +13,14 @@ use warnings;
 
 # We want any compile errors to get to the browser, if possible.
 BEGIN {
-    # This makes sure we're in a CGI. mod_perl doesn't support Carp
-    # and Plack reports errors elsewhere.
-    # We cannot call i_am_persistent() from here as its module is
-    # not loaded yet.
-    if ($ENV{SERVER_SOFTWARE} && !($ENV{MOD_PERL} || $ENV{BZ_PLACK})) {
-        require CGI::Carp;
-        CGI::Carp->import('fatalsToBrowser');
-    }
+  # This makes sure we're in a CGI. mod_perl doesn't support Carp
+  # and Plack reports errors elsewhere.
+  # We cannot call i_am_persistent() from here as its module is
+  # not loaded yet.
+  if ($ENV{SERVER_SOFTWARE} && !($ENV{MOD_PERL} || $ENV{BZ_PLACK})) {
+    require CGI::Carp;
+    CGI::Carp->import('fatalsToBrowser');
+  }
 }
 
 use Bugzilla::Auth;
@@ -46,6 +46,7 @@ use File::Basename;
 use File::Spec::Functions;
 use DateTime::TimeZone;
 use Date::Parse;
+
 # Bug 1270550 - Tie::Hash::NamedCapture must be loaded before Safe.
 use Tie::Hash::NamedCapture;
 use Safe;
@@ -57,15 +58,15 @@ use List::Util qw(first);
 
 # 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
@@ -78,117 +79,122 @@ 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();
-    }
-    else {
-        binmode STDOUT, ':utf8';
-    }
+  if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) {
+    init_console();
+  }
+  else {
+    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'};
 
-    my $script = basename($0);
+    # Some modules throw undefined errors (notably File::Spec::Win32) if
+    # PATH is undefined.
+    $ENV{'PATH'} = $path;
+  }
 
-    # 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;
-        }
-        # Plack requires to exit differently.
-        return -1 if $ENV{BZ_PLACK};
-        _shutdown();
+  # 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);
+
+  # 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;
     }
+
+    # Plack requires to exit differently.
+    return -1 if $ENV{BZ_PLACK};
+    _shutdown();
+  }
 }
 
 sub _shutdown {
-    # For security reasons, log out users when Bugzilla is down.
-    # Bugzilla->login() is required to catch the logincookie, if any.
-    my $user = eval { 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();
-
-    # Generate and return a message about the downtime, appropriately
-    # for if we're a command-line script or a CGI script.
-    my $cgi = Bugzilla->cgi;
-    my $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 $cgi->header(-status => 503,
-                           -retry_after => SHUTDOWNHTML_RETRY_AFTER);
-
-        if (!$cgi->param('ctype') || $cgi->param('ctype') eq 'html') {
-            $extension = 'html';
-        }
+
+  # For security reasons, log out users when Bugzilla is down.
+  # Bugzilla->login() is required to catch the logincookie, if any.
+  my $user = eval { 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();
+
+  # Generate and return a message about the downtime, appropriately
+  # for if we're a command-line script or a CGI script.
+  my $cgi       = Bugzilla->cgi;
+  my $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 $cgi->header(-status => 503, -retry_after => SHUTDOWNHTML_RETRY_AFTER);
+
+    if (!$cgi->param('ctype') || $cgi->param('ctype') eq 'html') {
+      $extension = 'html';
     }
+  }
 
-    my $template = Bugzilla->template;
-    my $vars = { message => 'shutdown', userid => $userid };
+  my $template = Bugzilla->template;
+  my $vars = {message => 'shutdown', userid => $userid};
 
-    $template->process("global/message.$extension.tmpl", $vars)
-      or ThrowTemplateError($template->error);
-    exit;
+  $template->process("global/message.$extension.tmpl", $vars)
+    or ThrowTemplateError($template->error);
+  exit;
 }
 
 #####################################################################
@@ -196,463 +202,477 @@ sub _shutdown {
 #####################################################################
 
 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();
+    }
+    my @extensions;
+    foreach my $package (@$extension_packages) {
+      my $extension = $package->new();
+      if ($extension->enabled) {
+        push(@extensions, $extension);
+      }
     }
-    return $cache->{extensions};
+    $cache->{extensions} = \@extensions;
+  }
+  return $cache->{extensions};
 }
 
 sub api_server {
-    my $class = shift;
-    my $cache = $class->request_cache;
-    return $cache->{api_server} if defined $cache->{api_server};
-    require Bugzilla::API::Server;
-    $cache->{api_server} = Bugzilla::API::Server->server;
-    if (my $load_error = $cache->{api_server}->load_error) {
-        my @error_params = ($load_error->{error}, $load_error->{vars});
-        ThrowCodeError(@error_params) if $load_error->{type} eq 'code';
-        ThrowUserError(@error_params) if $load_error->{type} eq 'user';
-    }
-    return $cache->{api_server};
+  my $class = shift;
+  my $cache = $class->request_cache;
+  return $cache->{api_server} if defined $cache->{api_server};
+  require Bugzilla::API::Server;
+  $cache->{api_server} = Bugzilla::API::Server->server;
+  if (my $load_error = $cache->{api_server}->load_error) {
+    my @error_params = ($load_error->{error}, $load_error->{vars});
+    ThrowCodeError(@error_params) if $load_error->{type} eq 'code';
+    ThrowUserError(@error_params) if $load_error->{type} eq 'user';
+  }
+  return $cache->{api_server};
 }
 
 sub feature {
-    my ($class, $feature_name) = @_;
-    return 0 unless CAN_HAS_FEATURE;
-    return 0 unless $class->has_feature($feature_name);
-
-    my $cache = $class->process_cache;
-    my $feature = $cache->{cpan_meta}->feature($feature_name);
-    # Bugzilla expects this will also load all the modules.. so we have to do that.
-    # Later we should put a deprecation warning here, and favor calling has_feature().
-
-    return 1 if $cache->{feature_loaded}{$feature_name};
-    my @modules = $feature->prereqs->merged_requirements->required_modules;
-    Module::Runtime::require_module($_) foreach @modules;
-    $cache->{feature_loaded}{$feature_name} = 1;
-    return 1;
+  my ($class, $feature_name) = @_;
+  return 0 unless CAN_HAS_FEATURE;
+  return 0 unless $class->has_feature($feature_name);
+
+  my $cache   = $class->process_cache;
+  my $feature = $cache->{cpan_meta}->feature($feature_name);
+
+# Bugzilla expects this will also load all the modules.. so we have to do that.
+# Later we should put a deprecation warning here, and favor calling has_feature().
+
+  return 1 if $cache->{feature_loaded}{$feature_name};
+  my @modules = $feature->prereqs->merged_requirements->required_modules;
+  Module::Runtime::require_module($_) foreach @modules;
+  $cache->{feature_loaded}{$feature_name} = 1;
+  return 1;
 }
 
 sub has_feature {
-    my ($class, $feature_name) = @_;
+  my ($class, $feature_name) = @_;
 
-    return 0 unless CAN_HAS_FEATURE;
+  return 0 unless CAN_HAS_FEATURE;
 
-    my $cache = $class->process_cache;
-    return $cache->{feature}->{$feature_name}
-        if exists $cache->{feature}->{$feature_name};
+  my $cache = $class->process_cache;
+  return $cache->{feature}->{$feature_name}
+    if exists $cache->{feature}->{$feature_name};
 
-    my $meta = $cache->{cpan_meta} //= load_cpan_meta();
-    my $feature = eval { $meta->feature($feature_name) }
-      or ThrowCodeError('invalid_feature', { feature => $feature_name });
+  my $meta = $cache->{cpan_meta} //= load_cpan_meta();
+  my $feature = eval { $meta->feature($feature_name) }
+    or ThrowCodeError('invalid_feature', {feature => $feature_name});
 
-    return $cache->{feature}{$feature_name} = check_cpan_feature($feature)->{ok};
+  return $cache->{feature}{$feature_name} = check_cpan_feature($feature)->{ok};
 }
 
 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 markdown {
-    return if !Bugzilla->feature('markdown');
+  return if !Bugzilla->feature('markdown');
 
-    require Bugzilla::Markdown;
-    return $_[0]->request_cache->{markdown} ||= Bugzilla::Markdown->new();
+  require Bugzilla::Markdown;
+  return $_[0]->request_cache->{markdown} ||= Bugzilla::Markdown->new();
 }
 
 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;
+  }
 
-    if (exists $class->request_cache->{error_mode}) {
-        return $class->request_cache->{error_mode};
-    }
-    else {
-        return (i_am_cgi() ? ERROR_MODE_WEBPAGE : ERROR_MODE_DIE);
-    }
+  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);
     }
-
-    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;
+  }
+
+  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');
 }
 
 use constant request_cache => Bugzilla::Install::Util::_cache();
 
 sub clear_request_cache {
-    %{ request_cache() } = ();
+  %{request_cache()} = ();
 }
 
 # This is a per-process cache.  Under mod_cgi it's identical to the
@@ -661,13 +681,13 @@ sub clear_request_cache {
 my $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
@@ -675,30 +695,31 @@ 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();
-    Bugzilla::Bug->CLEANUP() if $INC{"Bugzilla/Bug.pm"};
-
-    # 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();
+  Bugzilla::Bug->CLEANUP() if $INC{"Bugzilla/Bug.pm"};
+
+  # 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 {
-    # This is managed in mod_perl.pl and app.psgi when running
-    # in a persistent environment.
-    _cleanup() unless i_am_persistent();
+
+  # This is managed in mod_perl.pl and app.psgi when running
+  # in a persistent environment.
+  _cleanup() unless i_am_persistent();
 }
 
 # Also managed in mod_perl.pl and app.psgi.
index ea5183bc3b4f2bc6162b5e4085b69781dc9f6221..ebb4b34e5ddb062973a71e45ec3d9baaf99f4e0d 100644 (file)
@@ -16,24 +16,24 @@ use Bugzilla::Hook;
 use parent qw(Exporter);
 
 our @EXPORT_OK = 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
 
-    REST_CONTENT_TYPE_WHITELIST
+  REST_CONTENT_TYPE_WHITELIST
 
-    API_AUTH_HEADERS
+  API_AUTH_HEADERS
 );
 
 # This maps the error names in global/*-error.html.tmpl to numbers.
@@ -55,179 +55,202 @@ our @EXPORT_OK = 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-9
-    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,
-    markdown_disabled => 140,
-    # 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,
-    product_admin_denied                 => 706,
-
-    # 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,
-    component_blank_name                   => 1210,
-    component_blank_description            => 1211,
-    multiple_components_update_not_allowed => 1212,
-    component_need_initialowner            => 1213,
-
-    # 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-9
+  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,
+  markdown_disabled         => 140,
+
+  # 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,
+  product_admin_denied                 => 706,
+
+  # 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,
+  component_blank_name                   => 1210,
+  component_blank_description            => 1211,
+  multiple_components_update_not_allowed => 1212,
+  component_need_initialowner            => 1213,
+
+  # 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
@@ -248,56 +271,56 @@ 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;
 
 # 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
 );
 
 # Custom HTTP headers that can be used for API authentication rather than
 # passing as URL parameters. This is useful if you do not want sensitive
 # information to show up in webserver log files.
 use constant API_AUTH_HEADERS => {
-    X_BUGZILLA_LOGIN    => 'Bugzilla_login',
-    X_BUGZILLA_PASSWORD => 'Bugzilla_password',
-    X_BUGZILLA_API_KEY  => 'Bugzilla_api_key',
-    X_BUGZILLA_TOKEN    => 'Bugzilla_token',
+  X_BUGZILLA_LOGIN    => 'Bugzilla_login',
+  X_BUGZILLA_PASSWORD => 'Bugzilla_password',
+  X_BUGZILLA_API_KEY  => 'Bugzilla_api_key',
+  X_BUGZILLA_TOKEN    => 'Bugzilla_token',
 };
 
 1;
index 5a26851a582d827aab7b2d979e270afa5e0d41ec..dba7fc33ead07ea55dd17847007591b14d4fd8d5 100644 (file)
@@ -27,7 +27,7 @@ use constant DATE_FIELDS => {};
 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.
@@ -46,8 +46,8 @@ use constant REST_RESOURCES => [];
 ##################
 
 sub login_exempt {
-    my ($class, $method) = @_;
-    return $class->LOGIN_EXEMPT->{$method};
+  my ($class, $method) = @_;
+  return $class->LOGIN_EXEMPT->{$method};
 }
 
 1;
index 2c62139a40d2eeba272e74b50125212ae431a314..2861ea7bdb11edbc4efa50b0d14ce85318985803 100644 (file)
@@ -45,202 +45,198 @@ extends 'Bugzilla::API::1_0::Resource';
 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
-    search_comment_tags
+  attachments
+  comments
+  fields
+  get
+  history
+  legal_values
+  search
+  search_comment_tags
 );
 
 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',
 };
 
 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;
 }
 
 ###########
@@ -248,1147 +244,1172 @@ sub REST_RESOURCES {
 ###########
 
 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                => as_int($field->id),
-           type              => as_int($field->type),
-           is_custom         => as_boolean($field->custom),
-           name              => as_string($field->name),
-           display_name      => as_string($field->description),
-           is_mandatory      => as_boolean($field->is_mandatory),
-           is_on_bug_entry   => as_boolean($field->enter_bug),
-           visibility_field  => as_string($visibility_field),
-           visibility_values => as_name_array($vis_values)
-        );
-        if ($has_values) {
-           $field_data{value_field} = as_string($value_field);
-           $field_data{values}      = \@values;
-        };
-        push(@fields_out, filter $params, \%field_data);
+    my %field_data = (
+      id                => as_int($field->id),
+      type              => as_int($field->type),
+      is_custom         => as_boolean($field->custom),
+      name              => as_string($field->name),
+      display_name      => as_string($field->description),
+      is_mandatory      => as_boolean($field->is_mandatory),
+      is_on_bug_entry   => as_boolean($field->enter_bug),
+      visibility_field  => as_string($visibility_field),
+      visibility_values => as_name_array($vis_values)
+    );
+    if ($has_values) {
+      $field_data{value_field} = as_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     => as_string($value->name),
-                    sort_key => as_int($sortkey),
-                    sortkey  => as_int($sortkey), # deprecated
-                    visibility_values => [ as_string($product_name) ],
-                    is_active => as_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              => as_string($value->name),
+            sort_key          => as_int($sortkey),
+            sortkey           => as_int($sortkey),                # deprecated
+            visibility_values => [as_string($product_name)],
+            is_active         => as_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 => as_string($change_to->name),
+          comment_required =>
+            as_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 => as_string($change_to->name),
-                    comment_required => as_boolean(
-                        $change_to->comment_required_on_change_from($status)),
-                );
-                push(@can_change_to, \%change_to_hash);
-            }
-
-            push (@result, {
-               name     => as_string($status->name),
-               is_open  => as_boolean($status->is_open),
-               sort_key => as_int($status->sortkey),
-               sortkey  => as_int($status->sortkey), # deprecated
-               can_change_to => \@can_change_to,
-               visibility_values => [],
-            });
+      push(
+        @result,
+        {
+          name              => as_string($status->name),
+          is_open           => as_boolean($status->is_open),
+          sort_key          => as_int($status->sortkey),
+          sortkey           => as_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        => as_string($value->name),
-               description => as_string($value->description),
-            });
+  elsif ($field_name eq 'keywords') {
+    my @legal_keywords = Bugzilla::Keyword->get_all;
+    foreach my $value (@legal_keywords) {
+      push(
+        @result,
+        {
+          name        => as_string($value->name),
+          description => as_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     => as_string($value->name),
-                sort_key => as_int($value->sortkey),
-                sortkey  => as_int($value->sortkey), # deprecated
-                visibility_values => [
-                    defined $vis_val ? as_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     => as_string($value->name),
+          sort_key => as_int($value->sortkey),
+          sortkey  => as_int($value->sortkey),    # deprecated
+          visibility_values => [defined $vis_val ? as_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 $bug_ids     = $params->{ids}         || [];
+  my $comment_ids = $params->{comment_ids} || [];
 
-    my $dbh  = Bugzilla->switch_to_shadow_db();
-    my $user = Bugzilla->user;
+  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 %bugs;
+  foreach my $bug_id (@$bug_ids) {
+    my $bug = Bugzilla::Bug->check($bug_id);
 
-        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;
-    }
+    # We want the API to always return comments in the same order.
 
-    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
+      = $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});
+      }
+    }
 
-        # 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);
+    # 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);
-        }
+    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 $markdown = $params->{markdown} ? 1 : 0;
-    my $tmpl = $markdown ? '[% text FILTER markdown(bug, { is_markdown => 1 }) %]' : '[% text FILTER markdown(bug) %]';
+  my $markdown = $params->{markdown} ? 1 : 0;
+  my $tmpl
+    = $markdown
+    ? '[% text FILTER markdown(bug, { is_markdown => 1 }) %]'
+    : '[% text FILTER markdown(bug) %]';
 
-    my $html;
-    my $template = Bugzilla->template;
-    $template->process(
-        \$tmpl,
-        { bug => $bug, text => $params->{text}},
-        \$html
-    );
+  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            => as_int($comment->id),
-        bug_id        => as_int($comment->bug_id),
-        creator       => as_login($comment->author->login),
-        time          => as_datetime($comment->creation_ts),
-        creation_time => as_datetime($comment->creation_ts),
-        is_private    => as_boolean($comment->is_private),
-        is_markdown   => as_boolean($comment->is_markdown),
-        text          => as_string($comment->body_full),
-        attachment_id => as_int($attach_id),
-        count         => as_int($comment->count),
-    };
-
-    # Don't load comment tags unless enabled
-    if (Bugzilla->params->{'comment_taggers_group'}) {
-        $comment_hash->{tags} = as_string_array($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            => as_int($comment->id),
+    bug_id        => as_int($comment->bug_id),
+    creator       => as_login($comment->author->login),
+    time          => as_datetime($comment->creation_ts),
+    creation_time => as_datetime($comment->creation_ts),
+    is_private    => as_boolean($comment->is_private),
+    is_markdown   => as_boolean($comment->is_markdown),
+    text          => as_string($comment->body_full),
+    attachment_id => as_int($attach_id),
+    count         => as_int($comment->count),
+  };
+
+  # Don't load comment tags unless enabled
+  if (Bugzilla->params->{'comment_taggers_group'}) {
+    $comment_hash->{tags} = as_string_array($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.
-    Bugzilla->api_server->etag(\@hashes);
+  # Set the ETag before inserting the update tokens
+  # since the tokens will always be unique even if
+  # the data has not changed.
+  Bugzilla->api_server->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
 # 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} = as_int($bug_id);
-
-        my ($activity) = $bug->get_activity(undef, $params->{new_since});
-
-        my @history;
-        foreach my $changeset (@$activity) {
-            my %bug_history;
-            $bug_history{when} = as_datetime($changeset->{when});
-            $bug_history{who}  = as_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} = as_int($attach_id);
-                }
-                $change->{removed}    = as_string($change->{removed});
-                $change->{added}      = as_string($change->{added});
-                $change->{field_name} = as_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} = as_int($bug_id);
+
+    my ($activity) = $bug->get_activity(undef, $params->{new_since});
+
+    my @history;
+    foreach my $changeset (@$activity) {
+      my %bug_history;
+      $bug_history{when}    = as_datetime($changeset->{when});
+      $bug_history{who}     = as_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} = as_int($attach_id);
         }
+        $change->{removed}    = as_string($change->{removed});
+        $change->{added}      = as_string($change->{added});
+        $change->{field_name} = as_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} = as_string_array($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} = as_string_array($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;
+  my ($self, $params) = @_;
+  my $user = Bugzilla->user;
+  my $dbh  = Bugzilla->dbh;
 
-    Bugzilla->switch_to_shadow_db();
+  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;
-    }
+  my $match_params = dclone($params);
+  delete $match_params->{include_fields};
+  delete $match_params->{exclude_fields};
 
-    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};
-    }
-
-    $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+)$/a, 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++;
-    }
+  # 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;
+  }
 
-    # 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+)$/a, 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';
     }
-
-    # Allow the use of order shortcuts similar to web UI
-    if ($match_params->{order}) {
-        # Convert the value of the "order" form field into a list of columns
-        # by which to sort the results.
-        my %order_types = (
-            "Bug Number"   => [ "bug_id" ],
-            "Importance"   => [ "priority", "bug_severity" ],
-            "Assignee"     => [ "assigned_to", "bug_status", "priority", "bug_id" ],
-            "Last Changed" => [ "changeddate", "bug_status", "priority",
-                                "assigned_to", "bug_id" ],
-        );
-        if ($order_types{$match_params->{order}}) {
-            $options{order} = $order_types{$match_params->{order}};
-        }
-        else {
-            $options{order} = [ split(/\s*,\s*/, $match_params->{order}) ];
-        }
+  }
+  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');
+  }
+
+  # Allow the use of order shortcuts similar to web UI
+  if ($match_params->{order}) {
+
+    # Convert the value of the "order" form field into a list of columns
+    # by which to sort the results.
+    my %order_types = (
+      "Bug Number" => ["bug_id"],
+      "Importance" => ["priority", "bug_severity"],
+      "Assignee"   => ["assigned_to", "bug_status", "priority", "bug_id"],
+      "Last Changed" =>
+        ["changeddate", "bug_status", "priority", "assigned_to", "bug_id"],
+    );
+    if ($order_types{$match_params->{order}}) {
+      $options{order} = $order_types{$match_params->{order}};
+    }
+    else {
+      $options{order} = [split(/\s*,\s*/, $match_params->{order})];
     }
+  }
 
-    $options{params} = $match_params;
+  $options{params} = $match_params;
 
-    my $search = new Bugzilla::Search(%options);
-    my ($data) = $search->data;
+  my $search = new Bugzilla::Search(%options);
+  my ($data) = $search->data;
 
-    if (!scalar @$data) {
-        return { bugs => [] };
-    }
+  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;
+# 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 };
+  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->flag_types, $bug->flags);
-            $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->flag_types, $bug->flags);
+      $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               => as_int($bug->id),
+      last_change_time => as_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               => as_int($bug->id),
-            last_change_time => as_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} = as_string_array($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 => as_string($change->[0]),
-                added   => as_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} = as_string_array($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 => as_string($change->[0]), added => as_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};
 
-    if ($flags) {
-        my $product = Bugzilla::Product->check($params->{product});
-        my $component = Bugzilla::Component->check({
-            product => $product,
-            name => $params->{component}
-        });
-        my $flag_types = Bugzilla::FlagType::match({
-            product_id => $product->id,
-            component_id => $component->id,
-            is_active => 1,
-        });
-
-        my(undef, $new_flags) =  extract_flags($flags, $flag_types);
-
-        $params->{flags} = $new_flags;
-    }
+  if ($flags) {
+    my $product = Bugzilla::Product->check($params->{product});
+    my $component
+      = Bugzilla::Component->check({
+      product => $product, name => $params->{component}
+      });
+    my $flag_types
+      = Bugzilla::FlagType::match({
+      product_id => $product->id, component_id => $component->id, is_active => 1,
+      });
 
-    # 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 (undef, $new_flags) = extract_flags($flags, $flag_types);
 
-    my $bug = Bugzilla::Bug->create($params);
+    $params->{flags} = $new_flags;
+  }
 
-    $dbh->bz_commit_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();
 
-    $bug->send_changes();
+  my $bug = Bugzilla::Bug->create($params);
+
+  $dbh->bz_commit_transaction();
 
-    return { id => as_int($bug->bug_id) };
+  $bug->send_changes();
+
+  return {id => as_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)) {
+
+    # 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});
 
-        $values = [map { $_->name } @objects];
+    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, as_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, as_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, $attachment->flag_types, $attachment->flags);
-            $attachment->set_flags($old_flags, $new_flags);
-        }
-
-        $attachment->update($timestamp);
-        my $comment = $params->{comment} || '';
+  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},
+    });
 
-        my $is_markdown = 0;
-        if (ref $params->{comment} eq 'HASH') {
-            $is_markdown = $params->{comment}->{is_markdown};
-            $comment     = $params->{comment}->{body};
-        }
+    if ($flags) {
+      my ($old_flags, $new_flags)
+        = extract_flags($flags, $attachment->flag_types, $attachment->flags);
+      $attachment->set_flags($old_flags, $new_flags);
+    }
 
-        ThrowUserError('markdown_disabled')
-            if $is_markdown && !Bugzilla->user->use_markdown();
+    $attachment->update($timestamp);
+    my $comment = $params->{comment} || '';
 
-        $attachment->bug->add_comment($comment,
-            { is_markdown => $is_markdown,
-              isprivate   => $attachment->isprivate,
-              type        => CMT_ATTACHMENT_CREATED,
-              extra_data  => $attachment->id });
-        push(@created, $attachment);
+    my $is_markdown = 0;
+    if (ref $params->{comment} eq 'HASH') {
+      $is_markdown = $params->{comment}->{is_markdown};
+      $comment     = $params->{comment}->{body};
     }
-    $_->bug->update($timestamp) foreach @created;
-    $dbh->bz_commit_transaction();
 
-    $_->send_changes() foreach @bugs;
+    ThrowUserError('markdown_disabled')
+      if $is_markdown && !Bugzilla->user->use_markdown();
+
+    $attachment->bug->add_comment(
+      $comment,
+      {
+        is_markdown => $is_markdown,
+        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 $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};
+  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};
+  my $is_markdown = 0;
+
+  if (ref $comment eq 'HASH') {
+    $is_markdown = $comment->{is_markdown};
+    $comment     = $comment->{body};
+  }
+
+  ThrowUserError('markdown_disabled') if $is_markdown && !$user->use_markdown();
+
+  # Update the values
+  foreach my $attachment (@attachments) {
+    my ($update_flags, $new_flags)
+      = $flags
+      ? extract_flags($flags, $attachment->flag_types, $attachment->flags)
+      : ([], []);
+    if ($attachment->validate_can_edit) {
+      $attachment->set_all($params);
+      $attachment->set_flags($update_flags, $new_flags) if $flags;
     }
-
-    $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;
+    elsif (scalar @$update_flags && !scalar(@$new_flags) && !scalar keys %$params) {
+
+      # Requestees can set flags targeted 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, []);
     }
-
-    my $flags = delete $params->{flags};
-    my $comment = delete $params->{comment};
-    my $is_markdown = 0;
-
-    if (ref $comment eq 'HASH') {
-        $is_markdown = $comment->{is_markdown};
-        $comment     = $comment->{body};
+    else {
+      ThrowUserError("illegal_attachment_edit", {attach_id => $attachment->id});
     }
+  }
 
-    ThrowUserError('markdown_disabled')
-        if $is_markdown && !$user->use_markdown();
-
-    # Update the values
-    foreach my $attachment (@attachments) {
-        my ($update_flags, $new_flags) = $flags
-            ? extract_flags($flags, $attachment->flag_types, $attachment->flags)
-            : ([], []);
-        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 targeted 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 });
-        }
-    }
+  $dbh->bz_start_transaction();
 
-    $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();
 
-    # 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,
-                { is_markdown => $is_markdown,
-                  isprivate   => $attachment->isprivate,
-                  type        => CMT_ATTACHMENT_UPDATED,
-                  extra_data  => $attachment->id });
+    if ($comment = trim($comment)) {
+      $attachment->bug->add_comment(
+        $comment,
+        {
+          is_markdown => $is_markdown,
+          isprivate   => $attachment->isprivate,
+          type        => CMT_ATTACHMENT_UPDATED,
+          extra_data  => $attachment->id
         }
+      );
+    }
 
-        $changes = translate($changes, ATTACHMENT_MAPPED_RETURNS);
-
-        my %hash = (
-            id               => as_int($attachment->id),
-            last_change_time => as_datetime($attachment->modification_time),
-            changes          => {},
-        );
+    $changes = translate($changes, ATTACHMENT_MAPPED_RETURNS);
 
-        foreach my $field (keys %$changes) {
-            my $change = $changes->{$field};
+    my %hash = (
+      id               => as_int($attachment->id),
+      last_change_time => as_datetime($attachment->modification_time),
+      changes          => {},
+    );
 
-            # 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 => as_string($change->[0] // ''),
-                added   => as_string($change->[1] // '')
-            };
-        }
+    foreach my $field (keys %$changes) {
+      my $change = $changes->{$field};
 
-        push(@result, \%hash);
+      # 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 => as_string($change->[0] // ''),
+        added   => as_string($change->[1] // '')
+      };
     }
 
-    $dbh->bz_commit_transaction();
+    push(@result, \%hash);
+  }
 
-    # Email users about the change
-    foreach my $bug (values %bugs) {
-        $bug->update();
-        $bug->send_changes();
-    }
+  $dbh->bz_commit_transaction();
+
+  # Email users about the change
+  foreach my $bug (values %bugs) {
+    $bug->update();
+    $bug->send_changes();
+  }
 
-    # Return the information to the user
-    return { attachments => \@result };
+  # Return the information to the user
+  return {attachments => \@result};
 }
 
 sub add_comment {
-    my ($self, $params) = @_;
+  my ($self, $params) = @_;
 
-    # The user must login in order add a comment
-    my $user = Bugzilla->login(LOGIN_REQUIRED);
+  # 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' });
+  # 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});
+  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};
-    }
+  # Backwards-compatibility for versions before 3.6
+  if (defined $params->{private}) {
+    $params->{is_private} = delete $params->{private};
+  }
 
-    ThrowUserError('markdown_disabled')
-        if $params->{is_markdown} && !$user->use_markdown();
+  ThrowUserError('markdown_disabled')
+    if $params->{is_markdown} && !$user->use_markdown();
 
-    # Append comment
-    $bug->add_comment($comment, { isprivate   => $params->{is_private},
-                                  is_markdown => $params->{is_markdown},
-                                  work_time   => $params->{work_time} });
+  # Append comment
+  $bug->add_comment(
+    $comment,
+    {
+      isprivate   => $params->{is_private},
+      is_markdown => $params->{is_markdown},
+      work_time   => $params->{work_time}
+    }
+  );
 
-    # Add comment tags
-    $bug->set_all({ comment_tags => $params->{comment_tags} })
-        if defined $params->{comment_tags};
+  # Add comment tags
+  $bug->set_all({comment_tags => $params->{comment_tags}})
+    if defined $params->{comment_tags};
 
-    $bug->update();
+  $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 => as_int($new_comment_id) };
+  return {id => as_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);
 
-        $removed = as_string_array($removed);
-        $added   = as_string_array($added);
+    $removed = as_string_array($removed);
+    $added   = as_string_array($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 =~ /^\d+$/a;
-
-    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 =~ /^\d+$/a;
+
+  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];
 }
 
 ##############################
@@ -1401,224 +1422,231 @@ 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         => as_string($bug->deadline),
-        id               => as_int($bug->bug_id),
-        is_confirmed     => as_boolean($bug->everconfirmed),
-        op_sys           => as_string($bug->op_sys),
-        platform         => as_string($bug->rep_platform),
-        priority         => as_string($bug->priority),
-        resolution       => as_string($bug->resolution),
-        severity         => as_string($bug->bug_severity),
-        status           => as_string($bug->bug_status),
-        summary          => as_string($bug->short_desc),
-        target_milestone => as_string($bug->target_milestone),
-        url              => as_string($bug->bug_file_loc),
-        version          => as_string($bug->version),
-        whiteboard       => as_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} = as_string_array($bug->alias);
-    }
-    if (filter_wants $params, 'assigned_to') {
-        $item{'assigned_to'} = as_login($bug->assigned_to->login);
-        $item{'assigned_to_detail'} = $self->_user_to_hash($bug->assigned_to, $params, undef, 'assigned_to');
-    }
-    if (filter_wants $params, 'blocks') {
-        $item{'blocks'} = as_int_array($bug->blocked);
-    }
-    if (filter_wants $params, 'classification') {
-        $item{classification} = as_string($bug->classification);
-    }
-    if (filter_wants $params, 'component') {
-        $item{component} = as_string($bug->component);
-    }
-    if (filter_wants $params, 'cc') {
-        $item{'cc'} = as_login_array($bug->cc);
-        $item{'cc_detail'} = [ map { $self->_user_to_hash($_, $params, undef, 'cc') } @{ $bug->cc_users } ];
-    }
-    if (filter_wants $params, 'creation_time') {
-        $item{'creation_time'} = as_datetime($bug->creation_ts);
-    }
-    if (filter_wants $params, 'creator') {
-        $item{'creator'} = as_login($bug->reporter->login);
-        $item{'creator_detail'} = $self->_user_to_hash($bug->reporter, $params, undef, 'creator');
-    }
-    if (filter_wants $params, 'depends_on') {
-        $item{'depends_on'} = as_int_array($bug->dependson);
-    }
-    if (filter_wants $params, 'dupe_of') {
-        $item{'dupe_of'} = as_int($bug->dup_id);
-    }
-    if (filter_wants $params, 'groups') {
-        $item{'groups'} = as_name_array($bug->groups_in);
-    }
-    if (filter_wants $params, 'is_open') {
-        $item{'is_open'} = as_boolean($bug->status->is_open);
-    }
-    if (filter_wants $params, 'keywords') {
-        $item{'keywords'} = as_name_array($bug->keyword_objects);
-    }
-    if (filter_wants $params, 'last_change_time') {
-        $item{'last_change_time'} = as_datetime($bug->delta_ts);
-    }
-    if (filter_wants $params, 'product') {
-        $item{product} = as_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         => as_string($bug->deadline),
+      id               => as_int($bug->bug_id),
+      is_confirmed     => as_boolean($bug->everconfirmed),
+      op_sys           => as_string($bug->op_sys),
+      platform         => as_string($bug->rep_platform),
+      priority         => as_string($bug->priority),
+      resolution       => as_string($bug->resolution),
+      severity         => as_string($bug->bug_severity),
+      status           => as_string($bug->bug_status),
+      summary          => as_string($bug->short_desc),
+      target_milestone => as_string($bug->target_milestone),
+      url              => as_string($bug->bug_file_loc),
+      version          => as_string($bug->version),
+      whiteboard       => as_string($bug->status_whiteboard),
     }
-    if (filter_wants $params, 'qa_contact') {
-        my $qa_login = $bug->qa_contact ? $bug->qa_contact->login : '';
-        $item{'qa_contact'} = as_login($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} = as_string_array($bug->alias);
+  }
+  if (filter_wants $params, 'assigned_to') {
+    $item{'assigned_to'} = as_login($bug->assigned_to->login);
+    $item{'assigned_to_detail'}
+      = $self->_user_to_hash($bug->assigned_to, $params, undef, 'assigned_to');
+  }
+  if (filter_wants $params, 'blocks') {
+    $item{'blocks'} = as_int_array($bug->blocked);
+  }
+  if (filter_wants $params, 'classification') {
+    $item{classification} = as_string($bug->classification);
+  }
+  if (filter_wants $params, 'component') {
+    $item{component} = as_string($bug->component);
+  }
+  if (filter_wants $params, 'cc') {
+    $item{'cc'} = as_login_array($bug->cc);
+    $item{'cc_detail'}
+      = [map { $self->_user_to_hash($_, $params, undef, 'cc') } @{$bug->cc_users}];
+  }
+  if (filter_wants $params, 'creation_time') {
+    $item{'creation_time'} = as_datetime($bug->creation_ts);
+  }
+  if (filter_wants $params, 'creator') {
+    $item{'creator'} = as_login($bug->reporter->login);
+    $item{'creator_detail'}
+      = $self->_user_to_hash($bug->reporter, $params, undef, 'creator');
+  }
+  if (filter_wants $params, 'depends_on') {
+    $item{'depends_on'} = as_int_array($bug->dependson);
+  }
+  if (filter_wants $params, 'dupe_of') {
+    $item{'dupe_of'} = as_int($bug->dup_id);
+  }
+  if (filter_wants $params, 'groups') {
+    $item{'groups'} = as_name_array($bug->groups_in);
+  }
+  if (filter_wants $params, 'is_open') {
+    $item{'is_open'} = as_boolean($bug->status->is_open);
+  }
+  if (filter_wants $params, 'keywords') {
+    $item{'keywords'} = as_name_array($bug->keyword_objects);
+  }
+  if (filter_wants $params, 'last_change_time') {
+    $item{'last_change_time'} = as_datetime($bug->delta_ts);
+  }
+  if (filter_wants $params, 'product') {
+    $item{product} = as_string($bug->product);
+  }
+  if (filter_wants $params, 'qa_contact') {
+    my $qa_login = $bug->qa_contact ? $bug->qa_contact->login : '';
+    $item{'qa_contact'} = as_login($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') {
-        $item{'see_also'} = as_name_array($bug->see_also);
+  }
+  if (filter_wants $params, 'see_also') {
+    $item{'see_also'} = as_name_array($bug->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;
+  }
+  if (filter_wants $params, 'duplicates', 'extra') {
+    $item{'duplicates'} = [map { as_int($_->id) } @{$bug->duplicates}];
+  }
+
+  # 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} = as_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} = as_datetime($bug->$name);
     }
-    if (filter_wants $params, 'tags', 'extra') {
-        $item{'tags'} = $bug->tags;
+    elsif ($field->type == FIELD_TYPE_MULTI_SELECT) {
+      $item{$name} = as_string_array($bug->$name);
     }
-    if (filter_wants $params, 'duplicates', 'extra') {
-        $item{'duplicates'} = [ map { as_int($_->id) } @{ $bug->duplicates } ];
+    else {
+      $item{$name} = as_string($bug->$name);
     }
+  }
 
-    # 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} = as_int($bug->$name);
-        }
-        elsif ($field->type == FIELD_TYPE_DATETIME
-               || $field->type == FIELD_TYPE_DATE)
-        {
-            $item{$name} = as_datetime($bug->$name);
-        }
-        elsif ($field->type == FIELD_TYPE_MULTI_SELECT) {
-            $item{$name} = as_string_array($bug->$name);
-        }
-        else {
-            $item{$name} = as_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'} = as_double($bug->estimated_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'} = as_double($bug->estimated_time);
-        }
-        if (filter_wants $params, 'remaining_time') {
-            $item{'remaining_time'} = as_double($bug->remaining_time);
-        }
-        if (filter_wants $params, 'actual_time') {
-            $item{'actual_time'} = as_double($bug->actual_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'} = as_boolean($bug->cclist_accessible);
+    if (filter_wants $params, 'remaining_time') {
+      $item{'remaining_time'} = as_double($bug->remaining_time);
     }
-    if (filter_wants $params, 'is_creator_accessible') {
-        $item{'is_creator_accessible'} = as_boolean($bug->reporter_accessible);
+    if (filter_wants $params, 'actual_time') {
+      $item{'actual_time'} = as_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'} = as_boolean($bug->cclist_accessible);
+  }
+  if (filter_wants $params, 'is_creator_accessible') {
+    $item{'is_creator_accessible'} = as_boolean($bug->reporter_accessible);
+  }
+
+  return \%item;
 }
 
 sub _user_to_hash {
-    my ($self, $user, $filters, $types, $prefix) = @_;
-    my $item = filter $filters, {
-        id        => as_int($user->id),
-        real_name => as_string($user->name),
-        name      => as_login($user->login),
-    }, $types, $prefix;
-    return $item;
+  my ($self, $user, $filters, $types, $prefix) = @_;
+  my $item = filter $filters,
+    {
+    id        => as_int($user->id),
+    real_name => as_string($user->name),
+    name      => as_login($user->login),
+    },
+    $types, $prefix;
+  return $item;
 }
 
 sub _attachment_to_hash {
-    my ($self, $attach, $filters, $types, $prefix) = @_;
-
-    my $item = filter $filters, {
-        creation_time    => as_datetime($attach->attached),
-        last_change_time => as_datetime($attach->modification_time),
-        id               => as_int($attach->id),
-        bug_id           => as_int($attach->bug_id),
-        file_name        => as_string($attach->filename),
-        summary          => as_string($attach->description),
-        content_type     => as_string($attach->contenttype),
-        is_private       => as_boolean($attach->isprivate),
-        is_obsolete      => as_boolean($attach->isobsolete),
-        is_patch         => as_boolean($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'} = as_login($attach->attacher->login);
-    }
-
-    if (filter_wants $filters, 'data', $types, $prefix) {
-        $item->{'data'} = as_base64($attach->data);
-    }
+  my ($self, $attach, $filters, $types, $prefix) = @_;
 
-    if (filter_wants $filters, 'size', $types, $prefix) {
-        $item->{'size'} = as_int($attach->datasize);
-    }
-
-    if (filter_wants $filters, 'flags', $types, $prefix) {
-        $item->{'flags'} = [ map { $self->_flag_to_hash($_) } @{$attach->flags} ];
-    }
-
-    return $item;
+  my $item = filter $filters,
+    {
+    creation_time    => as_datetime($attach->attached),
+    last_change_time => as_datetime($attach->modification_time),
+    id               => as_int($attach->id),
+    bug_id           => as_int($attach->bug_id),
+    file_name        => as_string($attach->filename),
+    summary          => as_string($attach->description),
+    content_type     => as_string($attach->contenttype),
+    is_private       => as_boolean($attach->isprivate),
+    is_obsolete      => as_boolean($attach->isobsolete),
+    is_patch         => as_boolean($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'} = as_login($attach->attacher->login);
+  }
+
+  if (filter_wants $filters, 'data', $types, $prefix) {
+    $item->{'data'} = as_base64($attach->data);
+  }
+
+  if (filter_wants $filters, 'size', $types, $prefix) {
+    $item->{'size'} = as_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                => as_int($flag->id),
-        name              => as_string($flag->name),
-        type_id           => as_int($flag->type_id),
-        creation_date     => as_datetime($flag->creation_date),
-        modification_date => as_datetime($flag->modification_date),
-        status            => as_string($flag->status)
-    };
-
-    foreach my $field (qw(setter requestee)) {
-        my $field_id = $field . "_id";
-        $item->{$field} = as_login($flag->$field->login)
-            if $flag->$field_id;
-    }
-
-    return $item;
+  my ($self, $flag) = @_;
+
+  my $item = {
+    id                => as_int($flag->id),
+    name              => as_string($flag->name),
+    type_id           => as_int($flag->type_id),
+    creation_date     => as_datetime($flag->creation_date),
+    modification_date => as_datetime($flag->modification_date),
+    status            => as_string($flag->status)
+  };
+
+  foreach my $field (qw(setter requestee)) {
+    my $field_id = $field . "_id";
+    $item->{$field} = as_login($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'} = as_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'} = as_string($token);
+  }
 }
 
 1;
index 20fdb158f4588d25d555b2787667339a7dff5a1c..3264e069fea319bac69a0ac940ed80df2f85b08b 100644 (file)
@@ -26,33 +26,34 @@ extends 'Bugzilla::API::1_0::Resource';
 ##############
 
 use constant READ_ONLY => qw(
-    get
+  get
 );
 
 use constant PUBLIC_METHODS => qw(
-    get
-    update
+  get
+  update
 );
 
 sub REST_RESOURCES {
-    use re '/a';
-    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]] };
-                },
-            },
+  use re '/a';
+  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]]};
+        },
+      },
+    },
+  ];
 }
 
 ############
@@ -60,75 +61,75 @@ sub REST_RESOURCES {
 ############
 
 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,
-            _bug_user_last_visit_to_hash(
-                $bug->bug_id, $last_visit_ts, $params
-            ));
-    }
-    $dbh->bz_commit_transaction();
+    push(@results,
+      _bug_user_last_visit_to_hash($bug->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 { _bug_user_last_visit_to_hash($_, $last_visit{$_}, $params) } @$ids;
-    }
-    else {
-        @last_visits = map {
-            _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 { _bug_user_last_visit_to_hash($_, $last_visit{$_}, $params) } @$ids;
+  }
+  else {
+    @last_visits
+      = map { _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 ($bug_id, $last_visit_ts, $params) = @_;
+  my ($bug_id, $last_visit_ts, $params) = @_;
 
-    my %result = (id            => as_int($bug_id),
-                  last_visit_ts => as_datetime($last_visit_ts));
+  my %result
+    = (id => as_int($bug_id), last_visit_ts => as_datetime($last_visit_ts));
 
-    return filter($params, \%result);
+  return filter($params, \%result);
 }
 
 1;
index fc2e15e9033190b164f669cc011b7f98e8220f2f..e88b17569654cb553e56bd2b49bdb84eabde1800 100644 (file)
@@ -26,104 +26,76 @@ extends 'Bugzilla::API::1_0::Resource';
 ##############
 
 # 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
-    last_audit_time
-    parameters
-    timezone
-    time
-    version
+  extensions
+  last_audit_time
+  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
-    defaultopsys
-    defaultplatform
-    defaultpriority
-    defaultseverity
-    duplicate_or_move_bug_status
-    emailregexpdesc
-    letsubmitterchoosemilestone
-    letsubmitterchoosepriority
-    mailfrom
-    maintainer
-    maxattachmentsize
-    maxlocalattachment
-    noresolveonopenblockers
-    password_complexity
-    rememberlogin
-    requirelogin
-    search_allow_no_criteria
-    urlbase
-    use_email_as_login
-    use_see_also
-    useclassification
-    usemenuforusers
-    useqacontact
-    usestatuswhiteboard
-    usetargetmilestone
+  allowemailchange
+  attachment_base
+  commentonchange_resolution
+  commentonduplicate
+  defaultopsys
+  defaultplatform
+  defaultpriority
+  defaultseverity
+  duplicate_or_move_bug_status
+  emailregexpdesc
+  letsubmitterchoosemilestone
+  letsubmitterchoosepriority
+  mailfrom
+  maintainer
+  maxattachmentsize
+  maxlocalattachment
+  noresolveonopenblockers
+  password_complexity
+  rememberlogin
+  requirelogin
+  search_allow_no_criteria
+  urlbase
+  use_email_as_login
+  use_see_also
+  useclassification
+  usemenuforusers
+  useqacontact
+  usestatuswhiteboard
+  usetargetmilestone
 );
 
 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;
 }
 
 ############
@@ -131,88 +103,86 @@ sub REST_RESOURCES {
 ############
 
 sub version {
-    my $self = shift;
-    return { version => as_string(BUGZILLA_VERSION) };
+  my $self = shift;
+  return {version => as_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} = as_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} = as_string($version);
+  }
+  return {extensions => \%retval};
 }
 
 sub timezone {
-    my $self = shift;
-    # All Webservices return times in UTC; Use UTC here for backwards compat.
-    return { timezone => as_string("+0000") };
+  my $self = shift;
+
+  # All Webservices return times in UTC; Use UTC here for backwards compat.
+  return {timezone => as_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  => as_datetime($db_time),
-        web_time => as_datetime($now_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 => as_datetime($db_time), web_time => as_datetime($now_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 => as_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 => as_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} = as_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} = as_string($params->{$param});
+  }
+
+  return {parameters => \%parameters};
 }
 
 1;
index 28ed2aa95ac49030a56809f3677d21aec30e87d6..acf089663ea56f3e3636fadd404496b32cbbf746 100644 (file)
@@ -25,26 +25,27 @@ extends 'Bugzilla::API::1_0::Resource';
 ##############
 
 use constant READ_ONLY => qw(
-    get
+  get
 );
 
 use constant PUBLIC_METHODS => qw(
-    get
+  get
 );
 
 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;
 }
 
 ############
@@ -52,57 +53,68 @@ sub REST_RESOURCES {
 ############
 
 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          => as_int($classification->id),
-        name        => as_string($classification->name),
-        description => as_string($classification->description),
-        sort_key    => as_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          => as_int($classification->id),
+    name        => as_string($classification->name),
+    description => as_string($classification->description),
+    sort_key    => as_int($classification->sortkey),
+    products    => [map { $self->_product_to_hash($_, $params) } @$products],
     };
 }
 
 sub _product_to_hash {
-    my ($self, $product, $params) = @_;
-
-    return filter $params, {
-       id          => as_int($product->id),
-       name        => as_string($product->name),
-       description => as_string($product->description),
-   }, undef, 'products';
+  my ($self, $product, $params) = @_;
+
+  return filter $params,
+    {
+    id          => as_int($product->id),
+    name        => as_string($product->name),
+    description => as_string($product->description),
+    },
+    undef, 'products';
 }
 
 1;
index ba9b27d62f5a6da7385144f920686db93d990f8a..c930d24b0c141f296a42151ccc0ac62d6473db1d 100644 (file)
@@ -27,67 +27,63 @@ extends 'Bugzilla::API::1_0::Resource';
 ##############
 
 use constant PUBLIC_METHODS => qw(
-    create
-    delete
-    update
+  create
+  delete
+  update
 );
 
 use constant CREATE_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',
 };
 
-use constant MAPPED_FIELDS => {
-    is_open => 'is_active',
-};
+use constant MAPPED_FIELDS => {is_open => 'is_active',};
 
 use constant MAPPED_RETURNS => {
-    initialowner     => 'default_assignee',
-    initialqacontact => 'default_qa_contact',
-    cc_list          => 'default_cc',
-    isactive         => 'isopen',
+  initialowner     => 'default_assignee',
+  initialqacontact => 'default_qa_contact',
+  cc_list          => 'default_cc',
+  isactive         => 'isopen',
 };
 
 sub REST_RESOURCES {
-    my $rest_resources = [
-        qr{^/component$}, {
-            POST => {
-                method => 'create',
-                success_code => STATUS_CREATED
-            }
-        },
-        qr{^/component/(\d+)$}, {
-            PUT => {
-                method => 'update',
-                params => sub {
-                    return { ids => [ $_[0] ] };
-                }
-            },
-            DELETE => {
-                method => 'delete',
-                params => sub {
-                    return { ids => [ $_[0] ] };
-                }
-            },
-        },
-        qr{^/component/([^/]+)/([^/]+)$}, {
-            PUT => {
-                method => 'update',
-                params => sub {
-                    return { names => [ { product => $_[0], component => $_[1] } ] };
-                }
-            },
-            DELETE => {
-                method => 'delete',
-                params => sub {
-                    return { names => [ { product => $_[0], component => $_[1] } ] };
-                }
-            },
-        },
-    ];
-    return $rest_resources;
+  my $rest_resources = [
+    qr{^/component$},
+    {POST => {method => 'create', success_code => STATUS_CREATED}},
+    qr{^/component/(\d+)$},
+    {
+      PUT => {
+        method => 'update',
+        params => sub {
+          return {ids => [$_[0]]};
+        }
+      },
+      DELETE => {
+        method => 'delete',
+        params => sub {
+          return {ids => [$_[0]]};
+        }
+      },
+    },
+    qr{^/component/([^/]+)/([^/]+)$},
+    {
+      PUT => {
+        method => 'update',
+        params => sub {
+          return {names => [{product => $_[0], component => $_[1]}]};
+        }
+      },
+      DELETE => {
+        method => 'delete',
+        params => sub {
+          return {names => [{product => $_[0], component => $_[1]}]};
+        }
+      },
+    },
+  ];
+  return $rest_resources;
 }
 
 ############
@@ -95,188 +91,185 @@ sub REST_RESOURCES {
 ############
 
 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, CREATE_MAPPED_FIELDS);
-    $values->{product} = $product;
+  # Translate the fields
+  my $values = translate($params, CREATE_MAPPED_FIELDS);
+  $values->{product} = $product;
 
-    # Create the component and return the newly created id.
-    my $component = Bugzilla::Component->create($values);
-    return { id => as_int($component->id) };
+  # Create the component and return the newly created id.
+  my $component = Bugzilla::Component->create($values);
+  return {id => as_int($component->id)};
 }
 
 sub _component_params_to_objects {
-    # We can't use Util's _param_to_objects since name is a hash
-    my $params = shift;
-    my $user   = Bugzilla->user;
 
-    my @components = ();
+  # We can't use Util's _param_to_objects since name is a hash
+  my $params = shift;
+  my $user   = Bugzilla->user;
 
-    if (defined $params->{ids}) {
-        push @components, @{ Bugzilla::Component->new_from_list($params->{ids}) };
-    }
+  my @components = ();
 
-    if (defined $params->{names}) {
-        # To get the component objects for product/component combination
-        # first obtain the product object from the passed product name
-        foreach my $name_hash (@{$params->{names}}) {
-            my $product = $user->check_can_admin_product($name_hash->{product});
-            push @components, @{ Bugzilla::Component->match({
-                product_id => $product->id,
-                name       => $name_hash->{component}
-            })};
-        }
-    }
+  if (defined $params->{ids}) {
+    push @components, @{Bugzilla::Component->new_from_list($params->{ids})};
+  }
 
-    my %seen_component_ids = ();
+  if (defined $params->{names}) {
 
-    my @accessible_components;
-    foreach my $component (@components) {
-        # Skip if we already included this component
-        next if $seen_component_ids{$component->id}++;
+    # To get the component objects for product/component combination
+    # first obtain the product object from the passed product name
+    foreach my $name_hash (@{$params->{names}}) {
+      my $product = $user->check_can_admin_product($name_hash->{product});
+      push @components,
+        @{Bugzilla::Component->match({
+          product_id => $product->id, name => $name_hash->{component}
+        })};
+    }
+  }
 
-        # Can the user see and admin this product?
-        my $product = $component->product;
-        $user->check_can_admin_product($product->name);
+  my %seen_component_ids = ();
 
-        push @accessible_components, $component;
-    }
+  my @accessible_components;
+  foreach my $component (@components) {
 
-    return \@accessible_components;
-}
+    # Skip if we already included this component
+    next if $seen_component_ids{$component->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 => "components" });
-
-    defined($params->{names}) || defined($params->{ids})
-        || ThrowCodeError('params_required',
-               { function => 'Component.update', params => ['ids', 'names'] });
-
-    my $component_objects = _component_params_to_objects($params);
-
-    # If the user tries to change component name for several
-    # components of the same product then throw an error
-    if ($params->{name}) {
-        my %unique_product_comps;
-        foreach my $comp (@$component_objects) {
-            if($unique_product_comps{$comp->product_id}) {
-                ThrowUserError("multiple_components_update_not_allowed");
-            }
-            else {
-                $unique_product_comps{$comp->product_id} = 1;
-            }
-        }
-    }
+    # Can the user see and admin this product?
+    my $product = $component->product;
+    $user->check_can_admin_product($product->name);
 
-    my $values = translate($params, MAPPED_FIELDS);
+    push @accessible_components, $component;
+  }
 
-    # We delete names and ids to keep only new values to set.
-    delete $values->{names};
-    delete $values->{ids};
+  return \@accessible_components;
+}
 
-    $dbh->bz_start_transaction();
-    foreach my $component (@$component_objects) {
-        $component->set_all($values);
+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 => "components"});
+
+  defined($params->{names})
+    || defined($params->{ids})
+    || ThrowCodeError('params_required',
+    {function => 'Component.update', params => ['ids', 'names']});
+
+  my $component_objects = _component_params_to_objects($params);
+
+  # If the user tries to change component name for several
+  # components of the same product then throw an error
+  if ($params->{name}) {
+    my %unique_product_comps;
+    foreach my $comp (@$component_objects) {
+      if ($unique_product_comps{$comp->product_id}) {
+        ThrowUserError("multiple_components_update_not_allowed");
+      }
+      else {
+        $unique_product_comps{$comp->product_id} = 1;
+      }
     }
-
-    my %changes;
-    foreach my $component (@$component_objects) {
-        my $returned_changes = $component->update();
-        $changes{$component->id} = translate($returned_changes, MAPPED_RETURNS);
+  }
+
+  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 $component (@$component_objects) {
+    $component->set_all($values);
+  }
+
+  my %changes;
+  foreach my $component (@$component_objects) {
+    my $returned_changes = $component->update();
+    $changes{$component->id} = translate($returned_changes, MAPPED_RETURNS);
+  }
+  $dbh->bz_commit_transaction();
+
+  my @result;
+  foreach my $component (@$component_objects) {
+    my %hash = (id => $component->id, changes => {},);
+
+    foreach my $field (keys %{$changes{$component->id}}) {
+      my $change = $changes{$component->id}->{$field};
+
+      if ( $field eq 'default_assignee'
+        || $field eq 'default_qa_contact'
+        || $field eq 'default_cc')
+      {
+        # We need to convert user ids to login names
+        my @old_user_ids = split(/[,\s]+/, $change->[0]);
+        my @new_user_ids = split(/[,\s]+/, $change->[1]);
+
+        my @old_users
+          = map { $_->login } @{Bugzilla::User->new_from_list(\@old_user_ids)};
+        my @new_users
+          = map { $_->login } @{Bugzilla::User->new_from_list(\@new_user_ids)};
+
+        $hash{changes}{$field} = {
+          removed => as_string(join(', ', @old_users)),
+          added   => as_string(join(', ', @new_users)),
+        };
+      }
+      else {
+        $hash{changes}{$field}
+          = {removed => as_string($change->[0]), added => as_string($change->[1])};
+      }
     }
-    $dbh->bz_commit_transaction();
-
-    my @result;
-    foreach my $component (@$component_objects) {
-        my %hash = (
-            id      => $component->id,
-            changes => {},
-        );
-
-        foreach my $field (keys %{ $changes{$component->id} }) {
-            my $change = $changes{$component->id}->{$field};
-
-            if ($field eq 'default_assignee'
-                || $field eq 'default_qa_contact'
-                || $field eq 'default_cc'
-            ) {
-                # We need to convert user ids to login names
-                my @old_user_ids = split(/[,\s]+/, $change->[0]);
-                my @new_user_ids = split(/[,\s]+/, $change->[1]);
-
-                my @old_users = map { $_->login }
-                    @{Bugzilla::User->new_from_list(\@old_user_ids)};
-                my @new_users = map { $_->login }
-                    @{Bugzilla::User->new_from_list(\@new_user_ids)};
-
-                $hash{changes}{$field} = {
-                    removed => as_string(join(', ', @old_users)),
-                    added   => as_string(join(', ', @new_users)),
-                };
-            }
-            else {
-                $hash{changes}{$field} = {
-                    removed => as_string($change->[0]),
-                    added   => as_string($change->[1])
-                };
-            }
-        }
 
-        push(@result, \%hash);
-    }
+    push(@result, \%hash);
+  }
 
-    return { components => \@result };
+  return {components => \@result};
 }
 
 sub delete {
-    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 => "components" });
-
-    defined($params->{names}) || defined($params->{ids})
-        || ThrowCodeError('params_required',
-               { function => 'Component.delete', params => ['ids', 'names'] });
-
-    my $component_objects = _component_params_to_objects($params);
-
-    $dbh->bz_start_transaction();
-    my %changes;
-    foreach my $component (@$component_objects) {
-        my $returned_changes = $component->remove_from_db();
-    }
-    $dbh->bz_commit_transaction();
-
-    my @result;
-    foreach my $component (@$component_objects) {
-        push @result, { id => $component->id };
-    }
-
-    return { components => \@result };
+  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 => "components"});
+
+  defined($params->{names})
+    || defined($params->{ids})
+    || ThrowCodeError('params_required',
+    {function => 'Component.delete', params => ['ids', 'names']});
+
+  my $component_objects = _component_params_to_objects($params);
+
+  $dbh->bz_start_transaction();
+  my %changes;
+  foreach my $component (@$component_objects) {
+    my $returned_changes = $component->remove_from_db();
+  }
+  $dbh->bz_commit_transaction();
+
+  my @result;
+  foreach my $component (@$component_objects) {
+    push @result, {id => $component->id};
+  }
+
+  return {components => \@result};
 }
 
 1;
index acc408bacba9b4713ffbc9bc7c8beb6c6b3cc6b8..23579e4c0ba6c050309ecbcf3fd973aec26e356b 100644 (file)
@@ -31,49 +31,46 @@ extends 'Bugzilla::API::1_0::Resource';
 ##############
 
 use constant READ_ONLY => qw(
-    get
+  get
 );
 
 use constant PUBLIC_METHODS => qw(
-    create
-    get
-    update
+  create
+  get
+  update
 );
 
 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;
 }
 
 ############
@@ -81,286 +78,300 @@ sub REST_RESOURCES {
 ############
 
 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};
+  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;
     }
+  }
 
-    # 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 => as_int($flagtype->id)  };
+  return {id => as_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 = ();
+    # Process the clusions
+    foreach my $type ('inclusions', 'exclusions') {
+      my $clusions = $type eq 'inclusions' ? $inclusions : $exclusions;
+      next if not defined $clusions;
 
-    foreach my $flag_type_id (@flag_type_ids) {
-        my ($flagtype, $can_fully_edit) = $user->check_can_admin_flagtype($flag_type_id);
+      my @extra_clusions = ();
+      if (!$user->in_group('editcomponents')) {
+        my $products = $user->get_products_by_permission('editcomponents');
 
-        if ($can_fully_edit) {
-            $flagtype->set_all($params);
-        }
-        elsif (scalar keys %$params) {
-            ThrowUserError('flag_type_not_editable', { flagtype => $flagtype });
+        # 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;
         }
+      }
 
-        # 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],
-            });
-        }
-
-        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      => as_int($flag_type_id),
-            name    => as_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 => as_string($change->[0]),
-                added   => as_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      => as_int($flag_type_id),
+      name    => as_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 => as_string($change->[0]), added => as_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          => as_int($flagtype->id),
-        name        => as_string($flagtype->name),
-        description => as_string($flagtype->description),
-        type        => as_string($flagtype->target_type),
-        values      => \@values,
-        is_active   => as_boolean($flagtype->is_active),
-        is_requesteeble  => as_boolean($flagtype->is_requesteeble),
-        is_multiplicable => as_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               => as_int($flagtype->id),
+    name             => as_string($flagtype->name),
+    description      => as_string($flagtype->description),
+    type             => as_string($flagtype->target_type),
+    values           => \@values,
+    is_active        => as_boolean($flagtype->is_active),
+    is_requesteeble  => as_boolean($flagtype->is_requesteeble),
+    is_multiplicable => as_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;
index f3b55b3fd8144b8b0cf039b1b0f0fc45646e1175..451c0c1bf25c1afbfe5b4747ae8d80f46b3949a2 100644 (file)
@@ -26,38 +26,33 @@ extends 'Bugzilla::API::1_0::Resource';
 ##############
 
 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 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;
 }
 
 ############
@@ -65,196 +60,199 @@ sub REST_RESOURCES {
 ############
 
 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 => as_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 => as_int($group->id)};
 }
 
 sub update {
-    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 ($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 => as_string($change->[0]), added => as_string($change->[1])};
+    }
+    push(@result, \%hash);
+  }
 
-    my $group_objects = params_to_objects($params, 'Bugzilla::Group');
+  return {groups => \@result};
+}
 
-    my %values = %$params;
+sub get {
+  my ($self, $params) = validate(@_, 'ids', 'names', 'type');
 
-    # We delete names and ids to keep only new values to set.
-    delete $values{names};
-    delete $values{ids};
+  Bugzilla->login(LOGIN_REQUIRED);
 
-    $dbh->bz_start_transaction();
-    foreach my $group (@$group_objects) {
-        $group->set_all(\%values);
-    }
+  # 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 %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 => as_string($change->[0]),
-                added   => as_string($change->[1])
-            };
-        }
-       push(@result, \%hash);
-    }
+  Bugzilla->switch_to_shadow_db();
 
-    return { groups => \@result };
-}
+  my $groups = [];
 
-sub get {
-    my ($self, $params) = validate(@_, 'ids', 'names', 'type');
+  if (defined $params->{ids}) {
 
-    Bugzilla->login(LOGIN_REQUIRED);
+    # Get the groups by id
+    $groups = Bugzilla::Group->new_from_list($params->{ids});
+  }
 
-    # 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          => as_int($group->id),
-        name        => as_string($group->name),
-        description => as_string($group->description),
-    };
-
-    if ($user->in_group('creategroups')) {
-        $field_data->{is_active}    = as_boolean($group->is_active);
-        $field_data->{is_bug_group} = as_boolean($group->is_bug_group);
-        $field_data->{user_regexp}  = as_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          => as_int($group->id),
+    name        => as_string($group->name),
+    description => as_string($group->description),
+  };
+
+  if ($user->in_group('creategroups')) {
+    $field_data->{is_active}    = as_boolean($group->is_active);
+    $field_data->{is_bug_group} = as_boolean($group->is_bug_group);
+    $field_data->{user_regexp}  = as_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'}) {
 
-    if (!$editusers && Bugzilla->params->{'usevisibilitygroups'}) {
-        # Show only users in visible groups.
-        $visibleGroups = $user->visible_groups_inherited;
+    # Show only users in visible groups.
+    $visibleGroups = $user->visible_groups_inherited;
 
-        if (scalar @$visibleGroups) {
-            $query .= qq{, user_group_map AS ugm
+    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                => as_int($_->id),
-            real_name         => as_string($_->name),
-            name              => as_login($_->login),
-            email             => as_email($_->email),
-            can_login         => as_boolean($_->is_enabled),
-            email_enabled     => as_boolean($_->email_enabled),
-            login_denied_text => as_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                => as_int($_->id),
+    real_name         => as_string($_->name),
+    name              => as_login($_->login),
+    email             => as_email($_->email),
+    can_login         => as_boolean($_->is_enabled),
+    email_enabled     => as_boolean($_->email_enabled),
+    login_denied_text => as_string($_->disabledtext),
+  } } @$user_objects;
+
+  return \@users;
 }
 
 1;
index 02581b0edc8497a48dd04165f870bb9b982bec66..a4b8546fb79df4a9ce1f797d6cb6b0215bd4ebc5 100644 (file)
@@ -28,81 +28,65 @@ extends 'Bugzilla::API::1_0::Resource';
 ##############
 
 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',};
 
 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;
 }
 
 ############
@@ -111,276 +95,271 @@ sub REST_RESOURCES {
 
 # 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 => as_int($product->id) };
+  }
+  my $product = Bugzilla::Product->create($args);
+  return {id => as_int($product->id)};
 }
 
 sub update {
-    my ($self, $params) = @_;
+  my ($self, $params) = @_;
 
-    my $dbh = Bugzilla->dbh;
+  my $dbh = Bugzilla->dbh;
 
-    Bugzilla->login(LOGIN_REQUIRED);
-    Bugzilla->user->in_group('editcomponents')
-        || ThrowUserError("auth_failure", { group  => "editcomponents",
-                                            action => "edit",
-                                            object => "products" });
+  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'] });
+  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 $product_objects = params_to_objects($params, 'Bugzilla::Product');
 
-    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 $product (@$product_objects) {
-        $product->set_all($values);
-    }
+  $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 => as_string($change->[0]),
-                added   => as_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 => {},);
 
-        push(@result, \%hash);
+    foreach my $field (keys %{$changes{$product->id}}) {
+      my $change = $changes{$product->id}->{$field};
+      $hash{changes}{$field}
+        = {removed => as_string($change->[0]), added => as_string($change->[1])};
     }
 
-    return { products => \@result };
+    push(@result, \%hash);
+  }
+
+  return {products => \@result};
 }
 
 sub _product_to_hash {
-    my ($self, $params, $product) = @_;
-
-    my $field_data = {
-        id          => as_int($product->id),
-        name        => as_string($product->name),
-        description => as_string($product->description),
-        is_active   => as_boolean($product->is_active),
-        default_milestone => as_string($product->default_milestone),
-        has_unconfirmed   => as_boolean($product->allows_unconfirmed),
-        classification    => as_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                => as_int($product->id),
+    name              => as_string($product->name),
+    description       => as_string($product->description),
+    is_active         => as_boolean($product->is_active),
+    default_milestone => as_string($product->default_milestone),
+    has_unconfirmed   => as_boolean($product->allows_unconfirmed),
+    classification    => as_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          => as_int($component->id),
-        name        => as_string($component->name),
-        description => as_string($component->description),
-        default_assigned_to =>
-            as_login($component->default_assignee->login),
-        default_qa_contact =>
-            as_login($component->default_qa_contact ?
-                     $component->default_qa_contact->login : ""),
-        sort_key => 0, # sort_key is returned to match Bug.fields
-        is_active => as_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                  => as_int($component->id),
+    name                => as_string($component->name),
+    description         => as_string($component->description),
+    default_assigned_to => as_login($component->default_assignee->login),
+    default_qa_contact  => as_login(
+      $component->default_qa_contact ? $component->default_qa_contact->login : ""
+    ),
+    sort_key => 0,    # sort_key is returned to match Bug.fields
+    is_active => as_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               => as_int($flag_type->id),
-        name             => as_string($flag_type->name),
-        description      => as_string($flag_type->description),
-        cc_list          => as_string($flag_type->cc_list),
-        sort_key         => as_int($flag_type->sortkey),
-        is_active        => as_boolean($flag_type->is_active),
-        is_requestable   => as_boolean($flag_type->is_requestable),
-        is_requesteeble  => as_boolean($flag_type->is_requesteeble),
-        is_multiplicable => as_boolean($flag_type->is_multiplicable),
-        grant_group      => as_int($flag_type->grant_group_id),
-        request_group    => as_int($flag_type->request_group_id),
-    }, undef, 'flag_types';
+  my ($self, $flag_type, $params) = @_;
+  return filter $params,
+    {
+    id               => as_int($flag_type->id),
+    name             => as_string($flag_type->name),
+    description      => as_string($flag_type->description),
+    cc_list          => as_string($flag_type->cc_list),
+    sort_key         => as_int($flag_type->sortkey),
+    is_active        => as_boolean($flag_type->is_active),
+    is_requestable   => as_boolean($flag_type->is_requestable),
+    is_requesteeble  => as_boolean($flag_type->is_requesteeble),
+    is_multiplicable => as_boolean($flag_type->is_multiplicable),
+    grant_group      => as_int($flag_type->grant_group_id),
+    request_group    => as_int($flag_type->request_group_id),
+    },
+    undef, 'flag_types';
 }
 
 sub _version_to_hash {
-    my ($self, $version, $params) = @_;
-    return filter $params, {
-        id        => as_int($version->id),
-        name      => as_string($version->name),
-        sort_key  => 0, # sort_key is returened to match Bug.fields
-        is_active => as_boolean($version->is_active),
-    }, undef, 'versions';
+  my ($self, $version, $params) = @_;
+  return filter $params, {
+    id   => as_int($version->id),
+    name => as_string($version->name),
+    sort_key => 0,    # sort_key is returened to match Bug.fields
+    is_active => as_boolean($version->is_active),
+    },
+    undef, 'versions';
 }
 
 sub _milestone_to_hash {
-    my ($self, $milestone, $params) = @_;
-    return filter $params, {
-        id        => as_int($milestone->id),
-        name      => as_string($milestone->name),
-        sort_key  => as_int($milestone->sortkey),
-        is_active => as_boolean($milestone->is_active),
-    }, undef, 'milestones';
+  my ($self, $milestone, $params) = @_;
+  return filter $params,
+    {
+    id        => as_int($milestone->id),
+    name      => as_string($milestone->name),
+    sort_key  => as_int($milestone->sortkey),
+    is_active => as_boolean($milestone->is_active),
+    },
+    undef, 'milestones';
 }
 
 1;
index e8e4a73798abf8ee95479b69f30e7cacaf8f803c..d2db0480010fd4bb5d518aba6c5237eaf4ca9d13 100644 (file)
@@ -30,92 +30,67 @@ extends 'Bugzilla::API::1_0::Resource';
 ##############
 
 # 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
-    login
-    logout
-    valid_login
-    whoami
+  get
+  login
+  logout
+  valid_login
+  whoami
 );
 
 use constant PUBLIC_METHODS => qw(
-    create
-    get
-    login
-    logout
-    offer_account_by_email
-    update
-    valid_login
-    whoami
+  create
+  get
+  login
+  logout
+  offer_account_by_email
+  update
+  valid_login
+  whoami
 );
 
-use constant MAPPED_FIELDS => {
-    full_name => 'name',
-    login_denied_text => 'disabledtext',
-};
+use constant MAPPED_FIELDS =>
+  {full_name => 'name', login_denied_text => 'disabledtext',};
 
-use constant MAPPED_RETURNS => {
-    realname => 'full_name',
-    disabledtext => 'login_denied_text',
-};
+use constant MAPPED_RETURNS =>
+  {realname => 'full_name', disabledtext => 'login_denied_text',};
 
 sub REST_RESOURCES {
-    my $rest_resources = [
-        qr{^/login$}, {
-            GET => {
-                method => 'login'
-            },
-            POST => {
-                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] ] };
-                }
-            }
-        },
-        qr{^/whoami$}, {
-            GET => {
-                method => 'whoami'
-            }
+  my $rest_resources = [
+    qr{^/login$},
+    {GET => {method => 'login'}, POST => {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]]};
         }
-    ];
-    return $rest_resources;
+      },
+      PUT => {
+        method => 'update',
+        params => sub {
+          my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names';
+          return {$param => [$_[0]]};
+        }
+      }
+    },
+    qr{^/whoami$},
+    {GET => {method => 'whoami'}}
+  ];
+  return $rest_resources;
 }
 
 ############
@@ -123,80 +98,81 @@ sub REST_RESOURCES {
 ############
 
 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 as_boolean(1);
-    }
-    return as_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 as_boolean(1);
+  }
+  return as_boolean(0);
 }
 
 sub offer_account_by_email {
-    my $self = shift;
-    my ($params) = @_;
-    my $email = trim($params->{email})
-        || ThrowCodeError('param_required', { param => 'email' });
-
-    my $login = Bugzilla->params->{use_email_as_login} ? $email : trim($params->{login});
-    $login or ThrowCodeError('param_required', { param => 'login' });
-
-    Bugzilla->user->check_account_creation_enabled;
-    Bugzilla->user->check_and_send_account_creation_confirmation($login, $email);
-    return undef;
+  my $self     = shift;
+  my ($params) = @_;
+  my $email    = trim($params->{email})
+    || ThrowCodeError('param_required', {param => 'email'});
+
+  my $login
+    = Bugzilla->params->{use_email_as_login} ? $email : trim($params->{login});
+  $login or ThrowCodeError('param_required', {param => 'login'});
+
+  Bugzilla->user->check_account_creation_enabled;
+  Bugzilla->user->check_and_send_account_creation_confirmation($login, $email);
+  return undef;
 }
 
 sub create {
-    my $self = shift;
-    my ($params) = @_;
+  my $self = shift;
+  my ($params) = @_;
 
-    Bugzilla->user->in_group('editusers')
-        || ThrowUserError("auth_failure", { group  => "editusers",
-                                            action => "add",
-                                            object => "users"});
+  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 $email = trim($params->{email})
+    || ThrowCodeError('param_required', {param => 'email'});
 
-    my $login = Bugzilla->params->{use_email_as_login} ? $email : trim($params->{login});
-    $login or ThrowCodeError('param_required', { param => 'login' });
+  my $login
+    = Bugzilla->params->{use_email_as_login} ? $email : trim($params->{login});
+  $login or ThrowCodeError('param_required', {param => 'login'});
 
-    my $realname = trim($params->{full_name});
-    my $password = trim($params->{password}) || '*';
+  my $realname = trim($params->{full_name});
+  my $password = trim($params->{password}) || '*';
 
-    my $user = Bugzilla::User->create({
-        login_name    => $login,
-        email         => $email,
-        realname      => $realname,
-        cryptpassword => $password
-    });
+  my $user = Bugzilla::User->create({
+    login_name    => $login,
+    email         => $email,
+    realname      => $realname,
+    cryptpassword => $password
+  });
 
-    return { id => as_int($user->id) };
+  return {id => as_int($user->id)};
 }
 
 
@@ -205,294 +181,299 @@ sub create {
 # $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');
+  my ($self, $params)
+    = validate(@_, 'names', 'ids', 'match', 'group_ids', 'groups');
 
-    Bugzilla->switch_to_shadow_db();
+  Bugzilla->switch_to_shadow_db();
 
-    defined($params->{names}) || defined($params->{ids})
-        || defined($params->{match})
-        || ThrowCodeError('params_required',
-               { function => 'User.get', params => ['ids', 'names', 'match'] });
+       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};
+  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;
+  # start filtering to remove duplicate user ids
+  my %unique_users = map { $_->id => $_ } @user_objects;
+  @user_objects = values %unique_users;
 
-    my @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        => as_int($_->id),
-                     real_name => as_string($_->name),
-                     name      => as_login($_->login),
-                 } } @$in_group;
-
-        return { users => \@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");
     }
-
-    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;
-            }
+    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        => as_int($_->id),
+        real_name => as_string($_->name),
+        name      => as_login($_->login),
         }
-        else {
-            ThrowUserError('auth_failure', {reason => "not_visible",
-                                            action => "access",
-                                            object => "user",
-                                            userid => $obj->id});
+    } @$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::API::1_0::Resource::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        => as_int($user->id),
+      real_name => as_string($user->name),
+      name      => as_login($user->login),
+      can_login => as_boolean($user->is_enabled ? 1 : 0),
+      };
+
+    if (Bugzilla->user->in_group('editusers')) {
+      $user_info->{email}           = as_email($user->email),
+        $user_info->{email_enabled} = as_boolean($user->email_enabled);
+      $user_info->{login_denied_text} = as_string($user->disabledtext);
     }
 
-    # User Matching
-    my $limit = Bugzilla->params->{maxusermatches};
-    if ($params->{limit}) {
-        detaint_natural($params->{limit})
-            || ThrowCodeError('param_must_be_numeric',
-                              { function => 'Bugzilla::API::1_0::Resource::User::match',
-                                param    => 'limit' });
-        $limit = $limit ? min($params->{limit}, $limit) : $params->{limit};
+    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}];
+      }
     }
 
-    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;
-            }
-        }
+    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);
+      }
     }
 
-    my $in_group = $self->_filter_users_by_group(\@user_objects, $params);
-    foreach my $user (@$in_group) {
-        my $user_info = filter $params, {
-            id        => as_int($user->id),
-            real_name => as_string($user->name),
-            name      => as_login($user->login),
-            can_login => as_boolean($user->is_enabled ? 1 : 0),
-        };
-
-        if (Bugzilla->user->in_group('editusers')) {
-            $user_info->{email}             = as_email($user->email),
-            $user_info->{email_enabled}     = as_boolean($user->email_enabled);
-            $user_info->{login_denied_text} = as_string($user->disabledtext);
-        }
+    push(@users, $user_info);
+  }
 
-        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 }
-                ];
-            }
-        }
+  return {users => \@users};
+}
 
-        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);
-            }
-        }
+sub update {
+  my ($self, $params) = @_;
 
-        push(@users, $user_info);
-    }
+  my $dbh = Bugzilla->dbh;
 
-    return { users => \@users };
-}
+  my $user = Bugzilla->login(LOGIN_REQUIRED);
 
-sub update {
-    my ($self, $params) = @_;
+  # Reject access if there is no sense in continuing.
+  $user->in_group('editusers')
+    || $user->can_bless()
+    || ThrowUserError("auth_failure",
+    {group => "editusers", action => "edit", object => "users"});
 
-    my $dbh = Bugzilla->dbh;
+  defined($params->{names})
+    || defined($params->{ids})
+    || ThrowCodeError('params_required',
+    {function => 'User.update', params => ['ids', 'names']});
 
-    my $user = Bugzilla->login(LOGIN_REQUIRED);
+  my $user_objects = params_to_objects($params, 'Bugzilla::User');
 
-    # Reject access if there is no sense in continuing.
-    $user->in_group('editusers')
-        || $user->can_bless()
-        || ThrowUserError("auth_failure", {group  => "editusers",
-                                           action => "edit",
-                                           object => "users"});
+  my $values = translate($params, MAPPED_FIELDS);
 
-    defined($params->{names}) || defined($params->{ids})
-        || ThrowCodeError('params_required',
-               { function => 'User.update', params => ['ids', 'names'] });
+  # We delete names and ids to keep only new values to set.
+  delete $values->{names};
+  delete $values->{ids};
 
-    my $user_objects = params_to_objects($params, 'Bugzilla::User');
+  $dbh->bz_start_transaction();
 
-    my $values = translate($params, MAPPED_FIELDS);
+  $values = {groups => $values->{groups}} unless $user->in_group('editusers');
+  foreach my $user (@$user_objects) {
+    $user->set_all($values);
+  }
 
-    # We delete names and ids to keep only new values to set.
-    delete $values->{names};
-    delete $values->{ids};
+  my %changes;
+  foreach my $user (@$user_objects) {
+    my $returned_changes = $user->update();
+    $changes{$user->id} = translate($returned_changes, MAPPED_RETURNS);
+  }
+  $dbh->bz_commit_transaction();
 
-    $dbh->bz_start_transaction();
+  my @result;
+  foreach my $user (@$user_objects) {
+    my %hash = (id => $user->id, changes => {},);
 
-    $values = { groups => $values->{groups} } unless $user->in_group('editusers');
-    foreach my $user (@$user_objects){
-        $user->set_all($values);
-    }
+    foreach my $field (keys %{$changes{$user->id}}) {
+      my $change = $changes{$user->id}->{$field};
 
-    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 => as_string($change->[0]),
-                added   => as_string($change->[1])
-            };
-        }
+      # 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];
 
-        push(@result, \%hash);
+      $hash{changes}{$field}
+        = {removed => as_string($change->[0]), added => as_string($change->[1])};
     }
 
-    return { users => \@result };
+    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;
+  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;
     }
-    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          => as_int($group->id),
-        name        => as_string($group->name),
-        description => as_string($group->description),
-    };
-    return $item;
+  my ($self, $group) = @_;
+  my $item = {
+    id          => as_int($group->id),
+    name        => as_string($group->name),
+    description => as_string($group->description),
+  };
+  return $item;
 }
 
 sub _query_to_hash {
-    my ($self, $query) = @_;
-    my $item = {
-        id    => as_int($query->id),
-        name  => as_string($query->name),
-        query => as_string($query->url),
-    };
-    return $item;
+  my ($self, $query) = @_;
+  my $item = {
+    id    => as_int($query->id),
+    name  => as_string($query->name),
+    query => as_string($query->url),
+  };
+  return $item;
 }
 
 sub _report_to_hash {
-    my ($self, $report) = @_;
-    my $item = {
-        id    => as_int($report->id),
-        name  => as_string($report->name),
-        query => as_string($report->query),
-    };
-    return $item;
+  my ($self, $report) = @_;
+  my $item = {
+    id    => as_int($report->id),
+    name  => as_string($report->name),
+    query => as_string($report->query),
+  };
+  return $item;
 }
 
 sub _login_to_hash {
-    my ($self, $user) = @_;
-    my $item = { id => as_int($user->id) };
-    if ($user->{_login_token}) {
-        $item->{'token'} = $user->id . "-" . $user->{_login_token};
-    }
-    return $item;
+  my ($self, $user) = @_;
+  my $item = {id => as_int($user->id)};
+  if ($user->{_login_token}) {
+    $item->{'token'} = $user->id . "-" . $user->{_login_token};
+  }
+  return $item;
 }
 
 sub whoami {
-    my ($self, $params) = @_;
-    my $user = Bugzilla->login(LOGIN_REQUIRED);
-    return filter $params, {
-        id        => as_int($user->id),
-        real_name => as_string($user->name),
-        name      => as_login($user->login),
+  my ($self, $params) = @_;
+  my $user = Bugzilla->login(LOGIN_REQUIRED);
+  return filter $params,
+    {
+    id        => as_int($user->id),
+    real_name => as_string($user->name),
+    name      => as_login($user->login),
     };
 }
 
index 40d6f3b15271b9497dbf794c56de5594031ad669..fb8fd93ba4b27472e7bde8beefac2d8b81c6c7d2 100644 (file)
@@ -12,7 +12,8 @@ use strict;
 use warnings;
 
 use Bugzilla::API::1_0::Constants qw(API_AUTH_HEADERS);
-use Bugzilla::API::1_0::Util qw(taint_data fix_credentials api_include_exclude datetime_format_inbound);
+use Bugzilla::API::1_0::Util
+  qw(taint_data fix_credentials api_include_exclude datetime_format_inbound);
 
 use Bugzilla::Constants;
 use Bugzilla::Error;
@@ -36,12 +37,13 @@ has api_version   => (is => 'ro', default => '1_0',  init_arg => undef);
 has api_namespace => (is => 'ro', default => 'core', init_arg => undef);
 
 sub _build_content_type {
-    # 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.
-    return $_[0]->_best_content_type(
-        @{ $_[0]->constants->{REST_CONTENT_TYPE_WHITELIST} });
+
+  # 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.
+  return $_[0]
+    ->_best_content_type(@{$_[0]->constants->{REST_CONTENT_TYPE_WHITELIST}});
 }
 
 ##################
@@ -49,157 +51,160 @@ sub _build_content_type {
 ##################
 
 sub handle {
-    my ($self)  = @_;
-
-   # 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) {
-        if ($self->request->method eq 'OPTIONS'
-            && $self->api_options)
-        {
-            my $response = $self->response_header($self->constants->{STATUS_OK}, "");
-            my $options_string = join(', ', @{ $self->api_options });
-            $response->header('Allow' => $options_string,
-                              'Access-Control-Allow-Methods' => $options_string);
-            return $self->print_response($response);
-        }
-
-        ThrowUserError("rest_invalid_resource",
-                       { path   => $self->cgi->path_info,
-                         method => $self->request->method });
+  my ($self) = @_;
+
+  # 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) {
+    if ($self->request->method eq 'OPTIONS' && $self->api_options) {
+      my $response = $self->response_header($self->constants->{STATUS_OK}, "");
+      my $options_string = join(', ', @{$self->api_options});
+      $response->header(
+        'Allow'                        => $options_string,
+        'Access-Control-Allow-Methods' => $options_string
+      );
+      return $self->print_response($response);
     }
 
-    my $params = $self->_retrieve_json_params;
-    $self->_params_check($params);
+    ThrowUserError("rest_invalid_resource",
+      {path => $self->cgi->path_info, method => $self->request->method});
+  }
 
-    fix_credentials($params);
+  my $params = $self->_retrieve_json_params;
+  $self->_params_check($params);
 
-    # Fix includes/excludes for each call
-    api_include_exclude($params);
+  fix_credentials($params);
 
-    # Set callback name if exists
-    $self->callback($params->{'callback'}) if $params->{'callback'};
+  # Fix includes/excludes for each call
+  api_include_exclude($params);
 
-    Bugzilla->input_params($params);
+  # Set callback name if exists
+  $self->callback($params->{'callback'}) if $params->{'callback'};
 
-    # Let's try to authenticate before executing
-    $self->handle_login;
+  Bugzilla->input_params($params);
 
-    # Execute the handler
-    my $result = $self->_handle;
+  # Let's try to authenticate before executing
+  $self->handle_login;
 
-    # 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 };
-    }
+  # Execute the handler
+  my $result = $self->_handle;
+
+  # 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};
+  }
 
-    $self->response($result);
+  $self->response($result);
 }
 
 sub response {
-    my ($self, $result) = @_;
-
-    # Error data needs to be formatted differently
-    my $status_code;
-    if (my $error = $self->return_error) {
-        $status_code = delete $error->{status_code};
-        $error->{documentation} = REST_DOC;
-        $result = $error;
-    }
-    else {
-        $status_code = $self->success_code;
-    }
-
-    Bugzilla::Hook::process('webservice_rest_result',
-        { api => $self, result => \$result });
-
-    # ETag support
-    my $etag = $self->etag;
-    $self->etag($result) if !$etag;
-
-    # If accessing through web browser, then display in readable format
-    my $content;
-    if ($self->content_type eq 'text/html') {
-        $result = $self->json->pretty->canonical->allow_nonref->encode($result);
-        my $template = Bugzilla->template;
-        $template->process("rest.html.tmpl", { result => $result }, \$content)
-            || ThrowTemplateError($template->error());
-    }
-    else {
-        $content = $self->json->encode($result);
-    }
-
-    if (my $callback = $self->callback) {
-        # Prepend the response with /**/ in order to protect
-        # against possible encoding attacks (e.g., affecting Flash).
-        $content = "/**/$callback($content)";
-    }
-
-    my $response = $self->response_header($status_code, $content);
-
-    Bugzilla::Hook::process('webservice_rest_response',
-        { api => $self, response => $response });
-
-    $self->print_response($response);
+  my ($self, $result) = @_;
+
+  # Error data needs to be formatted differently
+  my $status_code;
+  if (my $error = $self->return_error) {
+    $status_code            = delete $error->{status_code};
+    $error->{documentation} = REST_DOC;
+    $result                 = $error;
+  }
+  else {
+    $status_code = $self->success_code;
+  }
+
+  Bugzilla::Hook::process('webservice_rest_result',
+    {api => $self, result => \$result});
+
+  # ETag support
+  my $etag = $self->etag;
+  $self->etag($result) if !$etag;
+
+  # If accessing through web browser, then display in readable format
+  my $content;
+  if ($self->content_type eq 'text/html') {
+    $result = $self->json->pretty->canonical->allow_nonref->encode($result);
+    my $template = Bugzilla->template;
+    $template->process("rest.html.tmpl", {result => $result}, \$content)
+      || ThrowTemplateError($template->error());
+  }
+  else {
+    $content = $self->json->encode($result);
+  }
+
+  if (my $callback = $self->callback) {
+
+    # Prepend the response with /**/ in order to protect
+    # against possible encoding attacks (e.g., affecting Flash).
+    $content = "/**/$callback($content)";
+  }
+
+  my $response = $self->response_header($status_code, $content);
+
+  Bugzilla::Hook::process('webservice_rest_response',
+    {api => $self, response => $response});
+
+  $self->print_response($response);
 }
 
 sub print_response {
-    my ($self, $response) = @_;
-
-    # Access Control
-    my @allowed_headers = qw(accept content-type origin user-agent x-requested-with);
-    foreach my $header (keys %{ API_AUTH_HEADERS() }) {
-        # We want to lowercase and replace _ with -
-        my $translated_header = $header;
-        $translated_header =~ tr/A-Z_/a-z\-/;
-        push(@allowed_headers, $translated_header);
-    }
-    $response->header("Access-Control-Allow-Origin", "*");
-    $response->header("Access-Control-Allow-Headers", join(', ', @allowed_headers));
-
-    # 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->etag;
-    if ($etag && $self->cgi->check_etag($etag)) {
-        push(@header_args, "-ETag", $etag);
-        print $self->cgi->header(-status => '304 Not Modified', @header_args);
-    }
-    else {
-        push(@header_args, "-ETag", $etag) if $etag;
-        print $self->cgi->header(-status => $response->code, @header_args);
-        print $response->content;
+  my ($self, $response) = @_;
+
+  # Access Control
+  my @allowed_headers
+    = qw(accept content-type origin user-agent x-requested-with);
+  foreach my $header (keys %{API_AUTH_HEADERS()}) {
+
+    # We want to lowercase and replace _ with -
+    my $translated_header = $header;
+    $translated_header =~ tr/A-Z_/a-z\-/;
+    push(@allowed_headers, $translated_header);
+  }
+  $response->header("Access-Control-Allow-Origin", "*");
+  $response->header("Access-Control-Allow-Headers", join(', ', @allowed_headers));
+
+  # 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->etag;
+  if ($etag && $self->cgi->check_etag($etag)) {
+    push(@header_args, "-ETag", $etag);
+    print $self->cgi->header(-status => '304 Not Modified', @header_args);
+  }
+  else {
+    push(@header_args, "-ETag", $etag) if $etag;
+    print $self->cgi->header(-status => $response->code, @header_args);
+    print $response->content;
+  }
 }
 
 sub handle_login {
-    my $self = shift;
-    my $controller = $self->controller;
-    my $method     = $self->method_name;
+  my $self       = shift;
+  my $controller = $self->controller;
+  my $method     = $self->method_name;
 
-    return if ($controller->login_exempt($method)
-               and !defined Bugzilla->input_params->{Bugzilla_login});
+  return
+    if ($controller->login_exempt($method)
+    and !defined Bugzilla->input_params->{Bugzilla_login});
 
-    Bugzilla->login();
+  Bugzilla->login();
 
-    Bugzilla::Hook::process('webservice_before_call',
-                            { rpc => $self, controller => $controller });
+  Bugzilla::Hook::process('webservice_before_call',
+    {rpc => $self, controller => $controller});
 }
 
 ###################
@@ -207,228 +212,232 @@ sub handle_login {
 ###################
 
 sub _handle {
-    my ($self) = shift;
-    my $method     = $self->method_name;
-    my $controller = $self->controller;
-    my $params     = Bugzilla->input_params;
-    my $cache      = Bugzilla->request_cache;
-
-    unless ($controller->can($method)) {
-        return $self->return_error(302, "No such a method : '$method'.");
-    }
-
-    # Let Bugzilla::Error know we are inside an eval() for exceptions
-    $cache->{in_eval} = 1;
-    my $result = eval { $controller->$method($params) };
-    $cache->{in_eval} = 0;
-
-    return $self->return_error if $self->return_error;
-
-    if ($@) {
-        return $self->return_error(500, "Procedure error: $@");
-    }
-
-    # Set the ETag if not already set in the webservice methods.
-    my $etag = $self->etag;
-    if (!$etag && ref $result) {
-        $self->etag($result);
-    }
-
-    return $result;
+  my ($self)     = shift;
+  my $method     = $self->method_name;
+  my $controller = $self->controller;
+  my $params     = Bugzilla->input_params;
+  my $cache      = Bugzilla->request_cache;
+
+  unless ($controller->can($method)) {
+    return $self->return_error(302, "No such a method : '$method'.");
+  }
+
+  # Let Bugzilla::Error know we are inside an eval() for exceptions
+  $cache->{in_eval} = 1;
+  my $result = eval { $controller->$method($params) };
+  $cache->{in_eval} = 0;
+
+  return $self->return_error if $self->return_error;
+
+  if ($@) {
+    return $self->return_error(500, "Procedure error: $@");
+  }
+
+  # Set the ETag if not already set in the webservice methods.
+  my $etag = $self->etag;
+  if (!$etag && ref $result) {
+    $self->etag($result);
+  }
+
+  return $result;
 }
 
 sub _params_check {
-    my ($self, $params) = @_;
-    my $method     = $self->method_name;
-    my $controller = $self->controller;
-
-    taint_data($params);
-
-    # Now, convert dateTime fields on input.
-    my @date_fields = @{ $controller->DATE_FIELDS->{$method} || [] };
-    foreach my $field (@date_fields) {
-        if (defined $params->{$field}) {
-            my $value = $params->{$field};
-            if (ref $value eq 'ARRAY') {
-                $params->{$field} =
-                    [ map { datetime_format_inbound($_) } @$value ];
-            }
-            else {
-                $params->{$field} = datetime_format_inbound($value);
-            }
-        }
+  my ($self, $params) = @_;
+  my $method     = $self->method_name;
+  my $controller = $self->controller;
+
+  taint_data($params);
+
+  # Now, convert dateTime fields on input.
+  my @date_fields = @{$controller->DATE_FIELDS->{$method} || []};
+  foreach my $field (@date_fields) {
+    if (defined $params->{$field}) {
+      my $value = $params->{$field};
+      if (ref $value eq 'ARRAY') {
+        $params->{$field} = [map { datetime_format_inbound($_) } @$value];
+      }
+      else {
+        $params->{$field} = datetime_format_inbound($value);
+      }
     }
-    my @base64_fields = @{ $controller->BASE64_FIELDS->{$method} || [] };
-    foreach my $field (@base64_fields) {
-        if (defined $params->{$field}) {
-            $params->{$field} = decode_base64($params->{$field});
-        }
+  }
+  my @base64_fields = @{$controller->BASE64_FIELDS->{$method} || []};
+  foreach my $field (@base64_fields) {
+    if (defined $params->{$field}) {
+      $params->{$field} = decode_base64($params->{$field});
     }
+  }
 
-    if ($self->request->method eq 'POST'
-        || $self->request->method eq 'PUT') {
-        # 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 });
-        }
+  if ($self->request->method eq 'POST' || $self->request->method eq 'PUT') {
+
+    # 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, $controller->READ_ONLY)) {
-            ThrowUserError('json_rpc_post_only',
-                           { method => $self->method_name });
-        }
+  }
+  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, $controller->READ_ONLY)) {
+      ThrowUserError('json_rpc_post_only', {method => $self->method_name});
     }
+  }
 
-    # Only allowed methods to be used from our whitelist
-    if (none { $_ eq $method} $controller->PUBLIC_METHODS) {
-        ThrowCodeError('unknown_method', { method => $self->method_name });
-    }
+  # Only allowed methods to be used from our whitelist
+  if (none { $_ eq $method } $controller->PUBLIC_METHODS) {
+    ThrowCodeError('unknown_method', {method => $self->method_name});
+  }
 }
 
 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 $api_params = $self->api_params) {
-        foreach my $param (keys %$api_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 $api_params->{$param}) {
-                $params->{$param} = $api_params->{$param};
-            }
-            # If param is a list then add any extra values to the list
-            elsif (ref $api_params->{$param}) {
-                my @extra_values = ref $params->{$param}
-                                   ? @{ $params->{$param} }
-                                   : ($params->{$param});
-                $params->{$param}
-                    = [ uniq (@{ $api_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 $api_params = $self->api_params) {
+    foreach my $param (keys %$api_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 $api_params->{$param}) {
+        $params->{$param} = $api_params->{$param};
+      }
+
+      # If param is a list then add any extra values to the list
+      elsif (ref $api_params->{$param}) {
+        my @extra_values
+          = ref $params->{$param} ? @{$params->{$param}} : ($params->{$param});
+        $params->{$param} = [uniq(@{$api_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) = @_;
-    my $api_version     = $self->api_version;
-    my $api_ext_version = $self->api_ext_version;
-    my $api_namespace   = $self->api_namespace;
-    my $api_path        = $self->api_path;
-    my $request_method  = $self->request->method;
-    my $resource_found  = 0;
-
-    my $resource_modules;
-    if ($api_ext_version) {
-        $resource_modules = File::Spec->catdir(bz_locations()->{extensionsdir},
-            $api_namespace, 'API', $api_ext_version, 'Resource', '*.pm');
-    }
-    else {
-        $resource_modules = File::Spec->catdir(bz_locations()->{cgi_path},
-            'Bugzilla','API', $api_version, 'Resource', '*.pm');
-    }
-
-    # Load in the WebService modules from the appropriate version directory
-    # and then call $module->REST_RESOURCES to get the resources array ref.
-    foreach my $module_file (bsd_glob($resource_modules)) {
-        # Create a controller object
-        trick_taint($module_file);
-        my $module_basename = basename($module_file, '.pm');
-        eval { require "$module_file"; } || die $@;
-        my $module_class = "Bugzilla::API::${api_version}::Resource::${module_basename}";
-        my $controller = $module_class->new;
-        next if !$controller || !$controller->can('REST_RESOURCES');
-
-        # The resource data for each module needs to be an array ref with an
-        # even number of elements to work correctly.
-        my $this_resources = $controller->REST_RESOURCES;
-        next if (ref $this_resources ne 'ARRAY' || scalar @$this_resources % 2 != 0);
-
-        while (my ($regex, $options_data) = splice(@$this_resources, 0, 2)) {
-            next if ref $options_data ne 'HASH';
-
-            if (my @matches = ($self->api_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->api_options([ keys %$options_data ]);
-
-                if ($options_data->{$request_method}) {
-                    my $resource_data = $options_data->{$request_method};
-
-                    # 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->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->api_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->success_code($resource_data->{success_code});
-                    }
-
-                    # Stash away for later
-                    $self->controller($controller);
-
-                    # No need to look further
-                    $resource_found = 1;
-                    last;
-                }
-            }
+  my ($self)          = @_;
+  my $api_version     = $self->api_version;
+  my $api_ext_version = $self->api_ext_version;
+  my $api_namespace   = $self->api_namespace;
+  my $api_path        = $self->api_path;
+  my $request_method  = $self->request->method;
+  my $resource_found  = 0;
+
+  my $resource_modules;
+  if ($api_ext_version) {
+    $resource_modules = File::Spec->catdir(bz_locations()->{extensionsdir},
+      $api_namespace, 'API', $api_ext_version, 'Resource', '*.pm');
+  }
+  else {
+    $resource_modules = File::Spec->catdir(bz_locations()->{cgi_path},
+      'Bugzilla', 'API', $api_version, 'Resource', '*.pm');
+  }
+
+  # Load in the WebService modules from the appropriate version directory
+  # and then call $module->REST_RESOURCES to get the resources array ref.
+  foreach my $module_file (bsd_glob($resource_modules)) {
+
+    # Create a controller object
+    trick_taint($module_file);
+    my $module_basename = basename($module_file, '.pm');
+    eval { require "$module_file"; } || die $@;
+    my $module_class
+      = "Bugzilla::API::${api_version}::Resource::${module_basename}";
+    my $controller = $module_class->new;
+    next if !$controller || !$controller->can('REST_RESOURCES');
+
+    # The resource data for each module needs to be an array ref with an
+    # even number of elements to work correctly.
+    my $this_resources = $controller->REST_RESOURCES;
+    next if (ref $this_resources ne 'ARRAY' || scalar @$this_resources % 2 != 0);
+
+    while (my ($regex, $options_data) = splice(@$this_resources, 0, 2)) {
+      next if ref $options_data ne 'HASH';
+
+      if (my @matches = ($self->api_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->api_options([keys %$options_data]);
+
+        if ($options_data->{$request_method}) {
+          my $resource_data = $options_data->{$request_method};
+
+          # 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->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->api_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->success_code($resource_data->{success_code});
+          }
+
+          # Stash away for later
+          $self->controller($controller);
+
+          # No need to look further
+          $resource_found = 1;
+          last;
         }
-        last if $resource_found;
+      }
     }
+    last if $resource_found;
+  }
 
-    return $resource_found;
+  return $resource_found;
 }
 
 1;
index 13c3eebacf60dc6ad90e9904b787682eb084265b..2307319bd329d06d48d056f6b3715b7fc9c72b95 100644 (file)
@@ -27,373 +27,384 @@ use List::MoreUtils qw(any none);
 use parent qw(Exporter);
 
 our @EXPORT = qw(
-    api_include_exclude
-    as_base64
-    as_boolean
-    as_datetime
-    as_double
-    as_email
-    as_int
-    as_int_array
-    as_login
-    as_login_array
-    as_name_array
-    as_string
-    as_string_array
-    datetime_format_inbound
-    datetime_format_outbound
-    extract_flags
-    filter
-    filter_wants
-    fix_credentials
-    params_to_objects
-    taint_data
-    translate
-    validate
+  api_include_exclude
+  as_base64
+  as_boolean
+  as_datetime
+  as_double
+  as_email
+  as_int
+  as_int_array
+  as_login
+  as_login_array
+  as_name_array
+  as_string
+  as_string_array
+  datetime_format_inbound
+  datetime_format_outbound
+  extract_flags
+  filter
+  filter_wants
+  fix_credentials
+  params_to_objects
+  taint_data
+  translate
+  validate
 );
 
 sub extract_flags {
-    my ($flags, $flag_types, $current_flags) = @_;
-    my (@new_flags, @old_flags);
+  my ($flags, $flag_types, $current_flags) = @_;
+  my (@new_flags, @old_flags);
 
-    $current_flags //= [];
+  $current_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;
         }
+      }
+    }
+
+    if ($flag->{id}) {
+      push(@old_flags, $flag);
+    }
+    else {
+      push(@new_flags, $flag);
     }
+  }
 
-    return (\@old_flags, \@new_flags);
+  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 wil
-            # 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 wil
+      # match \w+, contain '.' or '-', so we delete any key that
+      # doesn't match that.
+      if ($key !~ /^[\w\.\-]+$/) {
+        delete $item->{$key};
+      }
     }
-    return @_;
+  }
+  return @_;
 }
 
 sub api_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;
 }
 
-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) = @_;
-    my $cgi = Bugzilla->cgi;
-
-    # Allow user to pass in authentication details in X-Headers
-    # This allows callers to keep credentials out of GET request query-strings
-    if ($cgi) {
-        foreach my $field (keys %{ API_AUTH_HEADERS() }) {
-            next if exists $params->{API_AUTH_HEADERS->{$field}} || ($cgi->http($field) // '') eq '';
-            $params->{API_AUTH_HEADERS->{$field}} = uri_unescape($cgi->http($field));
-        }
-    }
-
-    # 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};
+  my ($params) = @_;
+  my $cgi = Bugzilla->cgi;
+
+  # Allow user to pass in authentication details in X-Headers
+  # This allows callers to keep credentials out of GET request query-strings
+  if ($cgi) {
+    foreach my $field (keys %{API_AUTH_HEADERS()}) {
+      next
+        if exists $params->{API_AUTH_HEADERS->{$field}}
+        || ($cgi->http($field) // '') eq '';
+      $params->{API_AUTH_HEADERS->{$field}} = uri_unescape($cgi->http($field));
     }
-    # 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 });
+  }
+
+  # 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});
 }
 
 sub datetime_format_inbound {
-    my ($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 ($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 ($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() . 'Z';
+  my ($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() . 'Z';
 }
 
 
 # simple types
 
-sub as_boolean  { $_[0] ? JSON::true : JSON::false }
-sub as_double   { defined $_[0] ? $_[0] + 0.0 : JSON::null }
-sub as_int      { defined $_[0] ? int($_[0])  : JSON::null }
-sub as_string   { defined $_[0] ? $_[0] . ''  : JSON::null }
+sub as_boolean { $_[0]         ? JSON::true  : JSON::false }
+sub as_double  { defined $_[0] ? $_[0] + 0.0 : JSON::null }
+sub as_int     { defined $_[0] ? int($_[0])  : JSON::null }
+sub as_string  { defined $_[0] ? $_[0] . ''  : JSON::null }
 
 # array types
 
-sub as_int_array    { [ map { as_int($_) }          @{ $_[0] // [] } ] }
-sub as_login_array  { [ map { as_login($_) }        @{ $_[0] // [] } ] }
-sub as_name_array   { [ map { as_string($_->name) } @{ $_[0] // [] } ] }
-sub as_string_array { [ map { as_string($_) }       @{ $_[0] // [] } ] }
+sub as_int_array    { [map { as_int($_) } @{$_[0] //          []}] }
+sub as_login_array  { [map { as_login($_) } @{$_[0] //        []}] }
+sub as_name_array   { [map { as_string($_->name) } @{$_[0] // []}] }
+sub as_string_array { [map { as_string($_) } @{$_[0] //       []}] }
 
 # complex types
 
 sub as_datetime {
-    return defined $_[0]
-        ? datetime_from($_[0], 'UTC')->iso8601() . 'Z'
-        : JSON::null;
+  return
+    defined $_[0] ? datetime_from($_[0], 'UTC')->iso8601() . 'Z' : JSON::null;
 }
 
-sub as_login    {
-    defined $_[0]
-        ? ( Bugzilla->params->{use_email_as_login} ? email_filter($_[0]) : $_[0] . '' )
-        : JSON::null;
+sub as_login {
+  defined $_[0]
+    ? (Bugzilla->params->{use_email_as_login} ? email_filter($_[0]) : $_[0] . '')
+    : JSON::null;
 }
 
-sub as_email    {
-    defined($_[0]) && Bugzilla->user->in_group('editusers') ? $_[0] . '' : JSON::null;
+sub as_email {
+  defined($_[0])
+    && Bugzilla->user->in_group('editusers') ? $_[0] . '' : JSON::null;
 }
 
-sub as_base64   {
-    utf8::encode($_[0]) if utf8::is_utf8($_[0]);
-    return encode_base64($_[0], '');
+sub as_base64 {
+  utf8::encode($_[0]) if utf8::is_utf8($_[0]);
+  return encode_base64($_[0], '');
 }
 
 1;
index 185d08430891de449ac0b414287d61b2062b3e0d..16e21c7a946e99f0bb1252ee4f854e842e961455 100644 (file)
@@ -40,178 +40,177 @@ has api_ext_version => (is => 'rw', default => '');
 has api_options     => (is => 'rw', default => sub { [] });
 has api_params      => (is => 'rw', default => sub { {} });
 has api_path        => (is => 'rw', default => '');
-has cgi             => (is => 'lazy');
-has content_type    => (is => 'rw', default => 'application/json');
-has controller      => (is => 'rw', default => undef);
-has json            => (is => 'lazy');
-has load_error      => (is => 'rw', default => undef);
-has method_name     => (is => 'rw', default => '');
-has request         => (is => 'lazy');
-has success_code    => (is => 'rw', default => 200);
+has cgi          => (is => 'lazy');
+has content_type => (is => 'rw', default => 'application/json');
+has controller   => (is => 'rw', default => undef);
+has json         => (is => 'lazy');
+has load_error   => (is => 'rw', default => undef);
+has method_name  => (is => 'rw', default => '');
+has request      => (is => 'lazy');
+has success_code => (is => 'rw', default => 200);
 
 ##################
 # Public methods #
 ##################
 
 sub server {
-    my ($class) = @_;
+  my ($class) = @_;
 
-    my $api_namespace = DEFAULT_API_NAMESPACE;
-    my $api_version   = DEFAULT_API_VERSION;
+  my $api_namespace = DEFAULT_API_NAMESPACE;
+  my $api_version   = DEFAULT_API_VERSION;
 
-    # First load the default server in case something fails
-    # we still have something to return.
-    my $server_class = "Bugzilla::API::${api_version}::Server";
-    require_module($server_class);
-    my $self = $server_class->new;
+  # First load the default server in case something fails
+  # we still have something to return.
+  my $server_class = "Bugzilla::API::${api_version}::Server";
+  require_module($server_class);
+  my $self = $server_class->new;
 
-    my $path_info = Bugzilla->cgi->path_info;
+  my $path_info = Bugzilla->cgi->path_info;
 
-    # If we do not match /<namespace>/<version>/ then we assume legacy calls
-    # and use the default namespace and version.
-    if ($path_info =~ m|^/([^/]+)/(\d+\.\d+(?:\.\d+)?)/|) {
-        # First figure out the namespace we are accessing (core is native)
-        $api_namespace = $1 if $path_info =~ s|^/([^/]+)||;
-        $api_namespace = $self->_check_namespace($api_namespace);
+  # If we do not match /<namespace>/<version>/ then we assume legacy calls
+  # and use the default namespace and version.
+  if ($path_info =~ m|^/([^/]+)/(\d+\.\d+(?:\.\d+)?)/|) {
 
-        # Figure out which version we are looking for based on path
-        $api_version = $1 if $path_info =~ s|^/(\d+\.\d+(?:\.\d+)?)(/.*)$|$2|;
-        $api_version = $self->_check_version($api_version, $api_namespace);
-    }
+    # First figure out the namespace we are accessing (core is native)
+    $api_namespace = $1 if $path_info =~ s|^/([^/]+)||;
+    $api_namespace = $self->_check_namespace($api_namespace);
 
-    # If the version pulled from the path is different than
-    # what the server is currently, then reload as the new version.
-    if ($api_version ne $self->api_version) {
-        my $server_class = "Bugzilla::API::${api_version}::Server";
-        require_module($server_class);
-        $self = $server_class->new;
-    }
+    # Figure out which version we are looking for based on path
+    $api_version = $1 if $path_info =~ s|^/(\d+\.\d+(?:\.\d+)?)(/.*)$|$2|;
+    $api_version = $self->_check_version($api_version, $api_namespace);
+  }
 
-    # Stuff away for later
-    $self->api_path($path_info);
+  # If the version pulled from the path is different than
+  # what the server is currently, then reload as the new version.
+  if ($api_version ne $self->api_version) {
+    my $server_class = "Bugzilla::API::${api_version}::Server";
+    require_module($server_class);
+    $self = $server_class->new;
+  }
+
+  # Stuff away for later
+  $self->api_path($path_info);
 
-    return $self;
+  return $self;
 }
 
 sub constants {
-    my ($self) = @_;
-    return $self->{_constants} if defined $self->{_constants};
-
-    no strict 'refs';
-
-    my $api_version = $self->api_version;
-    my $class = "Bugzilla::API::${api_version}::Constants";
-    require_module($class);
-
-    $self->{_constants} = {};
-    foreach my $constant (@{$class . "::EXPORT_OK"}) {
-        if (ref $class->$constant) {
-            $self->{_constants}->{$constant} = $class->$constant;
-        }
-        else {
-            my @list = ($class->$constant);
-            $self->{_constants}->{$constant} = (scalar(@list) == 1) ? $list[0] : \@list;
-        }
+  my ($self) = @_;
+  return $self->{_constants} if defined $self->{_constants};
+
+  no strict 'refs';
+
+  my $api_version = $self->api_version;
+  my $class       = "Bugzilla::API::${api_version}::Constants";
+  require_module($class);
+
+  $self->{_constants} = {};
+  foreach my $constant (@{$class . "::EXPORT_OK"}) {
+    if (ref $class->$constant) {
+      $self->{_constants}->{$constant} = $class->$constant;
+    }
+    else {
+      my @list = ($class->$constant);
+      $self->{_constants}->{$constant} = (scalar(@list) == 1) ? $list[0] : \@list;
     }
+  }
 
-    return $self->{_constants};
+  return $self->{_constants};
 }
 
 sub response_header {
-    my ($self, $code, $result) = @_;
-    # 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($result)) {
-        utf8::encode($result);
-        # Since we're going to just be sending raw bytes, we need to
-        # set STDOUT to not expect utf8.
-        disable_utf8();
-    }
-    my $h = HTTP::Headers->new;
-    $h->header('Content-Type' => $self->content_type . '; charset=UTF-8');
-    return HTTP::Response->new($code => undef, $h, $result);
+  my ($self, $code, $result) = @_;
+
+  # 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($result)) {
+    utf8::encode($result);
+
+    # Since we're going to just be sending raw bytes, we need to
+    # set STDOUT to not expect utf8.
+    disable_utf8();
+  }
+  my $h = HTTP::Headers->new;
+  $h->header('Content-Type' => $self->content_type . '; charset=UTF-8');
+  return HTTP::Response->new($code => undef, $h, $result);
 }
 
 ###################################
 # Public methods to be overridden #
 ###################################
 
-sub handle { }
-sub response { }
+sub handle         { }
+sub response       { }
 sub print_response { }
-sub handle_login { }
+sub handle_login   { }
 
 ###################
 # Utility methods #
 ###################
 
 sub return_error {
-    my ($self, $status_code, $message, $error_code) = @_;
-    if ($status_code && $message) {
-        $self->{_return_error} = {
-            status_code => $status_code,
-            error       => JSON::true,
-            message     => $message
-        };
-        $self->{_return_error}->{code} = $error_code if $error_code;
-    }
-    return $self->{_return_error};
+  my ($self, $status_code, $message, $error_code) = @_;
+  if ($status_code && $message) {
+    $self->{_return_error}
+      = {status_code => $status_code, error => JSON::true, message => $message};
+    $self->{_return_error}->{code} = $error_code if $error_code;
+  }
+  return $self->{_return_error};
 }
 
 sub 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->{_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->{_callback};
+    $self->{_callback} = $value;
+
+    # JSONP needs to be parsed by a JS parser, not by a JSON parser.
+    $self->content_type('text/javascript');
+  }
+  return $self->{_callback};
 }
 
 # ETag support
 sub 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->{'_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->{'_etag'};
+
+    $cache->{'_etag'} = md5_base64($data);
+  }
+  return $cache->{'_etag'};
 }
 
 # HACK: Allow error tag checking to work with t/012throwables.t
 sub ThrowUserError {
-    my ($error, $self, $vars) = @_;
-    $self->load_error({ type  => 'user',
-                        error => $error,
-                        vars  => $vars });
+  my ($error, $self, $vars) = @_;
+  $self->load_error({type => 'user', error => $error, vars => $vars});
 }
 
 sub ThrowCodeError {
-    my ($error, $self, $vars) = @_;
-    $self->load_error({ type  => 'code',
-                        error => $error,
-                        vars  => $vars });
+  my ($error, $self, $vars) = @_;
+  $self->load_error({type => 'code', error => $error, vars => $vars});
 }
 
 ###################
@@ -219,143 +218,141 @@ sub ThrowCodeError {
 ###################
 
 sub _build_cgi {
-    return Bugzilla->cgi;
+  return Bugzilla->cgi;
 }
 
 sub _build_json {
-    # This may seem a little backwards to set utf8(0), but what this really
-    # means is "don't convert our utf8 into byte strings, just leave it as a
-    # utf8 string."
-    return JSON->new->utf8(0)
-           ->allow_blessed(1)
-           ->convert_blessed(1);
+
+  # This may seem a little backwards to set utf8(0), but what this really
+  # means is "don't convert our utf8 into byte strings, just leave it as a
+  # utf8 string."
+  return JSON->new->utf8(0)->allow_blessed(1)->convert_blessed(1);
 }
 
 sub _build_request {
-    return HTTP::Request->new($_[0]->cgi->request_method, $_[0]->cgi->url);
+  return HTTP::Request->new($_[0]->cgi->request_method, $_[0]->cgi->url);
 }
 
 sub _check_namespace {
-    my ($self, $namespace) = @_;
+  my ($self, $namespace) = @_;
 
-    # No need to do anything else if native api
-    return $namespace if lc($namespace) eq lc(DEFAULT_API_NAMESPACE);
+  # No need to do anything else if native api
+  return $namespace if lc($namespace) eq lc(DEFAULT_API_NAMESPACE);
 
-    # Check if namespace matches an extension name
-    my $found = 0;
-    foreach my $extension (@{ Bugzilla->extensions }) {
-        $found = 1 if lc($extension->NAME) eq lc($namespace);
-    }
-    # Make sure we have this namespace available
-    if (!$found) {
-        ThrowUserError('unknown_api_namespace', $self,
-                       { api_namespace => $namespace });
-        return DEFAULT_API_NAMESPACE;
-    }
+  # Check if namespace matches an extension name
+  my $found = 0;
+  foreach my $extension (@{Bugzilla->extensions}) {
+    $found = 1 if lc($extension->NAME) eq lc($namespace);
+  }
 
-    return $namespace;
+  # Make sure we have this namespace available
+  if (!$found) {
+    ThrowUserError('unknown_api_namespace', $self, {api_namespace => $namespace});
+    return DEFAULT_API_NAMESPACE;
+  }
+
+  return $namespace;
 }
 
 sub _check_version {
-    my ($self, $version, $namespace) = @_;
-
-    return DEFAULT_API_VERSION if !defined $version;
-
-    my $old_version = $version;
-    $version =~ s/\./_/g;
-
-    my $version_dir;
-    if (lc($namespace) eq 'core') {
-        $version_dir = File::Spec->catdir('Bugzilla', 'API', $version);
-    }
-    else {
-        $version_dir = File::Spec->catdir(bz_locations()->{extensionsdir},
-                                          $namespace, 'API', $version);
+  my ($self, $version, $namespace) = @_;
+
+  return DEFAULT_API_VERSION if !defined $version;
+
+  my $old_version = $version;
+  $version =~ s/\./_/g;
+
+  my $version_dir;
+  if (lc($namespace) eq 'core') {
+    $version_dir = File::Spec->catdir('Bugzilla', 'API', $version);
+  }
+  else {
+    $version_dir = File::Spec->catdir(bz_locations()->{extensionsdir},
+      $namespace, 'API', $version);
+  }
+
+  # Make sure we actual have this version installed
+  if (!-d $version_dir) {
+    ThrowUserError('unknown_api_version', $self,
+      {api_version => $old_version, api_namespace => $namespace});
+    return DEFAULT_API_VERSION;
+  }
+
+  # If we using an extension API, we need to determining which version of
+  # the Core API it was written for.
+  if (lc($namespace) ne 'core') {
+    my $core_api_version;
+    foreach my $extension (@{Bugzilla->extensions}) {
+      next if lc($extension->NAME) ne lc($namespace);
+      if ($extension->API_VERSION_MAP && $extension->API_VERSION_MAP->{$version}) {
+        $self->api_ext_version($version);
+        $version = $extension->API_VERSION_MAP->{$version};
+      }
     }
+  }
 
-    # Make sure we actual have this version installed
-    if (!-d $version_dir) {
-        ThrowUserError('unknown_api_version', $self,
-                       { api_version   => $old_version,
-                         api_namespace => $namespace });
-        return DEFAULT_API_VERSION;
-    }
-
-    # If we using an extension API, we need to determining which version of
-    # the Core API it was written for.
-    if (lc($namespace) ne 'core') {
-        my $core_api_version;
-        foreach my $extension (@{ Bugzilla->extensions }) {
-            next if lc($extension->NAME) ne lc($namespace);
-            if ($extension->API_VERSION_MAP
-                && $extension->API_VERSION_MAP->{$version})
-            {
-                $self->api_ext_version($version);
-                $version = $extension->API_VERSION_MAP->{$version};
-            }
-        }
-    }
-
-    return $version;
+  return $version;
 }
 
 sub _best_content_type {
-    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[0];
-    }
-    my $score = sub { $self->_score_type(shift, @accept_types) };
-    my @scored_types = sort {$score->($b) <=> $score->($a)} @types;
-    return $scored_types[0] || '*/*';
+  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[0];
+  }
+  my $score = sub { $self->_score_type(shift, @accept_types) };
+  my @scored_types = sort { $score->($b) <=> $score->($a) } @types;
+  return $scored_types[0] || '*/*';
 }
 
 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;
 }
 
 ####################################
 # Private methods to be overridden #
 ####################################
 
-sub _handle { }
-sub _params_check { }
+sub _handle               { }
+sub _params_check         { }
 sub _retrieve_json_params { }
-sub _find_resource { }
+sub _find_resource        { }
 
 1;
 
index 7d96beedab262e6d8f22a2a93303449fe51d24ad..94fecd7f63957b0451994de63f6c05703489ac38 100644 (file)
@@ -58,55 +58,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      ######
@@ -127,7 +123,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
@@ -141,14 +137,14 @@ the bug object to which the attachment is attached
 =cut
 
 sub bug {
-    my ($self) = @_;
-    require Bugzilla::Bug;
-    return $self->{bug} if defined $self->{bug};
-
-    # note $bug exists as a strong reference to keep $self->{bug} defined until the end of this method
-    my $bug = $self->{bug} = Bugzilla::Bug->new({ id => $_[0]->bug_id, cache => 1 });
-    weaken($self->{bug}) unless isweak($self->{bug});
-    return $bug;
+  my ($self) = @_;
+  require Bugzilla::Bug;
+  return $self->{bug} if defined $self->{bug};
+
+# note $bug exists as a strong reference to keep $self->{bug} defined until the end of this method
+  my $bug = $self->{bug} = Bugzilla::Bug->new({id => $_[0]->bug_id, cache => 1});
+  weaken($self->{bug}) unless isweak($self->{bug});
+  return $bug;
 }
 
 =over
@@ -162,7 +158,7 @@ user-provided text describing the attachment
 =cut
 
 sub description {
-    return $_[0]->{description};
+  return $_[0]->{description};
 }
 
 =over
@@ -176,7 +172,7 @@ the attachment's MIME media type
 =cut
 
 sub contenttype {
-    return $_[0]->{mimetype};
+  return $_[0]->{mimetype};
 }
 
 =over
@@ -190,8 +186,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
@@ -205,7 +201,7 @@ the date and time on which the attacher attached the attachment
 =cut
 
 sub attached {
-    return $_[0]->{creation_ts};
+  return $_[0]->{creation_ts};
 }
 
 =over
@@ -219,7 +215,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
@@ -233,7 +229,7 @@ the name of the file the attacher attached
 =cut
 
 sub filename {
-    return $_[0]->{filename};
+  return $_[0]->{filename};
 }
 
 =over
@@ -247,7 +243,7 @@ whether or not the attachment is a patch
 =cut
 
 sub ispatch {
-    return $_[0]->{ispatch};
+  return $_[0]->{ispatch};
 }
 
 =over
@@ -261,7 +257,7 @@ whether or not the attachment is obsolete
 =cut
 
 sub isobsolete {
-    return $_[0]->{isobsolete};
+  return $_[0]->{isobsolete};
 }
 
 =over
@@ -275,7 +271,7 @@ whether or not the attachment is private
 =cut
 
 sub isprivate {
-    return $_[0]->{isprivate};
+  return $_[0]->{isprivate};
 }
 
 =over
@@ -292,23 +288,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
@@ -323,34 +320,37 @@ As a side-effect, sets $self->is_on_filesystem.
 =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);
-
-    # Setting the property here is cheap, as opposed to making an extra
-    # query later, and hitting the filesystem to see if the file is
-    # still there.
-    $self->{is_on_filesystem} = 0;
-    # 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())) {
-            # file is actually on disk.
-            $self->{is_on_filesystem} = 1;
-            local $/;
-            binmode AH;
-            $self->{data} = <AH>;
-            close(AH);
-        }
+                                                      WHERE id = ?", undef,
+    $self->id
+  );
+
+  # Setting the property here is cheap, as opposed to making an extra
+  # query later, and hitting the filesystem to see if the file is
+  # still there.
+  $self->{is_on_filesystem} = 0;
+
+  # 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())) {
+
+      # file is actually on disk.
+      $self->{is_on_filesystem} = 1;
+      local $/;
+      binmode AH;
+      $self->{data} = <AH>;
+      close(AH);
     }
+  }
 
-    return $self->{data};
+  return $self->{data};
 }
 
 =over
@@ -370,13 +370,14 @@ parameter), as opposed to in the database.
 # footprint. e.g. bug 906010 and bug 1073241.
 
 sub is_on_filesystem {
-    my $self = shift;
-    return $self->{is_on_filesystem} if exists $self->{is_on_filesystem};
-    # In order to serve an attachment, you also send the datasize in the
-    # content-length header. Making additional queries which are exactly
-    # the same as found in the datasize code path is just wasteful.
-    my $datasize = $self->datasize;
-    return $self->{is_on_filesystem};
+  my $self = shift;
+  return $self->{is_on_filesystem} if exists $self->{is_on_filesystem};
+
+  # In order to serve an attachment, you also send the datasize in the
+  # content-length header. Making additional queries which are exactly
+  # the same as found in the datasize code path is just wasteful.
+  my $datasize = $self->datasize;
+  return $self->{is_on_filesystem};
 }
 
 =over
@@ -399,43 +400,45 @@ As a side-effect, sets $self->is_on_filesystem.
 # 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;
-
-    # Setting the property here is cheap, as opposed to making an extra
-    # query later, and hitting the filesystem to see if the file is
-    # still there.
-    $self->{is_on_filesystem} = 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())) {
-            # file is actually on disk.
-            $self->{is_on_filesystem} = 1;
-            binmode AH;
-            $self->{datasize} = (stat(AH))[7];
-            close(AH);
-        }
+                                        WHERE id = ?", undef, $self->id
+  ) || 0;
+
+  # Setting the property here is cheap, as opposed to making an extra
+  # query later, and hitting the filesystem to see if the file is
+  # still there.
+  $self->{is_on_filesystem} = 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())) {
+
+      # file is actually on disk.
+      $self->{is_on_filesystem} = 1;
+      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
@@ -449,8 +452,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
@@ -465,200 +469,214 @@ 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 ( (Bugzilla->input_params->{contenttypemethod} // '') eq 'autodetect'
+    && ($content_type eq 'application/octet-stream' || $content_type =~ m{text/x-})
+    && 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 ((Bugzilla->input_params->{contenttypemethod} // '') eq 'autodetect'
-        && ($content_type eq 'application/octet-stream' || $content_type =~ m{text/x-})
-        && 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)
-        && (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)
+    && (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
@@ -680,69 +698,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
@@ -761,13 +784,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<validate_obsolete($bug, $attach_ids)>
@@ -786,37 +811,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;
 }
 
 ###############################
@@ -851,112 +875,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
@@ -974,30 +1005,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});
+  }
 }
 
 ###############################
@@ -1006,37 +1040,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(scalar $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(scalar $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;
 }
 
 
index 1cc3f0bcacf917ed388b437ac733dee531ef6a3c..2f8974b8ae75ecdec137d6167973d42d6d8e10fe 100644 (file)
@@ -23,181 +23,196 @@ 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, so we have
-        # to encode attachment data as utf8.
-        $attachment->data; # Populate ->{data}
-        utf8::decode($attachment->{data});
-        $reader->iterate_string('Attachment ' . $attachment->id, $attachment->data);
-    }
-}
+  require PatchReader::Raw;
+  my $reader = new PatchReader::Raw;
 
-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.
-    if ($format ne 'raw') {
-        $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') {
-        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: $!";
-
-    # 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';
-    }
+    setup_template_patch_reader($reader, $vars);
 
-    my $reader = new PatchReader::Raw;
+    # The patch is going to be displayed in a HTML page, so we have
+    # to encode attachment data as utf8.
+    $attachment->data;    # Populate ->{data}
+    utf8::decode($attachment->{data});
+    $reader->iterate_string('Attachment ' . $attachment->id, $attachment->data);
+  }
+}
 
-    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.
+  if ($format ne 'raw') {
+    $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') {
+    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);
 }
 
 ######################
@@ -205,92 +220,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') {
-        # 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') {
 
-    # 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;
index 9322be24bbd627b06217225877c41339e16c5567..50bb7f513227cc9e402b1ba2efbdad2366a99d63 100644 (file)
@@ -12,9 +12,9 @@ use strict;
 use warnings;
 
 use fields qw(
-    _info_getter
-    _verifier
-    _persister
+  _info_getter
+  _verifier
+  _persister
 );
 
 use Bugzilla::Constants;
@@ -28,222 +28,227 @@ 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_login {
-    return $_[0]->user_can_create_account;
+  return $_[0]->user_can_create_account;
 }
 
 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 persistence 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 persistence 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;
index cd075a3cff0b54ffbe716f715e19a09cbee11392..50f454c108dc2a438a6a47b458271f05afd0ae25 100644 (file)
@@ -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;
index c7829f1ce00ed8c36fba7d92e7ab2eda50758d93..fdb7475ffd47d11f365f6330bedd51a6d7f1734d 100644 (file)
@@ -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;
index d2934e6d09e5a3ecc3d845d4bd7e0375aefd757d..58f84b798e6dc0c278dc508da764a43a332189c5 100644 (file)
@@ -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;
index d8bf2f08f8c8ed04e1d1ba9414411b7e13ba55d8..1b0f6eacae010137f06044c3e74394826511ceea 100644 (file)
@@ -23,128 +23,132 @@ 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;
-        }
+  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()) {
-            if (exists 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 });
-                }
-            }
-            elsif ($login_cookie && Bugzilla->usage_mode == USAGE_MODE_REST) {
-                # REST requires an api-token when using cookie authentication
-                # fall back to a non-authenticated request
-                $login_cookie = '';
-            }
+    # If the call is for a web service, and an api token is provided, check
+    # it is valid.
+    if (i_am_webservice()) {
+      if (exists 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});
         }
-    }
+      }
+      elsif ($login_cookie && Bugzilla->usage_mode == USAGE_MODE_REST) {
 
-    # 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'});
+        # REST requires an api-token when using cookie authentication
+        # fall back to a non-authenticated request
+        $login_cookie = '';
+      }
     }
+  }
+
+  # If no cookies were provided, we also look for a login token
+  # passed in the parameters of a webservice
+  my $token = $self->login_token;
+  if ($token && (!$login_cookie || !$user_id)) {
+    ($user_id, $login_cookie) = ($token->{'user_id'}, $token->{'login_token'});
+  }
+
+  my $ip_addr = remote_ip();
 
-    my $ip_addr = remote_ip();
+  if ($login_cookie && $user_id) {
 
-    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);
+    # 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
+    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;
index 2ff81e82e71491b5cd948e4c7bce96ab905b8b62..1c9181ebbf70f63d67cace4476bc963ea1f20fad 100644 (file)
@@ -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;
index a7c24a3fb006d6a26733fc7e5c42f49179c93b54..fc356d67da351ea18106adf2a0400de821096e9b 100644 (file)
@@ -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;
index 69f6e9c60ac9fc71efdc5de2cecad67c3d28f442..a4ef1fa999efd93f5dba8df3d5785b8125ebbf6c 100644 (file)
@@ -20,149 +20,158 @@ 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->sudoer || Bugzilla->user;
-    my $type = $param->{type} || LOGOUT_ALL;
-
-    if ($type == LOGOUT_ALL) {
-        $dbh->do("DELETE FROM logincookies WHERE userid = ?",
-                 undef, $user->id);
-        $dbh->do("DELETE FROM tokens WHERE userid = ? AND tokentype = 'sudo'",
-                 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);
-        my $token = $cgi->cookie('sudo');
-        delete_token($token);
-    } 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->sudoer || Bugzilla->user;
+  my $type = $param->{type} || LOGOUT_ALL;
+
+  if ($type == LOGOUT_ALL) {
+    $dbh->do("DELETE FROM logincookies WHERE userid = ?", undef, $user->id);
+    $dbh->do("DELETE FROM tokens WHERE userid = ? AND tokentype = 'sudo'",
+      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
+    );
+    my $token = $cgi->cookie('sudo');
+    delete_token($token);
+  }
+  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;
index 20318b3a546ca083877aec9b78f4fd8b32a4946f..cc9361890848b1fc8674830983719fa074c13699 100644 (file)
@@ -19,119 +19,131 @@ 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 $login     = $params->{bz_username} || $params->{username};
-    my $email     = Bugzilla->params->{use_email_as_login} ? $login : $params->{email};
-    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 $login_user_id = login_to_id($login || '');
-        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 login, and they are
-        # not the same id, then we have a conflict.
-        if ($login_user_id && $extern_user_id
-            && $login_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    => $login} };
-        }
-
-        # If we have a valid login, but no valid id,
-        # then we have to create the user. This happens when we're
-        # passed only a login, and that login doesn't exist already.
-        if ($login && !$login_user_id && !$extern_user_id) {
-            validate_email_syntax($email)
-              || return { failure => AUTH_ERROR,
-                          error   => 'auth_invalid_email',
-                          details => {addr => $email} };
-            # 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    => $login,
-                email         => $email,
-                cryptpassword => $password,
-                realname      => $real_name});
-            $login_user_id = $user->id;
-        }
-
-        # If we have a valid login id and an extern_id, but no valid
-        # extern_user_id, then we have to set the user's extern_id.
-        if ($extern_id && $login_user_id && !$extern_user_id) {
-            $dbh->do('UPDATE profiles SET extern_id = ? WHERE userid = ?',
-                     undef, $extern_id, $login_user_id);
-            Bugzilla->memcached->clear({ table => 'profiles', id => $login_user_id });
-        }
-
-        # Finally, at this point, one of these will give us a valid user id.
-        $user_id = $extern_user_id || $login_user_id;
+  my ($self, $params) = @_;
+  my $dbh = Bugzilla->dbh;
+
+  my $extern_id = $params->{extern_id};
+  my $login = $params->{bz_username} || $params->{username};
+  my $email = Bugzilla->params->{use_email_as_login} ? $login : $params->{email};
+  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 $login_user_id = login_to_id($login || '');
+    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 ($email && lc($user->email) ne lc($email)) {
-        validate_email_syntax($email)
-          || return { failure => AUTH_ERROR, error => 'auth_invalid_email',
-                      details => {addr => $email} };
-        $user->set_email($email);
-        $changed = 1;
+    # If we have both a valid extern_id and a valid login, and they are
+    # not the same id, then we have a conflict.
+    if ($login_user_id && $extern_user_id && $login_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 => $login}
+      };
     }
-    if ($login && lc($user->login) ne lc($login)) {
-        $user->set_login($login);
-        $changed = 1;
+
+    # If we have a valid login, but no valid id,
+    # then we have to create the user. This happens when we're
+    # passed only a login, and that login doesn't exist already.
+    if ($login && !$login_user_id && !$extern_user_id) {
+      validate_email_syntax($email) || return {
+        failure => AUTH_ERROR,
+        error   => 'auth_invalid_email',
+        details => {addr => $email}
+      };
+
+      # 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    => $login,
+        email         => $email,
+        cryptpassword => $password,
+        realname      => $real_name
+      });
+      $login_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 login id and an extern_id, but no valid
+    # extern_user_id, then we have to set the user's extern_id.
+    if ($extern_id && $login_user_id && !$extern_user_id) {
+      $dbh->do('UPDATE profiles SET extern_id = ? WHERE userid = ?',
+        undef, $extern_id, $login_user_id);
+      Bugzilla->memcached->clear({table => 'profiles', id => $login_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 || $login_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 ($email && lc($user->email) ne lc($email)) {
+    validate_email_syntax($email) || return {
+      failure => AUTH_ERROR,
+      error   => 'auth_invalid_email',
+      details => {addr => $email}
+    };
+    $user->set_email($email);
+    $changed = 1;
+  }
+  if ($login && lc($user->login) ne lc($login)) {
+    $user->set_login($login);
+    $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;
index fd11d196009d879b74d89d4fe1df3b13b926c56c..a15d1d9983c1c59b563a3d465cd1a4e669e686fe 100644 (file)
@@ -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;
index 0e6b0ddfe1ccb08327adbfb1ba1b417160032e35..34195804c6e0e8100849261eb1c1dc7d9da9ab5d 100644 (file)
@@ -13,7 +13,7 @@ use warnings;
 
 use base qw(Bugzilla::Auth::Verify);
 use fields qw(
-    ldap
+  ldap
 );
 
 use Bugzilla::Constants;
@@ -28,124 +28,136 @@ 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->{email} = $emails[0];
 
-        # Default to the first email address returned.
-        $params->{email} = $emails[0];
+    if (@emails > 1) {
 
-        if (@emails > 1) {
-            # Cycle through the addresses and check if they're Bugzilla logins.
-            # Use the first one that returns a valid id. 
-            foreach my $email (@emails) {
-                if ( email_to_id($email) ) {
-                    $params->{email} = $email;
-                    last;
-                }
-            }
+      # Cycle through the addresses and check if they're Bugzilla logins.
+      # Use the first one that returns a valid id.
+      foreach my $email (@emails) {
+        if (email_to_id($email)) {
+          $params->{email} = $email;
+          last;
         }
+      }
     }
+  }
 
-    $params->{bz_username} = $username;
-    $params->{realname}  ||= $user_entry->get_value("displayName");
-    $params->{realname}  ||= $user_entry->get_value("cn");
+  $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
@@ -154,27 +166,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;
index 163058c54010829bd1630dfd7efaf266b9867e5d..14ed03b9a59a06650530a0c771b4d6a58b85ca74 100644 (file)
@@ -23,35 +23,39 @@ 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 };
-
-    $params->{bz_username} = $username;
-
-    # Build the user account's e-mail address.
-    $params->{email} = $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};
+
+  $params->{bz_username} = $username;
+
+  # Build the user account's e-mail address.
+  $params->{email} = $username . $address_suffix;
+
+  return $params;
 }
 
 1;
index 2e36388a645cfd766d9e32e982c1fbf61e4346e9..a54432a19b3a7ede3229ef7985d696aa5dc67c0b 100644 (file)
@@ -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;
index 38a27836a05dbdd3bb8d310f8affad97dd158672..b2666a55d8b8bcf71f148bab75eb443218ea36ed 100644 (file)
@@ -38,9 +38,9 @@ use Scalar::Util qw(blessed weaken);
 
 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
 );
 
 # This hash keeps a weak copy of every bug created.
@@ -54,199 +54,200 @@ 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 = (
-        alias            => ['product'],
-        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 = (
+    alias            => ['product'],
+    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
@@ -258,30 +259,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)
@@ -299,381 +298,395 @@ 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;
-
-    # Let's not be abused by pseudo-numbers.
-    use re '/a';
-
-    # Remove leading "#" mark if we've just been passed an id.
-    if (!ref $param && $param =~ /^#(\d+)$/) {
-        $param = $1;
-    }
-
-    # If we get something that looks like a word (not a number),
-    # make it the "name" param.
-    if (!defined $param
-        || (!ref($param) && $param !~ /^\d+$/)
-        || (ref($param) && $param->{id} !~ /^\d+$/))
-    {
-        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;
+
+  # Let's not be abused by pseudo-numbers.
+  use re '/a';
+
+  # Remove leading "#" mark if we've just been passed an id.
+  if (!ref $param && $param =~ /^#(\d+)$/) {
+    $param = $1;
+  }
+
+  # If we get something that looks like a word (not a number),
+  # make it the "name" param.
+  if ( !defined $param
+    || (!ref($param) && $param !~ /^\d+$/)
+    || (ref($param)  && $param->{id} !~ /^\d+$/))
+  {
+    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;
+  }
 
-    $CLEANUP{$self->id} = $self;
-    weaken($CLEANUP{$self->id});
+  $CLEANUP{$self->id} = $self;
+  weaken($CLEANUP{$self->id});
 
-    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;
 }
 
 # This is called by Bugzilla::_cleanup() at the end of requests in a persistent environment
 # (such as mod_perl)
 sub CLEANUP {
-    foreach my $bug (values %CLEANUP) {
-        # $bug will be undef if there are no other references to it.
-        next unless $bug;
-        delete $bug->{depends_on_obj};
-        delete $bug->{blocks_obj};
-    }
-    %CLEANUP = ();
+  foreach my $bug (values %CLEANUP) {
+
+    # $bug will be undef if there are no other references to it.
+    next unless $bug;
+    delete $bug->{depends_on_obj};
+    delete $bug->{blocks_obj};
+  }
+  %CLEANUP = ();
 }
 
 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;
-
-    # Let's not be abused by pseudo-numbers.
-    use re '/a';
-
-    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(\d+)(?:\#c\d+)?/g;
-        # bug X
-        my $bug_re = qr/\Q$bug_word\E$s*\#?$s*(\d+)/i;
-        push @bug_ids, $text =~ /\b$bug_re/g;
-        # bugs X, Y, Z
-        my $bugs_re = qr/\Q$bugs_word\E$s*\#?$s*(\d+)(?:$s*,$s*\#?$s*(\d+))+/i;
-        push @bug_ids, $text =~ /\b$bugs_re/g;
-        # Old duplicate markers
-        push @bug_ids, $text =~ /(?<=^\*\*\*\ This\ bug\ has\ been\ marked\ as\ a\ duplicate\ of\ )(\d+)(?=\ \*\*\*\Z)/;
-    }
-    # 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 });
+  # Let's not be abused by pseudo-numbers.
+  use re '/a';
 
-    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;
+  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;
     }
-    # 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(\d+)(?:\#c\d+)?/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*(\d+)/i;
+    push @bug_ids, $text =~ /\b$bug_re/g;
+
+    # bugs X, Y, Z
+    my $bugs_re = qr/\Q$bugs_word\E$s*\#?$s*(\d+)(?:$s*,$s*\#?$s*(\d+))+/i;
+    push @bug_ids, $text =~ /\b$bugs_re/g;
+
+    # Old duplicate markers
+    push @bug_ids, $text
+      =~ /(?<=^\*\*\*\ This\ bug\ has\ been\ marked\ as\ a\ duplicate\ of\ )(\d+)(?=\ \*\*\*\Z)/;
+  }
+
+  # Make sure to filter invalid bug IDs.
+  @bug_ids = grep { $_ < MAX_INT_32 } @bug_ids;
+  return [uniq @bug_ids];
+}
 
-        $where_sql = join(' OR ', @where);
-        $relevance_sql = join(' + ', @relevance);
+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);
+  }
+
+  my $product_ids = join(',', map { $_->id } @$products);
+  my $product_sql = $product_ids ? "AND product_id IN ($product_ids)" : "";
 
-    # Because we collapse duplicates, we want to get slightly more bugs
-    # than were actually asked for.
-    my $sql_limit = $limit + 5;
+  # 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,
+  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
@@ -705,620 +718,650 @@ sub possible_duplicates {
 #
 # C<assigned_to> - The full login name of the user who the bug is
 #                  initially assigned to.
-# C<qa_contact>  - The full login name of the QA Contact for this bug. 
+# C<qa_contact>  - The full login name of the QA Contact for this bug.
 #                  Will be ignored if C<useqacontact> is off.
 #
-# C<estimated_time> - For time-tracking. Will be ignored if 
+# C<estimated_time> - For time-tracking. Will be ignored if
 #                     C<timetrackinggroup> is not set, or if the current
 #                     user is not a member of the timetrackinggroup.
 # C<deadline>       - For time-tracking. Will be ignored for the same
 #                     reasons as C<estimated_time>.
 # C<flags>        - An array of flags that will be applied to the bug.
 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};
-    $params->{is_markdown} = 0
-      unless defined $params->{is_markdown} && $params->{is_markdown} eq '1';
-
-    $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 $is_markdown      = delete $params->{is_markdown};
-    my $see_also         = delete $params->{see_also};
-    my $comment_tags     = delete $params->{comment_tags};
-    my $flags            = delete $params->{flags};
-
-    # 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};
-    }
-
-    # Apply any flags.
-    if (defined $flags) {
-        $bug->set_flags($flags);
-        foreach my $flag (@{$bug->flags}) {
-            Bugzilla::Flag->create($flag);
-        }
-        delete $bug->{flag_types}; # cause flag_types to be reloaded with newly created flags
-    }
-
-    # Comment #0 handling...
-
-    # We now have a bug id so we can fill this out
-    $creation_comment->{'bug_id'} = $bug->id;
-    $creation_comment->{'is_markdown'} = $is_markdown;
-
-    # Insert the comment. We always insert a comment on bug creation,
-    # but sometimes it's blank.
-    my $comment = Bugzilla::Comment->insert_create_data($creation_comment);
-
-    # Add comment tags
-    if (defined $comment_tags && Bugzilla->user->can_tag_comments) {
-        $comment_tags = ref $comment_tags ? $comment_tags : [ $comment_tags ];
-        foreach my $tag (@{$comment_tags}) {
-            $comment->add_tag($tag) if defined $tag;
-        }
-        $comment->update();
-    }
-
-    # 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};
+  $params->{is_markdown} = 0
+    unless defined $params->{is_markdown} && $params->{is_markdown} eq '1';
+
+  $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 $is_markdown      = delete $params->{is_markdown};
+  my $see_also         = delete $params->{see_also};
+  my $comment_tags     = delete $params->{comment_tags};
+  my $flags            = delete $params->{flags};
+
+  # 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};
+  }
+
+  # Apply any flags.
+  if (defined $flags) {
+    $bug->set_flags($flags);
+    foreach my $flag (@{$bug->flags}) {
+      Bugzilla::Flag->create($flag);
+    }
+    delete $bug->{flag_types}; # cause flag_types to be reloaded with newly created flags
+  }
+
+  # Comment #0 handling...
+
+  # We now have a bug id so we can fill this out
+  $creation_comment->{'bug_id'}      = $bug->id;
+  $creation_comment->{'is_markdown'} = $is_markdown;
+
+  # Insert the comment. We always insert a comment on bug creation,
+  # but sometimes it's blank.
+  my $comment = Bugzilla::Comment->insert_create_data($creation_comment);
+
+  # Add comment tags
+  if (defined $comment_tags && Bugzilla->user->can_tag_comments) {
+    $comment_tags = ref $comment_tags ? $comment_tags : [$comment_tags];
+    foreach my $tag (@{$comment_tags}) {
+      $comment->add_tag($tag) if defined $tag;
+    }
+    $comment->update();
+  }
+
+  # 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(@_);
+  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;
+  # 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 @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;
+  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};
+  # 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};
-    }
+  if ($params->{estimated_time}) {
+    $params->{remaining_time} = $params->{estimated_time};
+  }
 
-    $class->_check_strict_isolation($params->{cc}, $params->{assigned_to},
-                                    $params->{qa_contact}, $product);
+  $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};
+  # 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 });
+  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};
+  # 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;
+  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];
-    }
-
-    # 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);
-    }
-    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)];
-        }
-    }
+  my $self = shift;
+  my $dbh  = Bugzilla->dbh;
+  my $user = Bugzilla->user;
 
-    # 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 and comment tags
-    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);
-        }
-        foreach my $tag (@{$self->{added_comment_tags} || []}) {
-            $comment->add_tag($tag) if defined $tag;
-        }
-        $comment->update() if @{$self->{added_comment_tags} || []};
-    }
-
-    # 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
+  # XXX This is just a temporary hack until all updating happens
+  # inside this function.
+  my $delta_ts = shift || $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
 
-    my ($removed_see, $added_see) =
-        diff_arrays($old_bug->see_also, $self->see_also, 'name');
+  $dbh->bz_start_transaction();
 
-    $_->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);
-    }
+  my ($changes, $old_bug) = $self->SUPER::update(@_);
 
-    # 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})
+  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 and comment tags
+  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);
+    }
+    foreach my $tag (@{$self->{added_comment_tags} || []}) {
+      $comment->add_tag($tag) if defined $tag;
+    }
+    $comment->update() if @{$self->{added_comment_tags} || []};
+  }
+
+  # 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);
 }
 
 #####################################################################
@@ -1326,97 +1369,124 @@ sub remove_from_db {
 #####################################################################
 
 sub send_changes {
-    my ($self, $changes, $vars, $minor_update) = @_;
+  my ($self, $changes, $vars, $minor_update) = @_;
 
-    my $user = Bugzilla->user;
+  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,
-    );
+  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]            : '';
 
-    _send_bugmail({ id => $self->id, type => 'bug', forced => \%forced,
-                    minor_update => $minor_update }, $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, minor_update => $minor_update }, $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,
-                           minor_update => $minor_update };
-
-            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, minor_update => $minor_update }, $vars);
-    }
+  my %forced = (
+    cc        => [split(/[,;]+/, $old_cc)],
+    owner     => $old_own,
+    qacontact => $old_qa,
+    changer   => $user,
+  );
 
-    # 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, minor_update => $minor_update }, $vars);
-    }
+  _send_bugmail(
+    {
+      id           => $self->id,
+      type         => 'bug',
+      forced       => \%forced,
+      minor_update => $minor_update
+    },
+    $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,
+        minor_update => $minor_update
+      },
+      $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,
+        minor_update => $minor_update
+      };
+
+      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,
+        minor_update => $minor_update
+      },
+      $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,
+        minor_update => $minor_update
+      },
+      $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;
+  }
 }
 
 #####################################################################
@@ -1424,941 +1494,961 @@ sub _send_bugmail {
 #####################################################################
 
 sub _check_alias {
-    my ($invocant, $aliases, undef, $params) = @_;
-    $aliases = ref $aliases ? $aliases : [split(/[\s,]+/, $aliases)];
+  my ($invocant, $aliases, undef, $params) = @_;
+  $aliases = ref $aliases ? $aliases : [split(/[\s,]+/, $aliases)];
 
-    # Remove empty aliases
-    @$aliases = grep { $_ } @$aliases;
+  # Remove empty aliases
+  @$aliases = grep {$_} @$aliases;
 
-    my $product = blessed($invocant) ? $invocant->product_obj
-                                     : $params->{product};
+  my $product = blessed($invocant) ? $invocant->product_obj : $params->{product};
 
-    # You need editbugs to edit these fields
-    unless (Bugzilla->user->in_group('editbugs', $product->id)) {
-        if (scalar @$aliases) {
-            ThrowUserError('illegal_change', { field  => 'alias',
-                                               action => 'set',
-                                               privs  => PRIVILEGES_REQUIRED_EMPOWERED });
-        }
+  # You need editbugs to edit these fields
+  unless (Bugzilla->user->in_group('editbugs', $product->id)) {
+    if (scalar @$aliases) {
+      ThrowUserError('illegal_change',
+        {field => 'alias', action => 'set', privs => PRIVILEGES_REQUIRED_EMPOWERED});
     }
+  }
 
-    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,
-                                             other_bug => $other_bug });
-        }
+    # 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, other_bug => $other_bug});
     }
-    return $id;
+  }
+
+  return $aliases;
+}
+
+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_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;
+  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_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 {
-                    $new_status = ($valid_statuses[0]->name ne 'UNCONFIRMED') ?
-                                  $valid_statuses[0] : $valid_statuses[1];
-                }
-            }
+  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 });
-    }
-
-    # 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 (!blessed $invocant) {
-        $params->{everconfirmed} = $new_status->name eq 'UNCONFIRMED' ? 0 : 1;
-    }
+  if (!blessed $invocant) {
+    $params->{everconfirmed} = $new_status->name eq 'UNCONFIRMED' ? 0 : 1;
+  }
 
-    return $new_status->name;
+  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;
+  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;
+  # 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 %cc_ids;
+  foreach my $person (@$ccs) {
+    $person = trim($person);
+    next unless $person;
+    my $id = login_to_id($person, THROW_ERROR);
+    $cc_ids{$id} = 1;
+  }
 
-    # Enforce Default CC
-    $cc_ids{$_->id} = 1 foreach (@{$component->initial_cc});
+  # Enforce Default CC
+  $cc_ids{$_->id} = 1 foreach (@{$component->initial_cc});
 
-    return [keys %cc_ids];
+  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) = @_;
-
-    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);
-    }
-
-    # 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 });
-                }
-            }
+  my ($invocant, $value, $field, $params) = @_;
+
+  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);
+  }
+
+  # 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});
         }
+      }
+    }
 
-        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+)$/a ? $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+)$/a ? $1 : $invocant->check($_, $type)->id } @bug_ids;
+    $deps_in{$type} = \@bug_ids;
+  }
 
-    $params->{$opposite} = $deps{$opposite};
-    $params->{_dependencies_validated} = 1;
-    return $deps{$field};
+  # 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};
 }
 
 sub _check_dup_id {
-    my ($self, $dupe_of) = @_;
-    my $dbh = Bugzilla->dbh;
+  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;
 
-    # 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;
-            # 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;
-    }
+  my $keyword_array = $keywords_in;
+  if (!ref $keyword_array) {
+    $keywords_in = trim($keywords_in);
+    $keyword_array = [split(/[\s,]+/, $keywords_in)];
+  }
 
-    my %old_kw_id;
-    if (blessed $invocant) {
-        my @old_keywords = @{$invocant->keyword_objects};
-        %old_kw_id  = map { $_->id => 1 } @old_keywords;
-    }
+  my %keywords;
+  foreach my $keyword (@$keyword_array) {
+    next unless $keyword;
+    my $obj = Bugzilla::Keyword->check($keyword);
+    $keywords{$obj->id} = $obj;
+  }
 
-    foreach my $keyword (values %keywords) {
-        next if $keyword->is_active || exists $old_kw_id{$keyword->id};
+  my %old_kw_id;
+  if (blessed $invocant) {
+    my @old_keywords = @{$invocant->keyword_objects};
+    %old_kw_id = map { $_->id => 1 } @old_keywords;
+  }
 
-        ThrowUserError('value_inactive',
-                        { value  => $keyword->name, class => ref $keyword });
-    }
+  foreach my $keyword (values %keywords) {
+    next if $keyword->is_active || exists $old_kw_id{$keyword->id};
+
+    ThrowUserError('value_inactive',
+      {value => $keyword->name, class => ref $keyword});
+  }
 
-    return [values %keywords];
+  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 if resolution_forbidden_with_open_blockers.
-    my $dependson = ref($invocant) ? $invocant->dependson
-                                   : ($params->{dependson} || []);
-    if (Bugzilla->params->{"resolution_forbidden_with_open_blockers"}
-        && $resolution eq
-                  Bugzilla->params->{"resolution_forbidden_with_open_blockers"}
-        && (!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 if resolution_forbidden_with_open_blockers.
+  my $dependson
+    = ref($invocant) ? $invocant->dependson : ($params->{dependson} || []);
+  if (
+       Bugzilla->params->{"resolution_forbidden_with_open_blockers"}
+    && $resolution eq Bugzilla->params->{"resolution_forbidden_with_open_blockers"}
+    && ( !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;
+  @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);
-        }
+  # 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);
-    # Tags are all lowercase.
-    return lc($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);
 }
 
 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) = @_;
+  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)];
-    }
+  # Allow users (mostly email_in.pl) to specify multi-selects as
+  # comma-separated values.
+  if (defined $values and !ref $values) {
 
-    return [] if !$values;
-    my @checked_values;
-    foreach my $value (@$values) {
-        push(@checked_values, $invocant->_check_select_field($value, $field));
-    }
-    return \@checked_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);
 }
 
 #####################################################################
@@ -2366,63 +2456,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);
 }
 
 
@@ -2433,366 +2523,397 @@ 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'},
+        is_markdown => $params->{'comment'}->{'is_markdown'}
+      }
+    );
+  }
 
-    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'},
-              is_markdown => $params->{'comment'}->{'is_markdown'} });
-    }
+  if (defined $params->{comment_tags} && Bugzilla->user->can_tag_comments()) {
+    $self->{added_comment_tags}
+      = ref $params->{comment_tags}
+      ? $params->{comment_tags}
+      : [$params->{comment_tags}];
+  }
 
-    if (defined $params->{comment_tags} && Bugzilla->user->can_tag_comments()) {
-        $self->{added_comment_tags} = ref $params->{comment_tags}
-                                      ? $params->{comment_tags}
-                                      : [ $params->{comment_tags} ];
-    }
+  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 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}};
+  # 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_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 {
+  my $et = $_[1];
+  $et =~ s/,/./g;
+  $_[0]->set('estimated_time', $et);
 }
-sub set_estimated_time { my $et = $_[1]; $et =~ s/,/./g; $_[0]->set('estimated_time', $et); }
 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;
-    }
-
-    $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})];
-        }
+  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;
 
-        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
@@ -2801,159 +2922,177 @@ 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 { my $rt = $_[1]; $rt =~ s/,/./g; $_[0]->set('remaining_time', $rt); }
+
+sub set_remaining_time {
+  my $rt = $_[1];
+  $rt =~ s/,/./g;
+  $_[0]->set('remaining_time', $rt);
+}
+
 # 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]); }
@@ -2970,383 +3109,398 @@ 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;
     }
-    push(@new_aliases, $alias) if !$found;
-    $self->{alias} = \@new_aliases;
+    else {
+      push(@new_aliases, $old_alias);
+    }
+  }
+  push(@new_aliases, $alias) if !$found;
+  $self->{alias} = \@new_aliases;
 }
 
 sub remove_alias {
-    my ($self, $alias) = @_;
+  my ($self, $alias) = @_;
 
-    my $privs;
-    my $can = $self->check_can_change_field('alias', '', $alias, \$privs);
-    if (!$can) {
-        ThrowUserError('illegal_change', { field    => 'alias',
-                                           action   => 'unset',
-                                           oldvalue => $alias,
-                                           privs    => $privs });
-    }
+  my $privs;
+  my $can = $self->check_can_change_field('alias', '', $alias, \$privs);
+  if (!$can) {
+    ThrowUserError('illegal_change',
+      {field => 'alias', action => 'unset', oldvalue => $alias, privs => $privs});
+  }
 
-    my $bug_aliases = $self->alias;
-    @$bug_aliases = grep { $_ ne $alias } @$bug_aliases;
+  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);
+  $input = trim($input);
+  return if !$input;
 
-    my $params = { value => $uri, bug_id => $self, class => $class };
-    $class->check_required_create_fields($params);
+  my ($class, $uri) = Bugzilla::BugUrl->class_for($input);
 
-    my $field_values = $class->run_create_validators($params);
-    my $value = $field_values->{value}->as_string;
-    trick_taint($value);
-    $field_values->{value} = $value;
+  my $params = {value => $uri, bug_id => $self, class => $class};
+  $class->check_required_create_fields($params);
 
-    # 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);
-    }
-}
-
-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 $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 ($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)
+    );
 
-    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));
+    # The list has changed.
+    delete $user->{tags};
+  }
 
-        $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};
+  # Do nothing if this tag is already set for this bug.
+  return if grep { $_ eq $tag } @{$self->tags};
 
-    # Increment the counter. Do it before the SQL call below,
-    # to not count the tag twice.
-    $user->tags->{$tag}->{bug_count}++;
+  # 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));
+  $dbh->do('INSERT INTO bug_tag (bug_id, tag_id) VALUES (?, ?)',
+    undef, ($self->id, $tag_id));
 
-    push(@{$self->{tags}}, $tag);
+  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};
 }
 
 #####################################################################
@@ -3356,30 +3510,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
@@ -3398,673 +3552,713 @@ 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 $class = shift;
 
-    my $status;
-    if (scalar(@statuses) == 1) {
-        $status = $statuses[0]->name;
-    }
-    else {
-        $status = ($statuses[0]->name ne 'UNCONFIRMED')
-                  ? $statuses[0]->name : $statuses[1]->name;
-    }
+  # XXX This should just call new_bug_statuses when the UI accepts closed
+  # bug statuses instead of accepting them as a parameter.
+  my @statuses = @_;
 
-    return $status;
+  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;
-            weaken($comment->{bug});
-            # 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) {
-                $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;
+      weaken($comment->{bug});
+
+      # 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) {
+        $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
@@ -4073,11 +4267,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);
 }
 
 #####################################################################
@@ -4087,21 +4281,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
@@ -4109,103 +4308,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
@@ -4218,24 +4421,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,
@@ -4247,174 +4452,187 @@ 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;
+  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;
-        }
-
-        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);
 }
 
 sub has_unsent_changes {
-    my $self = shift;
-    return 1 if !defined $self->lastdiffed;
-    return datetime_from($self->lastdiffed) < datetime_from($self->delta_ts) ? 1 : 0;
+  my $self = shift;
+  return 1 if !defined $self->lastdiffed;
+  return datetime_from($self->lastdiffed) < datetime_from($self->delta_ts)
+    ? 1
+    : 0;
 }
 
 # 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;
     }
-    return \%field_values;
+    else {
+      $field_name = FIELD_MAP->{$field} || $field;
+    }
+    $field_values{$field_name} = $params->{$field};
+  }
+  return \%field_values;
 }
 
 ################################################################################
@@ -4434,170 +4652,193 @@ 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;
-    }
-
-    # 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;
-        }
-    }
+      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;
+  }
+
+  # You need editbugs in order to change the alias
+  if ($field eq 'alias') {
+    $$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'}) {
 
-    # You need editbugs in order to change the alias
-    if ($field eq 'alias') {
-       $$PrivilegesRequired = PRIVILEGES_REQUIRED_EMPOWERED;
-       return 0;
+    # 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;
     }
 
-    # *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'});
+    # 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;
     }
+  }
 
-    # 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;
-        }
+  # 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 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;
-        }
-    }
+  # 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;
+  }
 
-    # 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.
+  # - change the QA contact
+  if ($field eq 'qa_contact') {
+    $$PrivilegesRequired = PRIVILEGES_REQUIRED_ASSIGNEE;
+    return 0;
+  }
 
-    # 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;
-    }
+  # - change the target milestone
+  if ($field eq 'target_milestone') {
+    $$PrivilegesRequired = PRIVILEGES_REQUIRED_ASSIGNEE;
+    return 0;
+  }
 
-    # The reporter is allowed to change anything else.
-    if (!$self->{'error'} && $self->{'reporter_id'} == $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;
+  }
 
-    # 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;
+  # - 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;
+  }
+
+  # 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;
 }
 
 #
@@ -4606,72 +4847,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;
 }
 
 
@@ -4680,51 +4926,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;
index 10716036e96971ee4a5a9d19fcc8024a83643f24..5cddf8415cebf186e566e796e7b8d417ea2fb713 100644 (file)
@@ -26,16 +26,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:
@@ -45,419 +46,446 @@ 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'}));
+  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));
     }
-    
-    if ($forced->{'qacontact'}) {
-        push (@qa_contacts, Bugzilla::User->check($forced->{'qacontact'}));
+  }
+  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));
+
+    $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);
+  }
+
+  ###########################################################################
+  # 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 $_;
     }
-    
-    if ($forced->{'cc'}) {
-        foreach my $cc (@{$forced->{'cc'}}) {
-            push(@ccs, Bugzilla::User->check($cc));
+  }
+
+  # 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;
+      }
     }
-    my %user_cache = map { $_->id => $_ } (@assignees, @qa_contacts, @ccs);
 
-    my @diffs;
-    if (!$start) {
-        @diffs = _get_new_bugmail_fields($bug);
+    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} // '');
     }
-
-    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} });
+  }
+
+  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
     }
-    else {
-        push(@diffs, _get_diffs($bug, $end, \%user_cache));
-
-        $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);
+  );
+
+  # 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]);
     }
-
-    ###########################################################################
-    # 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 $_;
+  }
+
+  # 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');
+
+  my $minor_update = $changer->in_group(Bugzilla->params->{minor_update_group})
+    && $params->{minor_update};
+
+  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, $minor_update
+        ))
+        {
+          $rels_which_want{$relationship} = $recipients{$user_id}->{$relationship};
         }
+      }
     }
 
-    # 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;
-            }
-        }
-
-        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} // '');
-        }
+    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])
-        : [];
+  # 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;
+  }
 
-    # 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 });
-
-    # 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]);
-        }
-    }
-
-    # 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');
-
-    my $minor_update = $changer->in_group(Bugzilla->params->{minor_update_group})
-                     && $params->{minor_update};
-
-    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,
-                                          $minor_update))
-                {
-                    $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;
-            }
-        }
-    }
-
-    # 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;
-    }
-
-    return {'sent' => \@sent};
+  return {'sent' => \@sent};
 }
 
 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;
-    }
-
-    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 $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 %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));
+    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 $templates = {
-        header => "email/bugmail-header.txt.tmpl",
-        text   => "email/bugmail.txt.tmpl",
-        html   => "email/bugmail.html.tmpl",
-    };
-    return generate_email($vars, $templates);
+  my ($vars) = @_;
+  my $templates = {
+    header => "email/bugmail-header.txt.tmpl",
+    text   => "email/bugmail.txt.tmpl",
+    html   => "email/bugmail.html.tmpl",
+  };
+  return generate_email($vars, $templates);
 }
 
 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
@@ -466,89 +494,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;
index 818253c69bd559b2c17ab1d8df5e05f2e34aa79a..5d0afdc3b7695e298c889d03d6074a795d5367c1 100644 (file)
@@ -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;
index 64718120273cc4c2529243eb889571f323eddead..8f6ab7475f69a45a61288e70e872e8bb281a238d 100644 (file)
@@ -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;
index e60cb04fc61f1c0392c9f9916817ac9a1f8feeb7..9941c3fb4a2ddc9375bceca99c73158e7251cfac 100644 (file)
@@ -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;
index 2067c9eb68c251a9b1a88c7c31ba9451697a4373..538e34cc8b659ec1e7d0f48fca80bbc8f5fb4342 100644 (file)
@@ -18,31 +18,35 @@ 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
-    #   https://debbugs.gnu.org/cgi/bugreport.cgi?bug=123
-    #   https://debbugs.gnu.org/123
-    return ((lc($uri->authority) eq 'bugs.debian.org'
-             or lc($uri->authority) eq 'debbugs.gnu.org')
-            and (($uri->path =~ /bugreport\.cgi$/
-                  and $uri->query_param('bug') =~ m|^\d+$|a)
-                 or $uri->path =~ m|^/\d+$|a)) ? 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
+  #   https://debbugs.gnu.org/cgi/bugreport.cgi?bug=123
+  #   https://debbugs.gnu.org/123
+  return (
+    (
+           lc($uri->authority) eq 'bugs.debian.org'
+        or lc($uri->authority) eq 'debbugs.gnu.org'
+    )
+      and
+      (($uri->path =~ /bugreport\.cgi$/ and $uri->query_param('bug') =~ m|^\d+$|a)
+      or $uri->path =~ m|^/\d+$|a)
+  ) ? 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+)$|a || $uri->query_param('bug') =~ m|^(\d+)$|a;
-    $uri = new URI('https://' . $uri->authority . '/' . $1);
+  # This is the shortest standard URL form for Debian BTS URLs,
+  # and so we reduce all URLs to this.
+  $uri->path =~ m|^/(\d+)$|a || $uri->query_param('bug') =~ m|^(\d+)$|a;
+  $uri = new URI('https://' . $uri->authority . '/' . $1);
 
-    return $uri;
+  return $uri;
 }
 
 1;
index 19ce5ef6b96bb747da6fdbcd4e716c3a55c20a05..46e4509a4164c87bf523657e23d2b446b0f3b243 100644 (file)
@@ -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;
index 4b5df7a87c37cea33982875d630e9fe1fbea3ea0..4bc0e3cbdac8f1bc891efd8e7d61f395d276497a 100644 (file)
@@ -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;
index d117b0255de51f2349dc313720e7f69f16120582..a46c1fd5e6edf16f67d35a12c86f656c0bcf5c56 100644 (file)
@@ -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;
index 36a911130cc35c92d790c81baa44d870d793f425..9988aa957784ab8ef5fbe55aa10cb04d76204966 100644 (file)
@@ -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+$|a) ? 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+$|a)
+    ? 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+)$|a;
-    $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+)$|a;
+  $uri = new URI("https://launchpad.net/bugs/$1");
 
-    return $uri;
+  return $uri;
 }
 
 1;
index f90e3f67b619bdb89c859aec2316bda90bf13b87..2e03ffa8ad84e10a9da899d46a06ccb298b26a1d 100644 (file)
@@ -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;
index c2986736294a0e3baff12e920fc7ed6e83842a30..277b75959182302274b725b97c19c1a78fef4874 100644 (file)
@@ -18,38 +18,45 @@ 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
-    # SourceForge Allura ticket URLs have several forms:
-    #  http://sourceforge.net/p/project/bugs/12345/
-    #  http://sourceforge.net/p/project/feature-requests/12345/
-    #  http://sourceforge.net/p/project/patches/12345/
-    #  http://sourceforge.net/p/project/support-requests/12345/
-    return (lc($uri->authority) eq 'sourceforge.net'
-            and (($uri->path eq '/tracker/'
-                  and $uri->query_param('func') eq 'detail'
-                  and $uri->query_param('aid')
-                  and $uri->query_param('group_id')
-                  and $uri->query_param('atid'))
-                or $uri->path =~ m!^/p/[^/]+/(?:bugs|feature-requests|patches|support-requests)/\d+/?$!)) ? 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
+  # SourceForge Allura ticket URLs have several forms:
+  #  http://sourceforge.net/p/project/bugs/12345/
+  #  http://sourceforge.net/p/project/feature-requests/12345/
+  #  http://sourceforge.net/p/project/patches/12345/
+  #  http://sourceforge.net/p/project/support-requests/12345/
+  return (
+    lc($uri->authority) eq 'sourceforge.net'
+      and (
+      (
+            $uri->path eq '/tracker/'
+        and $uri->query_param('func') eq 'detail'
+        and $uri->query_param('aid')
+        and $uri->query_param('group_id')
+        and $uri->query_param('atid')
+      )
+      or $uri->path
+      =~ m!^/p/[^/]+/(?:bugs|feature-requests|patches|support-requests)/\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);
 
-    # Make sure the trailing slash is present
-    my $path = $uri->path;
-    $path =~ s!/*$!/!;
-    $uri->path($path);
+  # Make sure the trailing slash is present
+  my $path = $uri->path;
+  $path =~ s!/*$!/!;
+  $uri->path($path);
 
-    return $uri;
+  return $uri;
 }
 
 1;
index edcd1c4539b08f2fde7b675862d042c65709e9a8..e8b1b6d79aed42ba019c55dd9f7c04b72aa9cbe2 100644 (file)
@@ -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;
index 0e1c006b7d436f330a0ab4aa25b05c47ced9c83d..c1f967510d31286a5d061b809a81830b09df6cf1 100644 (file)
@@ -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;
index 988482b958de0731f3e514b56853ec22a92ceab3..a2adf3a5c1843596922ec4977c7e4d13fe6231ba 100644 (file)
@@ -23,300 +23,318 @@ use Bugzilla::Install::Util qw(i_am_persistent);
 use File::Basename;
 
 use constant DEFAULT_CSP => (
-    default_src => [ 'self' ],
-    script_src  => [ 'self', 'unsafe-inline', 'unsafe-eval' ],
-    style_src   => [ 'self', 'unsafe-inline' ],
+  default_src => ['self'],
+  script_src  => ['self', 'unsafe-inline', 'unsafe-eval'],
+  style_src   => ['self', 'unsafe-inline'],
 );
 
 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 :unique_headers :utf8));
+  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 :unique_headers :utf8));
 }
 
 BEGIN { __PACKAGE__->_init_bz_cgi_globals() if i_am_cgi(); }
 
 sub new {
-    my ($invocant, @args) = @_;
-    my $class = ref($invocant) || $invocant;
+  my ($invocant, @args) = @_;
+  my $class = ref($invocant) || $invocant;
 
-    $class->_init_bz_cgi_globals() if i_am_persistent();
+  $class->_init_bz_cgi_globals() if i_am_persistent();
 
-    my $self = $class->SUPER::new(@args);
+  my $self = $class->SUPER::new(@args);
 
-    # Make sure our outgoing cookie list is empty on each invocation
-    $self->{Bugzilla_cookie_list} = [];
+  # Make sure our outgoing cookie list is empty on each invocation
+  $self->{Bugzilla_cookie_list} = [];
 
-    my $script = basename($0);
+  my $script = basename($0);
 
-    # attachment.cgi handles this itself.
-    if ($script ne 'attachment.cgi') {
-        $self->do_ssl_redirect_if_required();
-        $self->redirect_to_urlbase if $self->url_is_attachment_base;
-    }
+  # attachment.cgi handles this itself.
+  if ($script ne 'attachment.cgi') {
+    $self->do_ssl_redirect_if_required();
+    $self->redirect_to_urlbase if $self->url_is_attachment_base;
+  }
 
 
-    # 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.
-    if ($self->script_name && $self->path_info) {
-        my @whitelist = ("rest.cgi");
-        Bugzilla::Hook::process('path_info_whitelist', { whitelist => \@whitelist });
-        if (!grep($_ eq $script, @whitelist)) {
-            print $self->redirect($self->url(-path => 0, -query => 1));
-        }
+  # 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.
+  if ($self->script_name && $self->path_info) {
+    my @whitelist = ("rest.cgi");
+    Bugzilla::Hook::process('path_info_whitelist', {whitelist => \@whitelist});
+    if (!grep($_ eq $script, @whitelist)) {
+      print $self->redirect($self->url(-path => 0, -query => 1));
     }
+  }
 
-    # Send appropriate charset
-    $self->charset('UTF-8');
+  # Send appropriate charset
+  $self->charset('UTF-8');
 
-    # Check for errors
-    # All of the Bugzilla code wants to do this, so do it here instead of
-    # in each script
+  # 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;
+  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.
+  if ($err) {
 
-        print $self->header(-status => $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.
 
-        # 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";
-    }
+    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;
+  return $self;
 }
 
 sub content_security_policy {
-    my ($self, %add_params) = @_;
-    if (Bugzilla->has_feature('csp')) {
-        require Bugzilla::CGI::ContentSecurityPolicy;
-        return $self->{Bugzilla_csp} if $self->{Bugzilla_csp};
-        my %params = DEFAULT_CSP;
-        if (%add_params) {
-            foreach my $key (keys %add_params) {
-                if (defined $add_params{$key}) {
-                    $params{$key} = $add_params{$key};
-                }
-                else {
-                    delete $params{$key};
-                }
-            }
+  my ($self, %add_params) = @_;
+  if (Bugzilla->has_feature('csp')) {
+    require Bugzilla::CGI::ContentSecurityPolicy;
+    return $self->{Bugzilla_csp} if $self->{Bugzilla_csp};
+    my %params = DEFAULT_CSP;
+    if (%add_params) {
+      foreach my $key (keys %add_params) {
+        if (defined $add_params{$key}) {
+          $params{$key} = $add_params{$key};
+        }
+        else {
+          delete $params{$key};
         }
-        return $self->{Bugzilla_csp} = Bugzilla::CGI::ContentSecurityPolicy->new(%params);
+      }
     }
-    return undef;
+    return $self->{Bugzilla_csp}
+      = Bugzilla::CGI::ContentSecurityPolicy->new(%params);
+  }
+  return undef;
 }
 
 sub csp_nonce {
-    my ($self) = @_;
+  my ($self) = @_;
 
-    if (Bugzilla->has_feature('csp')) {
-        my $csp = $self->content_security_policy;
-        return $csp->nonce if $csp->has_nonce;
-    }
+  if (Bugzilla->has_feature('csp')) {
+    my $csp = $self->content_security_policy;
+    return $csp->nonce if $csp->has_nonce;
+  }
 
-    return '';
+  return '';
 }
 
 # 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->multi_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->multi_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->multi_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->multi_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->multi_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->multi_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;
-    # We have to explicitly pass the charset.
-    my $headers = $self->SUPER::multipart_start(@_, -charset => $self->charset());
-    # 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 multipart_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;
+
+  # We have to explicitly pass the charset.
+  my $headers = $self->SUPER::multipart_start(@_, -charset => $self->charset());
+
+  # 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 multipart_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
@@ -330,12 +348,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
@@ -343,357 +362,373 @@ 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 $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;
+    my $params = Bugzilla->params;
+    if ($params->{ssl_redirect} || $params->{urlbase} =~ /^https/i) {
+      $args{'-secure'} = 1;
+    }
+
+    $self->send_cookie(
+      -name     => 'Bugzilla_login_request_cookie',
+      -value    => generate_random_password(),
+      -httponly => 1,
+      %args
+    );
+  }
 
-    my %headers;
-    my $user = Bugzilla->user;
+  # Add the cookies in if we have any
+  if (scalar(@{$self->{Bugzilla_cookie_list}})) {
+    $headers{'-cookie'} = $self->{Bugzilla_cookie_list};
+  }
 
-    # If there's only one parameter, then it's a Content-Type.
-    if (scalar(@_) == 1) {
-        %headers = ('-type' => shift(@_));
-    }
-    else {
-        %headers = @_;
+  # 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';
     }
-    $self->_prevent_unsafe_response(\%headers);
 
-    if ($self->{'_content_disp'}) {
-        $headers{'-content_disposition'} = $self->{'_content_disp'};
-    }
+    $headers{'-strict_transport_security'} = $sts_opts;
+  }
 
-    if (!$user->id && $user->authorizer->can_login
-        && !$self->cookie('Bugzilla_login_request_cookie'))
-    {
-        my %args;
-        my $params = Bugzilla->params;
-        if ($params->{ssl_redirect} || $params->{urlbase} =~ /^https/i) {
-            $args{'-secure'} = 1;
-        }
+  # Add X-Frame-Options header to prevent framing and subsequent
+  # possible clickjacking problems.
+  unless ($self->url_is_attachment_base) {
+    $headers{'-x_frame_options'} = 'SAMEORIGIN';
+  }
 
-        $self->send_cookie(-name => 'Bugzilla_login_request_cookie',
-                           -value => generate_random_password(),
-                           -httponly => 1,
-                           %args);
-    }
+  # 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 the cookies in if we have any
-    if (scalar(@{$self->{Bugzilla_cookie_list}})) {
-        $headers{'-cookie'} = $self->{Bugzilla_cookie_list};
-    }
+  # 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 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;
-    }
+  my $csp = $self->content_security_policy;
+  $csp->add_cgi_headers(\%headers) if defined $csp;
 
-    # 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-Content-Type-Options header to prevent browsers sniffing
-    # the MIME type away from the declared Content-Type.
-    $headers{'-x_content_type_options'} = 'nosniff';
-
-    my $csp = $self->content_security_policy;
-    $csp->add_cgi_headers(\%headers) if defined $csp;
+  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;
-
-    my @caller = caller(0);
-    if (wantarray && $caller[0] ne 'CGI') {
-        warn 'Illegal call to $cgi->param in list context from ' . $caller[0];
+  my $self = shift;
+
+  my @caller = caller(0);
+  if (wantarray && $caller[0] ne 'CGI') {
+    warn 'Illegal call to $cgi->param in list context from ' . $caller[0];
+  }
+
+  # When we are just requesting the value of a parameter...
+  if (scalar(@_) == 1) {
+    my @result = $self->SUPER::multi_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(@_);
     }
 
-    # When we are just requesting the value of a parameter...
-    if (scalar(@_) == 1) {
-        my @result = $self->SUPER::multi_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(@_);
-        }
+    return wantarray ? @result : $result[0];
+  }
 
-        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::multi_param();
-        my @url_params  = $self->url_param;
-        my %params = map { $_ => 1 } (@post_params, @url_params);
-        return keys %params;
-    }
+  # 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::multi_param();
+    my @url_params  = $self->url_param;
+    my %params      = map { $_ => 1 } (@post_params, @url_params);
+    return keys %params;
+  }
 
-    return $self->SUPER::multi_param(@_);
+  return $self->SUPER::multi_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 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, %paramhash) = @_;
+  my ($self, %paramhash) = @_;
 
-    # Complain if -value is not given or empty (bug 268146).
-    ThrowCodeError('cookies_need_value') unless $paramhash{'-value'};
+  # Complain if -value is not given or empty (bug 268146).
+  ThrowCodeError('cookies_need_value') unless $paramhash{'-value'};
 
-    # Add the default path and the domain in.
-    my $uri = URI->new(Bugzilla->params->{urlbase});
-    $paramhash{'-path'} = $uri->path;
-    $paramhash{'-domain'} = $uri->host if $uri->can('host') && $uri->host;
+  # Add the default path and the domain in.
+  my $uri = URI->new(Bugzilla->params->{urlbase});
+  $paramhash{'-path'} = $uri->path;
+  $paramhash{'-domain'} = $uri->host if $uri->can('host') && $uri->host;
 
-    push(@{$self->{'Bugzilla_cookie_list'}}, $self->cookie(%paramhash));
+  push(@{$self->{'Bugzilla_cookie_list'}}, $self->cookie(%paramhash));
 }
 
 # 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 retrieving 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 $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.
-    if ($self->request_method() ne 'POST' or length($self->self_url) < CGI_URI_LIMIT) {
-        $self->redirect_to_urlbase();
-    }
+  my $self = shift;
+
+  # If there is no parameter, there is nothing to do.
+  return unless $self->param;
+
+  # If we're retrieving 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 $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.
+  if ($self->request_method() ne 'POST'
+    or length($self->self_url) < CGI_URI_LIMIT)
+  {
+    $self->redirect_to_urlbase();
+  }
 }
 
 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 unless use_attachbase() && 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";
-
-    my $url = $self->url;
-
-    # If we are behind a reverse proxy, we need to determine the original
-    # URL, else the comparison with the attachment_base URL will fail.
-    if (Bugzilla->params->{'inbound_proxies'}) {
-        # X-Forwarded-Proto is defined in RFC 7239.
-        my $protocol = $ENV{HTTP_X_FORWARDED_PROTO} || $self->protocol;
-        my $host = $self->virtual_host;
-        # X-Forwarded-URI is not standard.
-        my $uri = $ENV{HTTP_X_FORWARDED_URI} || $self->request_uri || '';
-        $url = "$protocol://$host$uri";
-    }
-    return ($url =~ $regex) ? 1 : 0;
+  my ($self, $id) = @_;
+  return 0 unless use_attachbase() && 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";
+
+  my $url = $self->url;
+
+  # If we are behind a reverse proxy, we need to determine the original
+  # URL, else the comparison with the attachment_base URL will fail.
+  if (Bugzilla->params->{'inbound_proxies'}) {
+
+    # X-Forwarded-Proto is defined in RFC 7239.
+    my $protocol = $ENV{HTTP_X_FORWARDED_PROTO} || $self->protocol;
+    my $host = $self->virtual_host;
+
+    # X-Forwarded-URI is not standard.
+    my $uri = $ENV{HTTP_X_FORWARDED_URI} || $self->request_uri || '';
+    $url = "$protocol://$host$uri";
+  }
+  return ($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->multi_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->multi_param($param);
+  return undef if !scalar(@result);
+  return $result[0] if scalar(@result) == 1;
+  return \@result;
 }
 
 1;
index d09dbef76a5598b98c233f9f8fd37c0bb1f8dcd8..63741f7e8e05505c7bdc39dbdb8a9d121c898c12 100644 (file)
@@ -17,122 +17,125 @@ use Type::Utils;
 
 use Bugzilla::Util qw(generate_random_password);
 
-my $SRC_KEYWORD = enum['none', 'self', 'unsafe-inline', 'unsafe-eval', 'nonce'];
+my $SRC_KEYWORD
+  = enum ['none', 'self', 'unsafe-inline', 'unsafe-eval', 'nonce'];
 my $SRC_URI = declare as Str, where {
-    $_ =~ m{
+  $_ =~ m{
         ^(?: https?:// )?  # optional http:// or https://
         [*A-Za-z0-9.-]+    # hostname including wildcards. Possibly too permissive.
         (?: :[0-9]+ )?     # optional port
     }x;
 };
-my $SRC      = $SRC_KEYWORD | $SRC_URI;
-my $SOURCE_LIST = ArrayRef[$SRC];
-my $REFERRER_KEYWORD = enum [qw(
+my $SRC              = $SRC_KEYWORD | $SRC_URI;
+my $SOURCE_LIST      = ArrayRef [$SRC];
+my $REFERRER_KEYWORD = enum [
+  qw(
     no-referrer no-referrer-when-downgrade
     origin      origin-when-cross-origin unsafe-url
-)];
+    )
+];
 
 my @ALL_BOOL = qw( sandbox upgrade_insecure_requests );
-my @ALL_SRC = qw(
-    default_src child_src  connect_src
-    font_src    img_src    media_src
-    object_src  script_src style_src
+my @ALL_SRC  = qw(
+  default_src child_src  connect_src
+  font_src    img_src    media_src
+  object_src  script_src style_src
 );
 
-has \@ALL_SRC     => ( is => 'ro', isa => $SOURCE_LIST, predicate => 1 );
-has \@ALL_BOOL    => ( is => 'ro', isa => Bool, default => 0 );
-has 'report_uri'  => ( is => 'ro', isa => Str, predicate => 1 );
-has 'base_uri'    => ( is => 'ro', isa => Str, predicate => 1 );
-has 'report_only' => ( is => 'ro', isa => Bool );
-has 'referrer'    => ( is => 'ro', isa => $REFERRER_KEYWORD, predicate => 1 );
-has 'value'       => ( is => 'lazy' );
-has 'nonce'       => ( is => 'lazy', init_arg => undef, predicate => 1 );
-has 'disable'     => ( is => 'ro', isa => Bool, default => 0 );
+has \@ALL_SRC => (is => 'ro', isa => $SOURCE_LIST, predicate => 1);
+has \@ALL_BOOL   => (is => 'ro', isa => Bool, default   => 0);
+has 'report_uri' => (is => 'ro', isa => Str,  predicate => 1);
+has 'base_uri'   => (is => 'ro', isa => Str,  predicate => 1);
+has 'report_only' => (is => 'ro', isa => Bool);
+has 'referrer' => (is => 'ro', isa => $REFERRER_KEYWORD, predicate => 1);
+has 'value'   => (is => 'lazy');
+has 'nonce'   => (is => 'lazy', init_arg => undef, predicate => 1);
+has 'disable' => (is => 'ro', isa => Bool, default => 0);
 
 sub _has_directive {
-    my ($self, $directive) = @_;
-    my $method = 'has_' . $directive;
-    return $self->$method;
+  my ($self, $directive) = @_;
+  my $method = 'has_' . $directive;
+  return $self->$method;
 }
 
 sub header_names {
-    my ($self) = @_;
-    my @names = ('Content-Security-Policy', 'X-Content-Security-Policy', 'X-WebKit-CSP');
-    if ($self->report_only) {
-        return map { $_ . '-Report-Only' } @names;
-    }
-    else {
-        return @names;
-    }
+  my ($self) = @_;
+  my @names
+    = ('Content-Security-Policy', 'X-Content-Security-Policy', 'X-WebKit-CSP');
+  if ($self->report_only) {
+    return map { $_ . '-Report-Only' } @names;
+  }
+  else {
+    return @names;
+  }
 }
 
 sub add_cgi_headers {
-    my ($self, $headers) = @_;
-    return if $self->disable;
-    foreach my $name ($self->header_names) {
-        $headers->{"-$name"} = $self->value;
-    }
+  my ($self, $headers) = @_;
+  return if $self->disable;
+  foreach my $name ($self->header_names) {
+    $headers->{"-$name"} = $self->value;
+  }
 }
 
 sub _build_value {
-    my $self = shift;
-    my @result;
-
-    my @list_directives = (@ALL_SRC);
-    my @boolean_directives = (@ALL_BOOL);
-    my @single_directives  = qw(report_uri base_uri);
-
-    foreach my $directive (@list_directives) {
-        next unless $self->_has_directive($directive);
-        my @values = map { $self->_quote($_) } @{ $self->$directive };
-        if (@values) {
-            push @result, join(' ', _name($directive), @values);
-        }
+  my $self = shift;
+  my @result;
+
+  my @list_directives    = (@ALL_SRC);
+  my @boolean_directives = (@ALL_BOOL);
+  my @single_directives  = qw(report_uri base_uri);
+
+  foreach my $directive (@list_directives) {
+    next unless $self->_has_directive($directive);
+    my @values = map { $self->_quote($_) } @{$self->$directive};
+    if (@values) {
+      push @result, join(' ', _name($directive), @values);
     }
+  }
 
-    foreach my $directive (@single_directives) {
-        next unless $self->_has_directive($directive);
-        my $value = $self->$directive;
-        if (defined $value) {
-            push @result, _name($directive) . ' ' . $value;
-        }
+  foreach my $directive (@single_directives) {
+    next unless $self->_has_directive($directive);
+    my $value = $self->$directive;
+    if (defined $value) {
+      push @result, _name($directive) . ' ' . $value;
     }
+  }
 
-    foreach my $directive (@boolean_directives) {
-        if ($self->$directive) {
-            push @result, _name($directive);
-        }
+  foreach my $directive (@boolean_directives) {
+    if ($self->$directive) {
+      push @result, _name($directive);
     }
+  }
 
-    return join('; ', @result);
+  return join('; ', @result);
 }
 
 sub _build_nonce {
-    return generate_random_password(48);
+  return generate_random_password(48);
 }
 
 sub _name {
-    my $name = shift;
-    $name =~ tr/_/-/;
-    return $name;
+  my $name = shift;
+  $name =~ tr/_/-/;
+  return $name;
 }
 
 sub _quote {
-    my ($self, $val) = @_;
-
-    if ($val eq 'nonce') {
-        return q{'nonce-} . $self->nonce . q{'};
-    }
-    elsif ($SRC_KEYWORD->check($val)) {
-        return qq{'$val'};
-    }
-    else {
-        return $val;
-    }
+  my ($self, $val) = @_;
+
+  if ($val eq 'nonce') {
+    return q{'nonce-} . $self->nonce . q{'};
+  }
+  elsif ($SRC_KEYWORD->check($val)) {
+    return qq{'$val'};
+  }
+  else {
+    return $val;
+  }
 }
 
 
-
 1;
 
 __END__
index f8d34fe6b192af9b981b6853b7a943cc5227493f..4a42d2fa907c5031dfa7dadbbd55fd6067b524c7 100644 (file)
@@ -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...
-    # &gt=1&labelgt=Grand+Total    
-    foreach my $param ($cgi->multi_param()) {
-        # Store all the lines
-        if ($param =~ /^line(\d+)$/a) {
-            foreach my $series_id ($cgi->multi_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+)$/a) {
-            $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...
+  # &gt=1&labelgt=Grand+Total
+  foreach my $param ($cgi->multi_param()) {
+
+    # Store all the lines
+    if ($param =~ /^line(\d+)$/a) {
+      foreach my $series_id ($cgi->multi_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+)$/a) {
+      $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 "<pre>Bugzilla::Chart object:";
-    print html_quote(Data::Dumper::Dumper($self));
-    print "</pre>";
+  my $self = shift;
+
+  # Make sure we've read in our data
+  my $data = $self->data;
+
+  require Data::Dumper;
+  say "<pre>Bugzilla::Chart object:";
+  print html_quote(Data::Dumper::Dumper($self));
+  print "</pre>";
 }
 
 1;
index 7f62e1ffab52ab15147909225b32ee4db0203c47..85de92b1a2b6a0b0b8b69f08e2550d4351bc6faf 100644 (file)
@@ -26,26 +26,26 @@ use parent qw(Bugzilla::Field::ChoiceInterface Bugzilla::Object Exporter);
 
 use constant IS_CONFIG => 1;
 
-use constant DB_TABLE => 'classifications';
+use constant DB_TABLE   => 'classifications';
 use constant LIST_ORDER => 'sortkey, name';
 
 use constant DB_COLUMNS => qw(
-    id
-    name
-    description
-    sortkey
+  id
+  name
+  description
+  sortkey
 );
 
 use constant UPDATE_COLUMNS => qw(
-    name
-    description
-    sortkey
+  name
+  description
+  sortkey
 );
 
 use constant VALIDATORS => {
-    name        => \&_check_name,
-    description => \&_check_description,
-    sortkey     => \&_check_sortkey,
+  name        => \&_check_name,
+  description => \&_check_description,
+  sortkey     => \&_check_sortkey,
 };
 
 ###############################
@@ -53,29 +53,31 @@ use constant VALIDATORS => {
 ###############################
 
 sub remove_from_db {
-    my $self = shift;
-    my $dbh = Bugzilla->dbh;
+  my $self = shift;
+  my $dbh  = Bugzilla->dbh;
 
-    ThrowUserError("classification_not_deletable") if ($self->id == 1);
+  ThrowUserError("classification_not_deletable") if ($self->id == 1);
 
-    $dbh->bz_start_transaction();
+  $dbh->bz_start_transaction();
 
-    # Reclassify products to the default classification, if needed.
-    my $product_ids = $dbh->selectcol_arrayref(
-        'SELECT id FROM products WHERE classification_id = ?', undef, $self->id);
-
-    if (@$product_ids) {
-        $dbh->do('UPDATE products SET classification_id = 1 WHERE '
-                  . $dbh->sql_in('id', $product_ids));
-        foreach my $id (@$product_ids) {
-            Bugzilla->memcached->clear({ table => 'products', id => $id });
-        }
-        Bugzilla->memcached->clear_config();
+  # Reclassify products to the default classification, if needed.
+  my $product_ids
+    = $dbh->selectcol_arrayref(
+    'SELECT id FROM products WHERE classification_id = ?',
+    undef, $self->id);
+
+  if (@$product_ids) {
+    $dbh->do('UPDATE products SET classification_id = 1 WHERE '
+        . $dbh->sql_in('id', $product_ids));
+    foreach my $id (@$product_ids) {
+      Bugzilla->memcached->clear({table => 'products', id => $id});
     }
+    Bugzilla->memcached->clear_config();
+  }
 
-    $self->SUPER::remove_from_db();
+  $self->SUPER::remove_from_db();
 
-    $dbh->bz_commit_transaction();
+  $dbh->bz_commit_transaction();
 
 }
 
@@ -84,38 +86,41 @@ sub remove_from_db {
 ###############################
 
 sub _check_name {
-    my ($invocant, $name) = @_;
-
-    $name = trim($name);
-    $name || ThrowUserError('classification_not_specified');
-
-    if (length($name) > MAX_CLASSIFICATION_SIZE) {
-        ThrowUserError('classification_name_too_long', {'name' => $name});
-    }
-
-    my $classification = new Bugzilla::Classification({name => $name});
-    if ($classification && (!ref $invocant || $classification->id != $invocant->id)) {
-        ThrowUserError("classification_already_exists", { name => $classification->name });
-    }
-    return $name;
+  my ($invocant, $name) = @_;
+
+  $name = trim($name);
+  $name || ThrowUserError('classification_not_specified');
+
+  if (length($name) > MAX_CLASSIFICATION_SIZE) {
+    ThrowUserError('classification_name_too_long', {'name' => $name});
+  }
+
+  my $classification = new Bugzilla::Classification({name => $name});
+  if ($classification && (!ref $invocant || $classification->id != $invocant->id))
+  {
+    ThrowUserError("classification_already_exists",
+      {name => $classification->name});
+  }
+  return $name;
 }
 
 sub _check_description {
-    my ($invocant, $description) = @_;
+  my ($invocant, $description) = @_;
 
-    $description  = trim($description || '');
-    return $description;
+  $description = trim($description || '');
+  return $description;
 }
 
 sub _check_sortkey {
-    my ($invocant, $sortkey) = @_;
-
-    $sortkey ||= 0;
-    my $stored_sortkey = $sortkey;
-    if (!detaint_natural($sortkey) || $sortkey > MAX_SMALLINT) {
-        ThrowUserError('classification_invalid_sortkey', { 'sortkey' => $stored_sortkey });
-    }
-    return $sortkey;
+  my ($invocant, $sortkey) = @_;
+
+  $sortkey ||= 0;
+  my $stored_sortkey = $sortkey;
+  if (!detaint_natural($sortkey) || $sortkey > MAX_SMALLINT) {
+    ThrowUserError('classification_invalid_sortkey',
+      {'sortkey' => $stored_sortkey});
+  }
+  return $sortkey;
 }
 
 #####################################
@@ -124,41 +129,45 @@ sub _check_sortkey {
 
 use constant FIELD_NAME => 'classification';
 use constant is_default => 0;
-use constant is_active => 1;
+use constant is_active  => 1;
 
 ###############################
 ####       Methods         ####
 ###############################
 
-sub set_name        { $_[0]->set('name', $_[1]); }
+sub set_name        { $_[0]->set('name',        $_[1]); }
 sub set_description { $_[0]->set('description', $_[1]); }
-sub set_sortkey     { $_[0]->set('sortkey', $_[1]); }
+sub set_sortkey     { $_[0]->set('sortkey',     $_[1]); }
 
 sub product_count {
-    my $self = shift;
-    my $dbh = Bugzilla->dbh;
+  my $self = shift;
+  my $dbh  = Bugzilla->dbh;
 
-    if (!defined $self->{'product_count'}) {
-        $self->{'product_count'} = $dbh->selectrow_array(q{
+  if (!defined $self->{'product_count'}) {
+    $self->{'product_count'} = $dbh->selectrow_array(
+      q{
             SELECT COUNT(*) FROM products
-            WHERE classification_id = ?}, undef, $self->id) || 0;
-    }
-    return $self->{'product_count'};
+            WHERE classification_id = ?}, undef, $self->id
+    ) || 0;
+  }
+  return $self->{'product_count'};
 }
 
 sub products {
-    my $self = shift;
-    my $dbh = Bugzilla->dbh;
+  my $self = shift;
+  my $dbh  = Bugzilla->dbh;
 
-    if (!$self->{'products'}) {
-        my $product_ids = $dbh->selectcol_arrayref(q{
+  if (!$self->{'products'}) {
+    my $product_ids = $dbh->selectcol_arrayref(
+      q{
             SELECT id FROM products
             WHERE classification_id = ?
-            ORDER BY name}, undef, $self->id);
+            ORDER BY name}, undef, $self->id
+    );
 
-        $self->{'products'} = Bugzilla::Product->new_from_list($product_ids);
-    }
-    return $self->{'products'};
+    $self->{'products'} = Bugzilla::Product->new_from_list($product_ids);
+  }
+  return $self->{'products'};
 }
 
 ###############################
@@ -166,7 +175,7 @@ sub products {
 ###############################
 
 sub description { return $_[0]->{'description'}; }
-sub sortkey     { return $_[0]->{'sortkey'};     }
+sub sortkey     { return $_[0]->{'sortkey'}; }
 
 
 ###############################
@@ -177,27 +186,32 @@ sub sortkey     { return $_[0]->{'sortkey'};     }
 # in global/choose-product.html.tmpl.
 
 sub sort_products_by_classification {
-    my $products = shift;
-    my $list;
-
-    if (Bugzilla->params->{'useclassification'}) {
-        my $class = {};
-        # Get all classifications with at least one product.
-        foreach my $product (@$products) {
-            $class->{$product->classification_id}->{'object'} ||=
-                new Bugzilla::Classification($product->classification_id);
-            # Nice way to group products per classification, without querying
-            # the DB again.
-            push(@{$class->{$product->classification_id}->{'products'}}, $product);
-        }
-        $list = [sort {$a->{'object'}->sortkey <=> $b->{'object'}->sortkey
-                       || lc($a->{'object'}->name) cmp lc($b->{'object'}->name)}
-                      (values %$class)];
-    }
-    else {
-        $list = [{object => undef, products => $products}];
+  my $products = shift;
+  my $list;
+
+  if (Bugzilla->params->{'useclassification'}) {
+    my $class = {};
+
+    # Get all classifications with at least one product.
+    foreach my $product (@$products) {
+      $class->{$product->classification_id}->{'object'}
+        ||= new Bugzilla::Classification($product->classification_id);
+
+      # Nice way to group products per classification, without querying
+      # the DB again.
+      push(@{$class->{$product->classification_id}->{'products'}}, $product);
     }
-    return $list;
+    $list = [
+      sort {
+        $a->{'object'}->sortkey <=> $b->{'object'}->sortkey
+          || lc($a->{'object'}->name) cmp lc($b->{'object'}->name)
+      } (values %$class)
+    ];
+  }
+  else {
+    $list = [{object => undef, products => $products}];
+  }
+  return $list;
 }
 
 1;
index 2f063934a8fa2732a3a8b773d9de61a61c3e3c73..a4c023c8816bd7b5e00204363b131209b7d823b8 100644 (file)
@@ -33,49 +33,50 @@ use constant AUDIT_CREATES => 0;
 use constant AUDIT_UPDATES => 0;
 
 use constant DB_COLUMNS => qw(
-    comment_id
-    bug_id
-    who
-    bug_when
-    work_time
-    thetext
-    isprivate
-    already_wrapped
-    type
-    extra_data
-    is_markdown
+  comment_id
+  bug_id
+  who
+  bug_when
+  work_time
+  thetext
+  isprivate
+  already_wrapped
+  type
+  extra_data
+  is_markdown
 );
 
 use constant UPDATE_COLUMNS => qw(
-    isprivate
-    type
-    extra_data
+  isprivate
+  type
+  extra_data
 );
 
 use constant DB_TABLE => 'longdescs';
 use constant ID_FIELD => 'comment_id';
+
 # In some rare cases, two comments can have identical timestamps. If
 # this happens, we want to be sure that the comment added later shows up
 # later in the sequence.
 use constant LIST_ORDER => 'bug_when, comment_id';
 
 use constant VALIDATORS => {
-    bug_id      => \&_check_bug_id,
-    who         => \&_check_who,
-    bug_when    => \&_check_bug_when,
-    work_time   => \&_check_work_time,
-    thetext     => \&_check_thetext,
-    isprivate   => \&_check_isprivate,
-    is_markdown => \&Bugzilla::Object::check_boolean,
-    extra_data  => \&_check_extra_data,
-    type        => \&_check_type,
+  bug_id      => \&_check_bug_id,
+  who         => \&_check_who,
+  bug_when    => \&_check_bug_when,
+  work_time   => \&_check_work_time,
+  thetext     => \&_check_thetext,
+  isprivate   => \&_check_isprivate,
+  is_markdown => \&Bugzilla::Object::check_boolean,
+  extra_data  => \&_check_extra_data,
+  type        => \&_check_type,
 };
 
 use constant VALIDATOR_DEPENDENCIES => {
-    extra_data => ['type'],
-    bug_id     => ['who'],
-    work_time  => ['who', 'bug_id'],
-    isprivate  => ['who'],
+  extra_data => ['type'],
+  bug_id     => ['who'],
+  work_time  => ['who', 'bug_id'],
+  isprivate  => ['who'],
 };
 
 #########################
@@ -83,95 +84,100 @@ use constant VALIDATOR_DEPENDENCIES => {
 #########################
 
 sub update {
-    my $self = shift;
-    my ($changes, $old_comment) = $self->SUPER::update(@_);
-
-    if (exists $changes->{'thetext'} || exists $changes->{'isprivate'}) {
-        $self->bug->_sync_fulltext( update_comments => 1);
-    }
-
-    my @old_tags = @{ $old_comment->tags };
-    my @new_tags = @{ $self->tags };
-    my ($removed_tags, $added_tags) = diff_arrays(\@old_tags, \@new_tags);
-
-    if (@$removed_tags || @$added_tags) {
-        my $dbh = Bugzilla->dbh;
-        my $when = $dbh->selectrow_array("SELECT LOCALTIMESTAMP(0)");
-        my $sth_delete = $dbh->prepare(
-            "DELETE FROM longdescs_tags WHERE comment_id = ? AND tag = ?"
-        );
-        my $sth_insert = $dbh->prepare(
-            "INSERT INTO longdescs_tags(comment_id, tag) VALUES (?, ?)"
-        );
-        my $sth_activity = $dbh->prepare(
-            "INSERT INTO longdescs_tags_activity
+  my $self = shift;
+  my ($changes, $old_comment) = $self->SUPER::update(@_);
+
+  if (exists $changes->{'thetext'} || exists $changes->{'isprivate'}) {
+    $self->bug->_sync_fulltext(update_comments => 1);
+  }
+
+  my @old_tags = @{$old_comment->tags};
+  my @new_tags = @{$self->tags};
+  my ($removed_tags, $added_tags) = diff_arrays(\@old_tags, \@new_tags);
+
+  if (@$removed_tags || @$added_tags) {
+    my $dbh        = Bugzilla->dbh;
+    my $when       = $dbh->selectrow_array("SELECT LOCALTIMESTAMP(0)");
+    my $sth_delete = $dbh->prepare(
+      "DELETE FROM longdescs_tags WHERE comment_id = ? AND tag = ?");
+    my $sth_insert
+      = $dbh->prepare("INSERT INTO longdescs_tags(comment_id, tag) VALUES (?, ?)");
+    my $sth_activity = $dbh->prepare(
+      "INSERT INTO longdescs_tags_activity
             (bug_id, comment_id, who, bug_when, added, removed)
             VALUES (?, ?, ?, ?, ?, ?)"
-        );
-
-        foreach my $tag (@$removed_tags) {
-            my $weighted = Bugzilla::Comment::TagWeights->new({ name => $tag });
-            if ($weighted) {
-                if ($weighted->weight == 1) {
-                    $weighted->remove_from_db();
-                } else {
-                    $weighted->set_weight($weighted->weight - 1);
-                    $weighted->update();
-                }
-            }
-            trick_taint($tag);
-            $sth_delete->execute($self->id, $tag);
-            $sth_activity->execute(
-                $self->bug_id, $self->id, Bugzilla->user->id, $when, '', $tag);
-        }
+    );
 
-        foreach my $tag (@$added_tags) {
-            my $weighted = Bugzilla::Comment::TagWeights->new({ name => $tag });
-            if ($weighted) {
-                $weighted->set_weight($weighted->weight + 1);
-                $weighted->update();
-            } else {
-                Bugzilla::Comment::TagWeights->create({ tag => $tag, weight => 1 });
-            }
-            trick_taint($tag);
-            $sth_insert->execute($self->id, $tag);
-            $sth_activity->execute(
-                $self->bug_id, $self->id, Bugzilla->user->id, $when, $tag, '');
+    foreach my $tag (@$removed_tags) {
+      my $weighted = Bugzilla::Comment::TagWeights->new({name => $tag});
+      if ($weighted) {
+        if ($weighted->weight == 1) {
+          $weighted->remove_from_db();
         }
+        else {
+          $weighted->set_weight($weighted->weight - 1);
+          $weighted->update();
+        }
+      }
+      trick_taint($tag);
+      $sth_delete->execute($self->id, $tag);
+      $sth_activity->execute($self->bug_id, $self->id, Bugzilla->user->id, $when, '',
+        $tag);
     }
 
-    return $changes;
+    foreach my $tag (@$added_tags) {
+      my $weighted = Bugzilla::Comment::TagWeights->new({name => $tag});
+      if ($weighted) {
+        $weighted->set_weight($weighted->weight + 1);
+        $weighted->update();
+      }
+      else {
+        Bugzilla::Comment::TagWeights->create({tag => $tag, weight => 1});
+      }
+      trick_taint($tag);
+      $sth_insert->execute($self->id, $tag);
+      $sth_activity->execute($self->bug_id, $self->id, Bugzilla->user->id, $when,
+        $tag, '');
+    }
+  }
+
+  return $changes;
 }
 
 # Speeds up displays of comment lists by loading all author objects and tags at
 # once for a whole list.
 sub preload {
-    my ($class, $comments) = @_;
-    # Author
-    my %user_ids = map { $_->{who} => 1 } @$comments;
-    my $users = Bugzilla::User->new_from_list([keys %user_ids]);
-    my %user_map = map { $_->id => $_ } @$users;
-    foreach my $comment (@$comments) {
-        $comment->{author} = $user_map{$comment->{who}};
-    }
-    # Tags
-    if (Bugzilla->params->{'comment_taggers_group'}) {
-        my $dbh = Bugzilla->dbh;
-        my @comment_ids = map { $_->id } @$comments;
-        my %comment_map = map { $_->id => $_ } @$comments;
-        my $rows = $dbh->selectall_arrayref(
-            "SELECT comment_id, " . $dbh->sql_group_concat('tag', "','") . "
+  my ($class, $comments) = @_;
+
+  # Author
+  my %user_ids = map { $_->{who} => 1 } @$comments;
+  my $users    = Bugzilla::User->new_from_list([keys %user_ids]);
+  my %user_map = map { $_->id => $_ } @$users;
+  foreach my $comment (@$comments) {
+    $comment->{author} = $user_map{$comment->{who}};
+  }
+
+  # Tags
+  if (Bugzilla->params->{'comment_taggers_group'}) {
+    my $dbh         = Bugzilla->dbh;
+    my @comment_ids = map { $_->id } @$comments;
+    my %comment_map = map { $_->id => $_ } @$comments;
+    my $rows        = $dbh->selectall_arrayref(
+      "SELECT comment_id, " . $dbh->sql_group_concat('tag', "','") . "
                FROM longdescs_tags
-              WHERE " . $dbh->sql_in('comment_id', \@comment_ids) . ' ' .
-              $dbh->sql_group_by('comment_id'));
-        foreach my $row (@$rows) {
-            $comment_map{$row->[0]}->{tags} = [ split(/,/, $row->[1]) ];
-        }
-        # Also sets the 'tags' attribute for comments which have no entry
-        # in the longdescs_tags table, else calling $comment->tags will
-        # trigger another SQL query again.
-        $comment_map{$_}->{tags} ||= [] foreach @comment_ids;
+              WHERE "
+        . $dbh->sql_in('comment_id', \@comment_ids) . ' '
+        . $dbh->sql_group_by('comment_id')
+    );
+    foreach my $row (@$rows) {
+      $comment_map{$row->[0]}->{tags} = [split(/,/, $row->[1])];
     }
+
+    # Also sets the 'tags' attribute for comments which have no entry
+    # in the longdescs_tags table, else calling $comment->tags will
+    # trigger another SQL query again.
+    $comment_map{$_}->{tags} ||= [] foreach @comment_ids;
+  }
 }
 
 ###############################
@@ -179,138 +185,140 @@ sub preload {
 ###############################
 
 sub already_wrapped { return $_[0]->{'already_wrapped'}; }
-sub body        { return $_[0]->{'thetext'};   }
-sub bug_id      { return $_[0]->{'bug_id'};    }
-sub creation_ts { return $_[0]->{'bug_when'};  }
-sub is_private  { return $_[0]->{'isprivate'}; }
-sub is_markdown { return $_[0]->{'is_markdown'}; }
-sub work_time   {
-    # Work time is returned as a string (see bug 607909)
-    return 0 if $_[0]->{'work_time'} + 0 == 0;
-    return $_[0]->{'work_time'};
+sub body            { return $_[0]->{'thetext'}; }
+sub bug_id          { return $_[0]->{'bug_id'}; }
+sub creation_ts     { return $_[0]->{'bug_when'}; }
+sub is_private      { return $_[0]->{'isprivate'}; }
+sub is_markdown     { return $_[0]->{'is_markdown'}; }
+
+sub work_time {
+
+  # Work time is returned as a string (see bug 607909)
+  return 0 if $_[0]->{'work_time'} + 0 == 0;
+  return $_[0]->{'work_time'};
 }
-sub type        { return $_[0]->{'type'};      }
-sub extra_data  { return $_[0]->{'extra_data'} }
+sub type       { return $_[0]->{'type'}; }
+sub extra_data { return $_[0]->{'extra_data'} }
 
 sub tags {
-    my ($self) = @_;
-    state $comment_taggers_group = Bugzilla->params->{'comment_taggers_group'};
-    return [] unless $comment_taggers_group;
-    $self->{'tags'} ||= Bugzilla->dbh->selectcol_arrayref(
-        "SELECT tag
+  my ($self) = @_;
+  state $comment_taggers_group = Bugzilla->params->{'comment_taggers_group'};
+  return [] unless $comment_taggers_group;
+  $self->{'tags'} ||= Bugzilla->dbh->selectcol_arrayref(
+    "SELECT tag
            FROM longdescs_tags
           WHERE comment_id = ?
-          ORDER BY tag",
-        undef, $self->id);
-    return $self->{'tags'};
+          ORDER BY tag", undef, $self->id
+  );
+  return $self->{'tags'};
 }
 
 sub collapsed {
-    my ($self) = @_;
-    state $comment_taggers_group = Bugzilla->params->{'comment_taggers_group'};
-    return 0 unless $comment_taggers_group;
-    return $self->{collapsed} if exists $self->{collapsed};
-
-    state $collapsed_comment_tags = Bugzilla->params->{'collapsed_comment_tags'};
-    $self->{collapsed} = 0;
-    Bugzilla->request_cache->{comment_tags_collapsed}
-            ||= [ split(/\s*,\s*/, $collapsed_comment_tags) ];
-    my @collapsed_tags = @{ Bugzilla->request_cache->{comment_tags_collapsed} };
-    foreach my $my_tag (@{ $self->tags }) {
-        $my_tag = lc($my_tag);
-        foreach my $collapsed_tag (@collapsed_tags) {
-            if ($my_tag eq lc($collapsed_tag)) {
-                $self->{collapsed} = 1;
-                last;
-            }
-        }
-        last if $self->{collapsed};
+  my ($self) = @_;
+  state $comment_taggers_group = Bugzilla->params->{'comment_taggers_group'};
+  return 0 unless $comment_taggers_group;
+  return $self->{collapsed} if exists $self->{collapsed};
+
+  state $collapsed_comment_tags = Bugzilla->params->{'collapsed_comment_tags'};
+  $self->{collapsed} = 0;
+  Bugzilla->request_cache->{comment_tags_collapsed}
+    ||= [split(/\s*,\s*/, $collapsed_comment_tags)];
+  my @collapsed_tags = @{Bugzilla->request_cache->{comment_tags_collapsed}};
+  foreach my $my_tag (@{$self->tags}) {
+    $my_tag = lc($my_tag);
+    foreach my $collapsed_tag (@collapsed_tags) {
+      if ($my_tag eq lc($collapsed_tag)) {
+        $self->{collapsed} = 1;
+        last;
+      }
     }
-    return $self->{collapsed};
+    last if $self->{collapsed};
+  }
+  return $self->{collapsed};
 }
 
 sub bug {
-    my $self = shift;
-    require Bugzilla::Bug;
+  my $self = shift;
+  require Bugzilla::Bug;
 
-    # note $bug exists as a strong reference to keep $self->{bug} defined until the end of this method
-    my $bug = $self->{bug} ||= new Bugzilla::Bug($self->bug_id);
-    weaken($self->{bug}) unless isweak($self->{bug});
-    return $bug;
+# note $bug exists as a strong reference to keep $self->{bug} defined until the end of this method
+  my $bug = $self->{bug} ||= new Bugzilla::Bug($self->bug_id);
+  weaken($self->{bug}) unless isweak($self->{bug});
+  return $bug;
 }
 
 sub is_about_attachment {
-    my ($self) = @_;
-    return 1 if ($self->type == CMT_ATTACHMENT_CREATED
-                 or $self->type == CMT_ATTACHMENT_UPDATED);
-    return 0;
+  my ($self) = @_;
+  return 1
+    if ($self->type == CMT_ATTACHMENT_CREATED
+    or $self->type == CMT_ATTACHMENT_UPDATED);
+  return 0;
 }
 
 sub attachment {
-    my ($self) = @_;
-    return undef if not $self->is_about_attachment;
-    $self->{attachment} ||=
-        new Bugzilla::Attachment({ id => $self->extra_data, cache => 1 });
-    return $self->{attachment};
+  my ($self) = @_;
+  return undef if not $self->is_about_attachment;
+  $self->{attachment}
+    ||= new Bugzilla::Attachment({id => $self->extra_data, cache => 1});
+  return $self->{attachment};
 }
 
-sub author { 
-    my $self = shift;
-    $self->{'author'}
-      ||= new Bugzilla::User({ id => $self->{'who'}, cache => 1 });
-    return $self->{'author'};
+sub author {
+  my $self = shift;
+  $self->{'author'} ||= new Bugzilla::User({id => $self->{'who'}, cache => 1});
+  return $self->{'author'};
 }
 
 sub body_full {
-    my ($self, $params) = @_;
-    $params ||= {};
-    my $template = Bugzilla->template_inner;
-    my $body;
-    if ($self->type) {
-        $template->process("bug/format_comment.txt.tmpl", 
-                           { comment => $self, %$params }, \$body)
-            || ThrowTemplateError($template->error());
-        $body =~ s/^X//;
-    }
-    else {
-        $body = $self->body;
-    }
-    if (!$self->is_markdown and !$self->already_wrapped) {
-        $body = wrap_cite($body);
-    }
-    if ($params->{wrap} and !$self->already_wrapped) {
-        $body = wrap_comment($body);
-    }
-    return $body;
+  my ($self, $params) = @_;
+  $params ||= {};
+  my $template = Bugzilla->template_inner;
+  my $body;
+  if ($self->type) {
+    $template->process("bug/format_comment.txt.tmpl", {comment => $self, %$params},
+      \$body)
+      || ThrowTemplateError($template->error());
+    $body =~ s/^X//;
+  }
+  else {
+    $body = $self->body;
+  }
+  if (!$self->is_markdown and !$self->already_wrapped) {
+    $body = wrap_cite($body);
+  }
+  if ($params->{wrap} and !$self->already_wrapped) {
+    $body = wrap_comment($body);
+  }
+  return $body;
 }
 
 ############
 # Mutators #
 ############
 
-sub set_is_private  { $_[0]->set('isprivate',  $_[1]); }
-sub set_type        { $_[0]->set('type',       $_[1]); }
-sub set_extra_data  { $_[0]->set('extra_data', $_[1]); }
+sub set_is_private  { $_[0]->set('isprivate',   $_[1]); }
+sub set_type        { $_[0]->set('type',        $_[1]); }
+sub set_extra_data  { $_[0]->set('extra_data',  $_[1]); }
 sub set_is_markdown { $_[0]->set('is_markdown', $_[1]); }
 
 sub add_tag {
-    my ($self, $tag) = @_;
-    $tag = $self->_check_tag($tag);
+  my ($self, $tag) = @_;
+  $tag = $self->_check_tag($tag);
 
-    my $tags = $self->tags;
-    return if grep { lc($tag) eq lc($_) } @$tags;
-    push @$tags, $tag;
-    $self->{'tags'} = [ sort @$tags ];
+  my $tags = $self->tags;
+  return if grep { lc($tag) eq lc($_) } @$tags;
+  push @$tags, $tag;
+  $self->{'tags'} = [sort @$tags];
 }
 
 sub remove_tag {
-    my ($self, $tag) = @_;
-    $tag = $self->_check_tag($tag);
+  my ($self, $tag) = @_;
+  $tag = $self->_check_tag($tag);
 
-    my $tags = $self->tags;
-    my $index = first { lc($tags->[$_]) eq lc($tag) } 0..scalar(@$tags) - 1;
-    return unless defined $index;
-    splice(@$tags, $index, 1);
+  my $tags = $self->tags;
+  my $index = first { lc($tags->[$_]) eq lc($tag) } 0 .. scalar(@$tags) - 1;
+  return unless defined $index;
+  splice(@$tags, $index, 1);
 }
 
 ##############
@@ -318,178 +326,177 @@ sub remove_tag {
 ##############
 
 sub run_create_validators {
-    my $self = shift;
-    my $params = $self->SUPER::run_create_validators(@_);
-    # Sometimes this run_create_validators is called with parameters that
-    # skip bug_id validation, so it might not exist in the resulting hash.
-    if (defined $params->{bug_id}) {
-        $params->{bug_id} = $params->{bug_id}->id;
-    }
-    return $params;
+  my $self   = shift;
+  my $params = $self->SUPER::run_create_validators(@_);
+
+  # Sometimes this run_create_validators is called with parameters that
+  # skip bug_id validation, so it might not exist in the resulting hash.
+  if (defined $params->{bug_id}) {
+    $params->{bug_id} = $params->{bug_id}->id;
+  }
+  return $params;
 }
 
 sub _check_extra_data {
-    my ($invocant, $extra_data, undef, $params) = @_;
-    my $type = blessed($invocant) ? $invocant->type : $params->{type};
+  my ($invocant, $extra_data, undef, $params) = @_;
+  my $type = blessed($invocant) ? $invocant->type : $params->{type};
 
-    if ($type == CMT_NORMAL) {
-        if (defined $extra_data) {
-            ThrowCodeError('comment_extra_data_not_allowed',
-                           { type => $type, extra_data => $extra_data });
-        }
+  if ($type == CMT_NORMAL) {
+    if (defined $extra_data) {
+      ThrowCodeError('comment_extra_data_not_allowed',
+        {type => $type, extra_data => $extra_data});
+    }
+  }
+  else {
+    if (!defined $extra_data) {
+      ThrowCodeError('comment_extra_data_required', {type => $type});
+    }
+    elsif ($type == CMT_ATTACHMENT_CREATED or $type == CMT_ATTACHMENT_UPDATED) {
+      my $attachment = Bugzilla::Attachment->check({id => $extra_data});
+      $extra_data = $attachment->id;
     }
     else {
-        if (!defined $extra_data) {
-            ThrowCodeError('comment_extra_data_required', { type => $type });
-        }
-        elsif ($type == CMT_ATTACHMENT_CREATED 
-               or $type == CMT_ATTACHMENT_UPDATED) 
-        {
-             my $attachment = Bugzilla::Attachment->check({ 
-                 id => $extra_data });
-             $extra_data = $attachment->id;
-        }
-        else {
-            my $original = $extra_data;
-            detaint_natural($extra_data) 
-              or ThrowCodeError('comment_extra_data_not_numeric',
-                                { type => $type, extra_data => $original });
-        }
+      my $original = $extra_data;
+      detaint_natural($extra_data)
+        or ThrowCodeError('comment_extra_data_not_numeric',
+        {type => $type, extra_data => $original});
     }
+  }
 
-    return $extra_data;
+  return $extra_data;
 }
 
 sub _check_type {
-    my ($invocant, $type) = @_;
-    $type ||= CMT_NORMAL;
-    my $original = $type;
-    detaint_natural($type)
-        or ThrowCodeError('comment_type_invalid', { type => $original });
-    return $type;
+  my ($invocant, $type) = @_;
+  $type ||= CMT_NORMAL;
+  my $original = $type;
+  detaint_natural($type)
+    or ThrowCodeError('comment_type_invalid', {type => $original});
+  return $type;
 }
 
 sub _check_bug_id {
-    my ($invocant, $bug_id) = @_;
-
-    ThrowCodeError('param_required', {function => 'Bugzilla::Comment->create',
-                                      param => 'bug_id'}) unless $bug_id;
-
-    my $bug;
-    if (blessed $bug_id) {
-        # We got a bug object passed in, use it
-        $bug = $bug_id;
-        $bug->check_is_visible;
-    }
-    else {
-        # We got a bug id passed in, check it and get the bug object
-        $bug = Bugzilla::Bug->check({ id => $bug_id });
-    }
-
-    # Make sure the user can edit the product
-    Bugzilla->user->can_edit_product($bug->{product_id});
-
-    # Make sure the user can comment
-    my $privs;
-    $bug->check_can_change_field('longdesc', 0, 1, \$privs)
-        || ThrowUserError('illegal_change', 
-                          { field => 'longdesc', privs => $privs });
-    return $bug;
+  my ($invocant, $bug_id) = @_;
+
+  ThrowCodeError('param_required',
+    {function => 'Bugzilla::Comment->create', param => 'bug_id'})
+    unless $bug_id;
+
+  my $bug;
+  if (blessed $bug_id) {
+
+    # We got a bug object passed in, use it
+    $bug = $bug_id;
+    $bug->check_is_visible;
+  }
+  else {
+    # We got a bug id passed in, check it and get the bug object
+    $bug = Bugzilla::Bug->check({id => $bug_id});
+  }
+
+  # Make sure the user can edit the product
+  Bugzilla->user->can_edit_product($bug->{product_id});
+
+  # Make sure the user can comment
+  my $privs;
+  $bug->check_can_change_field('longdesc', 0, 1, \$privs)
+    || ThrowUserError('illegal_change', {field => 'longdesc', privs => $privs});
+  return $bug;
 }
 
 sub _check_who {
-    my ($invocant, $who) = @_;
-    Bugzilla->login(LOGIN_REQUIRED);
-    return Bugzilla->user->id;
+  my ($invocant, $who) = @_;
+  Bugzilla->login(LOGIN_REQUIRED);
+  return Bugzilla->user->id;
 }
 
 sub _check_bug_when {
-    my ($invocant, $when) = @_;
+  my ($invocant, $when) = @_;
 
-    # Make sure the timestamp is defined, default to a timestamp from the db
-    if (!defined $when) {
-        $when = Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
-    }
+  # Make sure the timestamp is defined, default to a timestamp from the db
+  if (!defined $when) {
+    $when = Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
+  }
 
-    # Make sure the timestamp parses
-    if (!datetime_from($when)) {
-        ThrowCodeError('invalid_timestamp', { timestamp => $when });
-    }
+  # Make sure the timestamp parses
+  if (!datetime_from($when)) {
+    ThrowCodeError('invalid_timestamp', {timestamp => $when});
+  }
 
-    return $when;
+  return $when;
 }
 
 sub _check_work_time {
-    my ($invocant, $value_in, $field, $params) = @_;
-
-    # Call down to Bugzilla::Object, letting it know negative
-    # values are ok
-    my $time = $invocant->check_time($value_in, $field, $params, 1);
-    my $privs;
-    $params->{bug_id}->check_can_change_field('work_time', 0, $time, \$privs)
-        || ThrowUserError('illegal_change',
-                          { field => 'work_time', privs => $privs });
-    return $time;
+  my ($invocant, $value_in, $field, $params) = @_;
+
+  # Call down to Bugzilla::Object, letting it know negative
+  # values are ok
+  my $time = $invocant->check_time($value_in, $field, $params, 1);
+  my $privs;
+  $params->{bug_id}->check_can_change_field('work_time', 0, $time, \$privs)
+    || ThrowUserError('illegal_change', {field => 'work_time', privs => $privs});
+  return $time;
 }
 
 sub _check_thetext {
-    my ($invocant, $thetext) = @_;
-
-    ThrowCodeError('param_required',{function => 'Bugzilla::Comment->create',
-                                     param => 'thetext'}) unless defined $thetext;
-
-    # Remove any trailing whitespace. Leading whitespace could be
-    # a valid part of the comment.
-    $thetext =~ s/\s*$//s;
-    $thetext =~ s/\r\n?/\n/g; # Get rid of \r.
-
-    # Characters above U+FFFF cannot be stored by MySQL older than 5.5.3 as they
-    # require the new utf8mb4 character set. Other DB servers are handling them
-    # without any problem. So we need to replace these characters if we use MySQL,
-    # else the comment is truncated.
-    # XXX - Once we use utf8mb4 for comments, this hack for MySQL can go away.
-    state $is_mysql = Bugzilla->dbh->isa('Bugzilla::DB::Mysql') ? 1 : 0;
-    if ($is_mysql) {
-        $thetext =~ s/([\x{10000}-\x{10FFFF}])/"\x{FDD0}[" . uc(sprintf('U+%04x', ord($1))) . "]\x{FDD1}"/eg;
-    }
-
-    ThrowUserError('comment_too_long') if length($thetext) > MAX_COMMENT_LENGTH;
-    return $thetext;
+  my ($invocant, $thetext) = @_;
+
+  ThrowCodeError('param_required',
+    {function => 'Bugzilla::Comment->create', param => 'thetext'})
+    unless defined $thetext;
+
+  # Remove any trailing whitespace. Leading whitespace could be
+  # a valid part of the comment.
+  $thetext =~ s/\s*$//s;
+  $thetext =~ s/\r\n?/\n/g;    # Get rid of \r.
+
+  # Characters above U+FFFF cannot be stored by MySQL older than 5.5.3 as they
+  # require the new utf8mb4 character set. Other DB servers are handling them
+  # without any problem. So we need to replace these characters if we use MySQL,
+  # else the comment is truncated.
+  # XXX - Once we use utf8mb4 for comments, this hack for MySQL can go away.
+  state $is_mysql = Bugzilla->dbh->isa('Bugzilla::DB::Mysql') ? 1 : 0;
+  if ($is_mysql) {
+    $thetext
+      =~ s/([\x{10000}-\x{10FFFF}])/"\x{FDD0}[" . uc(sprintf('U+%04x', ord($1))) . "]\x{FDD1}"/eg;
+  }
+
+  ThrowUserError('comment_too_long') if length($thetext) > MAX_COMMENT_LENGTH;
+  return $thetext;
 }
 
 sub _check_isprivate {
-    my ($invocant, $isprivate) = @_;
-    if ($isprivate && !Bugzilla->user->is_insider) {
-        ThrowUserError('user_not_insider');
-    }
-    return $isprivate ? 1 : 0;
+  my ($invocant, $isprivate) = @_;
+  if ($isprivate && !Bugzilla->user->is_insider) {
+    ThrowUserError('user_not_insider');
+  }
+  return $isprivate ? 1 : 0;
 }
 
 sub _check_tag {
-    my ($invocant, $tag) = @_;
-    length($tag) < MIN_COMMENT_TAG_LENGTH
-        and ThrowUserError('comment_tag_too_short', { tag => $tag });
-    length($tag) > MAX_COMMENT_TAG_LENGTH
-        and ThrowUserError('comment_tag_too_long', { tag => $tag });
-    $tag =~ /^[\w\d\._-]+$/
-        or ThrowUserError('comment_tag_invalid', { tag => $tag });
-    return $tag;
+  my ($invocant, $tag) = @_;
+  length($tag) < MIN_COMMENT_TAG_LENGTH
+    and ThrowUserError('comment_tag_too_short', {tag => $tag});
+  length($tag) > MAX_COMMENT_TAG_LENGTH
+    and ThrowUserError('comment_tag_too_long', {tag => $tag});
+  $tag =~ /^[\w\d\._-]+$/ or ThrowUserError('comment_tag_invalid', {tag => $tag});
+  return $tag;
 }
 
 sub count {
-    my ($self) = @_;
+  my ($self) = @_;
 
-    return $self->{'count'} if defined $self->{'count'};
+  return $self->{'count'} if defined $self->{'count'};
 
-    my $dbh = Bugzilla->dbh;
-    ($self->{'count'}) = $dbh->selectrow_array(
-        "SELECT COUNT(*)
+  my $dbh = Bugzilla->dbh;
+  ($self->{'count'}) = $dbh->selectrow_array(
+    "SELECT COUNT(*)
            FROM longdescs 
           WHERE bug_id = ? 
-                AND bug_when <= ?",
-        undef, $self->bug_id, $self->creation_ts);
+                AND bug_when <= ?", undef, $self->bug_id, $self->creation_ts
+  );
 
-    return --$self->{'count'};
+  return --$self->{'count'};
 }
 
 1;
index f4aa519a303ecd4aa4e33503dd44dc8eaa8abf64..42200f663ce0d89f1b25ede5b99e502e53843e57 100644 (file)
@@ -21,20 +21,20 @@ use constant AUDIT_UPDATES => 0;
 use constant AUDIT_REMOVES => 0;
 
 use constant DB_COLUMNS => qw(
-    id
-    tag
-    weight
+  id
+  tag
+  weight
 );
 
 use constant UPDATE_COLUMNS => qw(
-    weight
+  weight
 );
 
 use constant DB_TABLE   => 'longdescs_tags_weights';
 use constant ID_FIELD   => 'id';
 use constant NAME_FIELD => 'tag';
 use constant LIST_ORDER => 'weight DESC';
-use constant VALIDATORS => { };
+use constant VALIDATORS => {};
 
 # There's no gain to caching these objects
 use constant USE_MEMCACHED => 0;
index 77d07e4c2757202973894973daef7d0723406993..b9688d009fa2762040eefc0596ff04715cfa1ae0 100644 (file)
@@ -27,150 +27,147 @@ use Scalar::Util qw(blessed);
 ###############################
 
 use constant DB_TABLE => 'components';
+
 # This is mostly for the editfields.cgi case where ->get_all is called.
 use constant LIST_ORDER => 'product_id, name';
 
 use constant DB_COLUMNS => qw(
-    id
-    name
-    product_id
-    initialowner
-    initialqacontact
-    description
-    isactive
+  id
+  name
+  product_id
+  initialowner
+  initialqacontact
+  description
+  isactive
 );
 
 use constant UPDATE_COLUMNS => qw(
-    name
-    initialowner
-    initialqacontact
-    description
-    isactive
+  name
+  initialowner
+  initialqacontact
+  description
+  isactive
 );
 
-use constant REQUIRED_FIELD_MAP => {
-    product_id => 'product',
-};
+use constant REQUIRED_FIELD_MAP => {product_id => 'product',};
 
 use constant VALIDATORS => {
-    create_series    => \&Bugzilla::Object::check_boolean,
-    product          => \&_check_product,
-    initialowner     => \&_check_initialowner,
-    initialqacontact => \&_check_initialqacontact,
-    description      => \&_check_description,
-    initial_cc       => \&_check_cc_list,
-    name             => \&_check_name,
-    isactive         => \&Bugzilla::Object::check_boolean,
+  create_series    => \&Bugzilla::Object::check_boolean,
+  product          => \&_check_product,
+  initialowner     => \&_check_initialowner,
+  initialqacontact => \&_check_initialqacontact,
+  description      => \&_check_description,
+  initial_cc       => \&_check_cc_list,
+  name             => \&_check_name,
+  isactive         => \&Bugzilla::Object::check_boolean,
 };
 
-use constant VALIDATOR_DEPENDENCIES => {
-    name => ['product'],
-};
+use constant VALIDATOR_DEPENDENCIES => {name => ['product'],};
 
 ###############################
 
 sub new {
-    my $class = shift;
-    my $param = shift;
-    my $dbh = Bugzilla->dbh;
-
-    my $product;
-    if (ref $param and !defined $param->{id}) {
-        $product = $param->{product};
-        my $name = $param->{name};
-        if (!defined $product) {
-            ThrowCodeError('bad_arg',
-                {argument => 'product',
-                 function => "${class}::new"});
-        }
-        if (!defined $name) {
-            ThrowCodeError('bad_arg',
-                {argument => 'name',
-                 function => "${class}::new"});
-        }
-
-        my $condition = 'product_id = ? AND name = ?';
-        my @values = ($product->id, $name);
-        $param = { condition => $condition, values => \@values };
+  my $class = shift;
+  my $param = shift;
+  my $dbh   = Bugzilla->dbh;
+
+  my $product;
+  if (ref $param and !defined $param->{id}) {
+    $product = $param->{product};
+    my $name = $param->{name};
+    if (!defined $product) {
+      ThrowCodeError('bad_arg', {argument => 'product', function => "${class}::new"});
     }
+    if (!defined $name) {
+      ThrowCodeError('bad_arg', {argument => 'name', function => "${class}::new"});
+    }
+
+    my $condition = 'product_id = ? AND name = ?';
+    my @values = ($product->id, $name);
+    $param = {condition => $condition, values => \@values};
+  }
 
-    unshift @_, $param;
-    my $component = $class->SUPER::new(@_);
-    # Add the product object as attribute only if the component exists.
-    $component->{product} = $product if ($component && $product);
-    return $component;
+  unshift @_, $param;
+  my $component = $class->SUPER::new(@_);
+
+  # Add the product object as attribute only if the component exists.
+  $component->{product} = $product if ($component && $product);
+  return $component;
 }
 
 sub create {
-    my $class = shift;
-    my $dbh = Bugzilla->dbh;
+  my $class = shift;
+  my $dbh   = Bugzilla->dbh;
 
-    $dbh->bz_start_transaction();
+  $dbh->bz_start_transaction();
 
-    $class->check_required_create_fields(@_);
-    my $params = $class->run_create_validators(@_);
-    my $cc_list = delete $params->{initial_cc};
-    my $create_series = delete $params->{create_series};
-    my $product = delete $params->{product};
-    $params->{product_id} = $product->id;
+  $class->check_required_create_fields(@_);
+  my $params        = $class->run_create_validators(@_);
+  my $cc_list       = delete $params->{initial_cc};
+  my $create_series = delete $params->{create_series};
+  my $product       = delete $params->{product};
+  $params->{product_id} = $product->id;
 
-    my $component = $class->insert_create_data($params);
-    $component->{product} = $product;
+  my $component = $class->insert_create_data($params);
+  $component->{product} = $product;
 
-    # We still have to fill the component_cc table.
-    $component->_update_cc_list($cc_list) if $cc_list;
+  # We still have to fill the component_cc table.
+  $component->_update_cc_list($cc_list) if $cc_list;
 
-    # Create series for the new component.
-    $component->_create_series() if $create_series;
+  # Create series for the new component.
+  $component->_create_series() if $create_series;
 
-    $dbh->bz_commit_transaction();
-    return $component;
+  $dbh->bz_commit_transaction();
+  return $component;
 }
 
 sub update {
-    my $self = shift;
-    my $changes = $self->SUPER::update(@_);
-
-    # Update the component_cc table if necessary.
-    if (defined $self->{cc_ids}) {
-        my $diff = $self->_update_cc_list($self->{cc_ids});
-        $changes->{cc_list} = $diff if defined $diff;
-    }
-    return $changes;
+  my $self    = shift;
+  my $changes = $self->SUPER::update(@_);
+
+  # Update the component_cc table if necessary.
+  if (defined $self->{cc_ids}) {
+    my $diff = $self->_update_cc_list($self->{cc_ids});
+    $changes->{cc_list} = $diff if defined $diff;
+  }
+  return $changes;
 }
 
 sub remove_from_db {
-    my $self = shift;
-    my $dbh = Bugzilla->dbh;
+  my $self = shift;
+  my $dbh  = Bugzilla->dbh;
 
-    $self->_check_if_controller(); # From ChoiceInterface
+  $self->_check_if_controller();    # From ChoiceInterface
 
-    $dbh->bz_start_transaction();
+  $dbh->bz_start_transaction();
 
-    # Products must have at least one component.
-    my @components = @{ $self->product->components };
-    if (scalar(@components) == 1) {
-        ThrowUserError('component_is_last', { comp => $self });
-    }
+  # Products must have at least one component.
+  my @components = @{$self->product->components};
+  if (scalar(@components) == 1) {
+    ThrowUserError('component_is_last', {comp => $self});
+  }
 
-    if ($self->bug_count) {
-        if (Bugzilla->params->{'allowbugdeletion'}) {
-            require Bugzilla::Bug;
-            foreach my $bug_id (@{$self->bug_ids}) {
-                # Note: We allow admins to delete bugs even if they can't
-                # see them, as long as they can see the product.
-                my $bug = new Bugzilla::Bug($bug_id);
-                $bug->remove_from_db();
-            }
-        } else {
-            ThrowUserError('component_has_bugs', {nb => $self->bug_count});
-        }
+  if ($self->bug_count) {
+    if (Bugzilla->params->{'allowbugdeletion'}) {
+      require Bugzilla::Bug;
+      foreach my $bug_id (@{$self->bug_ids}) {
+
+        # Note: We allow admins to delete bugs even if they can't
+        # see them, as long as they can see the product.
+        my $bug = new Bugzilla::Bug($bug_id);
+        $bug->remove_from_db();
+      }
+    }
+    else {
+      ThrowUserError('component_has_bugs', {nb => $self->bug_count});
     }
-    # Update the list of components in the product object.
-    $self->product->{components} = [grep { $_->id != $self->id } @components];
-    $self->SUPER::remove_from_db();
+  }
+
+  # Update the list of components in the product object.
+  $self->product->{components} = [grep { $_->id != $self->id } @components];
+  $self->SUPER::remove_from_db();
 
-    $dbh->bz_commit_transaction();
+  $dbh->bz_commit_transaction();
 }
 
 ################################
@@ -178,69 +175,70 @@ sub remove_from_db {
 ################################
 
 sub _check_name {
-    my ($invocant, $name, undef, $params) = @_;
-    my $product = blessed($invocant) ? $invocant->product : $params->{product};
-
-    $name = trim($name);
-    $name || ThrowUserError('component_blank_name');
-
-    if (length($name) > MAX_COMPONENT_SIZE) {
-        ThrowUserError('component_name_too_long', {'name' => $name});
-    }
-
-    my $component = new Bugzilla::Component({product => $product, name => $name});
-    if ($component && (!ref $invocant || $component->id != $invocant->id)) {
-        ThrowUserError('component_already_exists', { name    => $component->name,
-                                                     product => $product });
-    }
-    return $name;
+  my ($invocant, $name, undef, $params) = @_;
+  my $product = blessed($invocant) ? $invocant->product : $params->{product};
+
+  $name = trim($name);
+  $name || ThrowUserError('component_blank_name');
+
+  if (length($name) > MAX_COMPONENT_SIZE) {
+    ThrowUserError('component_name_too_long', {'name' => $name});
+  }
+
+  my $component = new Bugzilla::Component({product => $product, name => $name});
+  if ($component && (!ref $invocant || $component->id != $invocant->id)) {
+    ThrowUserError('component_already_exists',
+      {name => $component->name, product => $product});
+  }
+  return $name;
 }
 
 sub _check_description {
-    my ($invocant, $description) = @_;
+  my ($invocant, $description) = @_;
 
-    $description = trim($description);
-    $description || ThrowUserError('component_blank_description');
-    return $description;
+  $description = trim($description);
+  $description || ThrowUserError('component_blank_description');
+  return $description;
 }
 
 sub _check_initialowner {
-    my ($invocant, $owner) = @_;
+  my ($invocant, $owner) = @_;
 
-    $owner || ThrowUserError('component_need_initialowner');
-    my $owner_id = Bugzilla::User->check($owner)->id;
-    return $owner_id;
+  $owner || ThrowUserError('component_need_initialowner');
+  my $owner_id = Bugzilla::User->check($owner)->id;
+  return $owner_id;
 }
 
 sub _check_initialqacontact {
-    my ($invocant, $qa_contact) = @_;
-
-    my $qa_contact_id;
-    if (Bugzilla->params->{'useqacontact'}) {
-        $qa_contact_id = Bugzilla::User->check($qa_contact)->id if $qa_contact;
-    }
-    elsif (ref $invocant) {
-        $qa_contact_id = $invocant->{initialqacontact};
-    }
-    return $qa_contact_id;
+  my ($invocant, $qa_contact) = @_;
+
+  my $qa_contact_id;
+  if (Bugzilla->params->{'useqacontact'}) {
+    $qa_contact_id = Bugzilla::User->check($qa_contact)->id if $qa_contact;
+  }
+  elsif (ref $invocant) {
+    $qa_contact_id = $invocant->{initialqacontact};
+  }
+  return $qa_contact_id;
 }
 
 sub _check_product {
-    my ($invocant, $product) = @_;
-    $product || ThrowCodeError('param_required', 
-                    { function => "$invocant->create", param => 'product' });
-    return Bugzilla->user->check_can_admin_product($product->name);
+  my ($invocant, $product) = @_;
+  $product
+    || ThrowCodeError('param_required',
+    {function => "$invocant->create", param => 'product'});
+  return Bugzilla->user->check_can_admin_product($product->name);
 }
 
 sub _check_cc_list {
-    my ($invocant, $cc_list) = @_;
-
-    my %cc_ids;
-    foreach my $cc (@$cc_list) {
-        my $id = login_to_id($cc, THROW_ERROR);
-        $cc_ids{$id} = 1;
-    }
-    return [keys %cc_ids];
+  my ($invocant, $cc_list) = @_;
+
+  my %cc_ids;
+  foreach my $cc (@$cc_list) {
+    my $id = login_to_id($cc, THROW_ERROR);
+    $cc_ids{$id} = 1;
+  }
+  return [keys %cc_ids];
 }
 
 ###############################
@@ -248,155 +246,176 @@ sub _check_cc_list {
 ###############################
 
 sub _update_cc_list {
-    my ($self, $cc_list) = @_;
-    my $dbh = Bugzilla->dbh;
+  my ($self, $cc_list) = @_;
+  my $dbh = Bugzilla->dbh;
 
-    my $old_cc_list =
-      $dbh->selectcol_arrayref('SELECT user_id FROM component_cc
-                                WHERE component_id = ?', undef, $self->id);
+  my $old_cc_list = $dbh->selectcol_arrayref(
+    'SELECT user_id FROM component_cc
+                                WHERE component_id = ?', undef, $self->id
+  );
 
-    my ($removed, $added) = diff_arrays($old_cc_list, $cc_list);
-    my $diff;
-    if (scalar @$removed || scalar @$added) {
-        $diff = [join(', ', @$removed), join(', ', @$added)];
-    }
+  my ($removed, $added) = diff_arrays($old_cc_list, $cc_list);
+  my $diff;
+  if (scalar @$removed || scalar @$added) {
+    $diff = [join(', ', @$removed), join(', ', @$added)];
+  }
 
-    $dbh->do('DELETE FROM component_cc WHERE component_id = ?', undef, $self->id);
+  $dbh->do('DELETE FROM component_cc WHERE component_id = ?', undef, $self->id);
 
-    my $sth = $dbh->prepare('INSERT INTO component_cc
-                             (user_id, component_id) VALUES (?, ?)');
-    $sth->execute($_, $self->id) foreach (@$cc_list);
+  my $sth = $dbh->prepare(
+    'INSERT INTO component_cc
+                             (user_id, component_id) VALUES (?, ?)'
+  );
+  $sth->execute($_, $self->id) foreach (@$cc_list);
 
-    return $diff;
+  return $diff;
 }
 
 sub _create_series {
-    my $self = shift;
-
-    # Insert default charting queries for this product.
-    # If they aren't using charting, this won't do any harm.
-    my $prodcomp = "&product="   . url_quote($self->product->name) .
-                   "&component=" . url_quote($self->name);
-
-    my $open_query = 'field0-0-0=resolution&type0-0-0=notregexp&value0-0-0=.' .
-                     $prodcomp;
-    my $nonopen_query = 'field0-0-0=resolution&type0-0-0=regexp&value0-0-0=.' .
-                        $prodcomp;
-
-    my @series = ([get_text('series_all_open'), $open_query],
-                  [get_text('series_all_closed'), $nonopen_query]);
-
-    foreach my $sdata (@series) {
-        my $series = new Bugzilla::Series(undef, $self->product->name,
-                                          $self->name, $sdata->[0],
-                                          Bugzilla->user->id, 1, $sdata->[1], 1);
-        $series->writeToDatabase();
-    }
+  my $self = shift;
+
+  # Insert default charting queries for this product.
+  # If they aren't using charting, this won't do any harm.
+  my $prodcomp
+    = "&product="
+    . url_quote($self->product->name)
+    . "&component="
+    . url_quote($self->name);
+
+  my $open_query
+    = 'field0-0-0=resolution&type0-0-0=notregexp&value0-0-0=.' . $prodcomp;
+  my $nonopen_query
+    = 'field0-0-0=resolution&type0-0-0=regexp&value0-0-0=.' . $prodcomp;
+
+  my @series = (
+    [get_text('series_all_open'),   $open_query],
+    [get_text('series_all_closed'), $nonopen_query]
+  );
+
+  foreach my $sdata (@series) {
+    my $series
+      = new Bugzilla::Series(undef, $self->product->name, $self->name, $sdata->[0],
+      Bugzilla->user->id, 1, $sdata->[1], 1);
+    $series->writeToDatabase();
+  }
 }
 
-sub set_name { $_[0]->set('name', $_[1]); }
+sub set_name        { $_[0]->set('name',        $_[1]); }
 sub set_description { $_[0]->set('description', $_[1]); }
-sub set_is_active { $_[0]->set('isactive', $_[1]); }
+sub set_is_active   { $_[0]->set('isactive',    $_[1]); }
+
 sub set_default_assignee {
-    my ($self, $owner) = @_;
+  my ($self, $owner) = @_;
 
-    $self->set('initialowner', $owner);
-    # Reset the default owner object.
-    delete $self->{default_assignee};
+  $self->set('initialowner', $owner);
+
+  # Reset the default owner object.
+  delete $self->{default_assignee};
 }
+
 sub set_default_qa_contact {
-    my ($self, $qa_contact) = @_;
+  my ($self, $qa_contact) = @_;
 
-    $self->set('initialqacontact', $qa_contact);
-    # Reset the default QA contact object.
-    delete $self->{default_qa_contact};
+  $self->set('initialqacontact', $qa_contact);
+
+  # Reset the default QA contact object.
+  delete $self->{default_qa_contact};
 }
+
 sub set_cc_list {
-    my ($self, $cc_list) = @_;
+  my ($self, $cc_list) = @_;
+
+  $self->{cc_ids} = $self->_check_cc_list($cc_list);
 
-    $self->{cc_ids} = $self->_check_cc_list($cc_list);
-    # Reset the list of CC user objects.
-    delete $self->{initial_cc};
+  # Reset the list of CC user objects.
+  delete $self->{initial_cc};
 }
 
 sub bug_count {
-    my $self = shift;
-    my $dbh = Bugzilla->dbh;
+  my $self = shift;
+  my $dbh  = Bugzilla->dbh;
 
-    if (!defined $self->{'bug_count'}) {
-        $self->{'bug_count'} = $dbh->selectrow_array(q{
+  if (!defined $self->{'bug_count'}) {
+    $self->{'bug_count'} = $dbh->selectrow_array(
+      q{
             SELECT COUNT(*) FROM bugs
-            WHERE component_id = ?}, undef, $self->id) || 0;
-    }
-    return $self->{'bug_count'};
+            WHERE component_id = ?}, undef, $self->id
+    ) || 0;
+  }
+  return $self->{'bug_count'};
 }
 
 sub bug_ids {
-    my $self = shift;
-    my $dbh = Bugzilla->dbh;
+  my $self = shift;
+  my $dbh  = Bugzilla->dbh;
 
-    if (!defined $self->{'bugs_ids'}) {
-        $self->{'bugs_ids'} = $dbh->selectcol_arrayref(q{
+  if (!defined $self->{'bugs_ids'}) {
+    $self->{'bugs_ids'} = $dbh->selectcol_arrayref(
+      q{
             SELECT bug_id FROM bugs
-            WHERE component_id = ?}, undef, $self->id);
-    }
-    return $self->{'bugs_ids'};
+            WHERE component_id = ?}, undef, $self->id
+    );
+  }
+  return $self->{'bugs_ids'};
 }
 
 sub default_assignee {
-    my $self = shift;
+  my $self = shift;
 
-    return $self->{'default_assignee'}
-      ||= new Bugzilla::User({ id => $self->{'initialowner'}, cache => 1 });
+  return $self->{'default_assignee'}
+    ||= new Bugzilla::User({id => $self->{'initialowner'}, cache => 1});
 }
 
 sub default_qa_contact {
-    my $self = shift;
+  my $self = shift;
 
-    return unless $self->{'initialqacontact'};
-    return $self->{'default_qa_contact'}
-      ||= new Bugzilla::User({id => $self->{'initialqacontact'}, cache => 1 });
+  return unless $self->{'initialqacontact'};
+  return $self->{'default_qa_contact'}
+    ||= new Bugzilla::User({id => $self->{'initialqacontact'}, cache => 1});
 }
 
 sub flag_types {
-    my $self = shift;
-
-    if (!defined $self->{'flag_types'}) {
-        my $flagtypes = Bugzilla::FlagType::match({ product_id   => $self->product_id,
-                                                    component_id => $self->id });
-
-        $self->{'flag_types'} = {};
-        $self->{'flag_types'}->{'bug'} =
-          [grep { $_->target_type eq 'bug' } @$flagtypes];
-        $self->{'flag_types'}->{'attachment'} =
-          [grep { $_->target_type eq 'attachment' } @$flagtypes];
-    }
-    return $self->{'flag_types'};
+  my $self = shift;
+
+  if (!defined $self->{'flag_types'}) {
+    my $flagtypes = Bugzilla::FlagType::match(
+      {product_id => $self->product_id, component_id => $self->id});
+
+    $self->{'flag_types'} = {};
+    $self->{'flag_types'}->{'bug'}
+      = [grep { $_->target_type eq 'bug' } @$flagtypes];
+    $self->{'flag_types'}->{'attachment'}
+      = [grep { $_->target_type eq 'attachment' } @$flagtypes];
+  }
+  return $self->{'flag_types'};
 }
 
 sub initial_cc {
-    my $self = shift;
-    my $dbh = Bugzilla->dbh;
-
-    if (!defined $self->{'initial_cc'}) {
-        # If set_cc_list() has been called but data are not yet written
-        # into the DB, we want the new values defined by it.
-        my $cc_ids = $self->{cc_ids}
-                     || $dbh->selectcol_arrayref('SELECT user_id FROM component_cc
-                                                  WHERE component_id = ?',
-                                                  undef, $self->id);
-
-        $self->{'initial_cc'} = Bugzilla::User->new_from_list($cc_ids);
-    }
-    return $self->{'initial_cc'};
+  my $self = shift;
+  my $dbh  = Bugzilla->dbh;
+
+  if (!defined $self->{'initial_cc'}) {
+
+    # If set_cc_list() has been called but data are not yet written
+    # into the DB, we want the new values defined by it.
+    my $cc_ids = $self->{cc_ids} || $dbh->selectcol_arrayref(
+      'SELECT user_id FROM component_cc
+                                                  WHERE component_id = ?', undef,
+      $self->id
+    );
+
+    $self->{'initial_cc'} = Bugzilla::User->new_from_list($cc_ids);
+  }
+  return $self->{'initial_cc'};
 }
 
 sub product {
-    my $self = shift;
+  my $self = shift;
 
-    require Bugzilla::Product;
-    $self->{'product'} ||= Bugzilla::Product->new({ id => $self->product_id, cache => 1 });
-    return $self->{'product'};
+  require Bugzilla::Product;
+  $self->{'product'}
+    ||= Bugzilla::Product->new({id => $self->product_id, cache => 1});
+  return $self->{'product'};
 }
 
 ###############################
@@ -404,8 +423,8 @@ sub product {
 ###############################
 
 sub description { return $_[0]->{'description'}; }
-sub product_id  { return $_[0]->{'product_id'};  }
-sub is_active   { return $_[0]->{'isactive'};    }
+sub product_id  { return $_[0]->{'product_id'}; }
+sub is_active   { return $_[0]->{'isactive'}; }
 
 ##############################################
 # Implement Bugzilla::Field::ChoiceInterface #
@@ -415,11 +434,11 @@ use constant FIELD_NAME => 'component';
 use constant is_default => 0;
 
 sub is_set_on_bug {
-    my ($self, $bug) = @_;
-    my $value = blessed($bug) ? $bug->component_id : $bug->{component};
-    $value = $value->id if blessed($value);
-    return 0 unless $value;
-    return $value == $self->id ? 1 : 0;
+  my ($self, $bug) = @_;
+  my $value = blessed($bug) ? $bug->component_id : $bug->{component};
+  $value = $value->id if blessed($value);
+  return 0 unless $value;
+  return $value == $self->id ? 1 : 0;
 }
 
 ###############################
index 0f1324080592ba1520cac47f40df31ce04c2e92e..378b7bd9f3757572015fdcb1dada3cd589e60165 100644 (file)
@@ -26,324 +26,332 @@ use File::Basename;
 # Don't export localvars by default - people should have to explicitly
 # ask for it, as a (probably futile) attempt to stop code using it
 # when it shouldn't
-%Bugzilla::Config::EXPORT_TAGS =
-  (
-   admin => [qw(update_params
-                SetParam
-                call_param_onchange_handlers
-                write_params)],
-  );
+%Bugzilla::Config::EXPORT_TAGS = (
+  admin => [
+    qw(update_params
+      SetParam
+      call_param_onchange_handlers
+      write_params)
+  ],
+);
 Exporter::export_ok_tags('admin');
 
 # new installs get these set of defaults (unless overridden by the answers file)
-my %NEW_INSTALL_DEFAULT = (
-    or_groups          => 1,
-    use_email_as_login => 0,
-);
+my %NEW_INSTALL_DEFAULT = (or_groups => 1, use_email_as_login => 0,);
 
 # INITIALISATION CODE
 # Perl throws a warning if we use bz_locations() directly after do.
 our %params;
+
 # Load in the param definitions
 sub _load_params {
-    my $panels = param_panels();
-    my %hook_panels;
-    foreach my $panel (keys %$panels) {
-        my $module = $panels->{$panel};
-        eval("require $module") || die $@;
-        my @new_param_list = $module->get_param_list();
-        $hook_panels{lc($panel)} = { params => \@new_param_list };
-    }
-    # This hook is also called in editparams.cgi. This call here is required
-    # to make SetParam work.
-    Bugzilla::Hook::process('config_modify_panels', 
-                            { panels => \%hook_panels });
-
-    foreach my $panel (keys %hook_panels) {
-        foreach my $item (@{$hook_panels{$panel}->{params}}) {
-            $params{$item->{'name'}} = $item;
-        }
+  my $panels = param_panels();
+  my %hook_panels;
+  foreach my $panel (keys %$panels) {
+    my $module = $panels->{$panel};
+    eval("require $module") || die $@;
+    my @new_param_list = $module->get_param_list();
+    $hook_panels{lc($panel)} = {params => \@new_param_list};
+  }
+
+  # This hook is also called in editparams.cgi. This call here is required
+  # to make SetParam work.
+  Bugzilla::Hook::process('config_modify_panels', {panels => \%hook_panels});
+
+  foreach my $panel (keys %hook_panels) {
+    foreach my $item (@{$hook_panels{$panel}->{params}}) {
+      $params{$item->{'name'}} = $item;
     }
+  }
 }
+
 # END INIT CODE
 
 # Subroutines go here
 
 sub param_panels {
-    my $param_panels = {};
-    my $libpath = bz_locations()->{'libpath'};
-    foreach my $item ((glob "$libpath/Bugzilla/Config/*.pm")) {
-        $item =~ m#/([^/]+)\.pm$#;
-        my $module = $1;
-        $param_panels->{$module} = "Bugzilla::Config::$module" unless $module eq 'Common';
-    }
-    # Now check for any hooked params
-    Bugzilla::Hook::process('config_add_panels', 
-                            { panel_modules => $param_panels });
-    return $param_panels;
+  my $param_panels = {};
+  my $libpath      = bz_locations()->{'libpath'};
+  foreach my $item ((glob "$libpath/Bugzilla/Config/*.pm")) {
+    $item =~ m#/([^/]+)\.pm$#;
+    my $module = $1;
+    $param_panels->{$module} = "Bugzilla::Config::$module"
+      unless $module eq 'Common';
+  }
+
+  # Now check for any hooked params
+  Bugzilla::Hook::process('config_add_panels', {panel_modules => $param_panels});
+  return $param_panels;
 }
 
 sub SetParam {
-    my ($name, $value) = @_;
+  my ($name, $value) = @_;
 
-    _load_params() unless %params;
-    die "Unknown param $name" unless (exists $params{$name});
+  _load_params() unless %params;
+  die "Unknown param $name" unless (exists $params{$name});
 
-    my $entry = $params{$name};
+  my $entry = $params{$name};
 
-    # sanity check the value
+  # sanity check the value
 
-    # XXX - This runs the checks. Which would be good, except that
-    # check_shadowdb creates the database as a side effect, and so the
-    # checker fails the second time around...
-    if ($name ne 'shadowdb' && exists $entry->{'checker'}) {
-        my $err = $entry->{'checker'}->($value, $entry);
-        die "Param $name is not valid: $err" unless $err eq '';
-    }
+  # XXX - This runs the checks. Which would be good, except that
+  # check_shadowdb creates the database as a side effect, and so the
+  # checker fails the second time around...
+  if ($name ne 'shadowdb' && exists $entry->{'checker'}) {
+    my $err = $entry->{'checker'}->($value, $entry);
+    die "Param $name is not valid: $err" unless $err eq '';
+  }
 
-    Bugzilla->params->{$name} = $value;
+  Bugzilla->params->{$name} = $value;
 }
 
 sub call_param_onchange_handlers {
-    my ($changes) = @_;
+  my ($changes) = @_;
 
-    _load_params() unless %params;
+  _load_params() unless %params;
 
-    foreach my $name (@$changes) {
-        my $param = $params{$name};
-        if (exists $param->{'onchange'}) {
-            $param->{'onchange'}->(Bugzilla->params->{$name});
-        }
+  foreach my $name (@$changes) {
+    my $param = $params{$name};
+    if (exists $param->{'onchange'}) {
+      $param->{'onchange'}->(Bugzilla->params->{$name});
     }
+  }
 }
 
 sub update_params {
-    my ($params) = @_;
-    my $answer = Bugzilla->installation_answers;
-    my $datadir = bz_locations()->{'datadir'};
-    my $param;
-
-    # If the old data/params file using Data::Dumper output still exists,
-    # read it. It will be deleted once the parameters are stored in the new
-    # data/params.json file.
-    my $old_file = "$datadir/params";
-
-    if (-e $old_file) {
-        require Safe;
-        my $s = new Safe;
-
-        $s->rdo($old_file);
-        die "Error reading $old_file: $!" if $!;
-        die "Error evaluating $old_file: $@" if $@;
-
-        # Now read the param back out from the sandbox.
-        $param = \%{ $s->varglob('param') };
+  my ($params) = @_;
+  my $answer   = Bugzilla->installation_answers;
+  my $datadir  = bz_locations()->{'datadir'};
+  my $param;
+
+  # If the old data/params file using Data::Dumper output still exists,
+  # read it. It will be deleted once the parameters are stored in the new
+  # data/params.json file.
+  my $old_file = "$datadir/params";
+
+  if (-e $old_file) {
+    require Safe;
+    my $s = new Safe;
+
+    $s->rdo($old_file);
+    die "Error reading $old_file: $!"    if $!;
+    die "Error evaluating $old_file: $@" if $@;
+
+    # Now read the param back out from the sandbox.
+    $param = \%{$s->varglob('param')};
+  }
+  else {
+    # Rename params.js to params.json if checksetup.pl
+    # was executed with an earlier version of this change
+    rename "$old_file.js", "$old_file.json"
+      if -e "$old_file.js" && !-e "$old_file.json";
+
+    # Read the new data/params.json file.
+    $param = read_param_file();
+  }
+
+  my %new_params;
+
+  # If we didn't return any param values, then this is a new installation.
+  my $new_install = !(keys %$param);
+
+  # --- UPDATE OLD PARAMS ---
+
+  # Change from usebrowserinfo to defaultplatform/defaultopsys combo
+  if (exists $param->{'usebrowserinfo'}) {
+    if (!$param->{'usebrowserinfo'}) {
+      if (!exists $param->{'defaultplatform'}) {
+        $new_params{'defaultplatform'} = 'Other';
+      }
+      if (!exists $param->{'defaultopsys'}) {
+        $new_params{'defaultopsys'} = 'Other';
+      }
     }
-    else {
-        # Rename params.js to params.json if checksetup.pl
-        # was executed with an earlier version of this change
-        rename "$old_file.js", "$old_file.json"
-            if -e "$old_file.js" && !-e "$old_file.json";
-
-        # Read the new data/params.json file.
-        $param = read_param_file();
+  }
+
+  # Change from a boolean for quips to multi-state
+  if (exists $param->{'usequip'} && !exists $param->{'enablequips'}) {
+    $new_params{'enablequips'} = $param->{'usequip'} ? 'on' : 'off';
+  }
+
+  # Modularise auth code
+  if (exists $param->{'useLDAP'} && !exists $param->{'loginmethod'}) {
+    $new_params{'loginmethod'} = $param->{'useLDAP'} ? "LDAP" : "DB";
+  }
+
+  # set verify method to whatever loginmethod was
+  if (exists $param->{'loginmethod'} && !exists $param->{'user_verify_class'}) {
+    $new_params{'user_verify_class'} = $param->{'loginmethod'};
+  }
+
+  # Remove quip-display control from parameters
+  # and give it to users via User Settings (Bug 41972)
+  if (exists $param->{'enablequips'}
+    && !exists $param->{'quip_list_entry_control'})
+  {
+    my $new_value;
+    ($param->{'enablequips'} eq 'on')       && do { $new_value = 'open'; };
+    ($param->{'enablequips'} eq 'approved') && do { $new_value = 'moderated'; };
+    ($param->{'enablequips'} eq 'frozen')   && do { $new_value = 'closed'; };
+    ($param->{'enablequips'} eq 'off')      && do { $new_value = 'closed'; };
+    $new_params{'quip_list_entry_control'} = $new_value;
+  }
+
+  # Old mail_delivery_method choices contained no uppercase characters
+  my $mta = $param->{'mail_delivery_method'};
+  if ($mta) {
+    if ($mta !~ /[A-Z]/) {
+      my %translation = (
+        'sendmail' => 'Sendmail',
+        'smtp'     => 'SMTP',
+        'qmail'    => 'Qmail',
+        'testfile' => 'Test',
+        'none'     => 'None'
+      );
+      $param->{'mail_delivery_method'} = $translation{$mta};
     }
 
-    my %new_params;
-
-    # If we didn't return any param values, then this is a new installation.
-    my $new_install = !(keys %$param);
-
-    # --- UPDATE OLD PARAMS ---
-
-    # Change from usebrowserinfo to defaultplatform/defaultopsys combo
-    if (exists $param->{'usebrowserinfo'}) {
-        if (!$param->{'usebrowserinfo'}) {
-            if (!exists $param->{'defaultplatform'}) {
-                $new_params{'defaultplatform'} = 'Other';
-            }
-            if (!exists $param->{'defaultopsys'}) {
-                $new_params{'defaultopsys'} = 'Other';
-            }
-        }
+    # This will force the parameter to be reset to its default value.
+    delete $param->{'mail_delivery_method'}
+      if $param->{'mail_delivery_method'} eq 'Qmail';
+  }
+
+  # Convert the old "ssl" parameter to the new "ssl_redirect" parameter.
+  # Both "authenticated sessions" and "always" turn on "ssl_redirect"
+  # when upgrading.
+  if (exists $param->{'ssl'} and $param->{'ssl'} ne 'never') {
+    $new_params{'ssl_redirect'} = 1;
+  }
+
+# "specific_search_allow_empty_words" has been renamed to "search_allow_no_criteria".
+  if (exists $param->{'specific_search_allow_empty_words'}) {
+    $new_params{'search_allow_no_criteria'}
+      = $param->{'specific_search_allow_empty_words'};
+  }
+
+  if (exists $param->{'noresolveonopenblockers'}) {
+    $new_params{'resolution_forbidden_with_open_blockers'}
+      = $param->{'noresolveonopenblockers'} ? 'FIXED' : "";
+  }
+
+  # --- DEFAULTS FOR NEW PARAMS ---
+
+  _load_params() unless %params;
+  foreach my $name (keys %params) {
+    my $item = $params{$name};
+    unless (exists $param->{$name}) {
+      print "New parameter: $name\n" unless $new_install;
+      if (exists $new_params{$name}) {
+        $param->{$name} = $new_params{$name};
+      }
+      elsif (exists $answer->{$name}) {
+        $param->{$name} = $answer->{$name};
+      }
+      elsif ($new_install and exists $NEW_INSTALL_DEFAULT{$name}) {
+        $param->{$name} = $NEW_INSTALL_DEFAULT{$name};
+      }
+      else {
+        $param->{$name} = $item->{'default'};
+      }
     }
+  }
 
-    # Change from a boolean for quips to multi-state
-    if (exists $param->{'usequip'} && !exists $param->{'enablequips'}) {
-        $new_params{'enablequips'} = $param->{'usequip'} ? 'on' : 'off';
-    }
+  # --- REMOVE OLD PARAMS ---
 
-    # Modularise auth code
-    if (exists $param->{'useLDAP'} && !exists $param->{'loginmethod'}) {
-        $new_params{'loginmethod'} = $param->{'useLDAP'} ? "LDAP" : "DB";
-    }
+  my %oldparams;
 
-    # set verify method to whatever loginmethod was
-    if (exists $param->{'loginmethod'} 
-        && !exists $param->{'user_verify_class'}) 
-    {
-        $new_params{'user_verify_class'} = $param->{'loginmethod'};
+  # Remove any old params
+  foreach my $item (keys %$param) {
+    if (!exists $params{$item}) {
+      $oldparams{$item} = delete $param->{$item};
     }
-
-    # Remove quip-display control from parameters
-    # and give it to users via User Settings (Bug 41972)
-    if ( exists $param->{'enablequips'} 
-         && !exists $param->{'quip_list_entry_control'}) 
-    {
-        my $new_value;
-        ($param->{'enablequips'} eq 'on')       && do {$new_value = 'open';};
-        ($param->{'enablequips'} eq 'approved') && do {$new_value = 'moderated';};
-        ($param->{'enablequips'} eq 'frozen')   && do {$new_value = 'closed';};
-        ($param->{'enablequips'} eq 'off')      && do {$new_value = 'closed';};
-        $new_params{'quip_list_entry_control'} = $new_value;
+  }
+
+  # Write any old parameters to old-params.txt
+  my $old_param_file = "$datadir/old-params.txt";
+  if (scalar(keys %oldparams)) {
+    my $op_file = new IO::File($old_param_file, '>>', 0600)
+      || die "Couldn't create $old_param_file: $!";
+
+    print "The following parameters are no longer used in Bugzilla,",
+      " and so have been\nmoved from your parameters file into",
+      " $old_param_file:\n";
+
+    my $comma = "";
+    foreach my $item (keys %oldparams) {
+      print $op_file "\n\n$item:\n" . $oldparams{$item} . "\n";
+      print "${comma}$item";
+      $comma = ", ";
     }
+    print "\n";
+    $op_file->close;
+  }
 
-    # Old mail_delivery_method choices contained no uppercase characters
-    my $mta = $param->{'mail_delivery_method'};
-    if ($mta) {
-        if ($mta !~ /[A-Z]/) {
-            my %translation = (
-                'sendmail' => 'Sendmail',
-                'smtp'     => 'SMTP',
-                'qmail'    => 'Qmail',
-                'testfile' => 'Test',
-                'none'     => 'None');
-            $param->{'mail_delivery_method'} = $translation{$mta};
-        }
-        # This will force the parameter to be reset to its default value.
-        delete $param->{'mail_delivery_method'} if $param->{'mail_delivery_method'} eq 'Qmail';
-    }
+  write_params($param);
 
-    # Convert the old "ssl" parameter to the new "ssl_redirect" parameter.
-    # Both "authenticated sessions" and "always" turn on "ssl_redirect"
-    # when upgrading.
-    if (exists $param->{'ssl'} and $param->{'ssl'} ne 'never') {
-        $new_params{'ssl_redirect'} = 1;
-    }
-
-    # "specific_search_allow_empty_words" has been renamed to "search_allow_no_criteria".
-    if (exists $param->{'specific_search_allow_empty_words'}) {
-        $new_params{'search_allow_no_criteria'} = $param->{'specific_search_allow_empty_words'};
-    }
-
-    if (exists $param->{'noresolveonopenblockers'}) {
-        $new_params{'resolution_forbidden_with_open_blockers'} = $param->{'noresolveonopenblockers'} ? 'FIXED' : "";
-    }
-
-    # --- DEFAULTS FOR NEW PARAMS ---
-
-    _load_params() unless %params;
-    foreach my $name (keys %params) {
-        my $item = $params{$name};
-        unless (exists $param->{$name}) {
-            print "New parameter: $name\n" unless $new_install;
-            if (exists $new_params{$name}) {
-                $param->{$name} = $new_params{$name};
-            }
-            elsif (exists $answer->{$name}) {
-                $param->{$name} = $answer->{$name};
-            }
-            elsif ($new_install and exists $NEW_INSTALL_DEFAULT{$name}) {
-                $param->{$name} = $NEW_INSTALL_DEFAULT{$name};
-            }
-            else {
-                $param->{$name} = $item->{'default'};
-            }
-        }
-    }
-
-    # --- REMOVE OLD PARAMS ---
-
-    my %oldparams;
-    # Remove any old params
-    foreach my $item (keys %$param) {
-        if (!exists $params{$item}) {
-            $oldparams{$item} = delete $param->{$item};
-        }
-    }
-
-    # Write any old parameters to old-params.txt
-    my $old_param_file = "$datadir/old-params.txt";
-    if (scalar(keys %oldparams)) {
-        my $op_file = new IO::File($old_param_file, '>>', 0600)
-          || die "Couldn't create $old_param_file: $!";
-
-        print "The following parameters are no longer used in Bugzilla,",
-              " and so have been\nmoved from your parameters file into",
-              " $old_param_file:\n";
-
-        my $comma = "";
-        foreach my $item (keys %oldparams) {
-            print $op_file "\n\n$item:\n" . $oldparams{$item} . "\n";
-            print "${comma}$item";
-            $comma = ", ";
-        }
-        print "\n";
-        $op_file->close;
-    }
-
-    write_params($param);
-
-    if (-e $old_file) {
-        unlink $old_file;
-        say "$old_file has been converted into $old_file.json, using the JSON format.";
-    }
+  if (-e $old_file) {
+    unlink $old_file;
+    say "$old_file has been converted into $old_file.json, using the JSON format.";
+  }
 
-    # Return deleted params and values so that checksetup.pl has a chance
-    # to convert old params to new data.
-    return %oldparams;
+  # Return deleted params and values so that checksetup.pl has a chance
+  # to convert old params to new data.
+  return %oldparams;
 }
 
 sub write_params {
-    my ($param_data) = @_;
-    $param_data ||= Bugzilla->params;
-    my $param_file = bz_locations()->{'datadir'} . '/params.json';
+  my ($param_data) = @_;
+  $param_data ||= Bugzilla->params;
+  my $param_file = bz_locations()->{'datadir'} . '/params.json';
 
-    my $json_data = JSON::XS->new->canonical->pretty->encode($param_data);
-    write_text($param_file, $json_data);
+  my $json_data = JSON::XS->new->canonical->pretty->encode($param_data);
+  write_text($param_file, $json_data);
 
-    # It's not common to edit parameters and loading
-    # Bugzilla::Install::Filesystem is slow.
-    require Bugzilla::Install::Filesystem;
-    Bugzilla::Install::Filesystem::fix_file_permissions($param_file);
+  # It's not common to edit parameters and loading
+  # Bugzilla::Install::Filesystem is slow.
+  require Bugzilla::Install::Filesystem;
+  Bugzilla::Install::Filesystem::fix_file_permissions($param_file);
 
-    # And now we have to reset the params cache so that Bugzilla will re-read
-    # them.
-    delete Bugzilla->request_cache->{params};
+  # And now we have to reset the params cache so that Bugzilla will re-read
+  # them.
+  delete Bugzilla->request_cache->{params};
 }
 
 sub read_param_file {
-    my $params;
-    my $file = bz_locations()->{'datadir'} . '/params.json';
-
-    if (-e $file) {
-        my $data = read_text($file);
-        trick_taint($data);
-
-        # If params.json has been manually edited and e.g. some quotes are
-        # missing, we don't want JSON::XS to leak the content of the file
-        # to all users in its error message, so we have to eval'uate it.
-        $params = eval { JSON::XS->new->decode($data) };
-        if ($@) {
-            my $error_msg = (basename($0) eq 'checksetup.pl') ?
-                $@ : 'run checksetup.pl to see the details.';
-            die "Error parsing $file: $error_msg";
-        }
+  my $params;
+  my $file = bz_locations()->{'datadir'} . '/params.json';
+
+  if (-e $file) {
+    my $data = read_text($file);
+    trick_taint($data);
+
+    # If params.json has been manually edited and e.g. some quotes are
+    # missing, we don't want JSON::XS to leak the content of the file
+    # to all users in its error message, so we have to eval'uate it.
+    $params = eval { JSON::XS->new->decode($data) };
+    if ($@) {
+      my $error_msg
+        = (basename($0) eq 'checksetup.pl')
+        ? $@
+        : 'run checksetup.pl to see the details.';
+      die "Error parsing $file: $error_msg";
     }
-    elsif ($ENV{'SERVER_SOFTWARE'}) {
-        # We're in a CGI, but the params file doesn't exist. We can't
-        # Template Toolkit, or even install_string, since checksetup
-        # might not have thrown an error. Bugzilla::CGI->new
-        # hasn't even been called yet, so we manually use CGI::Carp here
-        # so that the user sees the error.
-        unless (i_am_persistent()) {
-            require CGI::Carp;
-            CGI::Carp->import('fatalsToBrowser');
-        }
-        die "The $file file does not exist."
-            . ' You probably need to run checksetup.pl.',
+  }
+  elsif ($ENV{'SERVER_SOFTWARE'}) {
+
+    # We're in a CGI, but the params file doesn't exist. We can't
+    # Template Toolkit, or even install_string, since checksetup
+    # might not have thrown an error. Bugzilla::CGI->new
+    # hasn't even been called yet, so we manually use CGI::Carp here
+    # so that the user sees the error.
+    unless (i_am_persistent()) {
+      require CGI::Carp;
+      CGI::Carp->import('fatalsToBrowser');
     }
-    return $params // {};
+    die "The $file file does not exist."
+      . ' You probably need to run checksetup.pl.',;
+  }
+  return $params // {};
 }
 
 1;
index c21ae5310b45807c0581962a4ab571150f75f155..44173366f5310fc5e2a94af287e74a36226f34f2 100644 (file)
@@ -16,32 +16,21 @@ use Bugzilla::Config::Common;
 our $sortkey = 200;
 
 sub get_param_list {
-  my $class = shift;
+  my $class      = shift;
   my @param_list = (
-  {
-   name => 'allowbugdeletion',
-   type => 'b',
-   default => 0
-  },
-
-  {
-   name => 'allowemailchange',
-   type => 'b',
-   default => 1
-  },
-
-  {
-   name => 'allowuserdeletion',
-   type => 'b',
-   default => 0
-  },
-
-  {
-   name => 'last_visit_keep_days',
-   type => 't',
-   default => 10,
-   checker => \&check_numeric
-  });
+    {name => 'allowbugdeletion', type => 'b', default => 0},
+
+    {name => 'allowemailchange', type => 'b', default => 1},
+
+    {name => 'allowuserdeletion', type => 'b', default => 0},
+
+    {
+      name    => 'last_visit_keep_days',
+      type    => 't',
+      default => 10,
+      checker => \&check_numeric
+    }
+  );
   return @param_list;
 }
 
index 7a61bf7c47b78526d82684bd3bb18d63e9d3cf72..5ef16568f48eb7be69362e5a70e39e91d24cb695 100644 (file)
@@ -17,36 +17,32 @@ our $sortkey = 1700;
 
 use constant get_param_list => (
   {
-   name => 'inbound_proxies',
-   type => 't',
-   default => '',
-   checker => \&check_inbound_proxies
+    name    => 'inbound_proxies',
+    type    => 't',
+    default => '',
+    checker => \&check_inbound_proxies
   },
 
-  {
-   name => 'proxy_url',
-   type => 't',
-   default => ''
-  },
+  {name => 'proxy_url', type => 't', default => ''},
 
   {
-   name => 'strict_transport_security',
-   type => 's',
-   choices => ['off', 'this_domain_only', 'include_subdomains'],
-   default => 'off',
-   checker => \&check_multi
+    name    => 'strict_transport_security',
+    type    => 's',
+    choices => ['off', 'this_domain_only', 'include_subdomains'],
+    default => 'off',
+    checker => \&check_multi
   },
 );
 
 sub check_inbound_proxies {
-    my $inbound_proxies = shift;
-
-    return "" if $inbound_proxies eq "*";
-    my @proxies = split(/[\s,]+/, $inbound_proxies);
-    foreach my $proxy (@proxies) {
-        validate_ip($proxy) || return "$proxy is not a valid IPv4 or IPv6 address";
-    }
-    return "";
+  my $inbound_proxies = shift;
+
+  return "" if $inbound_proxies eq "*";
+  my @proxies = split(/[\s,]+/, $inbound_proxies);
+  foreach my $proxy (@proxies) {
+    validate_ip($proxy) || return "$proxy is not a valid IPv4 or IPv6 address";
+  }
+  return "";
 }
 
 1;
index daa844ba59ed4a80f13ffdc5d68fcec2d335a6ca..a37b2b151999ac72dcd93f9cc68a72ab1591531a 100644 (file)
@@ -16,56 +16,49 @@ use Bugzilla::Config::Common;
 our $sortkey = 400;
 
 sub get_param_list {
-  my $class = shift;
+  my $class      = shift;
   my @param_list = (
-  {
-   name => 'allow_attachment_display',
-   type => 'b',
-   default => 0
-  },
+    {name => 'allow_attachment_display', type => 'b', default => 0},
 
-  {
-   name => 'attachment_base',
-   type => 't',
-   default => '',
-   checker => \&check_urlbase
-  },
+    {
+      name    => 'attachment_base',
+      type    => 't',
+      default => '',
+      checker => \&check_urlbase
+    },
 
-  {
-  name => 'allow_attachment_deletion',
-  type => 'b',
-  default => 0
-  },
+    {name => 'allow_attachment_deletion', type => 'b', default => 0},
 
-  {
-  name => 'xsendfile_header',
-  type => 's',
-  choices => ['off', 'X-Sendfile', 'X-Accel-Redirect', 'X-LIGHTTPD-send-file'],
-  default => 'off',
-  checker => \&check_multi
-  },
+    {
+      name    => 'xsendfile_header',
+      type    => 's',
+      choices => ['off', 'X-Sendfile', 'X-Accel-Redirect', 'X-LIGHTTPD-send-file'],
+      default => 'off',
+      checker => \&check_multi
+    },
 
-  {
-   name => 'maxattachmentsize',
-   type => 't',
-   default => '1000',
-   checker => \&check_maxattachmentsize
-  },
+    {
+      name    => 'maxattachmentsize',
+      type    => 't',
+      default => '1000',
+      checker => \&check_maxattachmentsize
+    },
 
-  # The maximum size (in bytes) for patches and non-patch attachments.
-  # The default limit is 1000KB, which is 24KB less than mysql's default
-  # maximum packet size (which determines how much data can be sent in a
-  # single mysql packet and thus how much data can be inserted into the
-  # database) to provide breathing space for the data in other fields of
-  # the attachment record as well as any mysql packet overhead (I don't
-  # know of any, but I suspect there may be some.)
+    # The maximum size (in bytes) for patches and non-patch attachments.
+    # The default limit is 1000KB, which is 24KB less than mysql's default
+    # maximum packet size (which determines how much data can be sent in a
+    # single mysql packet and thus how much data can be inserted into the
+    # database) to provide breathing space for the data in other fields of
+    # the attachment record as well as any mysql packet overhead (I don't
+    # know of any, but I suspect there may be some.)
 
-  {
-   name => 'maxlocalattachment',
-   type => 't',
-   default => '0',
-   checker => \&check_numeric
-  } );
+    {
+      name    => 'maxlocalattachment',
+      type    => 't',
+      default => '0',
+      checker => \&check_numeric
+    }
+  );
   return @param_list;
 }
 
index 326c4cd3f6a7683135b3564183a98280c946e0d2..d559e3b247f544f0774351c43bd410a8a994f8f5 100644 (file)
@@ -16,111 +16,89 @@ use Bugzilla::Config::Common;
 our $sortkey = 300;
 
 sub get_param_list {
-  my $class = shift;
+  my $class      = shift;
   my @param_list = (
-  {
-   name => 'auth_env_id',
-   type    => 't',
-   default => '',
-  },
-
-  {
-   name    => 'auth_env_email',
-   type    => 't',
-   default => '',
-  },
-
-  {
-   name    => 'auth_env_realname',
-   type    => 't',
-   default => '',
-  },
-
-  # XXX in the future:
-  #
-  # user_verify_class and user_info_class should have choices gathered from
-  # whatever sits in their respective directories
-  #
-  # rather than comma-separated lists, these two should eventually become
-  # arrays, but that requires alterations to editparams first
-
-  {
-   name => 'user_info_class',
-   type => 's',
-   choices => [ 'CGI', 'Env', 'Env,CGI' ],
-   default => 'CGI',
-   checker => \&check_multi
-  },
-
-  {
-   name => 'user_verify_class',
-   type => 'o',
-   choices => [ 'DB', 'RADIUS', 'LDAP' ],
-   default => 'DB',
-   checker => \&check_user_verify_class
-  },
-
-  {
-   name => 'rememberlogin',
-   type => 's',
-   choices => ['on', 'defaulton', 'defaultoff', 'off'],
-   default => 'on',
-   checker => \&check_multi
-  },
-
-  {
-   name => 'requirelogin',
-   type => 'b',
-   default => '0'
-  },
-
-  {
-   name => 'emailregexp',
-   type => 't',
-   default => q:^[\\w\\.\\+\\-=']+@[\\w\\.\\-]+\\.[\\w\\-]+$:,
-   checker => \&check_regexp
-  },
-
-  {
-   name => 'emailregexpdesc',
-   type => 'l',
-   default => 'A legal address must contain exactly one \'@\', and at least ' .
-              'one \'.\' after the @.'
-  },
-
-  {
-   name => 'use_email_as_login',
-   type => 'b',
-   default => '1',
-   onchange => \&change_use_email_as_login
-  },
-
-  {
-   name => 'createemailregexp',
-   type => 't',
-   default => q:.*:,
-   checker => \&check_regexp
-  },
-
-  {
-   name => 'password_complexity',
-   type => 's',
-   choices => [ 'no_constraints', 'mixed_letters', 'letters_numbers',
-                'letters_numbers_specialchars' ],
-   default => 'no_constraints',
-   checker => \&check_multi
-  },
-
-  {
-   name => 'password_check_on_login',
-   type => 'b',
-   default => '1'
-  },
-  {
-   name => 'auth_delegation',
-   type => 'b',
-   default => 0,
-  },
+    {name => 'auth_env_id', type => 't', default => '',},
+
+    {name => 'auth_env_email', type => 't', default => '',},
+
+    {name => 'auth_env_realname', type => 't', default => '',},
+
+    # XXX in the future:
+    #
+    # user_verify_class and user_info_class should have choices gathered from
+    # whatever sits in their respective directories
+    #
+    # rather than comma-separated lists, these two should eventually become
+    # arrays, but that requires alterations to editparams first
+
+    {
+      name    => 'user_info_class',
+      type    => 's',
+      choices => ['CGI', 'Env', 'Env,CGI'],
+      default => 'CGI',
+      checker => \&check_multi
+    },
+
+    {
+      name    => 'user_verify_class',
+      type    => 'o',
+      choices => ['DB', 'RADIUS', 'LDAP'],
+      default => 'DB',
+      checker => \&check_user_verify_class
+    },
+
+    {
+      name    => 'rememberlogin',
+      type    => 's',
+      choices => ['on', 'defaulton', 'defaultoff', 'off'],
+      default => 'on',
+      checker => \&check_multi
+    },
+
+    {name => 'requirelogin', type => 'b', default => '0'},
+
+    {
+      name    => 'emailregexp',
+      type    => 't',
+      default => q:^[\\w\\.\\+\\-=']+@[\\w\\.\\-]+\\.[\\w\\-]+$:,
+      checker => \&check_regexp
+    },
+
+    {
+      name    => 'emailregexpdesc',
+      type    => 'l',
+      default => 'A legal address must contain exactly one \'@\', and at least '
+        . 'one \'.\' after the @.'
+    },
+
+    {
+      name     => 'use_email_as_login',
+      type     => 'b',
+      default  => '1',
+      onchange => \&change_use_email_as_login
+    },
+
+    {
+      name    => 'createemailregexp',
+      type    => 't',
+      default => q:.*:,
+      checker => \&check_regexp
+    },
+
+    {
+      name    => 'password_complexity',
+      type    => 's',
+      choices => [
+        'no_constraints',  'mixed_letters',
+        'letters_numbers', 'letters_numbers_specialchars'
+      ],
+      default => 'no_constraints',
+      checker => \&check_multi
+    },
+
+    {name => 'password_check_on_login', type => 'b', default => '1'},
+    {name => 'auth_delegation',         type => 'b', default => 0,},
   );
   return @param_list;
 }
index b6cd7ae43688b6c0d20420ec80c44dfe53742ac7..0028cf95f6b34765fb879c4f786bf65c2052bc72 100644 (file)
@@ -27,60 +27,47 @@ sub get_param_list {
   # and bug_status.is_open is not yet defined (hence the eval), so we use
   # the bug statuses above as they are still hardcoded.
   eval {
-      my @current_closed_states = map {$_->name} closed_bug_statuses();
-      # If no closed state was found, use the default list above.
-      @closed_bug_statuses = @current_closed_states if scalar(@current_closed_states);
+    my @current_closed_states = map { $_->name } closed_bug_statuses();
+
+    # If no closed state was found, use the default list above.
+    @closed_bug_statuses = @current_closed_states if scalar(@current_closed_states);
   };
 
   my @param_list = (
-  {
-   name => 'duplicate_or_move_bug_status',
-   type => 's',
-   choices => \@closed_bug_statuses,
-   default => $closed_bug_statuses[0],
-   checker => \&check_bug_status,
-   onchange => \&change_duplicate_or_move_bug_status
-  },
-
-  {
-   name => 'letsubmitterchoosepriority',
-   type => 'b',
-   default => 1
-  },
-
-  {
-   name => 'letsubmitterchoosemilestone',
-   type => 'b',
-   default => 1
-  },
-
-  {
-   name => 'commentonchange_resolution',
-   type => 'b',
-   default => 0
-  },
-
-  {
-   name => 'commentonduplicate',
-   type => 'b',
-   default => 0
-  },
-
-  {
-   name => 'resolution_forbidden_with_open_blockers',
-   type => 's',
-   choices => \&_get_resolutions,
-   default => '',
-   checker => \&check_resolution,
-  } );
+    {
+      name     => 'duplicate_or_move_bug_status',
+      type     => 's',
+      choices  => \@closed_bug_statuses,
+      default  => $closed_bug_statuses[0],
+      checker  => \&check_bug_status,
+      onchange => \&change_duplicate_or_move_bug_status
+    },
+
+    {name => 'letsubmitterchoosepriority', type => 'b', default => 1},
+
+    {name => 'letsubmitterchoosemilestone', type => 'b', default => 1},
+
+    {name => 'commentonchange_resolution', type => 'b', default => 0},
+
+    {name => 'commentonduplicate', type => 'b', default => 0},
+
+    {
+      name    => 'resolution_forbidden_with_open_blockers',
+      type    => 's',
+      choices => \&_get_resolutions,
+      default => '',
+      checker => \&check_resolution,
+    }
+  );
 
   return @param_list;
 }
 
 sub _get_resolutions {
-    my $resolution_field = Bugzilla::Field->new({ name => 'resolution', cache => 1 });
-    # The empty resolution is included - it represents "no value".
-    return [ map { $_->name } @{ $resolution_field->legal_values } ];
+  my $resolution_field = Bugzilla::Field->new({name => 'resolution', cache => 1});
+
+  # The empty resolution is included - it represents "no value".
+  return [map { $_->name } @{$resolution_field->legal_values}];
 }
 
 1;
index f6c20cd121df5e909fc60d41c8fb372f86f722c6..adc6fe7ccb136f81dfa78912fe09cc29e30637c5 100644 (file)
@@ -25,73 +25,50 @@ sub get_param_list {
   my @legal_OS         = @{get_legal_field_values('op_sys')};
 
   my @param_list = (
-  {
-   name => 'useclassification',
-   type => 'b',
-   default => 0
-  },
-
-  {
-   name => 'usetargetmilestone',
-   type => 'b',
-   default => 0
-  },
-
-  {
-   name => 'useqacontact',
-   type => 'b',
-   default => 0
-  },
-
-  {
-   name => 'usestatuswhiteboard',
-   type => 'b',
-   default => 0
-  },
-
-  {
-   name => 'use_see_also',
-   type => 'b',
-   default => 1
-  },
-
-  {
-   name => 'defaultpriority',
-   type => 's',
-   choices => \@legal_priorities,
-   default => $legal_priorities[-1],
-   checker => \&check_priority
-  },
-
-  {
-   name => 'defaultseverity',
-   type => 's',
-   choices => \@legal_severities,
-   default => $legal_severities[-1],
-   checker => \&check_severity
-  },
-
-  {
-   name => 'defaultplatform',
-   type => 's',
-   choices => ['', @legal_platforms],
-   default => '',
-   checker => \&check_platform
-  },
-
-  {
-   name => 'defaultopsys',
-   type => 's',
-   choices => ['', @legal_OS],
-   default => '',
-   checker => \&check_opsys
-  },
-
-  {
-   name => 'collapsed_comment_tags',
-   type => 't',
-   default => 'obsolete, spam',
-  });
+    {name => 'useclassification', type => 'b', default => 0},
+
+    {name => 'usetargetmilestone', type => 'b', default => 0},
+
+    {name => 'useqacontact', type => 'b', default => 0},
+
+    {name => 'usestatuswhiteboard', type => 'b', default => 0},
+
+    {name => 'use_see_also', type => 'b', default => 1},
+
+    {
+      name    => 'defaultpriority',
+      type    => 's',
+      choices => \@legal_priorities,
+      default => $legal_priorities[-1],
+      checker => \&check_priority
+    },
+
+    {
+      name    => 'defaultseverity',
+      type    => 's',
+      choices => \@legal_severities,
+      default => $legal_severities[-1],
+      checker => \&check_severity
+    },
+
+    {
+      name    => 'defaultplatform',
+      type    => 's',
+      choices => ['', @legal_platforms],
+      default => '',
+      checker => \&check_platform
+    },
+
+    {
+      name    => 'defaultopsys',
+      type    => 's',
+      choices => ['', @legal_OS],
+      default => '',
+      checker => \&check_opsys
+    },
+
+    {name => 'collapsed_comment_tags', type => 't', default => 'obsolete, spam',}
+  );
   return @param_list;
 }
 
index 633325291accdffb4b47231210ed364d773c150f..3f7e3a04619f6e4585d5fbf3c58f5d6c063f931d 100644 (file)
@@ -21,347 +21,357 @@ use Bugzilla::Group;
 use Bugzilla::Status;
 
 use parent qw(Exporter);
-@Bugzilla::Config::Common::EXPORT =
-    qw(check_multi check_numeric check_regexp check_group
-       check_sslbase check_priority check_severity check_platform
-       check_opsys check_shadowdb check_urlbase check_user_verify_class
-       check_ip check_mail_delivery_method check_notification
-       check_bug_status check_smtp_auth check_theschwartz_available
-       check_maxattachmentsize check_email check_smtp_ssl
-       check_comment_taggers_group check_smtp_server check_resolution
-
-       change_use_email_as_login change_duplicate_or_move_bug_status
+@Bugzilla::Config::Common::EXPORT
+  = qw(check_multi check_numeric check_regexp check_group
+  check_sslbase check_priority check_severity check_platform
+  check_opsys check_shadowdb check_urlbase check_user_verify_class
+  check_ip check_mail_delivery_method check_notification
+  check_bug_status check_smtp_auth check_theschwartz_available
+  check_maxattachmentsize check_email check_smtp_ssl
+  check_comment_taggers_group check_smtp_server check_resolution
+
+  change_use_email_as_login change_duplicate_or_move_bug_status
 );
 
 # Checking functions for the various values
 
 sub check_multi {
-    my ($value, $param) = (@_);
+  my ($value, $param) = (@_);
 
-    if ($param->{'type'} eq "s") {
-        unless (scalar(grep {$_ eq $value} (@{$param->{'choices'}}))) {
-            return "Invalid choice '$value' for single-select list param '$param->{'name'}'";
-        }
-
-        return "";
+  if ($param->{'type'} eq "s") {
+    unless (scalar(grep { $_ eq $value } (@{$param->{'choices'}}))) {
+      return
+        "Invalid choice '$value' for single-select list param '$param->{'name'}'";
     }
-    elsif ($param->{'type'} eq 'm' || $param->{'type'} eq 'o') {
-        if (ref($value) ne "ARRAY") {
-            $value = [split(',', $value)]
-        }
-        foreach my $chkParam (@$value) {
-            unless (scalar(grep {$_ eq $chkParam} (@{$param->{'choices'}}))) {
-                return "Invalid choice '$chkParam' for multi-select list param '$param->{'name'}'";
-            }
-        }
-
-        return "";
+
+    return "";
+  }
+  elsif ($param->{'type'} eq 'm' || $param->{'type'} eq 'o') {
+    if (ref($value) ne "ARRAY") {
+      $value = [split(',', $value)];
     }
-    else {
-        return "Invalid param type '$param->{'type'}' for check_multi(); " .
-          "contact your Bugzilla administrator";
+    foreach my $chkParam (@$value) {
+      unless (scalar(grep { $_ eq $chkParam } (@{$param->{'choices'}}))) {
+        return
+          "Invalid choice '$chkParam' for multi-select list param '$param->{'name'}'";
+      }
     }
+
+    return "";
+  }
+  else {
+    return "Invalid param type '$param->{'type'}' for check_multi(); "
+      . "contact your Bugzilla administrator";
+  }
 }
 
 sub check_numeric {
-    my ($value) = (@_);
-    if ($value !~ /^[0-9]+$/) {
-        return "must be a numeric value";
-    }
-    return "";
+  my ($value) = (@_);
+  if ($value !~ /^[0-9]+$/) {
+    return "must be a numeric value";
+  }
+  return "";
 }
 
 sub check_regexp {
-    my ($value) = (@_);
-    eval { qr/$value/ };
-    return $@;
+  my ($value) = (@_);
+  eval {qr/$value/};
+  return $@;
 }
 
 sub check_email {
-    my ($value) = @_;
-    if ($value !~ $Email::Address::mailbox) {
-        return "must be a valid email address.";
-    }
-    return "";
+  my ($value) = @_;
+  if ($value !~ $Email::Address::mailbox) {
+    return "must be a valid email address.";
+  }
+  return "";
 }
 
 sub check_sslbase {
-    my $url = shift;
-    if ($url ne '') {
-        if ($url !~ m#^https://([^/]+).*/$#) {
-            return "must be a legal URL, that starts with https and ends with a slash";
-        }
-        my $host = $1;
-        # Fall back to port 443 if for some reason getservbyname() fails.
-        my $port = getservbyname('https', 'tcp') || 443;
-        if ($host =~ /^(.+):(\d+)$/) {
-            $host = $1;
-            $port = $2;
-        }
-        local *SOCK;
-        my $proto = getprotobyname('tcp');
-        socket(SOCK, PF_INET, SOCK_STREAM, $proto);
-        my $iaddr = inet_aton($host) || return "The host $host cannot be resolved";
-        my $sin = sockaddr_in($port, $iaddr);
-        if (!connect(SOCK, $sin)) {
-            return "Failed to connect to $host:$port ($!); unable to enable SSL";
-        }
-        close(SOCK);
+  my $url = shift;
+  if ($url ne '') {
+    if ($url !~ m#^https://([^/]+).*/$#) {
+      return "must be a legal URL, that starts with https and ends with a slash";
     }
-    return "";
+    my $host = $1;
+
+    # Fall back to port 443 if for some reason getservbyname() fails.
+    my $port = getservbyname('https', 'tcp') || 443;
+    if ($host =~ /^(.+):(\d+)$/) {
+      $host = $1;
+      $port = $2;
+    }
+    local *SOCK;
+    my $proto = getprotobyname('tcp');
+    socket(SOCK, PF_INET, SOCK_STREAM, $proto);
+    my $iaddr = inet_aton($host) || return "The host $host cannot be resolved";
+    my $sin = sockaddr_in($port, $iaddr);
+    if (!connect(SOCK, $sin)) {
+      return "Failed to connect to $host:$port ($!); unable to enable SSL";
+    }
+    close(SOCK);
+  }
+  return "";
 }
 
 sub check_ip {
-    my $inbound_proxies = shift;
-    my @proxies = split(/[\s,]+/, $inbound_proxies);
-    foreach my $proxy (@proxies) {
-        validate_ip($proxy) || return "$proxy is not a valid IPv4 or IPv6 address";
-    }
-    return "";
+  my $inbound_proxies = shift;
+  my @proxies = split(/[\s,]+/, $inbound_proxies);
+  foreach my $proxy (@proxies) {
+    validate_ip($proxy) || return "$proxy is not a valid IPv4 or IPv6 address";
+  }
+  return "";
 }
 
 sub check_priority {
-    my ($value) = (@_);
-    my $legal_priorities = get_legal_field_values('priority');
-    if (!grep($_ eq $value, @$legal_priorities)) {
-        return "Must be a legal priority value: one of " .
-            join(", ", @$legal_priorities);
-    }
-    return "";
+  my ($value) = (@_);
+  my $legal_priorities = get_legal_field_values('priority');
+  if (!grep($_ eq $value, @$legal_priorities)) {
+    return "Must be a legal priority value: one of "
+      . join(", ", @$legal_priorities);
+  }
+  return "";
 }
 
 sub check_severity {
-    my ($value) = (@_);
-    my $legal_severities = get_legal_field_values('bug_severity');
-    if (!grep($_ eq $value, @$legal_severities)) {
-        return "Must be a legal severity value: one of " .
-            join(", ", @$legal_severities);
-    }
-    return "";
+  my ($value) = (@_);
+  my $legal_severities = get_legal_field_values('bug_severity');
+  if (!grep($_ eq $value, @$legal_severities)) {
+    return "Must be a legal severity value: one of "
+      . join(", ", @$legal_severities);
+  }
+  return "";
 }
 
 sub check_platform {
-    my ($value) = (@_);
-    my $legal_platforms = get_legal_field_values('rep_platform');
-    if (!grep($_ eq $value, '', @$legal_platforms)) {
-        return "Must be empty or a legal platform value: one of " .
-            join(", ", @$legal_platforms);
-    }
-    return "";
+  my ($value) = (@_);
+  my $legal_platforms = get_legal_field_values('rep_platform');
+  if (!grep($_ eq $value, '', @$legal_platforms)) {
+    return "Must be empty or a legal platform value: one of "
+      . join(", ", @$legal_platforms);
+  }
+  return "";
 }
 
 sub check_opsys {
-    my ($value) = (@_);
-    my $legal_OS = get_legal_field_values('op_sys');
-    if (!grep($_ eq $value, '', @$legal_OS)) {
-        return "Must be empty or a legal operating system value: one of " .
-            join(", ", @$legal_OS);
-    }
-    return "";
+  my ($value) = (@_);
+  my $legal_OS = get_legal_field_values('op_sys');
+  if (!grep($_ eq $value, '', @$legal_OS)) {
+    return "Must be empty or a legal operating system value: one of "
+      . join(", ", @$legal_OS);
+  }
+  return "";
 }
 
 sub check_bug_status {
-    my $bug_status = shift;
-    my @closed_bug_statuses = map {$_->name} closed_bug_statuses();
-    if (!grep($_ eq $bug_status, @closed_bug_statuses)) {
-        return "Must be a valid closed status: one of " . join(', ', @closed_bug_statuses);
-    }
-    return "";
+  my $bug_status = shift;
+  my @closed_bug_statuses = map { $_->name } closed_bug_statuses();
+  if (!grep($_ eq $bug_status, @closed_bug_statuses)) {
+    return "Must be a valid closed status: one of "
+      . join(', ', @closed_bug_statuses);
+  }
+  return "";
 }
 
 sub check_resolution {
-    my $resolution = shift;
-    my $resolution_field = Bugzilla::Field->new({ name => 'resolution', cache => 1 });
-    # The empty resolution is included - it represents "no value"
-    my @resolutions = map {$_->name} @{ $resolution_field->legal_values };
+  my $resolution = shift;
+  my $resolution_field = Bugzilla::Field->new({name => 'resolution', cache => 1});
 
-    if (!grep($_ eq $resolution, @resolutions)) {
-        return "Must be blank or a valid resolution: one of " . join(', ', @resolutions);
-    }
-    return "";
+  # The empty resolution is included - it represents "no value"
+  my @resolutions = map { $_->name } @{$resolution_field->legal_values};
+
+  if (!grep($_ eq $resolution, @resolutions)) {
+    return "Must be blank or a valid resolution: one of "
+      . join(', ', @resolutions);
+  }
+  return "";
 }
 
 sub check_group {
-    my $group_name = shift;
-    return "" unless $group_name;
-    my $group = new Bugzilla::Group({'name' => $group_name});
-    unless (defined $group) {
-        return "Must be an existing group name";
-    }
-    return "";
+  my $group_name = shift;
+  return "" unless $group_name;
+  my $group = new Bugzilla::Group({'name' => $group_name});
+  unless (defined $group) {
+    return "Must be an existing group name";
+  }
+  return "";
 }
 
 sub check_shadowdb {
-    my ($value) = (@_);
-    $value = trim($value);
-    if ($value eq "") {
-        return "";
-    }
+  my ($value) = (@_);
+  $value = trim($value);
+  if ($value eq "") {
+    return "";
+  }
 
-    if (!Bugzilla->params->{'shadowdbhost'}) {
-        return "You need to specify a host when using a shadow database";
-    }
+  if (!Bugzilla->params->{'shadowdbhost'}) {
+    return "You need to specify a host when using a shadow database";
+  }
 
-    # Can't test existence of this because ConnectToDatabase uses the param,
-    # but we can't set this before testing....
-    # This can really only be fixed after we can use the DBI more openly
-    return "";
+  # Can't test existence of this because ConnectToDatabase uses the param,
+  # but we can't set this before testing....
+  # This can really only be fixed after we can use the DBI more openly
+  return "";
 }
 
 sub check_urlbase {
-    my ($url) = (@_);
-    if ($url && $url !~ m:^http.*/$:) {
-        return "must be a legal URL, that starts with http and ends with a slash.";
-    }
-    return "";
+  my ($url) = (@_);
+  if ($url && $url !~ m:^http.*/$:) {
+    return "must be a legal URL, that starts with http and ends with a slash.";
+  }
+  return "";
 }
 
 sub check_user_verify_class {
-    # doeditparams traverses the list of params, and for each one it checks,
-    # then updates. This means that if one param checker wants to look at 
-    # other params, it must be below that other one. So you can't have two 
-    # params mutually dependent on each other.
-    # This means that if someone clears the LDAP config params after setting
-    # the login method as LDAP, we won't notice, but all logins will fail.
-    # So don't do that.
-
-    my $params = Bugzilla->params;
-    my ($list, $entry) = @_;
-    $list || return 'You need to specify at least one authentication mechanism';
-    for my $class (split /,\s*/, $list) {
-        my $res = check_multi($class, $entry);
-        return $res if $res;
-        if ($class eq 'RADIUS') {
-            if (!Bugzilla->feature('auth_radius')) {
-                return "RADIUS support is not available. Run checksetup.pl"
-                       . " for more details";
-            }
-            return "RADIUS servername (RADIUS_server) is missing"
-                if !$params->{"RADIUS_server"};
-            return "RADIUS_secret is empty" if !$params->{"RADIUS_secret"};
-        }
-        elsif ($class eq 'LDAP') {
-            if (!Bugzilla->feature('auth_ldap')) {
-                return "LDAP support is not available. Run checksetup.pl"
-                       . " for more details";
-            }
-            return "LDAP servername (LDAPserver) is missing" 
-                if !$params->{"LDAPserver"};
-            return "LDAPBaseDN is empty" if !$params->{"LDAPBaseDN"};
-        }
+
+  # doeditparams traverses the list of params, and for each one it checks,
+  # then updates. This means that if one param checker wants to look at
+  # other params, it must be below that other one. So you can't have two
+  # params mutually dependent on each other.
+  # This means that if someone clears the LDAP config params after setting
+  # the login method as LDAP, we won't notice, but all logins will fail.
+  # So don't do that.
+
+  my $params = Bugzilla->params;
+  my ($list, $entry) = @_;
+  $list || return 'You need to specify at least one authentication mechanism';
+  for my $class (split /,\s*/, $list) {
+    my $res = check_multi($class, $entry);
+    return $res if $res;
+    if ($class eq 'RADIUS') {
+      if (!Bugzilla->feature('auth_radius')) {
+        return "RADIUS support is not available. Run checksetup.pl"
+          . " for more details";
+      }
+      return "RADIUS servername (RADIUS_server) is missing"
+        if !$params->{"RADIUS_server"};
+      return "RADIUS_secret is empty" if !$params->{"RADIUS_secret"};
     }
-    return "";
+    elsif ($class eq 'LDAP') {
+      if (!Bugzilla->feature('auth_ldap')) {
+        return "LDAP support is not available. Run checksetup.pl" . " for more details";
+      }
+      return "LDAP servername (LDAPserver) is missing" if !$params->{"LDAPserver"};
+      return "LDAPBaseDN is empty"                     if !$params->{"LDAPBaseDN"};
+    }
+  }
+  return "";
 }
 
 sub check_mail_delivery_method {
-    my $check = check_multi(@_);
-    return $check if $check;
-    my $mailer = shift;
-    if ($mailer eq 'Sendmail' and ON_WINDOWS) {
-        # look for sendmail.exe 
-        return "Failed to locate " . SENDMAIL_EXE
-            unless -e SENDMAIL_EXE;
-    }
-    return "";
+  my $check = check_multi(@_);
+  return $check if $check;
+  my $mailer = shift;
+  if ($mailer eq 'Sendmail' and ON_WINDOWS) {
+
+    # look for sendmail.exe
+    return "Failed to locate " . SENDMAIL_EXE unless -e SENDMAIL_EXE;
+  }
+  return "";
 }
 
 sub check_maxattachmentsize {
-    my $check = check_numeric(@_);
-    return $check if $check;
-    my $size = shift;
-    my $dbh = Bugzilla->dbh;
-    if ($dbh->isa('Bugzilla::DB::Mysql')) {
-        my (undef, $max_packet) = $dbh->selectrow_array(
-            q{SHOW VARIABLES LIKE 'max\_allowed\_packet'});
-        my $byte_size = $size * 1024;
-        if ($max_packet < $byte_size) {
-            return "You asked for a maxattachmentsize of $byte_size bytes,"
-                   . " but the max_allowed_packet setting in MySQL currently"
-                   . " only allows packets up to $max_packet bytes";
-        }
+  my $check = check_numeric(@_);
+  return $check if $check;
+  my $size = shift;
+  my $dbh  = Bugzilla->dbh;
+  if ($dbh->isa('Bugzilla::DB::Mysql')) {
+    my (undef, $max_packet)
+      = $dbh->selectrow_array(q{SHOW VARIABLES LIKE 'max\_allowed\_packet'});
+    my $byte_size = $size * 1024;
+    if ($max_packet < $byte_size) {
+      return
+          "You asked for a maxattachmentsize of $byte_size bytes,"
+        . " but the max_allowed_packet setting in MySQL currently"
+        . " only allows packets up to $max_packet bytes";
     }
-    return "";
+  }
+  return "";
 }
 
 sub check_notification {
-    my $option = shift;
-    my @current_version =
-        (BUGZILLA_VERSION =~ m/^(\d+)\.(\d+)(?:(rc|\.)(\d+))?\+?$/);
-    if ($current_version[1] % 2 && $option eq 'stable_branch_release') {
-        return "You are currently running a development snapshot, and so your " .
-               "installation is not based on a branch. If you want to be notified " .
-               "about the next stable release, you should select " .
-               "'latest_stable_release' instead";
-    }
-    if ($option ne 'disabled' && !Bugzilla->feature('updates')) {
-        return "Some Perl modules are missing to get notifications about " .
-               "new releases. See the output of checksetup.pl for more information";
-    }
-    return "";
+  my $option = shift;
+  my @current_version
+    = (BUGZILLA_VERSION =~ m/^(\d+)\.(\d+)(?:(rc|\.)(\d+))?\+?$/);
+  if ($current_version[1] % 2 && $option eq 'stable_branch_release') {
+    return
+        "You are currently running a development snapshot, and so your "
+      . "installation is not based on a branch. If you want to be notified "
+      . "about the next stable release, you should select "
+      . "'latest_stable_release' instead";
+  }
+  if ($option ne 'disabled' && !Bugzilla->feature('updates')) {
+    return "Some Perl modules are missing to get notifications about "
+      . "new releases. See the output of checksetup.pl for more information";
+  }
+  return "";
 }
 
 sub check_smtp_server {
-    my $host = shift;
-    my $port;
+  my $host = shift;
+  my $port;
 
-    return '' unless $host;
+  return '' unless $host;
 
-    if ($host =~ /:/) {
-        ($host, $port) = split(/:/, $host, 2);
-        unless ($port && detaint_natural($port)) {
-            return "Invalid port. It must be an integer (typically 25, 465 or 587)";
-        }
+  if ($host =~ /:/) {
+    ($host, $port) = split(/:/, $host, 2);
+    unless ($port && detaint_natural($port)) {
+      return "Invalid port. It must be an integer (typically 25, 465 or 587)";
     }
-    trick_taint($host);
-    # Let's first try to connect using SSL. If this fails, we fall back to
-    # an unencrypted connection.
-    foreach my $method (['Net::SMTP::SSL', 465], ['Net::SMTP', 25]) {
-        my ($class, $default_port) = @$method;
-        next if $class eq 'Net::SMTP::SSL' && !Bugzilla->feature('smtp_ssl');
-        eval "require $class";
-        my $smtp = $class->new($host, Port => $port || $default_port, Timeout => 5);
-        if ($smtp) {
-            # The connection works!
-            $smtp->quit;
-            return '';
-        }
+  }
+  trick_taint($host);
+
+  # Let's first try to connect using SSL. If this fails, we fall back to
+  # an unencrypted connection.
+  foreach my $method (['Net::SMTP::SSL', 465], ['Net::SMTP', 25]) {
+    my ($class, $default_port) = @$method;
+    next if $class eq 'Net::SMTP::SSL' && !Bugzilla->feature('smtp_ssl');
+    eval "require $class";
+    my $smtp = $class->new($host, Port => $port || $default_port, Timeout => 5);
+    if ($smtp) {
+
+      # The connection works!
+      $smtp->quit;
+      return '';
     }
-    return "Cannot connect to $host" . ($port ? " using port $port" : "");
+  }
+  return "Cannot connect to $host" . ($port ? " using port $port" : "");
 }
 
 sub check_smtp_auth {
-    my $username = shift;
-    if ($username and !Bugzilla->feature('smtp_auth')) {
-        return "SMTP Authentication is not available. Run checksetup.pl for"
-               . " more details";
-    }
-    return "";
+  my $username = shift;
+  if ($username and !Bugzilla->feature('smtp_auth')) {
+    return "SMTP Authentication is not available. Run checksetup.pl for"
+      . " more details";
+  }
+  return "";
 }
 
 sub check_smtp_ssl {
-    my $use_ssl = shift;
-    if ($use_ssl && !Bugzilla->feature('smtp_ssl')) {
-        return "SSL support is not available. Run checksetup.pl for more details";
-    }
-    return "";
+  my $use_ssl = shift;
+  if ($use_ssl && !Bugzilla->feature('smtp_ssl')) {
+    return "SSL support is not available. Run checksetup.pl for more details";
+  }
+  return "";
 }
 
 sub check_theschwartz_available {
-    my $use_queue = shift;
-    if ($use_queue && !Bugzilla->feature('jobqueue')) {
-        return "Using the job queue requires that you have certain Perl"
-               . " modules installed. See the output of checksetup.pl"
-               . " for more information";
-    }
-    return "";
+  my $use_queue = shift;
+  if ($use_queue && !Bugzilla->feature('jobqueue')) {
+    return
+        "Using the job queue requires that you have certain Perl"
+      . " modules installed. See the output of checksetup.pl"
+      . " for more information";
+  }
+  return "";
 }
 
 sub check_comment_taggers_group {
-    my $group_name = shift;
-    if ($group_name && !Bugzilla->feature('jsonrpc')) {
-        return "Comment tagging requires installation of the JSONRPC feature";
-    }
-    return check_group($group_name);
+  my $group_name = shift;
+  if ($group_name && !Bugzilla->feature('jsonrpc')) {
+    return "Comment tagging requires installation of the JSONRPC feature";
+  }
+  return check_group($group_name);
 }
 
 # Change handler functions for various parameters
@@ -369,15 +379,15 @@ sub check_comment_taggers_group {
 # If use_email_as_login is turned on, update all login names to be email
 # addresses.
 sub change_use_email_as_login {
-    my $newvalue = shift;
-    if ($newvalue) {
-        Bugzilla->dbh->do('UPDATE profiles SET login_name = email');
-    }
+  my $newvalue = shift;
+  if ($newvalue) {
+    Bugzilla->dbh->do('UPDATE profiles SET login_name = email');
+  }
 }
 
 sub change_duplicate_or_move_bug_status {
-    my $newvalue = shift;
-    Bugzilla::Status::add_missing_bug_status_transitions($newvalue);
+  my $newvalue = shift;
+  Bugzilla::Status::add_missing_bug_status_transitions($newvalue);
 }
 
 # OK, here are the parameter definitions themselves.
@@ -435,7 +445,7 @@ sub change_duplicate_or_move_bug_status {
 #       }
 #
 #      Here, 'b' is the default option, and 'a' and 'c' are other possible
-#      options, but only one at a time! 
+#      options, but only one at a time!
 #
 #      &check_multi should always be used as the param verification function
 #      for list (single and multiple) parameter types.
@@ -448,7 +458,7 @@ sub change_duplicate_or_move_bug_status {
 #      For instance: default => 'a,c'.
 
 sub get_param_list {
-    return;
+  return;
 }
 
 1;
index c9a291d4c41550be28dbaabd5d4f4207dca43abb..96d763c5db294f29fab42cbcc6d084b6d4d280b6 100644 (file)
@@ -16,25 +16,11 @@ use Bugzilla::Config::Common;
 our $sortkey = 100;
 
 use constant get_param_list => (
-  {
-   name => 'urlbase',
-   type => 't',
-   default => '',
-   checker => \&check_urlbase
-  },
-
-  {
-   name => 'ssl_redirect',
-   type => 'b',
-   default => 0
-  },
-
-  {
-   name => 'sslbase',
-   type => 't',
-   default => '',
-   checker => \&check_sslbase
-  },
+  {name => 'urlbase', type => 't', default => '', checker => \&check_urlbase},
+
+  {name => 'ssl_redirect', type => 'b', default => 0},
+
+  {name => 'sslbase', type => 't', default => '', checker => \&check_sslbase},
 );
 
 1;
index 1a03995f33cf97c655856662d8ea4cf393944208..673806b331280bbf156ddf3d8ab9620541a34a19 100644 (file)
@@ -17,32 +17,26 @@ our $sortkey = 150;
 
 use constant get_param_list => (
   {
-   name => 'maintainer',
-   type => 't',
-   no_reset => '1',
-   default => '',
-   checker => \&check_email
+    name     => 'maintainer',
+    type     => 't',
+    no_reset => '1',
+    default  => '',
+    checker  => \&check_email
   },
 
-  {
-   name => 'shutdownhtml',
-   type => 'l',
-   default => ''
-  },
+  {name => 'shutdownhtml', type => 'l', default => ''},
 
-  {
-   name => 'announcehtml',
-   type => 'l',
-   default => ''
-  },
+  {name => 'announcehtml', type => 'l', default => ''},
 
   {
-   name => 'upgrade_notification',
-   type => 's',
-   choices => ['development_snapshot', 'latest_stable_release',
-               'stable_branch_release', 'disabled'],
-   default => 'latest_stable_release',
-   checker => \&check_notification
+    name    => 'upgrade_notification',
+    type    => 's',
+    choices => [
+      'development_snapshot',  'latest_stable_release',
+      'stable_branch_release', 'disabled'
+    ],
+    default => 'latest_stable_release',
+    checker => \&check_notification
   },
 );
 
index ba81a468e3678ea927cb03826818fc9f254bde93..0b05d23b2544907a484f786ec1f2d916c31a7a2d 100644 (file)
@@ -20,86 +20,75 @@ sub get_param_list {
   my $class = shift;
 
   my @param_list = (
-  {
-   name => 'chartgroup',
-   type => 's',
-   choices => \&_get_all_group_names,
-   default => 'editbugs',
-   checker => \&check_group
-  },
-
-  {
-   name => 'insidergroup',
-   type => 's',
-   choices => \&_get_all_group_names,
-   default => '',
-   checker => \&check_group
-  },
-
-  {
-   name => 'timetrackinggroup',
-   type => 's',
-   choices => \&_get_all_group_names,
-   default => 'editbugs',
-   checker => \&check_group
-  },
-
-  {
-   name => 'querysharegroup',
-   type => 's',
-   choices => \&_get_all_group_names,
-   default => 'editbugs',
-   checker => \&check_group
-  },
-
-  {
-   name => 'comment_taggers_group',
-   type => 's',
-   choices => \&_get_all_group_names,
-   default => 'editbugs',
-   checker => \&check_comment_taggers_group
-  },
-
-  {
-   name => 'minor_update_group',
-   type => 's',
-   choices => \&_get_all_group_names,
-   default => '',
-   checker => \&check_group
-  },
-
-  {
-   name => 'debug_group',
-   type => 's',
-   choices => \&_get_all_group_names,
-   default => 'admin',
-   checker => \&check_group
-  },
-  
-  {
-   name => 'usevisibilitygroups',
-   type => 'b',
-   default => 0
-  }, 
-  
-  {
-   name => 'strict_isolation',
-   type => 'b',
-   default => 0
-  },
-
-  {
-   name => 'or_groups',
-   type => 'b',
-   default => 0
-  } );
+    {
+      name    => 'chartgroup',
+      type    => 's',
+      choices => \&_get_all_group_names,
+      default => 'editbugs',
+      checker => \&check_group
+    },
+
+    {
+      name    => 'insidergroup',
+      type    => 's',
+      choices => \&_get_all_group_names,
+      default => '',
+      checker => \&check_group
+    },
+
+    {
+      name    => 'timetrackinggroup',
+      type    => 's',
+      choices => \&_get_all_group_names,
+      default => 'editbugs',
+      checker => \&check_group
+    },
+
+    {
+      name    => 'querysharegroup',
+      type    => 's',
+      choices => \&_get_all_group_names,
+      default => 'editbugs',
+      checker => \&check_group
+    },
+
+    {
+      name    => 'comment_taggers_group',
+      type    => 's',
+      choices => \&_get_all_group_names,
+      default => 'editbugs',
+      checker => \&check_comment_taggers_group
+    },
+
+    {
+      name    => 'minor_update_group',
+      type    => 's',
+      choices => \&_get_all_group_names,
+      default => '',
+      checker => \&check_group
+    },
+
+    {
+      name    => 'debug_group',
+      type    => 's',
+      choices => \&_get_all_group_names,
+      default => 'admin',
+      checker => \&check_group
+    },
+
+    {name => 'usevisibilitygroups', type => 'b', default => 0},
+
+    {name => 'strict_isolation', type => 'b', default => 0},
+
+    {name => 'or_groups', type => 'b', default => 0}
+  );
   return @param_list;
 }
 
 sub _get_all_group_names {
-    my @group_names = map {$_->name} Bugzilla::Group->get_all;
-    unshift(@group_names, '');
-    return \@group_names;
+  my @group_names = map { $_->name } Bugzilla::Group->get_all;
+  unshift(@group_names, '');
+  return \@group_names;
 }
 
 1;
index 1970b5791e87fb636dca29ead2a7bdbb6057c0a7..1aac80dc84b3fa0d0b0cdbe9571a67cfc859d9a8 100644 (file)
@@ -16,49 +16,22 @@ use Bugzilla::Config::Common;
 our $sortkey = 1000;
 
 sub get_param_list {
-  my $class = shift;
+  my $class      = shift;
   my @param_list = (
-  {
-   name => 'LDAPserver',
-   type => 't',
-   default => ''
-  },
+    {name => 'LDAPserver', type => 't', default => ''},
 
-  {
-   name => 'LDAPstarttls',
-   type => 'b',
-   default => 0
-  },
+    {name => 'LDAPstarttls', type => 'b', default => 0},
 
-  {
-   name => 'LDAPbinddn',
-   type => 't',
-   default => ''
-  },
+    {name => 'LDAPbinddn', type => 't', default => ''},
 
-  {
-   name => 'LDAPBaseDN',
-   type => 't',
-   default => ''
-  },
+    {name => 'LDAPBaseDN', type => 't', default => ''},
 
-  {
-   name => 'LDAPuidattribute',
-   type => 't',
-   default => 'uid'
-  },
+    {name => 'LDAPuidattribute', type => 't', default => 'uid'},
 
-  {
-   name => 'LDAPmailattribute',
-   type => 't',
-   default => 'mail'
-  },
+    {name => 'LDAPmailattribute', type => 't', default => 'mail'},
 
-  {
-   name => 'LDAPfilter',
-   type => 't',
-   default => '',
-  } );
+    {name => 'LDAPfilter', type => 't', default => '',}
+  );
   return @param_list;
 }
 
index 37f61aa20d2f12c508fbeffbbb1aa4bfa7a1fa0d..bb85fdc966eb6d00fb662c1c21d75d7c1e19a218 100644 (file)
@@ -16,68 +16,43 @@ use Bugzilla::Config::Common;
 our $sortkey = 1200;
 
 sub get_param_list {
-  my $class = shift;
+  my $class      = shift;
   my @param_list = (
-  {
-   name => 'mail_delivery_method',
-   type => 's',
-   choices => ['Sendmail', 'SMTP', 'Test', 'None'],
-   default => 'Sendmail',
-   checker => \&check_mail_delivery_method
-  },
-
-  {
-   name => 'mailfrom',
-   type => 't',
-   default => 'bugzilla-daemon'
-  },
-
-  {
-   name => 'use_mailer_queue',
-   type => 'b',
-   default => 0,
-   checker => \&check_theschwartz_available,
-  },
-
-  {
-   name => 'smtpserver',
-   type => 't',
-   default => 'localhost',
-   checker => \&check_smtp_server
-  },
-  {
-   name => 'smtp_username',
-   type => 't',
-   default => '',
-   checker => \&check_smtp_auth
-  },
-  {
-   name => 'smtp_password',
-   type => 'p',
-   default => ''
-  },
-  {
-   name => 'smtp_ssl',
-   type => 'b',
-   default => 0,
-   checker => \&check_smtp_ssl
-  },
-  {
-   name => 'smtp_debug',
-   type => 'b',
-   default => 0
-  },
-  {
-   name => 'whinedays',
-   type => 't',
-   default => 7,
-   checker => \&check_numeric
-  },
-  {
-   name => 'globalwatchers',
-   type => 't',
-   default => '',
-  }, );
+    {
+      name    => 'mail_delivery_method',
+      type    => 's',
+      choices => ['Sendmail', 'SMTP', 'Test', 'None'],
+      default => 'Sendmail',
+      checker => \&check_mail_delivery_method
+    },
+
+    {name => 'mailfrom', type => 't', default => 'bugzilla-daemon'},
+
+    {
+      name    => 'use_mailer_queue',
+      type    => 'b',
+      default => 0,
+      checker => \&check_theschwartz_available,
+    },
+
+    {
+      name    => 'smtpserver',
+      type    => 't',
+      default => 'localhost',
+      checker => \&check_smtp_server
+    },
+    {
+      name    => 'smtp_username',
+      type    => 't',
+      default => '',
+      checker => \&check_smtp_auth
+    },
+    {name => 'smtp_password', type => 'p', default => ''},
+    {name => 'smtp_ssl',   type => 'b', default => 0, checker => \&check_smtp_ssl},
+    {name => 'smtp_debug', type => 'b', default => 0},
+    {name => 'whinedays',  type => 't', default => 7, checker => \&check_numeric},
+    {name => 'globalwatchers', type => 't', default => '',},
+  );
   return @param_list;
 }
 
index e45c33da199246fff2901abf586450e750336a74..e3f80033043097d5cc909e4d9d6dd225fda8a64a 100644 (file)
@@ -17,16 +17,8 @@ our $sortkey = 1550;
 
 sub get_param_list {
   return (
-    {
-        name    => 'memcached_servers',
-        type    => 't',
-        default => ''
-    },
-    {
-        name    => 'memcached_namespace',
-        type    => 't',
-        default => 'bugzilla:',
-    },
+    {name => 'memcached_servers',   type => 't', default => ''},
+    {name => 'memcached_namespace', type => 't', default => 'bugzilla:',},
   );
 }
 
index 5e0d72e338d2f48584f7dc138469468401fd0f3e..346551df1193d326a065d18a1e7a1fed0a2e14e8 100644 (file)
@@ -16,47 +16,45 @@ use Bugzilla::Config::Common;
 our $sortkey = 1400;
 
 sub get_param_list {
-  my $class = shift;
+  my $class      = shift;
   my @param_list = (
-  {
-   name => 'quip_list_entry_control',
-   type => 's',
-   choices => ['open', 'moderated', 'closed'],
-   default => 'open',
-   checker => \&check_multi
-  },
-
-  {
-   name => 'mybugstemplate',
-   type => 't',
-   default => 'buglist.cgi?resolution=---&amp;emailassigned_to1=1&amp;emailreporter1=1&amp;emailtype1=exact&amp;email1=%userid%'
-  },
-
-  {
-   name => 'defaultquery',
-   type => 't',
-   default => 'resolution=---&emailassigned_to1=1&emailassigned_to2=1&emailreporter2=1&emailcc2=1&emailqa_contact2=1&emaillongdesc3=1&order=Importance&long_desc_type=substring'
-  },
-
-  {
-   name => 'search_allow_no_criteria',
-   type => 'b',
-   default => 1
-  },
-
-  {
-    name => 'default_search_limit',
-    type => 't',
-    default => '500',
-    checker => \&check_numeric
-  },
-
-  {
-    name => 'max_search_results',
-    type => 't',
-    default => '10000',
-    checker => \&check_numeric
-  },
+    {
+      name    => 'quip_list_entry_control',
+      type    => 's',
+      choices => ['open', 'moderated', 'closed'],
+      default => 'open',
+      checker => \&check_multi
+    },
+
+    {
+      name => 'mybugstemplate',
+      type => 't',
+      default =>
+        'buglist.cgi?resolution=---&amp;emailassigned_to1=1&amp;emailreporter1=1&amp;emailtype1=exact&amp;email1=%userid%'
+    },
+
+    {
+      name => 'defaultquery',
+      type => 't',
+      default =>
+        'resolution=---&emailassigned_to1=1&emailassigned_to2=1&emailreporter2=1&emailcc2=1&emailqa_contact2=1&emaillongdesc3=1&order=Importance&long_desc_type=substring'
+    },
+
+    {name => 'search_allow_no_criteria', type => 'b', default => 1},
+
+    {
+      name    => 'default_search_limit',
+      type    => 't',
+      default => '500',
+      checker => \&check_numeric
+    },
+
+    {
+      name    => 'max_search_results',
+      type    => 't',
+      default => '10000',
+      checker => \&check_numeric
+    },
   );
   return @param_list;
 }
index ddc8d95242869500d03bcfdabc1b86adac3db44b..f952785e94e89941446db0c565dc65f7ae1b84c0 100644 (file)
@@ -16,31 +16,15 @@ use Bugzilla::Config::Common;
 our $sortkey = 1100;
 
 sub get_param_list {
-  my $class = shift;
+  my $class      = shift;
   my @param_list = (
-  {
-   name =>    'RADIUS_server',
-   type =>    't',
-   default => ''
-  },
-
-  {
-   name =>    'RADIUS_secret',
-   type =>    't',
-   default => ''
-  },
-
-  {
-   name =>    'RADIUS_NAS_IP',
-   type =>    't',
-   default => ''
-  },
-
-  {
-   name =>    'RADIUS_email_suffix',
-   type =>    't',
-   default => ''
-  },
+    {name => 'RADIUS_server', type => 't', default => ''},
+
+    {name => 'RADIUS_secret', type => 't', default => ''},
+
+    {name => 'RADIUS_NAS_IP', type => 't', default => ''},
+
+    {name => 'RADIUS_email_suffix', type => 't', default => ''},
   );
   return @param_list;
 }
index 20212629cf0f91da30fdd53bb8defe67ee767aa5..3f0e0e02190e653172c513f14dae11bf813669a5 100644 (file)
@@ -16,35 +16,23 @@ use Bugzilla::Config::Common;
 our $sortkey = 1500;
 
 sub get_param_list {
-  my $class = shift;
+  my $class      = shift;
   my @param_list = (
-  {
-   name => 'shadowdbhost',
-   type => 't',
-   default => '',
-  },
-
-  {
-   name => 'shadowdbport',
-   type => 't',
-   default => '3306',
-   checker => \&check_numeric,
-  },
-
-  {
-   name => 'shadowdbsock',
-   type => 't',
-   default => '',
-  },
-
-  # This entry must be _after_ the shadowdb{host,port,sock} settings so that
-  # they can be used in the validation here
-  {
-   name => 'shadowdb',
-   type => 't',
-   default => '',
-   checker => \&check_shadowdb
-  } );
+    {name => 'shadowdbhost', type => 't', default => '',},
+
+    {
+      name    => 'shadowdbport',
+      type    => 't',
+      default => '3306',
+      checker => \&check_numeric,
+    },
+
+    {name => 'shadowdbsock', type => 't', default => '',},
+
+    # This entry must be _after_ the shadowdb{host,port,sock} settings so that
+    # they can be used in the validation here
+    {name => 'shadowdb', type => 't', default => '', checker => \&check_shadowdb}
+  );
   return @param_list;
 }
 
index c8b6f8dc38885e4d14d96e816f4ec3aa1b03369d..cbd8515fa651d1b95d1eee9019ef4dbd4486562a 100644 (file)
@@ -16,32 +16,21 @@ use Bugzilla::Config::Common;
 our $sortkey = 1600;
 
 sub get_param_list {
-  my $class = shift;
+  my $class      = shift;
   my @param_list = (
-  {
-   name => 'usemenuforusers',
-   type => 'b',
-   default => '0'
-  },
-
-  {
-   name    => 'ajax_user_autocompletion', 
-   type    => 'b', 
-   default => '1', 
-  },
-
-  {
-   name    => 'maxusermatches',
-   type    => 't',
-   default => '1000',
-   checker => \&check_numeric
-  },
-
-  {
-   name    => 'confirmuniqueusermatch',
-   type    => 'b',
-   default => 1,
-  } );
+    {name => 'usemenuforusers', type => 'b', default => '0'},
+
+    {name => 'ajax_user_autocompletion', type => 'b', default => '1',},
+
+    {
+      name    => 'maxusermatches',
+      type    => 't',
+      default => '1000',
+      checker => \&check_numeric
+    },
+
+    {name => 'confirmuniqueusermatch', type => 'b', default => 1,}
+  );
   return @param_list;
 }
 
index d7562f5081b7b20fc76499d37fff3a86ef080c7a..7d0a4c7eb2d599a59268990f5ba3df1ef803527a 100644 (file)
@@ -17,191 +17,191 @@ use parent qw(Exporter);
 use File::Basename;
 
 @Bugzilla::Constants::EXPORT = qw(
-    BUGZILLA_VERSION
-    REST_DOC
-
-    REMOTE_FILE
-    LOCAL_FILE
+  BUGZILLA_VERSION
+  REST_DOC
 
-    bz_locations
+  REMOTE_FILE
+  LOCAL_FILE
 
-    CAN_HAS_FEATURE
+  bz_locations
 
-    CONCATENATE_ASSETS
-
-    IS_NULL
-    NOT_NULL
-
-    CONTROLMAPNA
-    CONTROLMAPSHOWN
-    CONTROLMAPDEFAULT
-    CONTROLMAPMANDATORY
-
-    AUTH_OK
-    AUTH_NODATA
-    AUTH_ERROR
-    AUTH_LOGINFAILED
-    AUTH_DISABLED
-    AUTH_NO_SUCH_USER
-    AUTH_LOCKOUT
-
-    USER_PASSWORD_MIN_LENGTH
-
-    LOGIN_OPTIONAL
-    LOGIN_NORMAL
-    LOGIN_REQUIRED
-
-    LOGOUT_ALL
-    LOGOUT_CURRENT
-    LOGOUT_KEEP_CURRENT
-
-    GRANT_DIRECT
-    GRANT_REGEXP
-
-    GROUP_MEMBERSHIP
-    GROUP_BLESS
-    GROUP_VISIBLE
-
-    MAILTO_USER
-    MAILTO_GROUP
-
-    DEFAULT_COLUMN_LIST
-    DEFAULT_QUERY_NAME
-    DEFAULT_MILESTONE
-
-    SAVE_NUM_SEARCHES
-
-    COMMENT_COLS
-    MAX_COMMENT_LENGTH
-
-    MIN_COMMENT_TAG_LENGTH
-    MAX_COMMENT_TAG_LENGTH
-
-    CMT_NORMAL
-    CMT_DUPE_OF
-    CMT_HAS_DUPE
-    CMT_ATTACHMENT_CREATED
-    CMT_ATTACHMENT_UPDATED
-
-    THROW_ERROR
-    
-    RELATIONSHIPS
-    REL_ASSIGNEE REL_QA REL_REPORTER REL_CC REL_GLOBAL_WATCHER
-    REL_ANY
-    
-    POS_EVENTS
-    EVT_OTHER EVT_ADDED_REMOVED EVT_COMMENT EVT_ATTACHMENT EVT_ATTACHMENT_DATA
-    EVT_PROJ_MANAGEMENT EVT_OPENED_CLOSED EVT_KEYWORD EVT_CC EVT_DEPEND_BLOCK
-    EVT_BUG_CREATED EVT_COMPONENT
-
-    NEG_EVENTS
-    EVT_UNCONFIRMED EVT_CHANGED_BY_ME EVT_MINOR_UPDATE
-        
-    GLOBAL_EVENTS
-    EVT_FLAG_REQUESTED EVT_REQUESTED_FLAG
-
-    ADMIN_GROUP_NAME
-    PER_PRODUCT_PRIVILEGES
-
-    SENDMAIL_EXE
-    SENDMAIL_PATH
-
-    FIELD_TYPE_UNKNOWN
-    FIELD_TYPE_FREETEXT
-    FIELD_TYPE_SINGLE_SELECT
-    FIELD_TYPE_MULTI_SELECT
-    FIELD_TYPE_TEXTAREA
-    FIELD_TYPE_DATETIME
-    FIELD_TYPE_DATE
-    FIELD_TYPE_BUG_ID
-    FIELD_TYPE_BUG_URLS
-    FIELD_TYPE_KEYWORDS
-    FIELD_TYPE_INTEGER
-    FIELD_TYPE_HIGHEST_PLUS_ONE
-
-    EMPTY_DATETIME_REGEX
-
-    ABNORMAL_SELECTS
-
-    TIMETRACKING_FIELDS
-
-    USAGE_MODE_BROWSER
-    USAGE_MODE_CMDLINE
-    USAGE_MODE_XMLRPC
-    USAGE_MODE_EMAIL
-    USAGE_MODE_JSON
-    USAGE_MODE_TEST
-    USAGE_MODE_REST
-
-    ERROR_MODE_WEBPAGE
-    ERROR_MODE_DIE
-    ERROR_MODE_DIE_SOAP_FAULT
-    ERROR_MODE_JSON_RPC
-    ERROR_MODE_TEST
-    ERROR_MODE_REST
-
-    COLOR_ERROR
-    COLOR_SUCCESS
-
-    INSTALLATION_MODE_INTERACTIVE
-    INSTALLATION_MODE_NON_INTERACTIVE
-
-    DB_MODULE
-    ROOT_USER
-    ON_WINDOWS
-    ON_ACTIVESTATE
-
-    MAX_TOKEN_AGE
-    MAX_LOGINCOOKIE_AGE
-    MAX_SUDO_TOKEN_AGE
-    MAX_LOGIN_ATTEMPTS
-    LOGIN_LOCKOUT_INTERVAL
-    ACCOUNT_CHANGE_INTERVAL
-    MAX_STS_AGE
-
-    SAFE_PROTOCOLS
-    LEGAL_CONTENT_TYPES
-
-    MIN_SMALLINT
-    MAX_SMALLINT
-    MAX_INT_32
-
-    MAX_LEN_QUERY_NAME
-    MAX_CLASSIFICATION_SIZE
-    MAX_PRODUCT_SIZE
-    MAX_MILESTONE_SIZE
-    MAX_COMPONENT_SIZE
-    MAX_FIELD_VALUE_SIZE
-    MAX_FIELD_LONG_DESC_LENGTH
-    MAX_FREETEXT_LENGTH
-    MAX_BUG_URL_LENGTH
-    MAX_POSSIBLE_DUPLICATES
-    MAX_ATTACH_FILENAME_LENGTH
-    MAX_QUIP_LENGTH
-    MAX_WEBDOT_BUGS
-
-    PASSWORD_DIGEST_ALGORITHM
-    PASSWORD_SALT_LENGTH
-    
-    CGI_URI_LIMIT
-
-    PRIVILEGES_REQUIRED_NONE
-    PRIVILEGES_REQUIRED_REPORTER
-    PRIVILEGES_REQUIRED_ASSIGNEE
-    PRIVILEGES_REQUIRED_EMPOWERED
-
-    AUDIT_CREATE
-    AUDIT_REMOVE
-
-    MOST_FREQUENT_THRESHOLD
-
-    MARKDOWN_TAB_WIDTH
-
-    EMAIL_LIMIT_PER_MINUTE
-    EMAIL_LIMIT_PER_HOUR
-    EMAIL_LIMIT_EXCEPTION
-
-    JOB_QUEUE_VIEW_MAX_JOBS
+  CAN_HAS_FEATURE
+
+  CONCATENATE_ASSETS
+
+  IS_NULL
+  NOT_NULL
+
+  CONTROLMAPNA
+  CONTROLMAPSHOWN
+  CONTROLMAPDEFAULT
+  CONTROLMAPMANDATORY
+
+  AUTH_OK
+  AUTH_NODATA
+  AUTH_ERROR
+  AUTH_LOGINFAILED
+  AUTH_DISABLED
+  AUTH_NO_SUCH_USER
+  AUTH_LOCKOUT
+
+  USER_PASSWORD_MIN_LENGTH
+
+  LOGIN_OPTIONAL
+  LOGIN_NORMAL
+  LOGIN_REQUIRED
+
+  LOGOUT_ALL
+  LOGOUT_CURRENT
+  LOGOUT_KEEP_CURRENT
+
+  GRANT_DIRECT
+  GRANT_REGEXP
+
+  GROUP_MEMBERSHIP
+  GROUP_BLESS
+  GROUP_VISIBLE
+
+  MAILTO_USER
+  MAILTO_GROUP
+
+  DEFAULT_COLUMN_LIST
+  DEFAULT_QUERY_NAME
+  DEFAULT_MILESTONE
+
+  SAVE_NUM_SEARCHES
+
+  COMMENT_COLS
+  MAX_COMMENT_LENGTH
+
+  MIN_COMMENT_TAG_LENGTH
+  MAX_COMMENT_TAG_LENGTH
+
+  CMT_NORMAL
+  CMT_DUPE_OF
+  CMT_HAS_DUPE
+  CMT_ATTACHMENT_CREATED
+  CMT_ATTACHMENT_UPDATED
+
+  THROW_ERROR
+
+  RELATIONSHIPS
+  REL_ASSIGNEE REL_QA REL_REPORTER REL_CC REL_GLOBAL_WATCHER
+  REL_ANY
+
+  POS_EVENTS
+  EVT_OTHER EVT_ADDED_REMOVED EVT_COMMENT EVT_ATTACHMENT EVT_ATTACHMENT_DATA
+  EVT_PROJ_MANAGEMENT EVT_OPENED_CLOSED EVT_KEYWORD EVT_CC EVT_DEPEND_BLOCK
+  EVT_BUG_CREATED EVT_COMPONENT
+
+  NEG_EVENTS
+  EVT_UNCONFIRMED EVT_CHANGED_BY_ME EVT_MINOR_UPDATE
+
+  GLOBAL_EVENTS
+  EVT_FLAG_REQUESTED EVT_REQUESTED_FLAG
+
+  ADMIN_GROUP_NAME
+  PER_PRODUCT_PRIVILEGES
+
+  SENDMAIL_EXE
+  SENDMAIL_PATH
+
+  FIELD_TYPE_UNKNOWN
+  FIELD_TYPE_FREETEXT
+  FIELD_TYPE_SINGLE_SELECT
+  FIELD_TYPE_MULTI_SELECT
+  FIELD_TYPE_TEXTAREA
+  FIELD_TYPE_DATETIME
+  FIELD_TYPE_DATE
+  FIELD_TYPE_BUG_ID
+  FIELD_TYPE_BUG_URLS
+  FIELD_TYPE_KEYWORDS
+  FIELD_TYPE_INTEGER
+  FIELD_TYPE_HIGHEST_PLUS_ONE
+
+  EMPTY_DATETIME_REGEX
+
+  ABNORMAL_SELECTS
+
+  TIMETRACKING_FIELDS
+
+  USAGE_MODE_BROWSER
+  USAGE_MODE_CMDLINE
+  USAGE_MODE_XMLRPC
+  USAGE_MODE_EMAIL
+  USAGE_MODE_JSON
+  USAGE_MODE_TEST
+  USAGE_MODE_REST
+
+  ERROR_MODE_WEBPAGE
+  ERROR_MODE_DIE
+  ERROR_MODE_DIE_SOAP_FAULT
+  ERROR_MODE_JSON_RPC
+  ERROR_MODE_TEST
+  ERROR_MODE_REST
+
+  COLOR_ERROR
+  COLOR_SUCCESS
+
+  INSTALLATION_MODE_INTERACTIVE
+  INSTALLATION_MODE_NON_INTERACTIVE
+
+  DB_MODULE
+  ROOT_USER
+  ON_WINDOWS
+  ON_ACTIVESTATE
+
+  MAX_TOKEN_AGE
+  MAX_LOGINCOOKIE_AGE
+  MAX_SUDO_TOKEN_AGE
+  MAX_LOGIN_ATTEMPTS
+  LOGIN_LOCKOUT_INTERVAL
+  ACCOUNT_CHANGE_INTERVAL
+  MAX_STS_AGE
+
+  SAFE_PROTOCOLS
+  LEGAL_CONTENT_TYPES
+
+  MIN_SMALLINT
+  MAX_SMALLINT
+  MAX_INT_32
+
+  MAX_LEN_QUERY_NAME
+  MAX_CLASSIFICATION_SIZE
+  MAX_PRODUCT_SIZE
+  MAX_MILESTONE_SIZE
+  MAX_COMPONENT_SIZE
+  MAX_FIELD_VALUE_SIZE
+  MAX_FIELD_LONG_DESC_LENGTH
+  MAX_FREETEXT_LENGTH
+  MAX_BUG_URL_LENGTH
+  MAX_POSSIBLE_DUPLICATES
+  MAX_ATTACH_FILENAME_LENGTH
+  MAX_QUIP_LENGTH
+  MAX_WEBDOT_BUGS
+
+  PASSWORD_DIGEST_ALGORITHM
+  PASSWORD_SALT_LENGTH
+
+  CGI_URI_LIMIT
+
+  PRIVILEGES_REQUIRED_NONE
+  PRIVILEGES_REQUIRED_REPORTER
+  PRIVILEGES_REQUIRED_ASSIGNEE
+  PRIVILEGES_REQUIRED_EMPOWERED
+
+  AUDIT_CREATE
+  AUDIT_REMOVE
+
+  MOST_FREQUENT_THRESHOLD
+
+  MARKDOWN_TAB_WIDTH
+
+  EMAIL_LIMIT_PER_MINUTE
+  EMAIL_LIMIT_PER_HOUR
+  EMAIL_LIMIT_EXCEPTION
+
+  JOB_QUEUE_VIEW_MAX_JOBS
 );
 
 @Bugzilla::Constants::EXPORT_OK = qw(contenttypes);
@@ -217,23 +217,25 @@ use constant REST_DOC => 'https://bugzilla.readthedocs.org/en/latest/api/';
 
 # Location of the remote and local XML files to track new releases.
 use constant REMOTE_FILE => 'http://updates.bugzilla.org/bugzilla-update.xml';
-use constant LOCAL_FILE  => 'bugzilla-update.xml'; # Relative to datadir.
+use constant LOCAL_FILE => 'bugzilla-update.xml';    # Relative to datadir.
 
 use constant CAN_HAS_FEATURE => eval {
-    require CPAN::Meta::Prereqs;
-    require CPAN::Meta::Requirements;
-    require Module::Metadata;
-    require Module::Runtime;
-    CPAN::Meta::Prereqs->VERSION('2.132830');
-    CPAN::Meta::Requirements->VERSION('2.121');
-    Module::Metadata->VERSION('1.000019');
-    1;
+  require CPAN::Meta::Prereqs;
+  require CPAN::Meta::Requirements;
+  require Module::Metadata;
+  require Module::Runtime;
+  CPAN::Meta::Prereqs->VERSION('2.132830');
+  CPAN::Meta::Requirements->VERSION('2.121');
+  Module::Metadata->VERSION('1.000019');
+  1;
 };
 
 # When true CSS and JavaScript assets will be concatanted and minified at
 # run-time, to reduce the number of requests required to render a page.
 # Setting this to a false value can help debugging.
-use constant CONCATENATE_ASSETS => $ENV{PLACK_ENV} ? $ENV{PLACK_ENV} ne "development" : 1;
+use constant CONCATENATE_ASSETS => $ENV{PLACK_ENV}
+  ? $ENV{PLACK_ENV} ne "development"
+  : 1;
 
 # These are unique values that are unlikely to match a string or a number,
 # to be used in criteria for match() functions and other things. They start
@@ -249,9 +251,9 @@ use constant NOT_NULL => '  __NOT_NULL__  ';
 #
 # ControlMap constants for group_control_map.
 # membercontol:othercontrol => meaning
-# Na:Na               => Bugs in this product may not be restricted to this 
+# Na:Na               => Bugs in this product may not be restricted to this
 #                        group.
-# Shown:Na            => Members of the group may restrict bugs 
+# Shown:Na            => Members of the group may restrict bugs
 #                        in this product to this group.
 # Shown:Shown         => Members of the group may restrict bugs
 #                        in this product to this group.
@@ -273,46 +275,46 @@ use constant NOT_NULL => '  __NOT_NULL__  ';
 # Mandatory:Mandatory => Bug will be forced into this group regardless.
 # All other combinations are illegal.
 
-use constant CONTROLMAPNA => 0;
-use constant CONTROLMAPSHOWN => 1;
-use constant CONTROLMAPDEFAULT => 2;
+use constant CONTROLMAPNA        => 0;
+use constant CONTROLMAPSHOWN     => 1;
+use constant CONTROLMAPDEFAULT   => 2;
 use constant CONTROLMAPMANDATORY => 3;
 
 # See Bugzilla::Auth for docs on AUTH_*, LOGIN_* and LOGOUT_*
 
-use constant AUTH_OK => 0;
-use constant AUTH_NODATA => 1;
-use constant AUTH_ERROR => 2;
-use constant AUTH_LOGINFAILED => 3;
-use constant AUTH_DISABLED => 4;
-use constant AUTH_NO_SUCH_USER  => 5;
-use constant AUTH_LOCKOUT => 6;
+use constant AUTH_OK           => 0;
+use constant AUTH_NODATA       => 1;
+use constant AUTH_ERROR        => 2;
+use constant AUTH_LOGINFAILED  => 3;
+use constant AUTH_DISABLED     => 4;
+use constant AUTH_NO_SUCH_USER => 5;
+use constant AUTH_LOCKOUT      => 6;
 
 # The minimum length a password must have.
 use constant USER_PASSWORD_MIN_LENGTH => 6;
 
 use constant LOGIN_OPTIONAL => 0;
-use constant LOGIN_NORMAL => 1;
+use constant LOGIN_NORMAL   => 1;
 use constant LOGIN_REQUIRED => 2;
 
-use constant LOGOUT_ALL => 0;
-use constant LOGOUT_CURRENT => 1;
+use constant LOGOUT_ALL          => 0;
+use constant LOGOUT_CURRENT      => 1;
 use constant LOGOUT_KEEP_CURRENT => 2;
 
 use constant GRANT_DIRECT => 0;
 use constant GRANT_REGEXP => 2;
 
 use constant GROUP_MEMBERSHIP => 0;
-use constant GROUP_BLESS => 1;
-use constant GROUP_VISIBLE => 2;
+use constant GROUP_BLESS      => 1;
+use constant GROUP_VISIBLE    => 2;
 
-use constant MAILTO_USER => 0;
+use constant MAILTO_USER  => 0;
 use constant MAILTO_GROUP => 1;
 
 # The default list of columns for buglist.cgi
 use constant DEFAULT_COLUMN_LIST => (
-    "product", "component", "assigned_to",
-    "bug_status", "resolution", "short_desc", "changeddate"
+  "product",    "component",  "assigned_to", "bug_status",
+  "resolution", "short_desc", "changeddate"
 );
 
 # Used by query.cgi and buglist.cgi as the named-query name
@@ -327,6 +329,7 @@ use constant SAVE_NUM_SEARCHES => 10;
 
 # The column width for comment textareas and comments in bugmails.
 use constant COMMENT_COLS => 80;
+
 # Used in _check_comment(). Gives the max length allowed for a comment.
 use constant MAX_COMMENT_LENGTH => 65535;
 
@@ -335,9 +338,10 @@ use constant MIN_COMMENT_TAG_LENGTH => 3;
 use constant MAX_COMMENT_TAG_LENGTH => 24;
 
 # The type of bug comments.
-use constant CMT_NORMAL => 0;
-use constant CMT_DUPE_OF => 1;
+use constant CMT_NORMAL   => 0;
+use constant CMT_DUPE_OF  => 1;
 use constant CMT_HAS_DUPE => 2;
+
 # Type 3 was CMT_POPULAR_VOTES, which moved to the Voting extension.
 # Type 4 was CMT_MOVED_TO, which moved to the OldBugMove extension.
 use constant CMT_ATTACHMENT_CREATED => 5;
@@ -347,27 +351,26 @@ use constant CMT_ATTACHMENT_UPDATED => 6;
 # an error when the validation fails.
 use constant THROW_ERROR => 1;
 
-use constant REL_ASSIGNEE           => 0;
-use constant REL_QA                 => 1;
-use constant REL_REPORTER           => 2;
-use constant REL_CC                 => 3;
+use constant REL_ASSIGNEE => 0;
+use constant REL_QA       => 1;
+use constant REL_REPORTER => 2;
+use constant REL_CC       => 3;
+
 # REL 4 was REL_VOTER, before it was moved ino an extension.
-use constant REL_GLOBAL_WATCHER     => 5;
+use constant REL_GLOBAL_WATCHER => 5;
 
 # We need these strings for the X-Bugzilla-Reasons header
 # Note: this hash uses "," rather than "=>" to avoid auto-quoting of the LHS.
 # This should be accessed through Bugzilla::BugMail::relationships() instead
 # of being accessed directly.
 use constant RELATIONSHIPS => {
-    REL_ASSIGNEE      , "AssignedTo",
-    REL_REPORTER      , "Reporter",
-    REL_QA            , "QAcontact",
-    REL_CC            , "CC",
-    REL_GLOBAL_WATCHER, "GlobalWatcher"
+  REL_ASSIGNEE,       "AssignedTo", REL_REPORTER, "Reporter",
+  REL_QA,             "QAcontact",  REL_CC,       "CC",
+  REL_GLOBAL_WATCHER, "GlobalWatcher"
 };
-                              
+
 # Used for global events like EVT_FLAG_REQUESTED
-use constant REL_ANY                => 100;
+use constant REL_ANY => 100;
 
 # There are two sorts of event - positive and negative. Positive events are
 # those for which the user says "I want mail if this happens." Negative events
@@ -375,35 +378,35 @@ use constant REL_ANY                => 100;
 #
 # Exactly when each event fires is defined in wants_bug_mail() in User.pm; I'm
 # not commenting them here in case the comments and the code get out of sync.
-use constant EVT_OTHER              => 0;
-use constant EVT_ADDED_REMOVED      => 1;
-use constant EVT_COMMENT            => 2;
-use constant EVT_ATTACHMENT         => 3;
-use constant EVT_ATTACHMENT_DATA    => 4;
-use constant EVT_PROJ_MANAGEMENT    => 5;
-use constant EVT_OPENED_CLOSED      => 6;
-use constant EVT_KEYWORD            => 7;
-use constant EVT_CC                 => 8;
-use constant EVT_DEPEND_BLOCK       => 9;
-use constant EVT_BUG_CREATED        => 10;
-use constant EVT_COMPONENT          => 11;
-
-use constant POS_EVENTS => EVT_OTHER, EVT_ADDED_REMOVED, EVT_COMMENT, 
-                           EVT_ATTACHMENT, EVT_ATTACHMENT_DATA, 
-                           EVT_PROJ_MANAGEMENT, EVT_OPENED_CLOSED, EVT_KEYWORD,
-                           EVT_CC, EVT_DEPEND_BLOCK, EVT_BUG_CREATED,
-                           EVT_COMPONENT;
-
-use constant EVT_UNCONFIRMED        => 50;
-use constant EVT_CHANGED_BY_ME      => 51;
-use constant EVT_MINOR_UPDATE       => 52;
+use constant EVT_OTHER           => 0;
+use constant EVT_ADDED_REMOVED   => 1;
+use constant EVT_COMMENT         => 2;
+use constant EVT_ATTACHMENT      => 3;
+use constant EVT_ATTACHMENT_DATA => 4;
+use constant EVT_PROJ_MANAGEMENT => 5;
+use constant EVT_OPENED_CLOSED   => 6;
+use constant EVT_KEYWORD         => 7;
+use constant EVT_CC              => 8;
+use constant EVT_DEPEND_BLOCK    => 9;
+use constant EVT_BUG_CREATED     => 10;
+use constant EVT_COMPONENT       => 11;
+
+use constant
+  POS_EVENTS => EVT_OTHER,
+  EVT_ADDED_REMOVED,   EVT_COMMENT,       EVT_ATTACHMENT, EVT_ATTACHMENT_DATA,
+  EVT_PROJ_MANAGEMENT, EVT_OPENED_CLOSED, EVT_KEYWORD,    EVT_CC,
+  EVT_DEPEND_BLOCK,    EVT_BUG_CREATED,   EVT_COMPONENT;
+
+use constant EVT_UNCONFIRMED   => 50;
+use constant EVT_CHANGED_BY_ME => 51;
+use constant EVT_MINOR_UPDATE  => 52;
 
 use constant NEG_EVENTS => EVT_UNCONFIRMED, EVT_CHANGED_BY_ME, EVT_MINOR_UPDATE;
 
 # These are the "global" flags, which aren't tied to a particular relationship.
 # and so use REL_ANY.
-use constant EVT_FLAG_REQUESTED     => 100; # Flag has been requested of me
-use constant EVT_REQUESTED_FLAG     => 101; # I have requested a flag
+use constant EVT_FLAG_REQUESTED => 100;    # Flag has been requested of me
+use constant EVT_REQUESTED_FLAG => 101;    # I have requested a flag
 
 use constant GLOBAL_EVENTS => EVT_FLAG_REQUESTED, EVT_REQUESTED_FLAG;
 
@@ -411,10 +414,12 @@ use constant GLOBAL_EVENTS => EVT_FLAG_REQUESTED, EVT_REQUESTED_FLAG;
 use constant ADMIN_GROUP_NAME => 'admin';
 
 # Privileges which can be per-product.
-use constant PER_PRODUCT_PRIVILEGES => ('editcomponents', 'editbugs', 'canconfirm');
+use constant PER_PRODUCT_PRIVILEGES =>
+  ('editcomponents', 'editbugs', 'canconfirm');
 
 # Path to sendmail.exe (Windows only)
 use constant SENDMAIL_EXE => '/usr/lib/sendmail.exe';
+
 # Paths to search for the sendmail binary (non-Windows)
 use constant SENDMAIL_PATH => '/usr/lib:/usr/sbin:/usr/ucblib';
 
@@ -425,45 +430,46 @@ use constant SENDMAIL_PATH => '/usr/lib:/usr/sbin:/usr/ucblib';
 # we do more than we would do for a standard integer type (f.e. we might
 # display a user picker).
 
-use constant FIELD_TYPE_UNKNOWN   => 0;
-use constant FIELD_TYPE_FREETEXT  => 1;
+use constant FIELD_TYPE_UNKNOWN       => 0;
+use constant FIELD_TYPE_FREETEXT      => 1;
 use constant FIELD_TYPE_SINGLE_SELECT => 2;
-use constant FIELD_TYPE_MULTI_SELECT => 3;
-use constant FIELD_TYPE_TEXTAREA  => 4;
-use constant FIELD_TYPE_DATETIME  => 5;
-use constant FIELD_TYPE_BUG_ID  => 6;
-use constant FIELD_TYPE_BUG_URLS => 7;
-use constant FIELD_TYPE_KEYWORDS => 8;
-use constant FIELD_TYPE_DATE => 9;
-use constant FIELD_TYPE_INTEGER => 10;
+use constant FIELD_TYPE_MULTI_SELECT  => 3;
+use constant FIELD_TYPE_TEXTAREA      => 4;
+use constant FIELD_TYPE_DATETIME      => 5;
+use constant FIELD_TYPE_BUG_ID        => 6;
+use constant FIELD_TYPE_BUG_URLS      => 7;
+use constant FIELD_TYPE_KEYWORDS      => 8;
+use constant FIELD_TYPE_DATE          => 9;
+use constant FIELD_TYPE_INTEGER       => 10;
+
 # Add new field types above this line, and change the below value in the
 # obvious fashion
 use constant FIELD_TYPE_HIGHEST_PLUS_ONE => 11;
 
-use constant EMPTY_DATETIME_REGEX => qr/^[0\-:\sA-Za-z]+$/; 
+use constant EMPTY_DATETIME_REGEX => qr/^[0\-:\sA-Za-z]+$/;
 
 # See the POD for Bugzilla::Field/is_abnormal to see why these are listed
 # here.
-use constant ABNORMAL_SELECTS => {
-    classification => 1,
-    component      => 1,
-    product        => 1,
-};
+use constant ABNORMAL_SELECTS =>
+  {classification => 1, component => 1, product => 1,};
 
 # The fields from fielddefs that are blocked from non-timetracking users.
 # work_time is sometimes called actual_time.
 use constant TIMETRACKING_FIELDS =>
-    qw(estimated_time remaining_time work_time actual_time percentage_complete);
+  qw(estimated_time remaining_time work_time actual_time percentage_complete);
 
 # The maximum number of days a token will remain valid.
 use constant MAX_TOKEN_AGE => 3;
+
 # How many days a logincookie will remain valid if not used.
 use constant MAX_LOGINCOOKIE_AGE => 30;
+
 # How many seconds (default is 6 hours) a sudo cookie remains valid.
 use constant MAX_SUDO_TOKEN_AGE => 21600;
 
 # Maximum failed logins to lock account for this IP
 use constant MAX_LOGIN_ATTEMPTS => 5;
+
 # If the maximum login attempts occur during this many minutes, the
 # account is locked.
 use constant LOGIN_LOCKOUT_INTERVAL => 30;
@@ -477,36 +483,39 @@ use constant ACCOUNT_CHANGE_INTERVAL => 10;
 use constant MAX_STS_AGE => 604800;
 
 # Protocols which are considered as safe.
-use constant SAFE_PROTOCOLS => ('afs', 'cid', 'ftp', 'gopher', 'http', 'https',
-                                'irc', 'ircs', 'mid', 'news', 'nntp', 'prospero',
-                                'telnet', 'view-source', 'wais');
+use constant SAFE_PROTOCOLS => (
+  'afs',    'cid',         'ftp', 'gopher', 'http', 'https',
+  'irc',    'ircs',        'mid', 'news',   'nntp', 'prospero',
+  'telnet', 'view-source', 'wais'
+);
 
 # Valid MIME types for attachments.
-use constant LEGAL_CONTENT_TYPES => ('application', 'audio', 'image', 'message',
-                                     'model', 'multipart', 'text', 'video');
-
-use constant contenttypes =>
-  {
-   "html" => "text/html" ,
-   "rdf"  => "application/rdf+xml" ,
-   "atom" => "application/atom+xml" ,
-   "xml"  => "application/xml" ,
-   "dtd"  => "application/xml-dtd" , 
-   "js"   => "application/x-javascript" ,
-   "json" => "application/json" ,
-   "csv"  => "text/csv" ,
-   "png"  => "image/png" ,
-   "ics"  => "text/calendar" ,
-  };
+use constant LEGAL_CONTENT_TYPES => (
+  'application', 'audio',     'image', 'message',
+  'model',       'multipart', 'text',  'video'
+);
+
+use constant contenttypes => {
+  "html" => "text/html",
+  "rdf"  => "application/rdf+xml",
+  "atom" => "application/atom+xml",
+  "xml"  => "application/xml",
+  "dtd"  => "application/xml-dtd",
+  "js"   => "application/x-javascript",
+  "json" => "application/json",
+  "csv"  => "text/csv",
+  "png"  => "image/png",
+  "ics"  => "text/calendar",
+};
 
 # Usage modes. Default USAGE_MODE_BROWSER. Use with Bugzilla->usage_mode.
-use constant USAGE_MODE_BROWSER    => 0;
-use constant USAGE_MODE_CMDLINE    => 1;
-use constant USAGE_MODE_XMLRPC     => 2;
-use constant USAGE_MODE_EMAIL      => 3;
-use constant USAGE_MODE_JSON       => 4;
-use constant USAGE_MODE_TEST       => 5;
-use constant USAGE_MODE_REST       => 6;
+use constant USAGE_MODE_BROWSER => 0;
+use constant USAGE_MODE_CMDLINE => 1;
+use constant USAGE_MODE_XMLRPC  => 2;
+use constant USAGE_MODE_EMAIL   => 3;
+use constant USAGE_MODE_JSON    => 4;
+use constant USAGE_MODE_TEST    => 5;
+use constant USAGE_MODE_REST    => 6;
 
 # Error modes. Default set by Bugzilla->usage_mode (so ERROR_MODE_WEBPAGE
 # usually). Use with Bugzilla->error_mode.
@@ -518,27 +527,33 @@ use constant ERROR_MODE_TEST           => 4;
 use constant ERROR_MODE_REST           => 5;
 
 # The ANSI colors of messages that command-line scripts use
-use constant COLOR_ERROR => 'red';
+use constant COLOR_ERROR   => 'red';
 use constant COLOR_SUCCESS => 'green';
 
 # The various modes that checksetup.pl can run in.
-use constant INSTALLATION_MODE_INTERACTIVE => 0;
+use constant INSTALLATION_MODE_INTERACTIVE     => 0;
 use constant INSTALLATION_MODE_NON_INTERACTIVE => 1;
 
 # Data about what we require for different databases.
 use constant DB_MODULE => {
-    # MySQL 5.0.15 was the first production 5.0.x release.
-    mysql   => { db => 'Bugzilla::DB::Mysql',  db_version => '5.0.15',    name => 'MySQL'},
-    pg      => { db => 'Bugzilla::DB::Pg',     db_version => '9.00.0000', name => 'PostgreSQL'},
-    oracle  => { db => 'Bugzilla::DB::Oracle', db_version => '10.02.0',   name => 'Oracle'},
-    # SQLite 3.6.22 fixes a WHERE clause problem that may affect us.
-    sqlite  => { db => 'Bugzilla::DB::Sqlite', db_version => '3.6.22',    name => 'SQLite'},
+
+  # MySQL 5.0.15 was the first production 5.0.x release.
+  mysql => {db => 'Bugzilla::DB::Mysql', db_version => '5.0.15', name => 'MySQL'},
+  pg =>
+    {db => 'Bugzilla::DB::Pg', db_version => '9.00.0000', name => 'PostgreSQL'},
+  oracle =>
+    {db => 'Bugzilla::DB::Oracle', db_version => '10.02.0', name => 'Oracle'},
+
+  # SQLite 3.6.22 fixes a WHERE clause problem that may affect us.
+  sqlite =>
+    {db => 'Bugzilla::DB::Sqlite', db_version => '3.6.22', name => 'SQLite'},
 };
 
 # True if we're on Win32.
 use constant ON_WINDOWS => $^O eq 'MSWin32' ? 1 : 0;
+
 # True if we're using ActiveState Perl (as opposed to Strawberry) on Windows.
-use constant ON_ACTIVESTATE => eval { &Win32::BuildNumber };
+use constant ON_ACTIVESTATE => eval {&Win32::BuildNumber};
 
 # The user who should be considered "root" when we're giving
 # instructions to Bugzilla administrators.
@@ -546,7 +561,7 @@ use constant ROOT_USER => ON_WINDOWS ? 'Administrator' : 'root';
 
 use constant MIN_SMALLINT => -32768;
 use constant MAX_SMALLINT => 32767;
-use constant MAX_INT_32 => 2147483647;
+use constant MAX_INT_32   => 2147483647;
 
 # The longest that a saved search name can be.
 use constant MAX_LEN_QUERY_NAME => 64;
@@ -595,6 +610,7 @@ use constant MAX_WEBDOT_BUGS => 2000;
 # Perl's "Digest" module. Note that if you change this, it won't take
 # effect until a user logs in or changes their password.
 use constant PASSWORD_DIGEST_ALGORITHM => 'SHA-256';
+
 # How long of a salt should we use? Note that if you change this, it
 # won't take effect until a user logs in or changes their password.
 use constant PASSWORD_SALT_LENGTH => 8;
@@ -603,7 +619,9 @@ use constant PASSWORD_SALT_LENGTH => 8;
 # via POST such as buglist.cgi. This value determines whether the redirect
 # can be safely done or not based on the web server's URI length setting.
 # See http://support.microsoft.com/kb/208427 for why MSIE is different
-use constant CGI_URI_LIMIT => ($ENV{'HTTP_USER_AGENT'} || '') =~ /MSIE/ ? 2083 : 8000;
+use constant CGI_URI_LIMIT => ($ENV{'HTTP_USER_AGENT'} || '') =~ /MSIE/
+  ? 2083
+  : 8000;
 
 # If the user isn't allowed to change a field, we must tell them who can.
 # We store the required permission set into the $PrivilegesRequired
@@ -632,79 +650,87 @@ use constant MARKDOWN_TAB_WIDTH => 2;
 # Setting a limit to 0 will disable this feature.
 use constant EMAIL_LIMIT_PER_MINUTE => 1000;
 use constant EMAIL_LIMIT_PER_HOUR   => 2500;
+
 # Don't change this exception message.
-use constant EMAIL_LIMIT_EXCEPTION  => "email_limit_exceeded\n";
+use constant EMAIL_LIMIT_EXCEPTION => "email_limit_exceeded\n";
 
 # The maximum number of jobs to show when viewing the job queue
 # (view_job_queue.cgi).
 use constant JOB_QUEUE_VIEW_MAX_JOBS => 500;
 
 sub bz_locations {
-    # Force $memoize to re-compute data per project, to avoid
-    # sharing the same data across different installations.
-    state $memoize = {};
-    my $project = $ENV{PROJECT};
-    my $key = $project // '__DEFAULT';
 
-    return $memoize->{$key} //= _bz_locations($project);
+  # Force $memoize to re-compute data per project, to avoid
+  # sharing the same data across different installations.
+  state $memoize = {};
+  my $project = $ENV{PROJECT};
+  my $key     = $project // '__DEFAULT';
+
+  return $memoize->{$key} //= _bz_locations($project);
 }
 
 sub _bz_locations {
-    my $project = shift;
-    # We know that Bugzilla/Constants.pm must be in %INC at this point.
-    # So the only question is, what's the name of the directory
-    # above it? This is the most reliable way to get our current working
-    # directory under both mod_cgi and mod_perl. We call dirname twice
-    # to get the name of the directory above the "Bugzilla/" directory.
-    #
-    # Calling dirname twice like that won't work on VMS or AmigaOS
-    # but I doubt anybody runs Bugzilla on those.
-    #
-    # On mod_cgi this will be a relative path. On mod_perl it will be an
-    # absolute path.
-    my $libpath = dirname(dirname($INC{'Bugzilla/Constants.pm'}));
-    # We have to detaint $libpath, but we can't use Bugzilla::Util here.
-    $libpath =~ /(.*)/;
-    $libpath = $1;
-
-    my ($localconfig, $datadir);
-    if ($project && $project =~ /^([\w-]+)$/) {
-        $project = $1;
-        $localconfig = "localconfig.$project";
-        $datadir = "data/$project";
-    } else {
-        $project = undef;
-        $localconfig = "localconfig";
-        $datadir = "data";
-    }
-
-    $datadir = "$libpath/$datadir";
-    # We have to return absolute paths for mod_perl. 
-    # That means that if you modify these paths, they must be absolute paths.
-    return {
-        'libpath'     => $libpath,
-        'ext_libpath' => "$libpath/lib",
-        # If you put the libraries in a different location than the CGIs,
-        # make sure this still points to the CGIs.
-        'cgi_path'    => $libpath,
-        'templatedir' => "$libpath/template",
-        'template_cache' => "$datadir/template",
-        'project'     => $project,
-        'localconfig' => "$libpath/$localconfig",
-        'datadir'     => $datadir,
-        'attachdir'   => "$datadir/attachments",
-        'skinsdir'    => "$libpath/skins",
-        'graphsdir'   => "$libpath/graphs",
-        # $webdotdir must be in the web server's tree somewhere. Even if you use a 
-        # local dot, we output images to there. Also, if $webdotdir is 
-        # not relative to the bugzilla root directory, you'll need to 
-        # change showdependencygraph.cgi to set image_url to the correct 
-        # location.
-        # The script should really generate these graphs directly...
-        'webdotdir'   => "$datadir/webdot",
-        'extensionsdir' => "$libpath/extensions",
-        'assetsdir'   => "$datadir/assets",
-    };
+  my $project = shift;
+
+  # We know that Bugzilla/Constants.pm must be in %INC at this point.
+  # So the only question is, what's the name of the directory
+  # above it? This is the most reliable way to get our current working
+  # directory under both mod_cgi and mod_perl. We call dirname twice
+  # to get the name of the directory above the "Bugzilla/" directory.
+  #
+  # Calling dirname twice like that won't work on VMS or AmigaOS
+  # but I doubt anybody runs Bugzilla on those.
+  #
+  # On mod_cgi this will be a relative path. On mod_perl it will be an
+  # absolute path.
+  my $libpath = dirname(dirname($INC{'Bugzilla/Constants.pm'}));
+
+  # We have to detaint $libpath, but we can't use Bugzilla::Util here.
+  $libpath =~ /(.*)/;
+  $libpath = $1;
+
+  my ($localconfig, $datadir);
+  if ($project && $project =~ /^([\w-]+)$/) {
+    $project     = $1;
+    $localconfig = "localconfig.$project";
+    $datadir     = "data/$project";
+  }
+  else {
+    $project     = undef;
+    $localconfig = "localconfig";
+    $datadir     = "data";
+  }
+
+  $datadir = "$libpath/$datadir";
+
+  # We have to return absolute paths for mod_perl.
+  # That means that if you modify these paths, they must be absolute paths.
+  return {
+    'libpath'     => $libpath,
+    'ext_libpath' => "$libpath/lib",
+
+    # If you put the libraries in a different location than the CGIs,
+    # make sure this still points to the CGIs.
+    'cgi_path'       => $libpath,
+    'templatedir'    => "$libpath/template",
+    'template_cache' => "$datadir/template",
+    'project'        => $project,
+    'localconfig'    => "$libpath/$localconfig",
+    'datadir'        => $datadir,
+    'attachdir'      => "$datadir/attachments",
+    'skinsdir'       => "$libpath/skins",
+    'graphsdir'      => "$libpath/graphs",
+
+    # $webdotdir must be in the web server's tree somewhere. Even if you use a
+    # local dot, we output images to there. Also, if $webdotdir is
+    # not relative to the bugzilla root directory, you'll need to
+    # change showdependencygraph.cgi to set image_url to the correct
+    # location.
+    # The script should really generate these graphs directly...
+    'webdotdir'     => "$datadir/webdot",
+    'extensionsdir' => "$libpath/extensions",
+    'assetsdir'     => "$datadir/assets",
+  };
 }
 
 1;
index ec5d1ed96d9338b48ded932903b1a8676c327aa3..be692cc8fc6d8ff99456c1916242bb2bbde0647f 100644 (file)
@@ -33,7 +33,7 @@ use Storable qw(dclone);
 # Constants
 #####################################################################
 
-use constant BLOB_TYPE => DBI::SQL_BLOB;
+use constant BLOB_TYPE       => DBI::SQL_BLOB;
 use constant ISOLATION_LEVEL => 'REPEATABLE READ';
 
 # Set default values for what used to be the enum types.  These values
@@ -46,14 +46,14 @@ use constant ISOLATION_LEVEL => 'REPEATABLE READ';
 # Bugzilla with enums. After that, they are either controlled through
 # the Bugzilla UI or through the DB.
 use constant ENUM_DEFAULTS => {
-    bug_severity  => ['blocker', 'critical', 'major', 'normal',
-                      'minor', 'trivial', 'enhancement'],
-    priority     => ["Highest", "High", "Normal", "Low", "Lowest", "---"],
-    op_sys       => ["All","Windows","Mac OS","Linux","Other"],
-    rep_platform => ["All","PC","Macintosh","Other"],
-    bug_status   => ["UNCONFIRMED","CONFIRMED","IN_PROGRESS","RESOLVED",
-                     "VERIFIED"],
-    resolution   => ["","FIXED","INVALID","WONTFIX", "DUPLICATE","WORKSFORME"],
+  bug_severity =>
+    ['blocker', 'critical', 'major', 'normal', 'minor', 'trivial', 'enhancement'],
+  priority     => ["Highest", "High",    "Normal",    "Low",   "Lowest", "---"],
+  op_sys       => ["All",     "Windows", "Mac OS",    "Linux", "Other"],
+  rep_platform => ["All",     "PC",      "Macintosh", "Other"],
+  bug_status =>
+    ["UNCONFIRMED", "CONFIRMED", "IN_PROGRESS", "RESOLVED", "VERIFIED"],
+  resolution => ["", "FIXED", "INVALID", "WONTFIX", "DUPLICATE", "WORKSFORME"],
 };
 
 # The character that means "OR" in a boolean fulltext search. If empty,
@@ -83,14 +83,14 @@ use constant WORD_END   => '($|[^[:alnum:]])';
 use constant INDEX_DROPS_REQUIRE_FK_DROPS => 1;
 
 #####################################################################
-# Overridden Superclass Methods 
+# Overridden Superclass Methods
 #####################################################################
 
 sub quote {
-    my $self = shift;
-    my $retval = $self->SUPER::quote(@_);
-    trick_taint($retval) if defined $retval;
-    return $retval;
+  my $self   = shift;
+  my $retval = $self->SUPER::quote(@_);
+  trick_taint($retval) if defined $retval;
+  return $retval;
 }
 
 #####################################################################
@@ -98,180 +98,185 @@ sub quote {
 #####################################################################
 
 sub connect_shadow {
-    my $params = Bugzilla->params;
-    die "Tried to connect to non-existent shadowdb" 
-        unless $params->{'shadowdb'};
+  my $params = Bugzilla->params;
+  die "Tried to connect to non-existent shadowdb" unless $params->{'shadowdb'};
 
-    # Instead of just passing in a new hashref, we locally modify the
-    # values of "localconfig", because some drivers access it while
-    # connecting.
-    my %connect_params = %{ Bugzilla->localconfig };
-    $connect_params{db_host} = $params->{'shadowdbhost'};
-    $connect_params{db_name} = $params->{'shadowdb'};
-    $connect_params{db_port} = $params->{'shadowdbport'};
-    $connect_params{db_sock} = $params->{'shadowdbsock'};
+  # Instead of just passing in a new hashref, we locally modify the
+  # values of "localconfig", because some drivers access it while
+  # connecting.
+  my %connect_params = %{Bugzilla->localconfig};
+  $connect_params{db_host} = $params->{'shadowdbhost'};
+  $connect_params{db_name} = $params->{'shadowdb'};
+  $connect_params{db_port} = $params->{'shadowdbport'};
+  $connect_params{db_sock} = $params->{'shadowdbsock'};
 
-    return _connect(\%connect_params);
+  return _connect(\%connect_params);
 }
 
 sub connect_main {
-    return _connect(Bugzilla->localconfig); 
+  return _connect(Bugzilla->localconfig);
 }
 
 sub _connect {
-    my ($params) = @_;
+  my ($params) = @_;
 
-    my $driver = $params->{db_driver};
-    my $pkg_module = DB_MODULE->{lc($driver)}->{db};
+  my $driver     = $params->{db_driver};
+  my $pkg_module = DB_MODULE->{lc($driver)}->{db};
 
-    # do the actual import
-    eval ("require $pkg_module")
-        || die ("'$driver' is not a valid choice for \$db_driver in "
-                . " localconfig: " . $@);
+  # do the actual import
+  eval("require $pkg_module")
+    || die(
+    "'$driver' is not a valid choice for \$db_driver in " . " localconfig: " . $@);
 
-    # instantiate the correct DB specific module
-    my $dbh = $pkg_module->new($params);
+  # instantiate the correct DB specific module
+  my $dbh = $pkg_module->new($params);
 
-    return $dbh;
+  return $dbh;
 }
 
 sub _handle_error {
-    require Carp;
+  require Carp;
 
-    # Cut down the error string to a reasonable size
-    $_[0] = substr($_[0], 0, 2000) . ' ... ' . substr($_[0], -2000)
-        if length($_[0]) > 4000;
-    $_[0] = Carp::longmess($_[0]);
+  # Cut down the error string to a reasonable size
+  $_[0] = substr($_[0], 0, 2000) . ' ... ' . substr($_[0], -2000)
+    if length($_[0]) > 4000;
+  $_[0] = Carp::longmess($_[0]);
 
-    if (!Bugzilla->request_cache->{in_error} && Bugzilla->usage_mode == USAGE_MODE_BROWSER) {
-        Bugzilla->request_cache->{in_error} = 1;
-        ThrowCodeError("db_error", {err_message => $_[0]});
-    }
+  if (!Bugzilla->request_cache->{in_error}
+    && Bugzilla->usage_mode == USAGE_MODE_BROWSER)
+  {
+    Bugzilla->request_cache->{in_error} = 1;
+    ThrowCodeError("db_error", {err_message => $_[0]});
+  }
 
-    Bugzilla->request_cache->{in_error} = undef;
+  Bugzilla->request_cache->{in_error} = undef;
 
-    return 0; # Now let DBI handle raising the error
+  return 0;    # Now let DBI handle raising the error
 }
 
 sub bz_check_requirements {
-    my ($output) = @_;
+  my ($output) = @_;
 
-    my $lc = Bugzilla->localconfig;
-    my $db = DB_MODULE->{lc($lc->{db_driver})};
+  my $lc = Bugzilla->localconfig;
+  my $db = DB_MODULE->{lc($lc->{db_driver})};
 
-    # Only certain values are allowed for $db_driver.
-    if (!defined $db) {
-        die "$lc->{db_driver} is not a valid choice for \$db_driver in"
-            . bz_locations()->{'localconfig'};
-    }
+  # Only certain values are allowed for $db_driver.
+  if (!defined $db) {
+    die "$lc->{db_driver} is not a valid choice for \$db_driver in"
+      . bz_locations()->{'localconfig'};
+  }
 
-    # We don't try to connect to the actual database if $db_check is
-    # disabled.
-    unless ($lc->{db_check}) {
-        print "\n" if $output;
-        return;
-    }
+  # We don't try to connect to the actual database if $db_check is
+  # disabled.
+  unless ($lc->{db_check}) {
+    print "\n" if $output;
+    return;
+  }
 
-    # And now check the version of the database server itself.
-    my $dbh = _get_no_db_connection();
-    $dbh->bz_check_server_version($db, $output);
+  # And now check the version of the database server itself.
+  my $dbh = _get_no_db_connection();
+  $dbh->bz_check_server_version($db, $output);
 
-    print "\n" if $output;
+  print "\n" if $output;
 }
 
 sub bz_check_server_version {
-    my ($self, $db, $output) = @_;
+  my ($self, $db, $output) = @_;
 
-    my $sql_vers = $self->bz_server_version;
-    $self->disconnect;
+  my $sql_vers = $self->bz_server_version;
+  $self->disconnect;
 
-    my $sql_want = $db->{db_version};
-    my $version_ok = vers_cmp($sql_vers, $sql_want) > -1 ? 1 : 0;
+  my $sql_want = $db->{db_version};
+  my $version_ok = vers_cmp($sql_vers, $sql_want) > -1 ? 1 : 0;
 
-    my $sql_server = $db->{name};
-    if ($output) {
-        Bugzilla::Install::Requirements::_checking_for({
-            package => $sql_server, wanted => $sql_want,
-            found   => $sql_vers, ok => $version_ok });
-    }
+  my $sql_server = $db->{name};
+  if ($output) {
+    Bugzilla::Install::Requirements::_checking_for({
+      package => $sql_server,
+      wanted  => $sql_want,
+      found   => $sql_vers,
+      ok      => $version_ok
+    });
+  }
 
-    # Check what version of the database server is installed and let
-    # the user know if the version is too old to be used with Bugzilla.
-    if (!$version_ok) {
-        die <<EOT;
+  # Check what version of the database server is installed and let
+  # the user know if the version is too old to be used with Bugzilla.
+  if (!$version_ok) {
+    die <<EOT;
 
 Your $sql_server v$sql_vers is too old. Bugzilla requires version
 $sql_want or later of $sql_server. Please download and install a
 newer version.
 
 EOT
-    }
+  }
 
-    # This is used by subclasses.
-    return $sql_vers;
+  # This is used by subclasses.
+  return $sql_vers;
 }
 
 # Note that this function requires that localconfig exist and
 # be valid.
 sub bz_create_database {
-    my $dbh;
-    # See if we can connect to the actual Bugzilla database.
-    my $conn_success = eval { $dbh = connect_main() };
-    my $db_name = Bugzilla->localconfig->{db_name};
-
-    if (!$conn_success) {
-        $dbh = _get_no_db_connection();
-        say "Creating database $db_name...";
-
-        # Try to create the DB, and if we fail print a friendly error.
-        my $success  = eval {
-            my @sql = $dbh->_bz_schema->get_create_database_sql($db_name);
-            # This ends with 1 because this particular do doesn't always
-            # return something.
-            $dbh->do($_) foreach @sql; 1;
-        };
-        if (!$success) {
-            my $error = $dbh->errstr || $@;
-            chomp($error);
-            die "The '$db_name' database could not be created.",
-                " The error returned was:\n\n    $error\n\n",
-                _bz_connect_error_reasons();
-        }
+  my $dbh;
+
+  # See if we can connect to the actual Bugzilla database.
+  my $conn_success = eval { $dbh = connect_main() };
+  my $db_name = Bugzilla->localconfig->{db_name};
+
+  if (!$conn_success) {
+    $dbh = _get_no_db_connection();
+    say "Creating database $db_name...";
+
+    # Try to create the DB, and if we fail print a friendly error.
+    my $success = eval {
+      my @sql = $dbh->_bz_schema->get_create_database_sql($db_name);
+
+      # This ends with 1 because this particular do doesn't always
+      # return something.
+      $dbh->do($_) foreach @sql;
+      1;
+    };
+    if (!$success) {
+      my $error = $dbh->errstr || $@;
+      chomp($error);
+      die "The '$db_name' database could not be created.",
+        " The error returned was:\n\n    $error\n\n", _bz_connect_error_reasons();
     }
+  }
 
-    $dbh->disconnect;
+  $dbh->disconnect;
 }
 
 # A helper for bz_create_database and bz_check_requirements.
 sub _get_no_db_connection {
-    my ($sql_server) = @_;
-    my $dbh;
-    my %connect_params = %{ Bugzilla->localconfig };
-    my $conn_success = eval {
-        $dbh = _connect(\%connect_params);
-    };
-    if (!$conn_success) {
-        my $driver = $connect_params{db_driver};
-        my $sql_server = DB_MODULE->{lc($driver)}->{name};
-        # Can't use $dbh->errstr because $dbh is undef.
-        my $error = $DBI::errstr || $@;
-        chomp($error);
-        die "There was an error connecting to $sql_server:\n\n",
-            "    $error\n\n", _bz_connect_error_reasons(), "\n";
-    }
-    return $dbh;    
+  my ($sql_server) = @_;
+  my $dbh;
+  my %connect_params = %{Bugzilla->localconfig};
+  my $conn_success = eval { $dbh = _connect(\%connect_params); };
+  if (!$conn_success) {
+    my $driver     = $connect_params{db_driver};
+    my $sql_server = DB_MODULE->{lc($driver)}->{name};
+
+    # Can't use $dbh->errstr because $dbh is undef.
+    my $error = $DBI::errstr || $@;
+    chomp($error);
+    die "There was an error connecting to $sql_server:\n\n", "    $error\n\n",
+      _bz_connect_error_reasons(), "\n";
+  }
+  return $dbh;
 }
 
 # Just a helper because we have to re-use this text.
 # We don't use this in db_new because it gives away the database
 # username, and db_new errors can show up on CGIs.
 sub _bz_connect_error_reasons {
-    my $lc_file = bz_locations()->{'localconfig'};
-    my $lc      = Bugzilla->localconfig;
-    my $db      = DB_MODULE->{lc($lc->{db_driver})};
-    my $server  = $db->{name};
+  my $lc_file = bz_locations()->{'localconfig'};
+  my $lc      = Bugzilla->localconfig;
+  my $db      = DB_MODULE->{lc($lc->{db_driver})};
+  my $server  = $db->{name};
 
-return <<EOT;
+  return <<EOT;
 This might have several reasons:
 
 * $server is not running.
@@ -290,154 +295,157 @@ EOT
 
 # List of abstract methods we are checking the derived class implements
 our @_abstract_methods = qw(new sql_regexp sql_not_regexp sql_limit sql_to_days
-                            sql_date_format sql_date_math bz_explain
-                            sql_group_concat);
+  sql_date_format sql_date_math bz_explain
+  sql_group_concat);
 
 # This overridden import method will check implementation of inherited classes
 # for missing implementation of abstract methods
 # See http://perlmonks.thepen.com/44265.html
 sub import {
-    my $pkg = shift;
-
-    # do not check this module
-    if ($pkg ne __PACKAGE__) {
-        # make sure all abstract methods are implemented
-        foreach my $meth (@_abstract_methods) {
-            $pkg->can($meth)
-                or die("Class $pkg does not define method $meth");
-        }
+  my $pkg = shift;
+
+  # do not check this module
+  if ($pkg ne __PACKAGE__) {
+
+    # make sure all abstract methods are implemented
+    foreach my $meth (@_abstract_methods) {
+      $pkg->can($meth) or die("Class $pkg does not define method $meth");
     }
+  }
 
-    # Now we want to call our superclass implementation.
-    # If our superclass is Exporter, which is using caller() to find
-    # a namespace to populate, we need to adjust for this extra call.
-    # All this can go when we stop using deprecated functions.
-    my $is_exporter = $pkg->isa('Exporter');
-    $Exporter::ExportLevel++ if $is_exporter;
-    $pkg->SUPER::import(@_);
-    $Exporter::ExportLevel-- if $is_exporter;
+  # Now we want to call our superclass implementation.
+  # If our superclass is Exporter, which is using caller() to find
+  # a namespace to populate, we need to adjust for this extra call.
+  # All this can go when we stop using deprecated functions.
+  my $is_exporter = $pkg->isa('Exporter');
+  $Exporter::ExportLevel++ if $is_exporter;
+  $pkg->SUPER::import(@_);
+  $Exporter::ExportLevel-- if $is_exporter;
 }
 
 sub sql_istrcmp {
-    my ($self, $left, $right, $op) = @_;
-    $op ||= "=";
+  my ($self, $left, $right, $op) = @_;
+  $op ||= "=";
 
-    return $self->sql_istring($left) . " $op " . $self->sql_istring($right);
+  return $self->sql_istring($left) . " $op " . $self->sql_istring($right);
 }
 
 sub sql_istring {
-    my ($self, $string) = @_;
+  my ($self, $string) = @_;
 
-    return "LOWER($string)";
+  return "LOWER($string)";
 }
 
 sub sql_iposition {
-    my ($self, $fragment, $text) = @_;
-    $fragment = $self->sql_istring($fragment);
-    $text = $self->sql_istring($text);
-    return $self->sql_position($fragment, $text);
+  my ($self, $fragment, $text) = @_;
+  $fragment = $self->sql_istring($fragment);
+  $text     = $self->sql_istring($text);
+  return $self->sql_position($fragment, $text);
 }
 
 sub sql_position {
-    my ($self, $fragment, $text) = @_;
+  my ($self, $fragment, $text) = @_;
 
-    return "POSITION($fragment IN $text)";
+  return "POSITION($fragment IN $text)";
 }
 
 sub sql_like {
-    my ($self, $fragment, $column) = @_;
+  my ($self, $fragment, $column) = @_;
 
-    my $quoted = $self->quote($fragment);
+  my $quoted = $self->quote($fragment);
 
-    return $self->sql_position($quoted, $column) . " > 0";
+  return $self->sql_position($quoted, $column) . " > 0";
 }
 
 sub sql_ilike {
-    my ($self, $fragment, $column) = @_;
+  my ($self, $fragment, $column) = @_;
 
-    my $quoted = $self->quote($fragment);
+  my $quoted = $self->quote($fragment);
 
-    return $self->sql_iposition($quoted, $column) . " > 0";
+  return $self->sql_iposition($quoted, $column) . " > 0";
 }
 
 sub sql_not_ilike {
-    my ($self, $fragment, $column) = @_;
+  my ($self, $fragment, $column) = @_;
 
-    my $quoted = $self->quote($fragment);
+  my $quoted = $self->quote($fragment);
 
-    return $self->sql_iposition($quoted, $column) . " = 0";
+  return $self->sql_iposition($quoted, $column) . " = 0";
 }
 
 
 sub sql_group_by {
-    my ($self, $needed_columns, $optional_columns) = @_;
+  my ($self, $needed_columns, $optional_columns) = @_;
+
+  my $expression = "GROUP BY $needed_columns";
+  $expression .= ", " . $optional_columns if $optional_columns;
 
-    my $expression = "GROUP BY $needed_columns";
-    $expression .= ", " . $optional_columns if $optional_columns;
-    
-    return $expression;
+  return $expression;
 }
 
 sub sql_string_concat {
-    my ($self, @params) = @_;
-    
-    return '(' . join(' || ', @params) . ')';
+  my ($self, @params) = @_;
+
+  return '(' . join(' || ', @params) . ')';
 }
 
 sub sql_string_until {
-    my ($self, $string, $substring) = @_;
+  my ($self, $string, $substring) = @_;
 
-    my $position = $self->sql_position($substring, $string);
-    return "CASE WHEN $position != 0"
-             . " THEN SUBSTR($string, 1, $position - 1)"
-             . " ELSE $string END";
+  my $position = $self->sql_position($substring, $string);
+  return
+      "CASE WHEN $position != 0"
+    . " THEN SUBSTR($string, 1, $position - 1)"
+    . " ELSE $string END";
 }
 
 sub sql_in {
-    my ($self, $column_name, $in_list_ref, $negate) = @_;
-    return " $column_name "
-             . ($negate ? "NOT " : "")
-             . "IN (" . join(',', @$in_list_ref) . ") ";
+  my ($self, $column_name, $in_list_ref, $negate) = @_;
+  return
+      " $column_name "
+    . ($negate ? "NOT " : "") . "IN ("
+    . join(',', @$in_list_ref) . ") ";
 }
 
 sub sql_fulltext_search {
-    my ($self, $column, $text) = @_;
-
-    # This is as close as we can get to doing full text search using
-    # standard ANSI SQL, without real full text search support. DB specific
-    # modules should override this, as this will be always much slower.
-
-    # make the string lowercase to do case insensitive search
-    my $lower_text = lc($text);
-
-    # split the text we're searching for into separate words. As a hack
-    # to allow quicksearch to work, if the field starts and ends with
-    # a double-quote, then we don't split it into words. We can't use
-    # Text::ParseWords here because it gets very confused by unbalanced
-    # quotes, which breaks searches like "don't try this" (because of the
-    # unbalanced single-quote in "don't").
-    my @words;
-    if ($lower_text =~ /^"/ and $lower_text =~ /"$/) {
-        $lower_text =~ s/^"//;
-        $lower_text =~ s/"$//;
-        @words = ($lower_text);
-    }
-    else {
-        @words = split(/\s+/, $lower_text);
-    }
-
-    # surround the words with wildcards and SQL quotes so we can use them
-    # in LIKE search clauses
-    @words = map($self->quote("\%$_\%"), @words);
-
-    # untaint words, since they are safe to use now that we've quoted them
-    trick_taint($_) foreach @words;
-
-    # turn the words into a set of LIKE search clauses
-    @words = map("LOWER($column) LIKE $_", @words);
-
-    # search for occurrences of all specified words in the column
-    return join (" AND ", @words), "CASE WHEN (" . join(" AND ", @words) . ") THEN 1 ELSE 0 END";
+  my ($self, $column, $text) = @_;
+
+  # This is as close as we can get to doing full text search using
+  # standard ANSI SQL, without real full text search support. DB specific
+  # modules should override this, as this will be always much slower.
+
+  # make the string lowercase to do case insensitive search
+  my $lower_text = lc($text);
+
+  # split the text we're searching for into separate words. As a hack
+  # to allow quicksearch to work, if the field starts and ends with
+  # a double-quote, then we don't split it into words. We can't use
+  # Text::ParseWords here because it gets very confused by unbalanced
+  # quotes, which breaks searches like "don't try this" (because of the
+  # unbalanced single-quote in "don't").
+  my @words;
+  if ($lower_text =~ /^"/ and $lower_text =~ /"$/) {
+    $lower_text =~ s/^"//;
+    $lower_text =~ s/"$//;
+    @words = ($lower_text);
+  }
+  else {
+    @words = split(/\s+/, $lower_text);
+  }
+
+  # surround the words with wildcards and SQL quotes so we can use them
+  # in LIKE search clauses
+  @words = map($self->quote("\%$_\%"), @words);
+
+  # untaint words, since they are safe to use now that we've quoted them
+  trick_taint($_) foreach @words;
+
+  # turn the words into a set of LIKE search clauses
+  @words = map("LOWER($column) LIKE $_", @words);
+
+  # search for occurrences of all specified words in the column
+  return join(" AND ", @words),
+    "CASE WHEN (" . join(" AND ", @words) . ") THEN 1 ELSE 0 END";
 }
 
 #####################################################################
@@ -446,24 +454,27 @@ sub sql_fulltext_search {
 
 # XXX - Needs to be documented.
 sub bz_server_version {
-    my ($self) = @_;
-    return $self->get_info(18); # SQL_DBMS_VER
+  my ($self) = @_;
+  return $self->get_info(18);    # SQL_DBMS_VER
 }
 
 sub bz_last_key {
-    my ($self, $table, $column) = @_;
+  my ($self, $table, $column) = @_;
 
-    return $self->last_insert_id(Bugzilla->localconfig->{db_name}, undef, 
-                                 $table, $column);
+  return $self->last_insert_id(Bugzilla->localconfig->{db_name},
+    undef, $table, $column);
 }
 
 sub bz_check_regexp {
-    my ($self, $pattern) = @_;
+  my ($self, $pattern) = @_;
 
-    eval { $self->do("SELECT " . $self->sql_regexp($self->quote("a"), $pattern, 1)) };
+  eval {
+    $self->do("SELECT " . $self->sql_regexp($self->quote("a"), $pattern, 1));
+  };
 
-    $@ && ThrowUserError('illegal_regexp', 
-        { value => $pattern, dberror => $self->errstr }); 
+  $@
+    && ThrowUserError('illegal_regexp',
+    {value => $pattern, dberror => $self->errstr});
 }
 
 #####################################################################
@@ -471,99 +482,100 @@ sub bz_check_regexp {
 #####################################################################
 
 sub bz_setup_database {
-    my ($self) = @_;
-
-    # If we haven't ever stored a serialized schema,
-    # set up the bz_schema table and store it.
-    $self->_bz_init_schema_storage();
-   
-    # We don't use bz_table_list here, because that uses _bz_real_schema.
-    # We actually want the table list from the ABSTRACT_SCHEMA in
-    # Bugzilla::DB::Schema.
-    my @desired_tables = $self->_bz_schema->get_table_list();
-    my $bugs_exists = $self->bz_table_info('bugs');
-    if (!$bugs_exists) {
-        say install_string('db_table_setup');
-    }
+  my ($self) = @_;
 
-    foreach my $table_name (@desired_tables) {
-        $self->bz_add_table($table_name, { silently => !$bugs_exists });
-    }
+  # If we haven't ever stored a serialized schema,
+  # set up the bz_schema table and store it.
+  $self->_bz_init_schema_storage();
+
+  # We don't use bz_table_list here, because that uses _bz_real_schema.
+  # We actually want the table list from the ABSTRACT_SCHEMA in
+  # Bugzilla::DB::Schema.
+  my @desired_tables = $self->_bz_schema->get_table_list();
+  my $bugs_exists    = $self->bz_table_info('bugs');
+  if (!$bugs_exists) {
+    say install_string('db_table_setup');
+  }
+
+  foreach my $table_name (@desired_tables) {
+    $self->bz_add_table($table_name, {silently => !$bugs_exists});
+  }
 }
 
 # This really just exists to get overridden in Bugzilla::DB::Mysql.
 sub bz_enum_initial_values {
-    return ENUM_DEFAULTS;
+  return ENUM_DEFAULTS;
 }
 
 sub bz_populate_enum_tables {
-    my ($self) = @_; 
+  my ($self) = @_;
 
-    my $any_severities = $self->selectrow_array(
-        'SELECT 1 FROM bug_severity ' . $self->sql_limit(1));
-    print install_string('db_enum_setup'), "\n  " if !$any_severities;
+  my $any_severities
+    = $self->selectrow_array('SELECT 1 FROM bug_severity ' . $self->sql_limit(1));
+  print install_string('db_enum_setup'), "\n  " if !$any_severities;
 
-    my $enum_values = $self->bz_enum_initial_values();
-    while (my ($table, $values) = each %$enum_values) {
-        $self->_bz_populate_enum_table($table, $values);
-    }
+  my $enum_values = $self->bz_enum_initial_values();
+  while (my ($table, $values) = each %$enum_values) {
+    $self->_bz_populate_enum_table($table, $values);
+  }
 
-    print "\n" if !$any_severities;
+  print "\n" if !$any_severities;
 }
 
 sub bz_setup_foreign_keys {
-    my ($self) = @_;
-
-    # profiles_activity was the first table to get foreign keys,
-    # so if it doesn't have them, then we're setting up FKs
-    # for the first time, and should be quieter about it.
-    my $activity_fk = $self->bz_fk_info('profiles_activity', 'userid');
-    my $any_fks = $activity_fk && $activity_fk->{created};
-    if (!$any_fks) {
-        say get_text('install_fk_setup');
-    }
+  my ($self) = @_;
+
+  # profiles_activity was the first table to get foreign keys,
+  # so if it doesn't have them, then we're setting up FKs
+  # for the first time, and should be quieter about it.
+  my $activity_fk = $self->bz_fk_info('profiles_activity', 'userid');
+  my $any_fks = $activity_fk && $activity_fk->{created};
+  if (!$any_fks) {
+    say get_text('install_fk_setup');
+  }
+
+  my @tables = $self->bz_table_list();
+  foreach my $table (@tables) {
+    my @columns = $self->bz_table_columns($table);
+    my %add_fks;
+    foreach my $column (@columns) {
 
-    my @tables = $self->bz_table_list();
-    foreach my $table (@tables) {
-        my @columns = $self->bz_table_columns($table);
-        my %add_fks;
-        foreach my $column (@columns) {
-            # First we check for any FKs that have created => 0,
-            # in the _bz_real_schema. This also picks up FKs with
-            # created => 1, but bz_add_fks will ignore those.
-            my $fk = $self->bz_fk_info($table, $column);
-            # Then we check the abstract schema to see if there
-            # should be an FK on this column, but one wasn't set in the
-            # _bz_real_schema for some reason. We do this to handle
-            # various problems caused by upgrading from versions
-            # prior to 4.2, and also to handle problems caused
-            # by enabling an extension pre-4.2, disabling it for
-            # the 4.2 upgrade, and then re-enabling it later.
-            unless ($fk && $fk->{created}) {
-                my $standard_def = 
-                    $self->_bz_schema->get_column_abstract($table, $column);
-                if (exists $standard_def->{REFERENCES}) {
-                    $fk = dclone($standard_def->{REFERENCES});
-                }
-            }
-
-            $add_fks{$column} = $fk if $fk;
+      # First we check for any FKs that have created => 0,
+      # in the _bz_real_schema. This also picks up FKs with
+      # created => 1, but bz_add_fks will ignore those.
+      my $fk = $self->bz_fk_info($table, $column);
+
+      # Then we check the abstract schema to see if there
+      # should be an FK on this column, but one wasn't set in the
+      # _bz_real_schema for some reason. We do this to handle
+      # various problems caused by upgrading from versions
+      # prior to 4.2, and also to handle problems caused
+      # by enabling an extension pre-4.2, disabling it for
+      # the 4.2 upgrade, and then re-enabling it later.
+      unless ($fk && $fk->{created}) {
+        my $standard_def = $self->_bz_schema->get_column_abstract($table, $column);
+        if (exists $standard_def->{REFERENCES}) {
+          $fk = dclone($standard_def->{REFERENCES});
         }
-        $self->bz_add_fks($table, \%add_fks, { silently => !$any_fks });
+      }
+
+      $add_fks{$column} = $fk if $fk;
     }
+    $self->bz_add_fks($table, \%add_fks, {silently => !$any_fks});
+  }
 }
 
 # This is used by contrib/bzdbcopy.pl, mostly.
 sub bz_drop_foreign_keys {
-    my ($self) = @_;
+  my ($self) = @_;
 
-    my @tables = $self->bz_table_list();
-    foreach my $table (@tables) {
-        my @columns = $self->bz_table_columns($table);
-        foreach my $column (@columns) {
-            $self->bz_drop_fk($table, $column);
-        }
+  my @tables = $self->bz_table_list();
+  foreach my $table (@tables) {
+    my @columns = $self->bz_table_columns($table);
+    foreach my $column (@columns) {
+      $self->bz_drop_fk($table, $column);
     }
+  }
 }
 
 #####################################################################
@@ -571,119 +583,121 @@ sub bz_drop_foreign_keys {
 #####################################################################
 
 sub bz_add_column {
-    my ($self, $table, $name, $new_def, $init_value) = @_;
-
-    # You can't add a NOT NULL column to a table with
-    # no DEFAULT statement, unless you have an init_value.
-    # SERIAL types are an exception, though, because they can
-    # auto-populate.
-    if ( $new_def->{NOTNULL} && !exists $new_def->{DEFAULT} 
-         && !defined $init_value && $new_def->{TYPE} !~ /SERIAL/)
-    {
-        ThrowCodeError('column_not_null_without_default',
-                       { name => "$table.$name" });
+  my ($self, $table, $name, $new_def, $init_value) = @_;
+
+  # You can't add a NOT NULL column to a table with
+  # no DEFAULT statement, unless you have an init_value.
+  # SERIAL types are an exception, though, because they can
+  # auto-populate.
+  if ( $new_def->{NOTNULL}
+    && !exists $new_def->{DEFAULT}
+    && !defined $init_value
+    && $new_def->{TYPE} !~ /SERIAL/)
+  {
+    ThrowCodeError('column_not_null_without_default', {name => "$table.$name"});
+  }
+
+  my $current_def = $self->bz_column_info($table, $name);
+
+  if (!$current_def) {
+
+    # REFERENCES need to happen later and not be created right away
+    my $trimmed_def = dclone($new_def);
+    delete $trimmed_def->{REFERENCES};
+    my @statements
+      = $self->_bz_real_schema->get_add_column_ddl($table, $name, $trimmed_def,
+      defined $init_value ? $self->quote($init_value) : undef);
+    print get_text('install_column_add', {column => $name, table => $table}) . "\n"
+      if Bugzilla->usage_mode == USAGE_MODE_CMDLINE;
+    foreach my $sql (@statements) {
+      $self->do($sql);
     }
 
-    my $current_def = $self->bz_column_info($table, $name);
-
-    if (!$current_def) {
-        # REFERENCES need to happen later and not be created right away
-        my $trimmed_def = dclone($new_def);
-        delete $trimmed_def->{REFERENCES};
-        my @statements = $self->_bz_real_schema->get_add_column_ddl(
-            $table, $name, $trimmed_def,
-            defined $init_value ? $self->quote($init_value) : undef);
-        print get_text('install_column_add',
-                       { column => $name, table => $table }) . "\n"
-            if Bugzilla->usage_mode == USAGE_MODE_CMDLINE;
-        foreach my $sql (@statements) {
-            $self->do($sql);
-        }
-
-        # To make things easier for callers, if they don't specify
-        # a REFERENCES item, we pull it from the _bz_schema if the
-        # column exists there and has a REFERENCES item.
-        # bz_setup_foreign_keys will then add this FK at the end of
-        # Install::DB.
-        my $col_abstract =
-            $self->_bz_schema->get_column_abstract($table, $name);
-        if (exists $col_abstract->{REFERENCES}) {
-            my $new_fk = dclone($col_abstract->{REFERENCES});
-            $new_fk->{created} = 0;
-            $new_def->{REFERENCES} = $new_fk;
-        }
-
-        $self->_bz_real_schema->set_column($table, $name, $new_def);
-        $self->_bz_store_real_schema;
+    # To make things easier for callers, if they don't specify
+    # a REFERENCES item, we pull it from the _bz_schema if the
+    # column exists there and has a REFERENCES item.
+    # bz_setup_foreign_keys will then add this FK at the end of
+    # Install::DB.
+    my $col_abstract = $self->_bz_schema->get_column_abstract($table, $name);
+    if (exists $col_abstract->{REFERENCES}) {
+      my $new_fk = dclone($col_abstract->{REFERENCES});
+      $new_fk->{created}     = 0;
+      $new_def->{REFERENCES} = $new_fk;
     }
+
+    $self->_bz_real_schema->set_column($table, $name, $new_def);
+    $self->_bz_store_real_schema;
+  }
 }
 
 sub bz_add_fk {
-    my ($self, $table, $column, $def) = @_;
-    $self->bz_add_fks($table, { $column => $def });
+  my ($self, $table, $column, $def) = @_;
+  $self->bz_add_fks($table, {$column => $def});
 }
 
 sub bz_add_fks {
-    my ($self, $table, $column_fks, $options) = @_;
-
-    my %add_these;
-    foreach my $column (keys %$column_fks) {
-        my $current_fk = $self->bz_fk_info($table, $column);
-        next if ($current_fk and $current_fk->{created});
-        my $new_fk = $column_fks->{$column};
-        $self->_check_references($table, $column, $new_fk);
-        $add_these{$column} = $new_fk;
-        if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE 
-            and !$options->{silently}) 
-        {
-            print get_text('install_fk_add',
-                           { table => $table, column => $column, 
-                             fk    => $new_fk }), "\n";
-        }
+  my ($self, $table, $column_fks, $options) = @_;
+
+  my %add_these;
+  foreach my $column (keys %$column_fks) {
+    my $current_fk = $self->bz_fk_info($table, $column);
+    next if ($current_fk and $current_fk->{created});
+    my $new_fk = $column_fks->{$column};
+    $self->_check_references($table, $column, $new_fk);
+    $add_these{$column} = $new_fk;
+    if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE and !$options->{silently}) {
+      print get_text(
+        'install_fk_add', {table => $table, column => $column, fk => $new_fk}
+        ),
+        "\n";
     }
+  }
 
-    return if !scalar(keys %add_these);
+  return if !scalar(keys %add_these);
 
-    my @sql = $self->_bz_real_schema->get_add_fks_sql($table, \%add_these);
-    $self->do($_) foreach @sql;
+  my @sql = $self->_bz_real_schema->get_add_fks_sql($table, \%add_these);
+  $self->do($_) foreach @sql;
 
-    foreach my $column (keys %add_these) {
-        my $fk_def = $add_these{$column};
-        $fk_def->{created} = 1;
-        $self->_bz_real_schema->set_fk($table, $column, $fk_def);
-    }
+  foreach my $column (keys %add_these) {
+    my $fk_def = $add_these{$column};
+    $fk_def->{created} = 1;
+    $self->_bz_real_schema->set_fk($table, $column, $fk_def);
+  }
 
-    $self->_bz_store_real_schema();
+  $self->_bz_store_real_schema();
 }
 
 sub bz_alter_column {
-    my ($self, $table, $name, $new_def, $set_nulls_to) = @_;
+  my ($self, $table, $name, $new_def, $set_nulls_to) = @_;
 
-    my $current_def = $self->bz_column_info($table, $name);
+  my $current_def = $self->bz_column_info($table, $name);
 
-    if (!$self->_bz_schema->columns_equal($current_def, $new_def)) {
-        # You can't change a column to be NOT NULL if you have no DEFAULT
-        # and no value for $set_nulls_to, if there are any NULL values 
-        # in that column.
-        if ($new_def->{NOTNULL} && 
-            !exists $new_def->{DEFAULT} && !defined $set_nulls_to)
-        {
-            # Check for NULLs
-            my $any_nulls = $self->selectrow_array(
-                "SELECT 1 FROM $table WHERE $name IS NULL");
-            ThrowCodeError('column_not_null_no_default_alter', 
-                           { name => "$table.$name" }) if ($any_nulls);
-        }
-        # Preserve foreign key definitions in the Schema object when altering
-        # types.
-        if (my $fk = $self->bz_fk_info($table, $name)) {
-            $new_def->{REFERENCES} = $fk;
-        }
-        $self->bz_alter_column_raw($table, $name, $new_def, $current_def,
-                                   $set_nulls_to);
-        $self->_bz_real_schema->set_column($table, $name, $new_def);
-        $self->_bz_store_real_schema;
+  if (!$self->_bz_schema->columns_equal($current_def, $new_def)) {
+
+    # You can't change a column to be NOT NULL if you have no DEFAULT
+    # and no value for $set_nulls_to, if there are any NULL values
+    # in that column.
+    if ( $new_def->{NOTNULL}
+      && !exists $new_def->{DEFAULT}
+      && !defined $set_nulls_to)
+    {
+      # Check for NULLs
+      my $any_nulls
+        = $self->selectrow_array("SELECT 1 FROM $table WHERE $name IS NULL");
+      ThrowCodeError('column_not_null_no_default_alter', {name => "$table.$name"})
+        if ($any_nulls);
+    }
+
+    # Preserve foreign key definitions in the Schema object when altering
+    # types.
+    if (my $fk = $self->bz_fk_info($table, $name)) {
+      $new_def->{REFERENCES} = $fk;
     }
+    $self->bz_alter_column_raw($table, $name, $new_def, $current_def,
+      $set_nulls_to);
+    $self->_bz_real_schema->set_column($table, $name, $new_def);
+    $self->_bz_store_real_schema;
+  }
 }
 
 
@@ -709,39 +723,40 @@ sub bz_alter_column {
 # Returns:     nothing
 #
 sub bz_alter_column_raw {
-    my ($self, $table, $name, $new_def, $current_def, $set_nulls_to) = @_;
-    my @statements = $self->_bz_real_schema->get_alter_column_ddl(
-        $table, $name, $new_def,
-        defined $set_nulls_to ? $self->quote($set_nulls_to) : undef);
-    my $new_ddl = $self->_bz_schema->get_type_ddl($new_def);
-    say "Updating column $name in table $table ...";
-    if (defined $current_def) {
-        my $old_ddl = $self->_bz_schema->get_type_ddl($current_def);
-        say "Old: $old_ddl";
-    }
-    say "New: $new_ddl";
-    $self->do($_) foreach (@statements);
+  my ($self, $table, $name, $new_def, $current_def, $set_nulls_to) = @_;
+  my @statements
+    = $self->_bz_real_schema->get_alter_column_ddl($table, $name, $new_def,
+    defined $set_nulls_to ? $self->quote($set_nulls_to) : undef);
+  my $new_ddl = $self->_bz_schema->get_type_ddl($new_def);
+  say "Updating column $name in table $table ...";
+  if (defined $current_def) {
+    my $old_ddl = $self->_bz_schema->get_type_ddl($current_def);
+    say "Old: $old_ddl";
+  }
+  say "New: $new_ddl";
+  $self->do($_) foreach (@statements);
 }
 
 sub bz_alter_fk {
-    my ($self, $table, $column, $fk_def) = @_;
-    my $current_fk = $self->bz_fk_info($table, $column);
-    ThrowCodeError('column_alter_nonexistent_fk',
-                   { table => $table, column => $column }) if !$current_fk;
-    $self->bz_drop_fk($table, $column);
-    $self->bz_add_fk($table, $column, $fk_def);
+  my ($self, $table, $column, $fk_def) = @_;
+  my $current_fk = $self->bz_fk_info($table, $column);
+  ThrowCodeError('column_alter_nonexistent_fk',
+    {table => $table, column => $column})
+    if !$current_fk;
+  $self->bz_drop_fk($table, $column);
+  $self->bz_add_fk($table, $column, $fk_def);
 }
 
 sub bz_add_index {
-    my ($self, $table, $name, $definition) = @_;
+  my ($self, $table, $name, $definition) = @_;
 
-    my $index_exists = $self->bz_index_info($table, $name);
+  my $index_exists = $self->bz_index_info($table, $name);
 
-    if (!$index_exists) {
-        $self->bz_add_index_raw($table, $name, $definition);
-        $self->_bz_real_schema->set_index($table, $name, $definition);
-        $self->_bz_store_real_schema;
-    }
+  if (!$index_exists) {
+    $self->bz_add_index_raw($table, $name, $definition);
+    $self->_bz_real_schema->set_index($table, $name, $definition);
+    $self->_bz_store_real_schema;
+  }
 }
 
 # bz_add_index_raw($table, $name, $silent)
@@ -761,36 +776,36 @@ sub bz_add_index {
 # Returns:     nothing
 #
 sub bz_add_index_raw {
-    my ($self, $table, $name, $definition, $silent) = @_;
-    my @statements = $self->_bz_schema->get_add_index_ddl(
-        $table, $name, $definition);
-    print "Adding new index '$name' to the $table table ...\n" unless $silent;
-    $self->do($_) foreach (@statements);
+  my ($self, $table, $name, $definition, $silent) = @_;
+  my @statements
+    = $self->_bz_schema->get_add_index_ddl($table, $name, $definition);
+  print "Adding new index '$name' to the $table table ...\n" unless $silent;
+  $self->do($_) foreach (@statements);
 }
 
 sub bz_add_table {
-    my ($self, $name, $options) = @_;
-
-    my $table_exists = $self->bz_table_info($name);
-
-    if (!$table_exists) {
-        $self->_bz_add_table_raw($name, $options);
-        my $table_def = dclone($self->_bz_schema->get_table_abstract($name));
-
-        my %fields = @{$table_def->{FIELDS}};
-        foreach my $col (keys %fields) {
-            # Foreign Key references have to be added by Install::DB after
-            # initial table creation, because column names have changed
-            # over history and it's impossible to keep track of that info
-            # in ABSTRACT_SCHEMA.
-            next unless exists $fields{$col}->{REFERENCES};
-            $fields{$col}->{REFERENCES}->{created} =
-                $self->_bz_real_schema->FK_ON_CREATE;
-        }
-        
-        $self->_bz_real_schema->add_table($name, $table_def);
-        $self->_bz_store_real_schema;
+  my ($self, $name, $options) = @_;
+
+  my $table_exists = $self->bz_table_info($name);
+
+  if (!$table_exists) {
+    $self->_bz_add_table_raw($name, $options);
+    my $table_def = dclone($self->_bz_schema->get_table_abstract($name));
+
+    my %fields = @{$table_def->{FIELDS}};
+    foreach my $col (keys %fields) {
+
+      # Foreign Key references have to be added by Install::DB after
+      # initial table creation, because column names have changed
+      # over history and it's impossible to keep track of that info
+      # in ABSTRACT_SCHEMA.
+      next unless exists $fields{$col}->{REFERENCES};
+      $fields{$col}->{REFERENCES}->{created} = $self->_bz_real_schema->FK_ON_CREATE;
     }
+
+    $self->_bz_real_schema->add_table($name, $table_def);
+    $self->_bz_store_real_schema;
+  }
 }
 
 # _bz_add_table_raw($name) - Private
@@ -802,164 +817,174 @@ sub bz_add_table {
 #              _bz_init_schema_storage. Used when you don't
 #              yet have a Schema object but you need to
 #              add a table, for some reason.
-# Params:      $name - The name of the table you're creating. 
-#                  The definition for the table is pulled from 
+# Params:      $name - The name of the table you're creating.
+#                  The definition for the table is pulled from
 #                  _bz_schema.
 # Returns:     nothing
 #
 sub _bz_add_table_raw {
-    my ($self, $name, $options) = @_;
-    my @statements = $self->_bz_schema->get_table_ddl($name);
-    if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE
-        and !$options->{silently})
-    {
-        say install_string('db_table_new', { table => $name });
-    }
-    $self->do($_) foreach (@statements);
+  my ($self, $name, $options) = @_;
+  my @statements = $self->_bz_schema->get_table_ddl($name);
+  if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE and !$options->{silently}) {
+    say install_string('db_table_new', {table => $name});
+  }
+  $self->do($_) foreach (@statements);
 }
 
 sub _bz_add_field_table {
-    my ($self, $name, $schema_ref) = @_;
-    # We do nothing if the table already exists.
-    return if $self->bz_table_info($name);
-
-    # Copy this so that we're not modifying the passed reference.
-    # (This avoids modifying a constant in Bugzilla::DB::Schema.)
-    my %table_schema = %$schema_ref;
-    my %indexes = @{ $table_schema{INDEXES} };
-    my %fixed_indexes;
-    foreach my $key (keys %indexes) {
-        $fixed_indexes{$name . "_" . $key} = $indexes{$key};
-    }
-    # INDEXES is supposed to be an arrayref, so we have to convert back.
-    my @indexes_array = %fixed_indexes;
-    $table_schema{INDEXES} = \@indexes_array;
-    # We add this to the abstract schema so that bz_add_table can find it.
-    $self->_bz_schema->add_table($name, \%table_schema);
-    $self->bz_add_table($name);
+  my ($self, $name, $schema_ref) = @_;
+
+  # We do nothing if the table already exists.
+  return if $self->bz_table_info($name);
+
+  # Copy this so that we're not modifying the passed reference.
+  # (This avoids modifying a constant in Bugzilla::DB::Schema.)
+  my %table_schema = %$schema_ref;
+  my %indexes      = @{$table_schema{INDEXES}};
+  my %fixed_indexes;
+  foreach my $key (keys %indexes) {
+    $fixed_indexes{$name . "_" . $key} = $indexes{$key};
+  }
+
+  # INDEXES is supposed to be an arrayref, so we have to convert back.
+  my @indexes_array = %fixed_indexes;
+  $table_schema{INDEXES} = \@indexes_array;
+
+  # We add this to the abstract schema so that bz_add_table can find it.
+  $self->_bz_schema->add_table($name, \%table_schema);
+  $self->bz_add_table($name);
 }
 
 sub bz_add_field_tables {
-    my ($self, $field) = @_;
-    
-    $self->_bz_add_field_table($field->name,
-                               $self->_bz_schema->FIELD_TABLE_SCHEMA, $field->type);
-    if ($field->type == FIELD_TYPE_MULTI_SELECT) {
-        my $ms_table = "bug_" . $field->name;
-        $self->_bz_add_field_table($ms_table,
-            $self->_bz_schema->MULTI_SELECT_VALUE_TABLE);
-
-        $self->bz_add_fks($ms_table, 
-            { bug_id => {TABLE => 'bugs', COLUMN => 'bug_id',
-                         DELETE => 'CASCADE'},
-
-              value  => {TABLE  => $field->name, COLUMN => 'value'} });
-    }
+  my ($self, $field) = @_;
+
+  $self->_bz_add_field_table($field->name, $self->_bz_schema->FIELD_TABLE_SCHEMA,
+    $field->type);
+  if ($field->type == FIELD_TYPE_MULTI_SELECT) {
+    my $ms_table = "bug_" . $field->name;
+    $self->_bz_add_field_table($ms_table,
+      $self->_bz_schema->MULTI_SELECT_VALUE_TABLE);
+
+    $self->bz_add_fks(
+      $ms_table,
+      {
+        bug_id => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'},
+
+        value => {TABLE => $field->name, COLUMN => 'value'}
+      }
+    );
+  }
 }
 
 sub bz_drop_field_tables {
-    my ($self, $field) = @_;
-    if ($field->type == FIELD_TYPE_MULTI_SELECT) {
-        $self->bz_drop_table('bug_' . $field->name);
-    }
-    $self->bz_drop_table($field->name);
+  my ($self, $field) = @_;
+  if ($field->type == FIELD_TYPE_MULTI_SELECT) {
+    $self->bz_drop_table('bug_' . $field->name);
+  }
+  $self->bz_drop_table($field->name);
 }
 
 sub bz_drop_column {
-    my ($self, $table, $column) = @_;
-
-    my $current_def = $self->bz_column_info($table, $column);
-
-    if ($current_def) {
-        my @statements = $self->_bz_real_schema->get_drop_column_ddl(
-            $table, $column);
-        print get_text('install_column_drop', 
-                       { table => $table, column => $column }) . "\n"
-            if Bugzilla->usage_mode == USAGE_MODE_CMDLINE;
-        foreach my $sql (@statements) {
-            # Because this is a deletion, we don't want to die hard if
-            # we fail because of some local customization. If something
-            # is already gone, that's fine with us!
-            eval { $self->do($sql); } or warn "Failed SQL: [$sql] Error: $@";
-        }
-        $self->_bz_real_schema->delete_column($table, $column);
-        $self->_bz_store_real_schema;
+  my ($self, $table, $column) = @_;
+
+  my $current_def = $self->bz_column_info($table, $column);
+
+  if ($current_def) {
+    my @statements = $self->_bz_real_schema->get_drop_column_ddl($table, $column);
+    print get_text('install_column_drop', {table => $table, column => $column})
+      . "\n"
+      if Bugzilla->usage_mode == USAGE_MODE_CMDLINE;
+    foreach my $sql (@statements) {
+
+      # Because this is a deletion, we don't want to die hard if
+      # we fail because of some local customization. If something
+      # is already gone, that's fine with us!
+      eval { $self->do($sql); } or warn "Failed SQL: [$sql] Error: $@";
     }
+    $self->_bz_real_schema->delete_column($table, $column);
+    $self->_bz_store_real_schema;
+  }
 }
 
 sub bz_drop_fk {
-    my ($self, $table, $column) = @_;
-
-    my $fk_def = $self->bz_fk_info($table, $column);
-    if ($fk_def and $fk_def->{created}) {
-        print get_text('install_fk_drop',
-                       { table => $table, column => $column, fk => $fk_def })
-            . "\n" if Bugzilla->usage_mode == USAGE_MODE_CMDLINE;
-        my @statements = 
-            $self->_bz_real_schema->get_drop_fk_sql($table, $column, $fk_def);
-        foreach my $sql (@statements) {
-            # Because this is a deletion, we don't want to die hard if
-            # we fail because of some local customization. If something
-            # is already gone, that's fine with us!
-            eval { $self->do($sql); } or warn "Failed SQL: [$sql] Error: $@";
-        }
-        # Under normal circumstances, we don't permanently drop the fk--
-        # we want checksetup to re-create it again later. The only
-        # time that FKs get permanently dropped is if the column gets
-        # dropped.
-        $fk_def->{created} = 0;
-        $self->_bz_real_schema->set_fk($table, $column, $fk_def);
-        $self->_bz_store_real_schema;
+  my ($self, $table, $column) = @_;
+
+  my $fk_def = $self->bz_fk_info($table, $column);
+  if ($fk_def and $fk_def->{created}) {
+    print get_text('install_fk_drop',
+      {table => $table, column => $column, fk => $fk_def})
+      . "\n"
+      if Bugzilla->usage_mode == USAGE_MODE_CMDLINE;
+    my @statements
+      = $self->_bz_real_schema->get_drop_fk_sql($table, $column, $fk_def);
+    foreach my $sql (@statements) {
+
+      # Because this is a deletion, we don't want to die hard if
+      # we fail because of some local customization. If something
+      # is already gone, that's fine with us!
+      eval { $self->do($sql); } or warn "Failed SQL: [$sql] Error: $@";
     }
 
+    # Under normal circumstances, we don't permanently drop the fk--
+    # we want checksetup to re-create it again later. The only
+    # time that FKs get permanently dropped is if the column gets
+    # dropped.
+    $fk_def->{created} = 0;
+    $self->_bz_real_schema->set_fk($table, $column, $fk_def);
+    $self->_bz_store_real_schema;
+  }
+
 }
 
 sub bz_get_related_fks {
-    my ($self, $table, $column) = @_;
-    my @tables = $self->_bz_real_schema->get_table_list();
-    my @related;
-    foreach my $check_table (@tables) {
-        my @columns = $self->bz_table_columns($check_table);
-        foreach my $check_column (@columns) {
-            my $fk = $self->bz_fk_info($check_table, $check_column);
-            if ($fk 
-                and (($fk->{TABLE} eq $table and $fk->{COLUMN} eq $column)
-                     or ($check_column eq $column and $check_table eq $table)))
-            {
-                push(@related, [$check_table, $check_column, $fk]);
-            }
-        } # foreach $column
-    } # foreach $table
-
-    return \@related;
+  my ($self, $table, $column) = @_;
+  my @tables = $self->_bz_real_schema->get_table_list();
+  my @related;
+  foreach my $check_table (@tables) {
+    my @columns = $self->bz_table_columns($check_table);
+    foreach my $check_column (@columns) {
+      my $fk = $self->bz_fk_info($check_table, $check_column);
+      if (
+        $fk
+        and (($fk->{TABLE} eq $table and $fk->{COLUMN} eq $column)
+          or ($check_column eq $column and $check_table eq $table))
+        )
+      {
+        push(@related, [$check_table, $check_column, $fk]);
+      }
+    }    # foreach $column
+  }    # foreach $table
+
+  return \@related;
 }
 
 sub bz_drop_related_fks {
-    my $self = shift;
-    my $related = $self->bz_get_related_fks(@_);
-    foreach my $item (@$related) {
-        my ($table, $column) = @$item;
-        $self->bz_drop_fk($table, $column);
-    }
-    return $related;
+  my $self    = shift;
+  my $related = $self->bz_get_related_fks(@_);
+  foreach my $item (@$related) {
+    my ($table, $column) = @$item;
+    $self->bz_drop_fk($table, $column);
+  }
+  return $related;
 }
 
 sub bz_drop_index {
-    my ($self, $table, $name) = @_;
+  my ($self, $table, $name) = @_;
 
-    my $index_exists = $self->bz_index_info($table, $name);
+  my $index_exists = $self->bz_index_info($table, $name);
 
-    if ($index_exists) {
-        if ($self->INDEX_DROPS_REQUIRE_FK_DROPS) {
-            # We cannot delete an index used by a FK.
-            foreach my $column (@{$index_exists->{FIELDS}}) {
-                $self->bz_drop_related_fks($table, $column);
-            }
-        }
-        $self->bz_drop_index_raw($table, $name);
-        $self->_bz_real_schema->delete_index($table, $name);
-        $self->_bz_store_real_schema;        
+  if ($index_exists) {
+    if ($self->INDEX_DROPS_REQUIRE_FK_DROPS) {
+
+      # We cannot delete an index used by a FK.
+      foreach my $column (@{$index_exists->{FIELDS}}) {
+        $self->bz_drop_related_fks($table, $column);
+      }
     }
+    $self->bz_drop_index_raw($table, $name);
+    $self->_bz_real_schema->delete_index($table, $name);
+    $self->_bz_store_real_schema;
+  }
 }
 
 # bz_drop_index_raw($table, $name, $silent)
@@ -968,7 +993,7 @@ sub bz_drop_index {
 #              Drops an index from the database
 #              without updating any Schema object. Generally
 #              should only be called by bz_drop_index.
-#              Used when either: (1) You don't yet have a Schema 
+#              Used when either: (1) You don't yet have a Schema
 #              object but you need to drop an index, for some reason.
 #              (2) You need to drop an index that somehow got into the
 #              database but doesn't exist in Schema.
@@ -979,108 +1004,111 @@ sub bz_drop_index {
 # Returns:     nothing
 #
 sub bz_drop_index_raw {
-    my ($self, $table, $name, $silent) = @_;
-    my @statements = $self->_bz_schema->get_drop_index_ddl(
-        $table, $name);
-    print "Removing index '$name' from the $table table...\n" unless $silent;
-    foreach my $sql (@statements) {
-        # Because this is a deletion, we don't want to die hard if
-        # we fail because of some local customization. If something
-        # is already gone, that's fine with us!
-        eval { $self->do($sql) } or warn "Failed SQL: [$sql] Error: $@";
-    }
+  my ($self, $table, $name, $silent) = @_;
+  my @statements = $self->_bz_schema->get_drop_index_ddl($table, $name);
+  print "Removing index '$name' from the $table table...\n" unless $silent;
+  foreach my $sql (@statements) {
+
+    # Because this is a deletion, we don't want to die hard if
+    # we fail because of some local customization. If something
+    # is already gone, that's fine with us!
+    eval { $self->do($sql) } or warn "Failed SQL: [$sql] Error: $@";
+  }
 }
 
 sub bz_drop_table {
-    my ($self, $name) = @_;
-
-    my $table_exists = $self->bz_table_info($name);
-
-    if ($table_exists) {
-        my @statements = $self->_bz_schema->get_drop_table_ddl($name);
-        print get_text('install_table_drop', { name => $name }) . "\n"
-            if Bugzilla->usage_mode == USAGE_MODE_CMDLINE;
-        foreach my $sql (@statements) {
-            # Because this is a deletion, we don't want to die hard if
-            # we fail because of some local customization. If something
-            # is already gone, that's fine with us!
-            eval { $self->do($sql); } or warn "Failed SQL: [$sql] Error: $@";
-        }
-        $self->_bz_real_schema->delete_table($name);
-        $self->_bz_store_real_schema;
+  my ($self, $name) = @_;
+
+  my $table_exists = $self->bz_table_info($name);
+
+  if ($table_exists) {
+    my @statements = $self->_bz_schema->get_drop_table_ddl($name);
+    print get_text('install_table_drop', {name => $name}) . "\n"
+      if Bugzilla->usage_mode == USAGE_MODE_CMDLINE;
+    foreach my $sql (@statements) {
+
+      # Because this is a deletion, we don't want to die hard if
+      # we fail because of some local customization. If something
+      # is already gone, that's fine with us!
+      eval { $self->do($sql); } or warn "Failed SQL: [$sql] Error: $@";
     }
+    $self->_bz_real_schema->delete_table($name);
+    $self->_bz_store_real_schema;
+  }
 }
 
 sub bz_fk_info {
-    my ($self, $table, $column) = @_;
-    my $col_info = $self->bz_column_info($table, $column);
-    return undef if !$col_info;
-    my $fk = $col_info->{REFERENCES};
-    return $fk;
+  my ($self, $table, $column) = @_;
+  my $col_info = $self->bz_column_info($table, $column);
+  return undef if !$col_info;
+  my $fk = $col_info->{REFERENCES};
+  return $fk;
 }
 
 sub bz_rename_column {
-    my ($self, $table, $old_name, $new_name) = @_;
+  my ($self, $table, $old_name, $new_name) = @_;
 
-    my $old_col_exists  = $self->bz_column_info($table, $old_name);
+  my $old_col_exists = $self->bz_column_info($table, $old_name);
 
-    if ($old_col_exists) {
-        my $already_renamed = $self->bz_column_info($table, $new_name);
-            ThrowCodeError('db_rename_conflict',
-                           { old => "$table.$old_name", 
-                             new => "$table.$new_name" }) if $already_renamed;
-        my @statements = $self->_bz_real_schema->get_rename_column_ddl(
-            $table, $old_name, $new_name);
+  if ($old_col_exists) {
+    my $already_renamed = $self->bz_column_info($table, $new_name);
+    ThrowCodeError('db_rename_conflict',
+      {old => "$table.$old_name", new => "$table.$new_name"})
+      if $already_renamed;
+    my @statements
+      = $self->_bz_real_schema->get_rename_column_ddl($table, $old_name, $new_name);
 
-        print get_text('install_column_rename', 
-                       { old => "$table.$old_name", new => "$table.$new_name" })
-               . "\n" if Bugzilla->usage_mode == USAGE_MODE_CMDLINE;
+    print get_text('install_column_rename',
+      {old => "$table.$old_name", new => "$table.$new_name"})
+      . "\n"
+      if Bugzilla->usage_mode == USAGE_MODE_CMDLINE;
 
-        foreach my $sql (@statements) {
-            $self->do($sql);
-        }
-        $self->_bz_real_schema->rename_column($table, $old_name, $new_name);
-        $self->_bz_store_real_schema;
+    foreach my $sql (@statements) {
+      $self->do($sql);
     }
+    $self->_bz_real_schema->rename_column($table, $old_name, $new_name);
+    $self->_bz_store_real_schema;
+  }
 }
 
 sub bz_rename_table {
-    my ($self, $old_name, $new_name) = @_;
-    my $old_table = $self->bz_table_info($old_name);
-    return if !$old_table;
-
-    my $new = $self->bz_table_info($new_name);
-    ThrowCodeError('db_rename_conflict', { old => $old_name,
-                                           new => $new_name }) if $new;
-
-    # FKs will all have the wrong names unless we drop and then let them
-    # be re-created later. Under normal circumstances, checksetup.pl will
-    # automatically re-create these dropped FKs at the end of its DB upgrade
-    # run, so we don't need to re-create them in this method.
-    my @columns = $self->bz_table_columns($old_name);
-    foreach my $column (@columns) {
-        # these just return silently if there's no FK to drop
-        $self->bz_drop_fk($old_name, $column);
-        $self->bz_drop_related_fks($old_name, $column);
-    }
-
-    my @sql = $self->_bz_real_schema->get_rename_table_sql($old_name, $new_name);
-    print get_text('install_table_rename', 
-                   { old => $old_name, new => $new_name }) . "\n"
-        if Bugzilla->usage_mode == USAGE_MODE_CMDLINE;
-    $self->do($_) foreach @sql;
-    $self->_bz_real_schema->rename_table($old_name, $new_name);
-    $self->_bz_store_real_schema;
+  my ($self, $old_name, $new_name) = @_;
+  my $old_table = $self->bz_table_info($old_name);
+  return if !$old_table;
+
+  my $new = $self->bz_table_info($new_name);
+  ThrowCodeError('db_rename_conflict', {old => $old_name, new => $new_name})
+    if $new;
+
+  # FKs will all have the wrong names unless we drop and then let them
+  # be re-created later. Under normal circumstances, checksetup.pl will
+  # automatically re-create these dropped FKs at the end of its DB upgrade
+  # run, so we don't need to re-create them in this method.
+  my @columns = $self->bz_table_columns($old_name);
+  foreach my $column (@columns) {
+
+    # these just return silently if there's no FK to drop
+    $self->bz_drop_fk($old_name, $column);
+    $self->bz_drop_related_fks($old_name, $column);
+  }
+
+  my @sql = $self->_bz_real_schema->get_rename_table_sql($old_name, $new_name);
+  print get_text('install_table_rename', {old => $old_name, new => $new_name})
+    . "\n"
+    if Bugzilla->usage_mode == USAGE_MODE_CMDLINE;
+  $self->do($_) foreach @sql;
+  $self->_bz_real_schema->rename_table($old_name, $new_name);
+  $self->_bz_store_real_schema;
 }
 
 sub bz_set_next_serial_value {
-    my ($self, $table, $column, $value) = @_;
-    if (!$value) {
-        $value = $self->selectrow_array("SELECT MAX($column) FROM $table") || 0;
-        $value++;
-    }
-    my @sql = $self->_bz_real_schema->get_set_serial_sql($table, $column, $value);
-    $self->do($_) foreach @sql;
+  my ($self, $table, $column, $value) = @_;
+  if (!$value) {
+    $value = $self->selectrow_array("SELECT MAX($column) FROM $table") || 0;
+    $value++;
+  }
+  my @sql = $self->_bz_real_schema->get_set_serial_sql($table, $column, $value);
+  $self->do($_) foreach @sql;
 }
 
 #####################################################################
@@ -1088,12 +1116,12 @@ sub bz_set_next_serial_value {
 #####################################################################
 
 sub _bz_schema {
-    my ($self) = @_;
-    return $self->{private_bz_schema} if exists $self->{private_bz_schema};
-    my @module_parts = split('::', ref $self);
-    my $module_name  = pop @module_parts;
-    $self->{private_bz_schema} = Bugzilla::DB::Schema->new($module_name);
-    return $self->{private_bz_schema};
+  my ($self) = @_;
+  return $self->{private_bz_schema} if exists $self->{private_bz_schema};
+  my @module_parts = split('::', ref $self);
+  my $module_name = pop @module_parts;
+  $self->{private_bz_schema} = Bugzilla::DB::Schema->new($module_name);
+  return $self->{private_bz_schema};
 }
 
 # _bz_get_initial_schema()
@@ -1107,53 +1135,54 @@ sub _bz_schema {
 # Returns:     A Schema object that can be serialized and written to disk
 #              for _bz_init_schema_storage.
 sub _bz_get_initial_schema {
-    my ($self) = @_;
-    return $self->_bz_schema->get_empty_schema();
+  my ($self) = @_;
+  return $self->_bz_schema->get_empty_schema();
 }
 
 sub bz_column_info {
-    my ($self, $table, $column) = @_;
-    my $def = $self->_bz_real_schema->get_column_abstract($table, $column);
-    # We dclone it so callers can't modify the Schema.
-    $def = dclone($def) if defined $def;
-    return $def;
+  my ($self, $table, $column) = @_;
+  my $def = $self->_bz_real_schema->get_column_abstract($table, $column);
+
+  # We dclone it so callers can't modify the Schema.
+  $def = dclone($def) if defined $def;
+  return $def;
 }
 
 sub bz_index_info {
-    my ($self, $table, $index) = @_;
-    my $index_def =
-        $self->_bz_real_schema->get_index_abstract($table, $index);
-    if (ref($index_def) eq 'ARRAY') {
-        $index_def = {FIELDS => $index_def, TYPE => ''};
-    }
-    return $index_def;
+  my ($self, $table, $index) = @_;
+  my $index_def = $self->_bz_real_schema->get_index_abstract($table, $index);
+  if (ref($index_def) eq 'ARRAY') {
+    $index_def = {FIELDS => $index_def, TYPE => ''};
+  }
+  return $index_def;
 }
 
 sub bz_table_info {
-    my ($self, $table) = @_;
-    return $self->_bz_real_schema->get_table_abstract($table);
+  my ($self, $table) = @_;
+  return $self->_bz_real_schema->get_table_abstract($table);
 }
 
 
 sub bz_table_columns {
-    my ($self, $table) = @_;
-    return $self->_bz_real_schema->get_table_columns($table);
+  my ($self, $table) = @_;
+  return $self->_bz_real_schema->get_table_columns($table);
 }
 
 sub bz_table_indexes {
-    my ($self, $table) = @_;
-    my $indexes = $self->_bz_real_schema->get_table_indexes_abstract($table);
-    my %return_indexes;
-    # We do this so that they're always hashes.
-    foreach my $name (keys %$indexes) {
-        $return_indexes{$name} = $self->bz_index_info($table, $name);
-    }
-    return \%return_indexes;
+  my ($self, $table) = @_;
+  my $indexes = $self->_bz_real_schema->get_table_indexes_abstract($table);
+  my %return_indexes;
+
+  # We do this so that they're always hashes.
+  foreach my $name (keys %$indexes) {
+    $return_indexes{$name} = $self->bz_index_info($table, $name);
+  }
+  return \%return_indexes;
 }
 
 sub bz_table_list {
-    my ($self) = @_;
-    return $self->_bz_real_schema->get_table_list();
+  my ($self) = @_;
+  return $self->_bz_real_schema->get_table_list();
 }
 
 #####################################################################
@@ -1172,9 +1201,9 @@ sub bz_table_list {
 # Returns:     An array of column names.
 #
 sub bz_table_columns_real {
-    my ($self, $table) = @_;
-    my $sth = $self->column_info(undef, undef, $table, '%');
-    return @{ $self->selectcol_arrayref($sth, {Columns => [4]}) };
+  my ($self, $table) = @_;
+  my $sth = $self->column_info(undef, undef, $table, '%');
+  return @{$self->selectcol_arrayref($sth, {Columns => [4]})};
 }
 
 # bz_table_list_real()
@@ -1184,27 +1213,25 @@ sub bz_table_columns_real {
 # Params:      none
 # Returns:     An array containing table names.
 sub bz_table_list_real {
-    my ($self) = @_;
-    my $table_sth = $self->table_info(undef, undef, undef, "TABLE");
-    return @{$self->selectcol_arrayref($table_sth, { Columns => [3] })};
+  my ($self) = @_;
+  my $table_sth = $self->table_info(undef, undef, undef, "TABLE");
+  return @{$self->selectcol_arrayref($table_sth, {Columns => [3]})};
 }
 
 sub bz_column_length {
-    my ( $self, $table, $column ) = @_;
-    state %column_length;
+  my ($self, $table, $column) = @_;
+  state %column_length;
 
-    if ( not exists $column_length{$table} ) {
-        my $csr = $self->column_info( undef, undef, $table, undef );
-        my $cols =
-          $csr->fetchall_arrayref( { COLUMN_NAME => 1, COLUMN_SIZE => 1 } );
+  if (not exists $column_length{$table}) {
+    my $csr = $self->column_info(undef, undef, $table, undef);
+    my $cols = $csr->fetchall_arrayref({COLUMN_NAME => 1, COLUMN_SIZE => 1});
 
-        foreach my $col (@$cols) {
-            $column_length{$table}{ $col->{COLUMN_NAME} } = $col->{COLUMN_SIZE}
-              // MAX_INT_32;
-        }
+    foreach my $col (@$cols) {
+      $column_length{$table}{$col->{COLUMN_NAME}} = $col->{COLUMN_SIZE} // MAX_INT_32;
     }
+  }
 
-    return $column_length{$table}{$column} // 0;
+  return $column_length{$table}{$column} // 0;
 }
 
 #####################################################################
@@ -1212,55 +1239,59 @@ sub bz_column_length {
 #####################################################################
 
 sub bz_in_transaction {
-    return $_[0]->{private_bz_transaction_count} ? 1 : 0;
+  return $_[0]->{private_bz_transaction_count} ? 1 : 0;
 }
 
 sub bz_start_transaction {
-    my ($self) = @_;
-
-    if ($self->bz_in_transaction) {
-        $self->{private_bz_transaction_count}++;
-    } else {
-        # Turn AutoCommit off and start a new transaction
-        $self->begin_work();
-        # REPEATABLE READ means "We work on a snapshot of the DB that
-        # is created when we execute our first SQL statement." It's
-        # what we need in Bugzilla to be safe, for what we do.
-        # Different DBs have different defaults for their isolation
-        # level, so we just set it here manually.
-        if ($self->ISOLATION_LEVEL) {
-            $self->do('SET TRANSACTION ISOLATION LEVEL ' 
-                      . $self->ISOLATION_LEVEL);
-        }
-        $self->{private_bz_transaction_count} = 1;
+  my ($self) = @_;
+
+  if ($self->bz_in_transaction) {
+    $self->{private_bz_transaction_count}++;
+  }
+  else {
+    # Turn AutoCommit off and start a new transaction
+    $self->begin_work();
+
+    # REPEATABLE READ means "We work on a snapshot of the DB that
+    # is created when we execute our first SQL statement." It's
+    # what we need in Bugzilla to be safe, for what we do.
+    # Different DBs have different defaults for their isolation
+    # level, so we just set it here manually.
+    if ($self->ISOLATION_LEVEL) {
+      $self->do('SET TRANSACTION ISOLATION LEVEL ' . $self->ISOLATION_LEVEL);
     }
+    $self->{private_bz_transaction_count} = 1;
+  }
 }
 
 sub bz_commit_transaction {
-    my ($self) = @_;
-
-    if ($self->{private_bz_transaction_count} > 1) {
-        $self->{private_bz_transaction_count}--;
-    } elsif ($self->bz_in_transaction) {
-        $self->commit();
-        $self->{private_bz_transaction_count} = 0;
-        Bugzilla::Mailer->send_staged_mail();
-    } else {
-       ThrowCodeError('not_in_transaction');
-    }
+  my ($self) = @_;
+
+  if ($self->{private_bz_transaction_count} > 1) {
+    $self->{private_bz_transaction_count}--;
+  }
+  elsif ($self->bz_in_transaction) {
+    $self->commit();
+    $self->{private_bz_transaction_count} = 0;
+    Bugzilla::Mailer->send_staged_mail();
+  }
+  else {
+    ThrowCodeError('not_in_transaction');
+  }
 }
 
 sub bz_rollback_transaction {
-    my ($self) = @_;
-
-    # Unlike start and commit, if we rollback at any point it happens
-    # instantly, even if we're in a nested transaction.
-    if (!$self->bz_in_transaction) {
-        ThrowCodeError("not_in_transaction");
-    } else {
-        $self->rollback();
-        $self->{private_bz_transaction_count} = 0;
-    }
+  my ($self) = @_;
+
+  # Unlike start and commit, if we rollback at any point it happens
+  # instantly, even if we're in a nested transaction.
+  if (!$self->bz_in_transaction) {
+    ThrowCodeError("not_in_transaction");
+  }
+  else {
+    $self->rollback();
+    $self->{private_bz_transaction_count} = 0;
+  }
 }
 
 #####################################################################
@@ -1268,42 +1299,43 @@ sub bz_rollback_transaction {
 #####################################################################
 
 sub db_new {
-    my ($class, $params) = @_;
-    my ($dsn, $user, $pass, $override_attrs) = 
-        @$params{qw(dsn user pass attrs)};
-
-    # set up default attributes used to connect to the database
-    # (may be overridden by DB driver implementations)
-    my $attributes = { RaiseError => 0,
-                       AutoCommit => 1,
-                       PrintError => 0,
-                       ShowErrorStatement => 1,
-                       HandleError => \&_handle_error,
-                       TaintIn => 1,
-                       # See https://rt.perl.org/rt3/Public/Bug/Display.html?id=30933
-                       # for the reason to use NAME instead of NAME_lc (bug 253696).
-                       FetchHashKeyName => 'NAME',
-                     };
-
-    if ($override_attrs) {
-        foreach my $key (keys %$override_attrs) {
-            $attributes->{$key} = $override_attrs->{$key};
-        }
+  my ($class, $params) = @_;
+  my ($dsn, $user, $pass, $override_attrs) = @$params{qw(dsn user pass attrs)};
+
+  # set up default attributes used to connect to the database
+  # (may be overridden by DB driver implementations)
+  my $attributes = {
+    RaiseError         => 0,
+    AutoCommit         => 1,
+    PrintError         => 0,
+    ShowErrorStatement => 1,
+    HandleError        => \&_handle_error,
+    TaintIn            => 1,
+
+    # See https://rt.perl.org/rt3/Public/Bug/Display.html?id=30933
+    # for the reason to use NAME instead of NAME_lc (bug 253696).
+    FetchHashKeyName => 'NAME',
+  };
+
+  if ($override_attrs) {
+    foreach my $key (keys %$override_attrs) {
+      $attributes->{$key} = $override_attrs->{$key};
     }
+  }
 
-    # connect using our known info to the specified db
-    my $self = DBI->connect($dsn, $user, $pass, $attributes)
-        or die "\nCan't connect to the database.\nError: $DBI::errstr\n"
-        . "  Is your database installed and up and running?\n  Do you have"
-        . " the correct username and password selected in localconfig?\n\n";
+  # connect using our known info to the specified db
+  my $self = DBI->connect($dsn, $user, $pass, $attributes)
+    or die "\nCan't connect to the database.\nError: $DBI::errstr\n"
+    . "  Is your database installed and up and running?\n  Do you have"
+    . " the correct username and password selected in localconfig?\n\n";
 
-    # RaiseError was only set to 0 so that we could catch the 
-    # above "die" condition.
-    $self->{RaiseError} = 1;
+  # RaiseError was only set to 0 so that we could catch the
+  # above "die" condition.
+  $self->{RaiseError} = 1;
 
-    bless ($self, $class);
+  bless($self, $class);
 
-    return $self;
+  return $self;
 }
 
 #####################################################################
@@ -1327,55 +1359,54 @@ These methods really are private. Do not override them in subclasses.
 =cut
 
 sub _bz_init_schema_storage {
-    my ($self) = @_;
-
-    my $table_size;
-    eval {
-        $table_size = 
-            $self->selectrow_array("SELECT COUNT(*) FROM bz_schema");
-    };
+  my ($self) = @_;
+
+  my $table_size;
+  eval { $table_size = $self->selectrow_array("SELECT COUNT(*) FROM bz_schema"); };
+
+  if (!$table_size) {
+    my $init_schema    = $self->_bz_get_initial_schema;
+    my $store_me       = $init_schema->serialize_abstract();
+    my $schema_version = $init_schema->SCHEMA_VERSION;
+
+    # If table_size is not defined, then we hit an error reading the
+    # bz_schema table, which means it probably doesn't exist yet. So,
+    # we have to create it. If we failed above for some other reason,
+    # we'll see the failure here.
+    # However, we must create the table after we do get_initial_schema,
+    # because some versions of get_initial_schema read that the table
+    # exists and then add it to the Schema, where other versions don't.
+    if (!defined $table_size) {
+      $self->_bz_add_table_raw('bz_schema');
+    }
 
-    if (!$table_size) {
-        my $init_schema = $self->_bz_get_initial_schema;
-        my $store_me = $init_schema->serialize_abstract();
-        my $schema_version = $init_schema->SCHEMA_VERSION;
-
-        # If table_size is not defined, then we hit an error reading the
-        # bz_schema table, which means it probably doesn't exist yet. So,
-        # we have to create it. If we failed above for some other reason,
-        # we'll see the failure here.
-        # However, we must create the table after we do get_initial_schema,
-        # because some versions of get_initial_schema read that the table
-        # exists and then add it to the Schema, where other versions don't.
-        if (!defined $table_size) {
-            $self->_bz_add_table_raw('bz_schema');
-        }
+    say install_string('db_schema_init');
+    my $sth = $self->prepare(
+      "INSERT INTO bz_schema " . " (schema_data, version) VALUES (?,?)");
+    $sth->bind_param(1, $store_me, $self->BLOB_TYPE);
+    $sth->bind_param(2, $schema_version);
+    $sth->execute();
 
-        say install_string('db_schema_init');
-        my $sth = $self->prepare("INSERT INTO bz_schema "
-                                 ." (schema_data, version) VALUES (?,?)");
-        $sth->bind_param(1, $store_me, $self->BLOB_TYPE);
-        $sth->bind_param(2, $schema_version);
-        $sth->execute();
-
-        # And now we have to update the on-disk schema to hold the bz_schema
-        # table, if the bz_schema table didn't exist when we were called.
-        if (!defined $table_size) {
-            $self->_bz_real_schema->add_table('bz_schema',
-                $self->_bz_schema->get_table_abstract('bz_schema'));
-            $self->_bz_store_real_schema;
-        }
-    } 
-    # Sanity check
-    elsif ($table_size > 1) {
-        # We tell them to delete the newer one. Better to have checksetup
-        # run migration code too many times than to have it not run the
-        # correct migration code at all.
-        die "Attempted to initialize the schema but there are already "
-            . " $table_size copies of it stored.\nThis should never happen.\n"
-            . " Compare the rows of the bz_schema table and delete the "
-            . "newer one(s).";
+    # And now we have to update the on-disk schema to hold the bz_schema
+    # table, if the bz_schema table didn't exist when we were called.
+    if (!defined $table_size) {
+      $self->_bz_real_schema->add_table('bz_schema',
+        $self->_bz_schema->get_table_abstract('bz_schema'));
+      $self->_bz_store_real_schema;
     }
+  }
+
+  # Sanity check
+  elsif ($table_size > 1) {
+
+    # We tell them to delete the newer one. Better to have checksetup
+    # run migration code too many times than to have it not run the
+    # correct migration code at all.
+    die "Attempted to initialize the schema but there are already "
+      . " $table_size copies of it stored.\nThis should never happen.\n"
+      . " Compare the rows of the bz_schema table and delete the "
+      . "newer one(s).";
+  }
 }
 
 =item C<_bz_real_schema()>
@@ -1389,24 +1420,23 @@ sub _bz_init_schema_storage {
 =cut
 
 sub _bz_real_schema {
-    my ($self) = @_;
-    return $self->{private_real_schema} if exists $self->{private_real_schema};
-
-    my $bz_schema;
-    unless ($bz_schema = Bugzilla->memcached->get({ key => 'bz_schema' })) {
-        $bz_schema = $self->selectrow_arrayref(
-            "SELECT schema_data, version FROM bz_schema"
-        );
-        Bugzilla->memcached->set({ key => 'bz_schema', value => $bz_schema });
-    }
+  my ($self) = @_;
+  return $self->{private_real_schema} if exists $self->{private_real_schema};
 
-    (die "_bz_real_schema tried to read the bz_schema table but it's empty!")
-        if !$bz_schema;
+  my $bz_schema;
+  unless ($bz_schema = Bugzilla->memcached->get({key => 'bz_schema'})) {
+    $bz_schema
+      = $self->selectrow_arrayref("SELECT schema_data, version FROM bz_schema");
+    Bugzilla->memcached->set({key => 'bz_schema', value => $bz_schema});
+  }
 
-    $self->{private_real_schema} =
-        $self->_bz_schema->deserialize_abstract($bz_schema->[0], $bz_schema->[1]);
+  (die "_bz_real_schema tried to read the bz_schema table but it's empty!")
+    if !$bz_schema;
 
-    return $self->{private_real_schema};
+  $self->{private_real_schema}
+    = $self->_bz_schema->deserialize_abstract($bz_schema->[0], $bz_schema->[1]);
+
+  return $self->{private_real_schema};
 }
 
 =item C<_bz_store_real_schema()>
@@ -1426,106 +1456,135 @@ sub _bz_real_schema {
 =cut
 
 sub _bz_store_real_schema {
-    my ($self) = @_;
-
-    # Make sure that there's a schema to update
-    my $table_size = $self->selectrow_array("SELECT COUNT(*) FROM bz_schema");
-
-    die "Attempted to update the bz_schema table but there's nothing "
-        . "there to update. Run checksetup." unless $table_size;
-
-    # We want to store the current object, not one
-    # that we read from the database. So we use the actual hash
-    # member instead of the subroutine call. If the hash
-    # member is not defined, we will (and should) fail.
-    my $update_schema = $self->{private_real_schema};
-    my $store_me = $update_schema->serialize_abstract();
-    my $schema_version = $update_schema->SCHEMA_VERSION;
-    my $sth = $self->prepare("UPDATE bz_schema 
-                                 SET schema_data = ?, version = ?");
-    $sth->bind_param(1, $store_me, $self->BLOB_TYPE);
-    $sth->bind_param(2, $schema_version);
-    $sth->execute();
+  my ($self) = @_;
+
+  # Make sure that there's a schema to update
+  my $table_size = $self->selectrow_array("SELECT COUNT(*) FROM bz_schema");
 
-    Bugzilla->memcached->clear({ key => 'bz_schema' });
+  die "Attempted to update the bz_schema table but there's nothing "
+    . "there to update. Run checksetup."
+    unless $table_size;
+
+  # We want to store the current object, not one
+  # that we read from the database. So we use the actual hash
+  # member instead of the subroutine call. If the hash
+  # member is not defined, we will (and should) fail.
+  my $update_schema  = $self->{private_real_schema};
+  my $store_me       = $update_schema->serialize_abstract();
+  my $schema_version = $update_schema->SCHEMA_VERSION;
+  my $sth            = $self->prepare(
+    "UPDATE bz_schema 
+                                 SET schema_data = ?, version = ?"
+  );
+  $sth->bind_param(1, $store_me, $self->BLOB_TYPE);
+  $sth->bind_param(2, $schema_version);
+  $sth->execute();
+
+  Bugzilla->memcached->clear({key => 'bz_schema'});
 }
 
 # For bz_populate_enum_tables
 sub _bz_populate_enum_table {
-    my ($self, $table, $valuelist) = @_;
-
-    my $sql_table = $self->quote_identifier($table);
-
-    # Check if there are any table entries
-    my $table_size = $self->selectrow_array("SELECT COUNT(*) FROM $sql_table");
-
-    # If the table is empty...
-    if (!$table_size) {
-        print " $table";
-        my $insert = $self->prepare(
-            "INSERT INTO $sql_table (value,sortkey) VALUES (?,?)");
-        my $sortorder = 0;
-        my $maxlen    = max(map(length($_), @$valuelist)) + 2;
-        foreach my $value (@$valuelist) {
-            $sortorder += 100;
-            $insert->execute($value, $sortorder);
-        }
+  my ($self, $table, $valuelist) = @_;
+
+  my $sql_table = $self->quote_identifier($table);
+
+  # Check if there are any table entries
+  my $table_size = $self->selectrow_array("SELECT COUNT(*) FROM $sql_table");
+
+  # If the table is empty...
+  if (!$table_size) {
+    print " $table";
+    my $insert
+      = $self->prepare("INSERT INTO $sql_table (value,sortkey) VALUES (?,?)");
+    my $sortorder = 0;
+    my $maxlen = max(map(length($_), @$valuelist)) + 2;
+    foreach my $value (@$valuelist) {
+      $sortorder += 100;
+      $insert->execute($value, $sortorder);
     }
+  }
 }
 
 # This is used before adding a foreign key to a column, to make sure
 # that the database won't fail adding the key.
 sub _check_references {
-    my ($self, $table, $column, $fk) = @_;
-    my $foreign_table = $fk->{TABLE};
-    my $foreign_column = $fk->{COLUMN};
-
-    # We use table aliases because sometimes we join a table to itself,
-    # and we can't use the same table name on both sides of the join.
-    # We also can't use the words "table" or "foreign" because those are
-    # reserved words.
-    my $bad_values = $self->selectcol_arrayref(
-        "SELECT DISTINCT tabl.$column 
+  my ($self, $table, $column, $fk) = @_;
+  my $foreign_table  = $fk->{TABLE};
+  my $foreign_column = $fk->{COLUMN};
+
+  # We use table aliases because sometimes we join a table to itself,
+  # and we can't use the same table name on both sides of the join.
+  # We also can't use the words "table" or "foreign" because those are
+  # reserved words.
+  my $bad_values = $self->selectcol_arrayref(
+    "SELECT DISTINCT tabl.$column 
            FROM $table AS tabl LEFT JOIN $foreign_table AS forn
                 ON tabl.$column = forn.$foreign_column
           WHERE forn.$foreign_column IS NULL
-                AND tabl.$column IS NOT NULL");
-
-    if (@$bad_values) {
-        my $delete_action = $fk->{DELETE} || '';
-        if ($delete_action eq 'CASCADE') {
-            $self->do("DELETE FROM $table WHERE $column IN (" 
-                      . join(',', ('?') x @$bad_values)  . ")",
-                      undef, @$bad_values);
-            if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) {
-                print "\n", get_text('install_fk_invalid_fixed',
-                    { table => $table, column => $column,
-                      foreign_table => $foreign_table,
-                      foreign_column => $foreign_column,
-                      'values' => $bad_values, action => 'delete' }), "\n";
-            }
-        }
-        elsif ($delete_action eq 'SET NULL') {
-            $self->do("UPDATE $table SET $column = NULL
+                AND tabl.$column IS NOT NULL"
+  );
+
+  if (@$bad_values) {
+    my $delete_action = $fk->{DELETE} || '';
+    if ($delete_action eq 'CASCADE') {
+      $self->do(
+        "DELETE FROM $table WHERE $column IN (" . join(',', ('?') x @$bad_values) . ")",
+        undef, @$bad_values
+      );
+      if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) {
+        print "\n",
+          get_text(
+          'install_fk_invalid_fixed',
+          {
+            table          => $table,
+            column         => $column,
+            foreign_table  => $foreign_table,
+            foreign_column => $foreign_column,
+            'values'       => $bad_values,
+            action         => 'delete'
+          }
+          ),
+          "\n";
+      }
+    }
+    elsif ($delete_action eq 'SET NULL') {
+      $self->do(
+        "UPDATE $table SET $column = NULL
                         WHERE $column IN ("
-                      . join(',', ('?') x @$bad_values)  . ")",
-                      undef, @$bad_values);
-            if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) {
-                print "\n", get_text('install_fk_invalid_fixed',
-                    { table => $table, column => $column,
-                      foreign_table => $foreign_table, 
-                      foreign_column => $foreign_column,
-                      'values' => $bad_values, action => 'null' }), "\n";
-            }
-        }
-        else {
-            die "\n", get_text('install_fk_invalid',
-                { table => $table, column => $column,
-                  foreign_table => $foreign_table,
-                  foreign_column => $foreign_column,
-                 'values' => $bad_values }), "\n";
+          . join(',', ('?') x @$bad_values) . ")", undef, @$bad_values
+      );
+      if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) {
+        print "\n",
+          get_text(
+          'install_fk_invalid_fixed',
+          {
+            table          => $table,
+            column         => $column,
+            foreign_table  => $foreign_table,
+            foreign_column => $foreign_column,
+            'values'       => $bad_values,
+            action         => 'null'
+          }
+          ),
+          "\n";
+      }
+    }
+    else {
+      die "\n",
+        get_text(
+        'install_fk_invalid',
+        {
+          table          => $table,
+          column         => $column,
+          foreign_table  => $foreign_table,
+          foreign_column => $foreign_column,
+          'values'       => $bad_values
         }
+        ),
+        "\n";
     }
+  }
 }
 
 1;
index 858b58fee2ea8e7f247febabb208ea6468c424b1..71649e01900dfcc1bff9e73a6d6d17eb657385fd 100644 (file)
@@ -37,257 +37,264 @@ use List::Util qw(max);
 use Text::ParseWords;
 
 # This is how many comments of MAX_COMMENT_LENGTH we expect on a single bug.
-# In reality, you could have a LOT more comments than this, because 
+# In reality, you could have a LOT more comments than this, because
 # MAX_COMMENT_LENGTH is big.
 use constant MAX_COMMENTS => 50;
 
 use constant FULLTEXT_OR => '|';
 
 sub new {
-    my ($class, $params) = @_;
-    my ($user, $pass, $host, $dbname, $port, $sock) =
-        @$params{qw(db_user db_pass db_host db_name db_port db_sock)};
-
-    # construct the DSN from the parameters we got
-    my $dsn = "dbi:mysql:host=$host;database=$dbname";
-    $dsn .= ";port=$port" if $port;
-    $dsn .= ";mysql_socket=$sock" if $sock;
-
-    my %attrs = (
-        mysql_enable_utf8 => 1,
-        # Needs to be explicitly specified for command-line processes.
-        mysql_auto_reconnect => 1,
-    );
-    
-    # MySQL SSL options
-    my ($ssl_ca_file, $ssl_ca_path, $ssl_cert, $ssl_key) =
-        @$params{qw(db_mysql_ssl_ca_file db_mysql_ssl_ca_path
-                    db_mysql_ssl_client_cert db_mysql_ssl_client_key)};
-    if ($ssl_ca_file || $ssl_ca_path || $ssl_cert || $ssl_key) {
-        $attrs{'mysql_ssl'}             = 1;
-        $attrs{'mysql_ssl_ca_file'}     = $ssl_ca_file if $ssl_ca_file;
-        $attrs{'mysql_ssl_ca_path'}     = $ssl_ca_path if $ssl_ca_path;
-        $attrs{'mysql_ssl_client_cert'} = $ssl_cert    if $ssl_cert;
-        $attrs{'mysql_ssl_client_key'}  = $ssl_key     if $ssl_key;
+  my ($class, $params) = @_;
+  my ($user, $pass, $host, $dbname, $port, $sock)
+    = @$params{qw(db_user db_pass db_host db_name db_port db_sock)};
+
+  # construct the DSN from the parameters we got
+  my $dsn = "dbi:mysql:host=$host;database=$dbname";
+  $dsn .= ";port=$port"         if $port;
+  $dsn .= ";mysql_socket=$sock" if $sock;
+
+  my %attrs = (
+    mysql_enable_utf8 => 1,
+
+    # Needs to be explicitly specified for command-line processes.
+    mysql_auto_reconnect => 1,
+  );
+
+  # MySQL SSL options
+  my ($ssl_ca_file, $ssl_ca_path, $ssl_cert, $ssl_key) = @$params{
+    qw(db_mysql_ssl_ca_file db_mysql_ssl_ca_path
+      db_mysql_ssl_client_cert db_mysql_ssl_client_key)
+  };
+  if ($ssl_ca_file || $ssl_ca_path || $ssl_cert || $ssl_key) {
+    $attrs{'mysql_ssl'}             = 1;
+    $attrs{'mysql_ssl_ca_file'}     = $ssl_ca_file if $ssl_ca_file;
+    $attrs{'mysql_ssl_ca_path'}     = $ssl_ca_path if $ssl_ca_path;
+    $attrs{'mysql_ssl_client_cert'} = $ssl_cert if $ssl_cert;
+    $attrs{'mysql_ssl_client_key'}  = $ssl_key if $ssl_key;
+  }
+
+  my $self = $class->db_new(
+    {dsn => $dsn, user => $user, pass => $pass, attrs => \%attrs});
+
+  # This makes sure that we return table names correctly.
+  $self->do("SET NAMES utf8");
+
+  # all class local variables stored in DBI derived class needs to have
+  # a prefix 'private_'. See DBI documentation.
+  $self->{private_bz_tables_locked} = "";
+
+  # Needed by TheSchwartz
+  $self->{private_bz_dsn} = $dsn;
+
+  bless($self, $class);
+
+  # Check for MySQL modes.
+  my ($var, $sql_mode)
+    = $self->selectrow_array("SHOW VARIABLES LIKE 'sql\\_mode'");
+
+  # Disable ANSI and strict modes, else Bugzilla will crash.
+  if ($sql_mode) {
+
+    # STRICT_TRANS_TABLE or STRICT_ALL_TABLES enable MySQL strict mode,
+    # causing bug 321645. TRADITIONAL sets these modes (among others) as
+    # well, so it has to be stipped as well
+    my $new_sql_mode = join(",",
+      grep { $_ !~ /^(?:ANSI|STRICT_(?:TRANS|ALL)_TABLES|TRADITIONAL)$/ }
+        split(/,/, $sql_mode));
+
+    if ($sql_mode ne $new_sql_mode) {
+      $self->do("SET SESSION sql_mode = ?", undef, $new_sql_mode);
     }
+  }
 
-    my $self = $class->db_new({ dsn => $dsn, user => $user, 
-                                pass => $pass, attrs => \%attrs });
-
-    # This makes sure that we return table names correctly.
-    $self->do("SET NAMES utf8");
-
-    # all class local variables stored in DBI derived class needs to have
-    # a prefix 'private_'. See DBI documentation.
-    $self->{private_bz_tables_locked} = "";
-
-    # Needed by TheSchwartz
-    $self->{private_bz_dsn} = $dsn;
+  # Allow large GROUP_CONCATs (largely for inserting comments
+  # into bugs_fulltext).
+  $self->do('SET SESSION group_concat_max_len = 128000000');
 
-    bless ($self, $class);
+  # MySQL 5.5.2 and older have this variable set to true, which causes
+  # trouble, see bug 870369.
+  $self->do('SET SESSION sql_auto_is_null = 0');
 
-    # Check for MySQL modes.
-    my ($var, $sql_mode) = $self->selectrow_array(
-        "SHOW VARIABLES LIKE 'sql\\_mode'");
-
-    # Disable ANSI and strict modes, else Bugzilla will crash.
-    if ($sql_mode) {
-        # STRICT_TRANS_TABLE or STRICT_ALL_TABLES enable MySQL strict mode,
-        # causing bug 321645. TRADITIONAL sets these modes (among others) as
-        # well, so it has to be stipped as well
-        my $new_sql_mode =
-            join(",", grep {$_ !~ /^(?:ANSI|STRICT_(?:TRANS|ALL)_TABLES|TRADITIONAL)$/}
-                            split(/,/, $sql_mode));
-
-        if ($sql_mode ne $new_sql_mode) {
-            $self->do("SET SESSION sql_mode = ?", undef, $new_sql_mode);
-        }
-    }
-
-    # Allow large GROUP_CONCATs (largely for inserting comments 
-    # into bugs_fulltext).
-    $self->do('SET SESSION group_concat_max_len = 128000000');
-
-    # MySQL 5.5.2 and older have this variable set to true, which causes
-    # trouble, see bug 870369.
-    $self->do('SET SESSION sql_auto_is_null = 0');
-
-    return $self;
+  return $self;
 }
 
 # when last_insert_id() is supported on MySQL by lowest DBI/DBD version
 # required by Bugzilla, this implementation can be removed.
 sub bz_last_key {
-    my ($self) = @_;
+  my ($self) = @_;
 
-    my ($last_insert_id) = $self->selectrow_array('SELECT LAST_INSERT_ID()');
+  my ($last_insert_id) = $self->selectrow_array('SELECT LAST_INSERT_ID()');
 
-    return $last_insert_id;
+  return $last_insert_id;
 }
 
 sub sql_group_concat {
-    my ($self, $column, $separator, $sort, $order_by) = @_;
-    $separator = $self->quote(', ') if !defined $separator;
-    $sort = 1 if !defined $sort;
-    if ($order_by) {
-        $column .= " ORDER BY $order_by";
-    }
-    elsif ($sort) {
-        my $sort_order = $column;
-        $sort_order =~ s/^DISTINCT\s+//i;
-        $column = "$column ORDER BY $sort_order";
-    }
-    return "GROUP_CONCAT($column SEPARATOR $separator)";
+  my ($self, $column, $separator, $sort, $order_by) = @_;
+  $separator = $self->quote(', ') if !defined $separator;
+  $sort = 1 if !defined $sort;
+  if ($order_by) {
+    $column .= " ORDER BY $order_by";
+  }
+  elsif ($sort) {
+    my $sort_order = $column;
+    $sort_order =~ s/^DISTINCT\s+//i;
+    $column = "$column ORDER BY $sort_order";
+  }
+  return "GROUP_CONCAT($column SEPARATOR $separator)";
 }
 
 sub sql_regexp {
-    my ($self, $expr, $pattern, $nocheck, $real_pattern) = @_;
-    $real_pattern ||= $pattern;
+  my ($self, $expr, $pattern, $nocheck, $real_pattern) = @_;
+  $real_pattern ||= $pattern;
 
-    $self->bz_check_regexp($real_pattern) if !$nocheck;
+  $self->bz_check_regexp($real_pattern) if !$nocheck;
 
-    return "$expr REGEXP $pattern";
+  return "$expr REGEXP $pattern";
 }
 
 sub sql_not_regexp {
-    my ($self, $expr, $pattern, $nocheck, $real_pattern) = @_;
-    $real_pattern ||= $pattern;
+  my ($self, $expr, $pattern, $nocheck, $real_pattern) = @_;
+  $real_pattern ||= $pattern;
 
-    $self->bz_check_regexp($real_pattern) if !$nocheck;
+  $self->bz_check_regexp($real_pattern) if !$nocheck;
 
-    return "$expr NOT REGEXP $pattern";
+  return "$expr NOT REGEXP $pattern";
 }
 
 sub sql_limit {
-    my ($self, $limit, $offset) = @_;
-
-    if (defined($offset)) {
-        return "LIMIT $offset, $limit";
-    } else {
-        return "LIMIT $limit";
-    }
+  my ($self, $limit, $offset) = @_;
+
+  if (defined($offset)) {
+    return "LIMIT $offset, $limit";
+  }
+  else {
+    return "LIMIT $limit";
+  }
 }
 
 sub sql_string_concat {
-    my ($self, @params) = @_;
-    
-    return 'CONCAT(' . join(', ', @params) . ')';
+  my ($self, @params) = @_;
+
+  return 'CONCAT(' . join(', ', @params) . ')';
 }
 
 sub sql_fulltext_search {
-    my ($self, $column, $text) = @_;
-
-    # Add the boolean mode modifier if the search string contains
-    # boolean operators at the start or end of a word.
-    my $mode = '';
-    if ($text =~ /(?:^|\W)[+\-<>~"()]/ || $text =~ /[()"*](?:$|\W)/) {
-        $mode = 'IN BOOLEAN MODE';
-
-        my @terms = split(quotemeta(FULLTEXT_OR), $text);
-        foreach my $term (@terms) {
-            # quote un-quoted compound words
-            my @words = quotewords('[\s()]+', 'delimiters', $term);
-            foreach my $word (@words) {
-                # match words that have non-word chars in the middle of them
-                if ($word =~ /\w\W+\w/ && $word !~ m/"/) {
-                    $word = '"' . $word . '"';
-                }
-            }
-            $term = join('', @words);
+  my ($self, $column, $text) = @_;
+
+  # Add the boolean mode modifier if the search string contains
+  # boolean operators at the start or end of a word.
+  my $mode = '';
+  if ($text =~ /(?:^|\W)[+\-<>~"()]/ || $text =~ /[()"*](?:$|\W)/) {
+    $mode = 'IN BOOLEAN MODE';
+
+    my @terms = split(quotemeta(FULLTEXT_OR), $text);
+    foreach my $term (@terms) {
+
+      # quote un-quoted compound words
+      my @words = quotewords('[\s()]+', 'delimiters', $term);
+      foreach my $word (@words) {
+
+        # match words that have non-word chars in the middle of them
+        if ($word =~ /\w\W+\w/ && $word !~ m/"/) {
+          $word = '"' . $word . '"';
         }
-        $text = join(FULLTEXT_OR, @terms);
+      }
+      $term = join('', @words);
     }
+    $text = join(FULLTEXT_OR, @terms);
+  }
 
-    # quote the text for use in the MATCH AGAINST expression
-    $text = $self->quote($text);
+  # quote the text for use in the MATCH AGAINST expression
+  $text = $self->quote($text);
 
-    # untaint the text, since it's safe to use now that we've quoted it
-    trick_taint($text);
+  # untaint the text, since it's safe to use now that we've quoted it
+  trick_taint($text);
 
-    return "MATCH($column) AGAINST($text $mode)";
+  return "MATCH($column) AGAINST($text $mode)";
 }
 
 sub sql_istring {
-    my ($self, $string) = @_;
-    
-    return $string;
+  my ($self, $string) = @_;
+
+  return $string;
 }
 
 sub sql_from_days {
-    my ($self, $days) = @_;
+  my ($self, $days) = @_;
 
-    return "FROM_DAYS($days)";
+  return "FROM_DAYS($days)";
 }
 
 sub sql_to_days {
-    my ($self, $date) = @_;
+  my ($self, $date) = @_;
 
-    return "TO_DAYS($date)";
+  return "TO_DAYS($date)";
 }
 
 sub sql_date_format {
-    my ($self, $date, $format) = @_;
+  my ($self, $date, $format) = @_;
+
+  $format = "%Y.%m.%d %H:%i:%s" if !$format;
 
-    $format = "%Y.%m.%d %H:%i:%s" if !$format;
-    
-    return "DATE_FORMAT($date, " . $self->quote($format) . ")";
+  return "DATE_FORMAT($date, " . $self->quote($format) . ")";
 }
 
 sub sql_date_math {
-    my ($self, $date, $operator, $interval, $units) = @_;
-    
-    return "$date $operator INTERVAL $interval $units";
+  my ($self, $date, $operator, $interval, $units) = @_;
+
+  return "$date $operator INTERVAL $interval $units";
 }
 
 sub sql_iposition {
-    my ($self, $fragment, $text) = @_;
-    return "INSTR($text, $fragment)";
+  my ($self, $fragment, $text) = @_;
+  return "INSTR($text, $fragment)";
 }
 
 sub sql_position {
-    my ($self, $fragment, $text) = @_;
+  my ($self, $fragment, $text) = @_;
 
-    return "INSTR(CAST($text AS BINARY), CAST($fragment AS BINARY))";
+  return "INSTR(CAST($text AS BINARY), CAST($fragment AS BINARY))";
 }
 
 sub sql_group_by {
-    my ($self, $needed_columns, $optional_columns) = @_;
+  my ($self, $needed_columns, $optional_columns) = @_;
 
-    # MySQL allows you to specify the minimal subset of columns to get
-    # a unique result. While it does allow specifying all columns as
-    # ANSI SQL requires, according to MySQL documentation, the fewer
-    # columns you specify, the faster the query runs.
-    return "GROUP BY $needed_columns";
+  # MySQL allows you to specify the minimal subset of columns to get
+  # a unique result. While it does allow specifying all columns as
+  # ANSI SQL requires, according to MySQL documentation, the fewer
+  # columns you specify, the faster the query runs.
+  return "GROUP BY $needed_columns";
 }
 
 sub bz_explain {
-    my ($self, $sql) = @_;
-    my $sth  = $self->prepare("EXPLAIN $sql");
-    $sth->execute();
-    my $columns = $sth->{'NAME'};
-    my $lengths = $sth->{'mysql_max_length'};
-    my $format_string = '|';
-    my $i = 0;
-    foreach my $column (@$columns) {
-        # Sometimes the column name is longer than the contents.
-        my $length = max($lengths->[$i], length($column));
-        $format_string .= ' %-' . $length . 's |';
-        $i++;
-    }
-
-    my $first_row = sprintf($format_string, @$columns);
-    my @explain_rows = ($first_row, '-' x length($first_row));
-    while (my $row = $sth->fetchrow_arrayref) {
-        my @fixed = map { defined $_ ? $_ : 'NULL' } @$row;
-        push(@explain_rows, sprintf($format_string, @fixed));
-    }
-
-    return join("\n", @explain_rows);
+  my ($self, $sql) = @_;
+  my $sth = $self->prepare("EXPLAIN $sql");
+  $sth->execute();
+  my $columns       = $sth->{'NAME'};
+  my $lengths       = $sth->{'mysql_max_length'};
+  my $format_string = '|';
+  my $i             = 0;
+  foreach my $column (@$columns) {
+
+    # Sometimes the column name is longer than the contents.
+    my $length = max($lengths->[$i], length($column));
+    $format_string .= ' %-' . $length . 's |';
+    $i++;
+  }
+
+  my $first_row = sprintf($format_string, @$columns);
+  my @explain_rows = ($first_row, '-' x length($first_row));
+  while (my $row = $sth->fetchrow_arrayref) {
+    my @fixed = map { defined $_ ? $_ : 'NULL' } @$row;
+    push(@explain_rows, sprintf($format_string, @fixed));
+  }
+
+  return join("\n", @explain_rows);
 }
 
 sub _bz_get_initial_schema {
-    my ($self) = @_;
-    return $self->_bz_build_schema_from_disk();
+  my ($self) = @_;
+  return $self->_bz_build_schema_from_disk();
 }
 
 #####################################################################
@@ -295,487 +302,497 @@ sub _bz_get_initial_schema {
 #####################################################################
 
 sub bz_check_server_version {
-    my $self = shift;
+  my $self = shift;
 
-    if (lc(Bugzilla->localconfig->{db_name}) eq 'mysql') {
-        die "It is not safe to run Bugzilla inside a database named 'mysql'.\n"
-            . " Please pick a different value for \$db_name in localconfig.\n";
-    }
+  if (lc(Bugzilla->localconfig->{db_name}) eq 'mysql') {
+    die "It is not safe to run Bugzilla inside a database named 'mysql'.\n"
+      . " Please pick a different value for \$db_name in localconfig.\n";
+  }
 
-    $self->SUPER::bz_check_server_version(@_);
+  $self->SUPER::bz_check_server_version(@_);
 }
 
 sub bz_setup_database {
-    my ($self) = @_;
-
-    # The "comments" field of the bugs_fulltext table could easily exceed
-    # MySQL's default max_allowed_packet. Also, MySQL should never have
-    # a max_allowed_packet smaller than our max_attachment_size. So, we
-    # warn the user here if max_allowed_packet is too small.
-    my $min_max_allowed = MAX_COMMENTS * MAX_COMMENT_LENGTH;
-    my (undef, $current_max_allowed) = $self->selectrow_array(
-        q{SHOW VARIABLES LIKE 'max\_allowed\_packet'});
-    # This parameter is not yet defined when the DB is being built for
-    # the very first time. The code below still works properly, however,
-    # because the default maxattachmentsize is smaller than $min_max_allowed.
-    my $max_attachment = (Bugzilla->params->{'maxattachmentsize'} || 0) * 1024;
-    my $needed_max_allowed = max($min_max_allowed, $max_attachment);
-    if ($current_max_allowed < $needed_max_allowed) {
-        warn install_string('max_allowed_packet',
-                            { current => $current_max_allowed,
-                              needed  => $needed_max_allowed }) . "\n";
+  my ($self) = @_;
+
+  # The "comments" field of the bugs_fulltext table could easily exceed
+  # MySQL's default max_allowed_packet. Also, MySQL should never have
+  # a max_allowed_packet smaller than our max_attachment_size. So, we
+  # warn the user here if max_allowed_packet is too small.
+  my $min_max_allowed = MAX_COMMENTS * MAX_COMMENT_LENGTH;
+  my (undef, $current_max_allowed)
+    = $self->selectrow_array(q{SHOW VARIABLES LIKE 'max\_allowed\_packet'});
+
+  # This parameter is not yet defined when the DB is being built for
+  # the very first time. The code below still works properly, however,
+  # because the default maxattachmentsize is smaller than $min_max_allowed.
+  my $max_attachment = (Bugzilla->params->{'maxattachmentsize'} || 0) * 1024;
+  my $needed_max_allowed = max($min_max_allowed, $max_attachment);
+  if ($current_max_allowed < $needed_max_allowed) {
+    warn install_string('max_allowed_packet',
+      {current => $current_max_allowed, needed => $needed_max_allowed})
+      . "\n";
+  }
+
+  # Make sure the installation has InnoDB turned on, or we're going to be
+  # doing silly things like making foreign keys on MyISAM tables, which is
+  # hard to fix later. We do this up here because none of the code below
+  # works if InnoDB is off. (Particularly if we've already converted the
+  # tables to InnoDB.)
+  my %engines = @{$self->selectcol_arrayref('SHOW ENGINES', {Columns => [1, 2]})};
+  if (!$engines{InnoDB} || $engines{InnoDB} !~ /^(YES|DEFAULT)$/) {
+    die install_string('mysql_innodb_disabled');
+  }
+
+
+  my ($sd_index_deleted, $longdescs_index_deleted);
+  my @tables = $self->bz_table_list_real();
+
+  # We want to convert tables to InnoDB, but it's possible that they have
+  # fulltext indexes on them, and conversion will fail unless we remove
+  # the indexes.
+  if (grep($_ eq 'bugs', @tables) and !grep($_ eq 'bugs_fulltext', @tables)) {
+    if ($self->bz_index_info_real('bugs', 'short_desc')) {
+      $self->bz_drop_index_raw('bugs', 'short_desc');
     }
-
-    # Make sure the installation has InnoDB turned on, or we're going to be
-    # doing silly things like making foreign keys on MyISAM tables, which is
-    # hard to fix later. We do this up here because none of the code below
-    # works if InnoDB is off. (Particularly if we've already converted the
-    # tables to InnoDB.)
-    my %engines = @{$self->selectcol_arrayref('SHOW ENGINES', {Columns => [1,2]})};
-    if (!$engines{InnoDB} || $engines{InnoDB} !~ /^(YES|DEFAULT)$/) {
-        die install_string('mysql_innodb_disabled');
+    if ($self->bz_index_info_real('bugs', 'bugs_short_desc_idx')) {
+      $self->bz_drop_index_raw('bugs', 'bugs_short_desc_idx');
+      $sd_index_deleted = 1;    # Used for later schema cleanup.
     }
-
-
-    my ($sd_index_deleted, $longdescs_index_deleted);
-    my @tables = $self->bz_table_list_real();
-    # We want to convert tables to InnoDB, but it's possible that they have 
-    # fulltext indexes on them, and conversion will fail unless we remove
-    # the indexes.
-    if (grep($_ eq 'bugs', @tables)
-        and !grep($_ eq 'bugs_fulltext', @tables))
-    {
-        if ($self->bz_index_info_real('bugs', 'short_desc')) {
-            $self->bz_drop_index_raw('bugs', 'short_desc');
-        }
-        if ($self->bz_index_info_real('bugs', 'bugs_short_desc_idx')) {
-            $self->bz_drop_index_raw('bugs', 'bugs_short_desc_idx');
-            $sd_index_deleted = 1; # Used for later schema cleanup.
-        }
+  }
+  if (grep($_ eq 'longdescs', @tables) and !grep($_ eq 'bugs_fulltext', @tables))
+  {
+    if ($self->bz_index_info_real('longdescs', 'thetext')) {
+      $self->bz_drop_index_raw('longdescs', 'thetext');
     }
-    if (grep($_ eq 'longdescs', @tables)
-        and !grep($_ eq 'bugs_fulltext', @tables))
-    {
-        if ($self->bz_index_info_real('longdescs', 'thetext')) {
-            $self->bz_drop_index_raw('longdescs', 'thetext');
-        }
-        if ($self->bz_index_info_real('longdescs', 'longdescs_thetext_idx')) {
-            $self->bz_drop_index_raw('longdescs', 'longdescs_thetext_idx');
-            $longdescs_index_deleted = 1; # For later schema cleanup.
-        }
+    if ($self->bz_index_info_real('longdescs', 'longdescs_thetext_idx')) {
+      $self->bz_drop_index_raw('longdescs', 'longdescs_thetext_idx');
+      $longdescs_index_deleted = 1;    # For later schema cleanup.
     }
-
-    # Upgrade tables from MyISAM to InnoDB
-    my $db_name = Bugzilla->localconfig->{db_name};
-    my $myisam_tables = $self->selectcol_arrayref(
-        'SELECT TABLE_NAME FROM information_schema.TABLES 
-          WHERE TABLE_SCHEMA = ? AND ENGINE = ?',
-        undef, $db_name, 'MyISAM');
-    foreach my $should_be_myisam (Bugzilla::DB::Schema::Mysql::MYISAM_TABLES) {
-        @$myisam_tables = grep { $_ ne $should_be_myisam } @$myisam_tables;
+  }
+
+  # Upgrade tables from MyISAM to InnoDB
+  my $db_name       = Bugzilla->localconfig->{db_name};
+  my $myisam_tables = $self->selectcol_arrayref(
+    'SELECT TABLE_NAME FROM information_schema.TABLES 
+          WHERE TABLE_SCHEMA = ? AND ENGINE = ?', undef, $db_name, 'MyISAM'
+  );
+  foreach my $should_be_myisam (Bugzilla::DB::Schema::Mysql::MYISAM_TABLES) {
+    @$myisam_tables = grep { $_ ne $should_be_myisam } @$myisam_tables;
+  }
+
+  if (scalar @$myisam_tables) {
+    print "Bugzilla now uses the InnoDB storage engine in MySQL for",
+      " most tables.\nConverting tables to InnoDB:\n";
+    foreach my $table (@$myisam_tables) {
+      print "Converting table $table... ";
+      $self->do("ALTER TABLE $table ENGINE = InnoDB");
+      print "done.\n";
     }
-
-    if (scalar @$myisam_tables) {
-        print "Bugzilla now uses the InnoDB storage engine in MySQL for",
-              " most tables.\nConverting tables to InnoDB:\n";
-        foreach my $table (@$myisam_tables) {
-            print "Converting table $table... ";
-            $self->do("ALTER TABLE $table ENGINE = InnoDB");
-            print "done.\n";
-        }
+  }
+
+  # Versions of Bugzilla before the existence of Bugzilla::DB::Schema did
+  # not provide explicit names for the table indexes. This means
+  # that our upgrades will not be reliable, because we look for the name
+  # of the index, not what fields it is on, when doing upgrades.
+  # (using the name is much better for cross-database compatibility
+  # and general reliability). It's also very important that our
+  # Schema object be consistent with what is on the disk.
+  #
+  # While we're at it, we also fix some inconsistent index naming
+  # from the original checkin of Bugzilla::DB::Schema.
+
+  # We check for the existence of a particular "short name" index that
+  # has existed at least since Bugzilla 2.8, and probably earlier.
+  # For fixing the inconsistent naming of Schema indexes,
+  # we also check for one of those inconsistently-named indexes.
+  if (
+    grep($_ eq 'bugs', @tables)
+    && ( $self->bz_index_info_real('bugs', 'assigned_to')
+      || $self->bz_index_info_real('flags', 'flags_bidattid_idx'))
+    )
+  {
+
+    # This is a check unrelated to the indexes, to see if people are
+    # upgrading from 2.18 or below, but somehow have a bz_schema table
+    # already. This only happens if they have done a mysqldump into
+    # a database without doing a DROP DATABASE first.
+    # We just do the check here since this check is a reliable way
+    # of telling that we are upgrading from a version pre-2.20.
+    if (grep($_ eq 'bz_schema', $self->bz_table_list_real())) {
+      die install_string('bz_schema_exists_before_220');
     }
-    
-    # Versions of Bugzilla before the existence of Bugzilla::DB::Schema did 
-    # not provide explicit names for the table indexes. This means
-    # that our upgrades will not be reliable, because we look for the name
-    # of the index, not what fields it is on, when doing upgrades.
-    # (using the name is much better for cross-database compatibility 
-    # and general reliability). It's also very important that our
-    # Schema object be consistent with what is on the disk.
-    #
-    # While we're at it, we also fix some inconsistent index naming
-    # from the original checkin of Bugzilla::DB::Schema.
-
-    # We check for the existence of a particular "short name" index that
-    # has existed at least since Bugzilla 2.8, and probably earlier.
-    # For fixing the inconsistent naming of Schema indexes,
-    # we also check for one of those inconsistently-named indexes.
-    if (grep($_ eq 'bugs', @tables)
-        && ($self->bz_index_info_real('bugs', 'assigned_to')
-            || $self->bz_index_info_real('flags', 'flags_bidattid_idx')) )
-    {
-
-        # This is a check unrelated to the indexes, to see if people are
-        # upgrading from 2.18 or below, but somehow have a bz_schema table
-        # already. This only happens if they have done a mysqldump into
-        # a database without doing a DROP DATABASE first.
-        # We just do the check here since this check is a reliable way
-        # of telling that we are upgrading from a version pre-2.20.
-        if (grep($_ eq 'bz_schema', $self->bz_table_list_real())) {
-            die install_string('bz_schema_exists_before_220');
-        }
 
-        my $bug_count = $self->selectrow_array("SELECT COUNT(*) FROM bugs");
-        # We estimate one minute for each 3000 bugs, plus 3 minutes just
-        # to handle basic MySQL stuff.
-        my $rename_time = int($bug_count / 3000) + 3;
-        # And 45 minutes for every 15,000 attachments, per some experiments.
-        my ($attachment_count) = 
-            $self->selectrow_array("SELECT COUNT(*) FROM attachments");
-        $rename_time += int(($attachment_count * 45) / 15000);
-        # If we're going to take longer than 5 minutes, we let the user know
-        # and allow them to abort.
-        if ($rename_time > 5) {
-            print "\n", install_string('mysql_index_renaming',
-                                       { minutes => $rename_time });
-            # Wait 45 seconds for them to respond.
-            sleep(45) unless Bugzilla->installation_answers->{NO_PAUSE};
-        }
-        print "Renaming indexes...\n";
-
-        # We can't be interrupted, because of how the "if"
-        # works above.
-        local $SIG{INT}  = 'IGNORE';
-        local $SIG{TERM} = 'IGNORE';
-        local $SIG{PIPE} = 'IGNORE';
-
-        # Certain indexes had names in Schema that did not easily conform
-        # to a standard. We store those names here, so that they
-        # can be properly renamed.
-        # Also, sometimes an old mysqldump would incorrectly rename
-        # unique indexes to "PRIMARY", so we address that here, also.
-        my $bad_names = {
-            # 'when' is a possible leftover from Bugzillas before 2.8
-            bugs_activity => ['when', 'bugs_activity_bugid_idx',
-                'bugs_activity_bugwhen_idx'],
-            cc => ['PRIMARY'],
-            longdescs => ['longdescs_bugid_idx',
-               'longdescs_bugwhen_idx'],
-            flags => ['flags_bidattid_idx'],
-            flaginclusions => ['flaginclusions_tpcid_idx'],
-            flagexclusions => ['flagexclusions_tpc_id_idx'],
-            keywords => ['PRIMARY'],
-            milestones => ['PRIMARY'],
-            profiles_activity => ['profiles_activity_when_idx'],
-            group_control_map => ['group_control_map_gid_idx', 'PRIMARY'],
-            user_group_map => ['PRIMARY'],
-            group_group_map => ['PRIMARY'],
-            email_setting => ['PRIMARY'],
-            bug_group_map => ['PRIMARY'],
-            category_group_map => ['PRIMARY'],
-            watch => ['PRIMARY'],
-            namedqueries => ['PRIMARY'],
-            series_data => ['PRIMARY'],
-            # series_categories is dealt with below, not here.
-        };
-
-        # The series table is broken and needs to have one index
-        # dropped before we begin the renaming, because it had a
-        # useless index on it that would cause a naming conflict here.
-        if (grep($_ eq 'series', @tables)) {
-            my $dropname;
-            # This is what the bad index was called before Schema.
-            if ($self->bz_index_info_real('series', 'creator_2')) {
-                $dropname = 'creator_2';
-            }
-            # This is what the bad index is called in Schema.
-            elsif ($self->bz_index_info_real('series', 'series_creator_idx')) {
-                    $dropname = 'series_creator_idx';
-            }
-            $self->bz_drop_index_raw('series', $dropname) if $dropname;
-        }
+    my $bug_count = $self->selectrow_array("SELECT COUNT(*) FROM bugs");
 
-        # The email_setting table also had the same problem.
-        if( grep($_ eq 'email_setting', @tables) 
-            && $self->bz_index_info_real('email_setting', 
-                                         'email_settings_user_id_idx') ) 
-        {
-            $self->bz_drop_index_raw('email_setting', 
-                                     'email_settings_user_id_idx');
-        }
+    # We estimate one minute for each 3000 bugs, plus 3 minutes just
+    # to handle basic MySQL stuff.
+    my $rename_time = int($bug_count / 3000) + 3;
 
-        # Go through all the tables.
-        foreach my $table (@tables) {
-            # Will contain the names of old indexes as keys, and the 
-            # definition of the new indexes as a value. The values
-            # include an extra hash key, NAME, with the new name of 
-            # the index.
-            my %rename_indexes;
-            # And go through all the columns on each table.
-            my @columns = $self->bz_table_columns_real($table);
-
-            # We also want to fix the silly naming of unique indexes
-            # that happened when we first checked-in Bugzilla::DB::Schema.
-            if ($table eq 'series_categories') {
-                # The series_categories index had a nonstandard name.
-                push(@columns, 'series_cats_unique_idx');
-            } 
-            elsif ($table eq 'email_setting') { 
-                # The email_setting table had a similar problem.
-                push(@columns, 'email_settings_unique_idx');
-            }
-            else {
-                push(@columns, "${table}_unique_idx");
-            }
-            # And this is how we fix the other inconsistent Schema naming.
-            push(@columns, @{$bad_names->{$table}})
-                if (exists $bad_names->{$table});
-            foreach my $column (@columns) {
-                # If we have an index named after this column, it's an 
-                # old-style-name index.
-                if (my $index = $self->bz_index_info_real($table, $column)) {
-                    # Fix the name to fit in with the new naming scheme.
-                    $index->{NAME} = $table . "_" .
-                                     $index->{FIELDS}->[0] . "_idx";
-                    print "Renaming index $column to " 
-                          . $index->{NAME} . "...\n";
-                    $rename_indexes{$column} = $index;
-                } # if
-            } # foreach column
-
-            my @rename_sql = $self->_bz_schema->get_rename_indexes_ddl(
-                $table, %rename_indexes);
-            $self->do($_) foreach (@rename_sql);
-
-        } # foreach table
-    } # if old-name indexes
-
-    # If there are no tables, but the DB isn't utf8 and it should be,
-    # then we should alter the database to be utf8. We know it should be
-    # if there are no params at all.
-    # This kind of situation happens when people create the database
-    # themselves, and if we don't do this they will get the big
-    # scary WARNING statement about conversion to UTF8.
-    if (!$self->bz_db_is_utf8 && !@tables) {
-        $self->_alter_db_charset_to_utf8();
-    }
+    # And 45 minutes for every 15,000 attachments, per some experiments.
+    my ($attachment_count)
+      = $self->selectrow_array("SELECT COUNT(*) FROM attachments");
+    $rename_time += int(($attachment_count * 45) / 15000);
 
-    # And now we create the tables and the Schema object.
-    $self->SUPER::bz_setup_database();
+    # If we're going to take longer than 5 minutes, we let the user know
+    # and allow them to abort.
+    if ($rename_time > 5) {
+      print "\n", install_string('mysql_index_renaming', {minutes => $rename_time});
 
-    if ($sd_index_deleted) {
-        $self->_bz_real_schema->delete_index('bugs', 'bugs_short_desc_idx');
-        $self->_bz_store_real_schema;
+      # Wait 45 seconds for them to respond.
+      sleep(45) unless Bugzilla->installation_answers->{NO_PAUSE};
     }
-    if ($longdescs_index_deleted) {
-        $self->_bz_real_schema->delete_index('longdescs', 
-                                             'longdescs_thetext_idx');
-        $self->_bz_store_real_schema;
+    print "Renaming indexes...\n";
+
+    # We can't be interrupted, because of how the "if"
+    # works above.
+    local $SIG{INT}  = 'IGNORE';
+    local $SIG{TERM} = 'IGNORE';
+    local $SIG{PIPE} = 'IGNORE';
+
+    # Certain indexes had names in Schema that did not easily conform
+    # to a standard. We store those names here, so that they
+    # can be properly renamed.
+    # Also, sometimes an old mysqldump would incorrectly rename
+    # unique indexes to "PRIMARY", so we address that here, also.
+    my $bad_names = {
+
+      # 'when' is a possible leftover from Bugzillas before 2.8
+      bugs_activity =>
+        ['when', 'bugs_activity_bugid_idx', 'bugs_activity_bugwhen_idx'],
+      cc                 => ['PRIMARY'],
+      longdescs          => ['longdescs_bugid_idx', 'longdescs_bugwhen_idx'],
+      flags              => ['flags_bidattid_idx'],
+      flaginclusions     => ['flaginclusions_tpcid_idx'],
+      flagexclusions     => ['flagexclusions_tpc_id_idx'],
+      keywords           => ['PRIMARY'],
+      milestones         => ['PRIMARY'],
+      profiles_activity  => ['profiles_activity_when_idx'],
+      group_control_map  => ['group_control_map_gid_idx', 'PRIMARY'],
+      user_group_map     => ['PRIMARY'],
+      group_group_map    => ['PRIMARY'],
+      email_setting      => ['PRIMARY'],
+      bug_group_map      => ['PRIMARY'],
+      category_group_map => ['PRIMARY'],
+      watch              => ['PRIMARY'],
+      namedqueries       => ['PRIMARY'],
+      series_data        => ['PRIMARY'],
+
+      # series_categories is dealt with below, not here.
+    };
+
+    # The series table is broken and needs to have one index
+    # dropped before we begin the renaming, because it had a
+    # useless index on it that would cause a naming conflict here.
+    if (grep($_ eq 'series', @tables)) {
+      my $dropname;
+
+      # This is what the bad index was called before Schema.
+      if ($self->bz_index_info_real('series', 'creator_2')) {
+        $dropname = 'creator_2';
+      }
+
+      # This is what the bad index is called in Schema.
+      elsif ($self->bz_index_info_real('series', 'series_creator_idx')) {
+        $dropname = 'series_creator_idx';
+      }
+      $self->bz_drop_index_raw('series', $dropname) if $dropname;
     }
 
-    # The old timestamp fields need to be adjusted here instead of in
-    # checksetup. Otherwise the UPDATE statements inside of bz_add_column
-    # will cause accidental timestamp updates.
-    # The code that does this was moved here from checksetup.
+    # The email_setting table also had the same problem.
+    if (grep($_ eq 'email_setting', @tables)
+      && $self->bz_index_info_real('email_setting', 'email_settings_user_id_idx'))
+    {
+      $self->bz_drop_index_raw('email_setting', 'email_settings_user_id_idx');
+    }
 
-    # 2002-08-14 - bbaetz@student.usyd.edu.au - bug 153578
-    # attachments creation time needs to be a datetime, not a timestamp
-    my $attach_creation = 
-        $self->bz_column_info("attachments", "creation_ts");
-    if ($attach_creation && $attach_creation->{TYPE} =~ /^TIMESTAMP/i) {
-        print "Fixing creation time on attachments...\n";
+    # Go through all the tables.
+    foreach my $table (@tables) {
 
-        my $sth = $self->prepare("SELECT COUNT(attach_id) FROM attachments");
-        $sth->execute();
-        my ($attach_count) = $sth->fetchrow_array();
+      # Will contain the names of old indexes as keys, and the
+      # definition of the new indexes as a value. The values
+      # include an extra hash key, NAME, with the new name of
+      # the index.
+      my %rename_indexes;
+
+      # And go through all the columns on each table.
+      my @columns = $self->bz_table_columns_real($table);
+
+      # We also want to fix the silly naming of unique indexes
+      # that happened when we first checked-in Bugzilla::DB::Schema.
+      if ($table eq 'series_categories') {
+
+        # The series_categories index had a nonstandard name.
+        push(@columns, 'series_cats_unique_idx');
+      }
+      elsif ($table eq 'email_setting') {
+
+        # The email_setting table had a similar problem.
+        push(@columns, 'email_settings_unique_idx');
+      }
+      else {
+        push(@columns, "${table}_unique_idx");
+      }
+
+      # And this is how we fix the other inconsistent Schema naming.
+      push(@columns, @{$bad_names->{$table}}) if (exists $bad_names->{$table});
+      foreach my $column (@columns) {
+
+        # If we have an index named after this column, it's an
+        # old-style-name index.
+        if (my $index = $self->bz_index_info_real($table, $column)) {
+
+          # Fix the name to fit in with the new naming scheme.
+          $index->{NAME} = $table . "_" . $index->{FIELDS}->[0] . "_idx";
+          print "Renaming index $column to " . $index->{NAME} . "...\n";
+          $rename_indexes{$column} = $index;
+        }    # if
+      }    # foreach column
+
+      my @rename_sql
+        = $self->_bz_schema->get_rename_indexes_ddl($table, %rename_indexes);
+      $self->do($_) foreach (@rename_sql);
+
+    }    # foreach table
+  }    # if old-name indexes
+
+  # If there are no tables, but the DB isn't utf8 and it should be,
+  # then we should alter the database to be utf8. We know it should be
+  # if there are no params at all.
+  # This kind of situation happens when people create the database
+  # themselves, and if we don't do this they will get the big
+  # scary WARNING statement about conversion to UTF8.
+  if (!$self->bz_db_is_utf8 && !@tables) {
+    $self->_alter_db_charset_to_utf8();
+  }
+
+  # And now we create the tables and the Schema object.
+  $self->SUPER::bz_setup_database();
+
+  if ($sd_index_deleted) {
+    $self->_bz_real_schema->delete_index('bugs', 'bugs_short_desc_idx');
+    $self->_bz_store_real_schema;
+  }
+  if ($longdescs_index_deleted) {
+    $self->_bz_real_schema->delete_index('longdescs', 'longdescs_thetext_idx');
+    $self->_bz_store_real_schema;
+  }
+
+  # The old timestamp fields need to be adjusted here instead of in
+  # checksetup. Otherwise the UPDATE statements inside of bz_add_column
+  # will cause accidental timestamp updates.
+  # The code that does this was moved here from checksetup.
+
+  # 2002-08-14 - bbaetz@student.usyd.edu.au - bug 153578
+  # attachments creation time needs to be a datetime, not a timestamp
+  my $attach_creation = $self->bz_column_info("attachments", "creation_ts");
+  if ($attach_creation && $attach_creation->{TYPE} =~ /^TIMESTAMP/i) {
+    print "Fixing creation time on attachments...\n";
+
+    my $sth = $self->prepare("SELECT COUNT(attach_id) FROM attachments");
+    $sth->execute();
+    my ($attach_count) = $sth->fetchrow_array();
 
-        if ($attach_count > 1000) {
-            print "This may take a while...\n";
-        }
-        my $i = 0;
-
-        # This isn't just as simple as changing the field type, because
-        # the creation_ts was previously updated when an attachment was made
-        # obsolete from the attachment creation screen. So we have to go
-        # and recreate these times from the comments..
-        $sth = $self->prepare("SELECT bug_id, attach_id, submitter_id " .
-                               "FROM attachments");
-        $sth->execute();
-
-        # Restrict this as much as possible in order to avoid false 
-        # positives, and keep the db search time down
-        my $sth2 = $self->prepare("SELECT bug_when FROM longdescs 
+    if ($attach_count > 1000) {
+      print "This may take a while...\n";
+    }
+    my $i = 0;
+
+    # This isn't just as simple as changing the field type, because
+    # the creation_ts was previously updated when an attachment was made
+    # obsolete from the attachment creation screen. So we have to go
+    # and recreate these times from the comments..
+    $sth = $self->prepare(
+      "SELECT bug_id, attach_id, submitter_id " . "FROM attachments");
+    $sth->execute();
+
+    # Restrict this as much as possible in order to avoid false
+    # positives, and keep the db search time down
+    my $sth2 = $self->prepare(
+      "SELECT bug_when FROM longdescs 
                                     WHERE bug_id=? AND who=? 
                                           AND thetext LIKE ?
-                                 ORDER BY bug_when " . $self->sql_limit(1));
-        while (my ($bug_id, $attach_id, $submitter_id) 
-                  = $sth->fetchrow_array()) 
+                                 ORDER BY bug_when " . $self->sql_limit(1)
+    );
+    while (my ($bug_id, $attach_id, $submitter_id) = $sth->fetchrow_array()) {
+      $sth2->execute($bug_id, $submitter_id,
+        "Created an attachment (id=$attach_id)%");
+      my ($when) = $sth2->fetchrow_array();
+      if ($when) {
+        $self->do("UPDATE attachments "
+            . "SET creation_ts='$when' "
+            . "WHERE attach_id=$attach_id");
+      }
+      else {
+        print "Warning - could not determine correct creation"
+          . " time for attachment $attach_id on bug $bug_id\n";
+      }
+      ++$i;
+      print "Converted $i of $attach_count attachments\n" if !($i % 1000);
+    }
+    print "Done - converted $i attachments\n";
+
+    $self->bz_alter_column("attachments", "creation_ts",
+      {TYPE => 'DATETIME', NOTNULL => 1});
+  }
+
+  # 2004-08-29 - Tomas.Kopal@altap.cz, bug 257303
+  # Change logincookies.lastused type from timestamp to datetime
+  my $login_lastused = $self->bz_column_info("logincookies", "lastused");
+  if ($login_lastused && $login_lastused->{TYPE} =~ /^TIMESTAMP/i) {
+    $self->bz_alter_column('logincookies', 'lastused',
+      {TYPE => 'DATETIME', NOTNULL => 1});
+  }
+
+  # 2005-01-17 - Tomas.Kopal@altap.cz, bug 257315
+  # Change bugs.delta_ts type from timestamp to datetime
+  my $bugs_deltats = $self->bz_column_info("bugs", "delta_ts");
+  if ($bugs_deltats && $bugs_deltats->{TYPE} =~ /^TIMESTAMP/i) {
+    $self->bz_alter_column('bugs', 'delta_ts', {TYPE => 'DATETIME', NOTNULL => 1});
+  }
+
+  # 2005-09-24 - bugreport@peshkin.net, bug 307602
+  # Make sure that default 4G table limit is overridden
+  my $attach_data_create = $self->selectrow_array(
+    'SELECT CREATE_OPTIONS FROM information_schema.TABLES 
+          WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?', undef, $db_name, 'attach_data'
+  );
+  if ($attach_data_create !~ /MAX_ROWS/i) {
+    print "Converting attach_data maximum size to 100G...\n";
+    $self->do(
+      "ALTER TABLE attach_data
+                   AVG_ROW_LENGTH=1000000,
+                   MAX_ROWS=100000"
+    );
+  }
+
+  # Convert the database to UTF-8.
+  # We check if any table isn't utf8, because lots of crazy
+  # partial-conversion situations can happen, and this handles anything
+  # that could come up (including having the DB charset be utf8 but not
+  # the table charsets.
+  #
+  # TABLE_COLLATION IS NOT NULL prevents us from trying to convert views.
+  my $non_utf8_tables = $self->selectrow_array(
+    "SELECT 1 FROM information_schema.TABLES 
+          WHERE TABLE_SCHEMA = ? AND TABLE_COLLATION IS NOT NULL 
+                AND TABLE_COLLATION NOT LIKE 'utf8%' 
+          LIMIT 1", undef, $db_name
+  );
+
+  if ($non_utf8_tables) {
+    print "\n", install_string('mysql_utf8_conversion');
+
+    if (!Bugzilla->installation_answers->{NO_PAUSE}) {
+      if (Bugzilla->installation_mode == INSTALLATION_MODE_NON_INTERACTIVE) {
+        die install_string('continue_without_answers'), "\n";
+      }
+      else {
+        print "\n         " . install_string('enter_or_ctrl_c');
+        getc;
+      }
+    }
+
+    say 'Converting table storage format to UTF-8. This may take a while.';
+    foreach my $table ($self->bz_table_list_real) {
+      my $info_sth = $self->prepare("SHOW FULL COLUMNS FROM $table");
+      $info_sth->execute();
+      my (@binary_sql, @utf8_sql);
+      while (my $column = $info_sth->fetchrow_hashref) {
+
+        # Our conversion code doesn't work on enum fields, but they
+        # all go away later in checksetup anyway.
+        next if $column->{Type} =~ /enum/i;
+
+        # If this particular column isn't stored in utf-8
+        if ( $column->{Collation}
+          && $column->{Collation} ne 'NULL'
+          && $column->{Collation} !~ /utf8/)
         {
-            $sth2->execute($bug_id, $submitter_id, 
-                "Created an attachment (id=$attach_id)%");
-            my ($when) = $sth2->fetchrow_array();
-            if ($when) {
-                $self->do("UPDATE attachments " .
-                             "SET creation_ts='$when' " .
-                           "WHERE attach_id=$attach_id");
-            } else {
-                print "Warning - could not determine correct creation"
-                      . " time for attachment $attach_id on bug $bug_id\n";
-            }
-            ++$i;
-            print "Converted $i of $attach_count attachments\n" if !($i % 1000);
-        }
-        print "Done - converted $i attachments\n";
+          my $name = $column->{Field};
 
-        $self->bz_alter_column("attachments", "creation_ts", 
-                               {TYPE => 'DATETIME', NOTNULL => 1});
-    }
+          say "$table.$name needs to be converted to UTF-8...";
 
-    # 2004-08-29 - Tomas.Kopal@altap.cz, bug 257303
-    # Change logincookies.lastused type from timestamp to datetime
-    my $login_lastused = $self->bz_column_info("logincookies", "lastused");
-    if ($login_lastused && $login_lastused->{TYPE} =~ /^TIMESTAMP/i) {
-        $self->bz_alter_column('logincookies', 'lastused', 
-                               { TYPE => 'DATETIME',  NOTNULL => 1});
-    }
+          # These will be automatically re-created at the end
+          # of checksetup.
+          $self->bz_drop_related_fks($table, $name);
 
-    # 2005-01-17 - Tomas.Kopal@altap.cz, bug 257315
-    # Change bugs.delta_ts type from timestamp to datetime 
-    my $bugs_deltats = $self->bz_column_info("bugs", "delta_ts");
-    if ($bugs_deltats && $bugs_deltats->{TYPE} =~ /^TIMESTAMP/i) {
-        $self->bz_alter_column('bugs', 'delta_ts', 
-                               {TYPE => 'DATETIME', NOTNULL => 1});
-    }
+          my $col_info = $self->bz_column_info_real($table, $name);
 
-    # 2005-09-24 - bugreport@peshkin.net, bug 307602
-    # Make sure that default 4G table limit is overridden
-    my $attach_data_create = $self->selectrow_array(
-        'SELECT CREATE_OPTIONS FROM information_schema.TABLES 
-          WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?',
-        undef, $db_name, 'attach_data');
-    if ($attach_data_create !~ /MAX_ROWS/i) {
-        print "Converting attach_data maximum size to 100G...\n";
-        $self->do("ALTER TABLE attach_data
-                   AVG_ROW_LENGTH=1000000,
-                   MAX_ROWS=100000");
-    }
+          # CHANGE COLUMN doesn't take PRIMARY KEY
+          delete $col_info->{PRIMARYKEY};
+          my $sql_def = $self->_bz_schema->get_type_ddl($col_info);
 
-    # Convert the database to UTF-8.
-    # We check if any table isn't utf8, because lots of crazy
-    # partial-conversion situations can happen, and this handles anything
-    # that could come up (including having the DB charset be utf8 but not
-    # the table charsets.
-    #
-    # TABLE_COLLATION IS NOT NULL prevents us from trying to convert views.
-    my $non_utf8_tables = $self->selectrow_array(
-        "SELECT 1 FROM information_schema.TABLES 
-          WHERE TABLE_SCHEMA = ? AND TABLE_COLLATION IS NOT NULL 
-                AND TABLE_COLLATION NOT LIKE 'utf8%' 
-          LIMIT 1", undef, $db_name);
-
-    if ($non_utf8_tables) {
-        print "\n", install_string('mysql_utf8_conversion');
-
-        if (!Bugzilla->installation_answers->{NO_PAUSE}) {
-            if (Bugzilla->installation_mode == 
-                INSTALLATION_MODE_NON_INTERACTIVE) 
-            {
-                die install_string('continue_without_answers'), "\n";
-            }
-            else {
-                print "\n         " . install_string('enter_or_ctrl_c');
-                getc;
-            }
+          # We don't want MySQL to actually try to *convert*
+          # from our current charset to UTF-8, we just want to
+          # transfer the bytes directly. This is how we do that.
+
+          # The CHARACTER SET part of the definition has to come
+          # right after the type, which will always come first.
+          my ($binary, $utf8) = ($sql_def, $sql_def);
+          my $type = $self->_bz_schema->convert_type($col_info->{TYPE});
+          $binary =~ s/(\Q$type\E)/$1 CHARACTER SET binary/;
+          $utf8 =~ s/(\Q$type\E)/$1 CHARACTER SET utf8/;
+          push(@binary_sql, "MODIFY COLUMN $name $binary");
+          push(@utf8_sql,   "MODIFY COLUMN $name $utf8");
+        }
+      }    # foreach column
+
+      if (@binary_sql) {
+        my %indexes = %{$self->bz_table_indexes($table)};
+        foreach my $index_name (keys %indexes) {
+          my $index = $indexes{$index_name};
+          if ($index->{TYPE} and $index->{TYPE} eq 'FULLTEXT') {
+            $self->bz_drop_index($table, $index_name);
+          }
+          else {
+            delete $indexes{$index_name};
+          }
         }
 
-        say 'Converting table storage format to UTF-8. This may take a while.';
-        foreach my $table ($self->bz_table_list_real) {
-            my $info_sth = $self->prepare("SHOW FULL COLUMNS FROM $table");
-            $info_sth->execute();
-            my (@binary_sql, @utf8_sql);
-            while (my $column = $info_sth->fetchrow_hashref) {
-                # Our conversion code doesn't work on enum fields, but they
-                # all go away later in checksetup anyway.
-                next if $column->{Type} =~ /enum/i;
-
-                # If this particular column isn't stored in utf-8
-                if ($column->{Collation}
-                    && $column->{Collation} ne 'NULL' 
-                    && $column->{Collation} !~ /utf8/) 
-                {
-                    my $name = $column->{Field};
-
-                    say "$table.$name needs to be converted to UTF-8...";
-
-                    # These will be automatically re-created at the end
-                    # of checksetup.
-                    $self->bz_drop_related_fks($table, $name);
-
-                    my $col_info =
-                        $self->bz_column_info_real($table, $name);
-                    # CHANGE COLUMN doesn't take PRIMARY KEY
-                    delete $col_info->{PRIMARYKEY};
-                    my $sql_def = $self->_bz_schema->get_type_ddl($col_info);
-                    # We don't want MySQL to actually try to *convert*
-                    # from our current charset to UTF-8, we just want to
-                    # transfer the bytes directly. This is how we do that.
-
-                    # The CHARACTER SET part of the definition has to come
-                    # right after the type, which will always come first.
-                    my ($binary, $utf8) = ($sql_def, $sql_def);
-                    my $type = $self->_bz_schema->convert_type($col_info->{TYPE});
-                    $binary =~ s/(\Q$type\E)/$1 CHARACTER SET binary/;
-                    $utf8   =~ s/(\Q$type\E)/$1 CHARACTER SET utf8/;
-                    push(@binary_sql, "MODIFY COLUMN $name $binary");
-                    push(@utf8_sql, "MODIFY COLUMN $name $utf8");
-                }
-            } # foreach column
-
-            if (@binary_sql) {
-                my %indexes = %{ $self->bz_table_indexes($table) };
-                foreach my $index_name (keys %indexes) {
-                    my $index = $indexes{$index_name};
-                    if ($index->{TYPE} and $index->{TYPE} eq 'FULLTEXT') {
-                        $self->bz_drop_index($table, $index_name);
-                    }
-                    else {
-                        delete $indexes{$index_name};
-                    }
-                }
-
-                say "Converting the $table table to UTF-8...";
-                my $bin = "ALTER TABLE $table " . join(', ', @binary_sql);
-                my $utf = "ALTER TABLE $table " . join(', ', @utf8_sql,
-                          'DEFAULT CHARACTER SET utf8');
-                $self->do($bin);
-                $self->do($utf);
-
-                # Re-add any removed FULLTEXT indexes.
-                foreach my $index (keys %indexes) {
-                    $self->bz_add_index($table, $index, $indexes{$index});
-                }
-            }
-            else {
-                $self->do("ALTER TABLE $table DEFAULT CHARACTER SET utf8");
-            }
-
-        } # foreach my $table (@tables)
-    }
+        say "Converting the $table table to UTF-8...";
+        my $bin = "ALTER TABLE $table " . join(', ', @binary_sql);
+        my $utf
+          = "ALTER TABLE $table " . join(', ', @utf8_sql, 'DEFAULT CHARACTER SET utf8');
+        $self->do($bin);
+        $self->do($utf);
 
-    # Sometimes you can have a situation where all the tables are utf8,
-    # but the database isn't. (This tends to happen when you've done
-    # a mysqldump.) So we have this change outside of the above block,
-    # so that it just happens silently if no actual *table* conversion
-    # needs to happen.
-    $self->_alter_db_charset_to_utf8() unless $self->bz_db_is_utf8;
-
-    $self->_fix_defaults();
-
-    # Bug 451735 highlighted a bug in bz_drop_index() which didn't
-    # check for FKs before trying to delete an index. Consequently,
-    # the series_creator_idx index was considered to be deleted
-    # despite it was still present in the DB. That's why we have to
-    # force the deletion, bypassing the DB schema.
-    if (!$self->bz_index_info('series', 'series_category_idx')) {
-        if (!$self->bz_index_info('series', 'series_creator_idx')
-            && $self->bz_index_info_real('series', 'series_creator_idx'))
-        {
-            foreach my $column (qw(creator category subcategory name)) {
-                $self->bz_drop_related_fks('series', $column);
-            }
-            $self->bz_drop_index_raw('series', 'series_creator_idx');
+        # Re-add any removed FULLTEXT indexes.
+        foreach my $index (keys %indexes) {
+          $self->bz_add_index($table, $index, $indexes{$index});
         }
+      }
+      else {
+        $self->do("ALTER TABLE $table DEFAULT CHARACTER SET utf8");
+      }
+
+    }    # foreach my $table (@tables)
+  }
+
+  # Sometimes you can have a situation where all the tables are utf8,
+  # but the database isn't. (This tends to happen when you've done
+  # a mysqldump.) So we have this change outside of the above block,
+  # so that it just happens silently if no actual *table* conversion
+  # needs to happen.
+  $self->_alter_db_charset_to_utf8() unless $self->bz_db_is_utf8;
+
+  $self->_fix_defaults();
+
+  # Bug 451735 highlighted a bug in bz_drop_index() which didn't
+  # check for FKs before trying to delete an index. Consequently,
+  # the series_creator_idx index was considered to be deleted
+  # despite it was still present in the DB. That's why we have to
+  # force the deletion, bypassing the DB schema.
+  if (!$self->bz_index_info('series', 'series_category_idx')) {
+    if (!$self->bz_index_info('series', 'series_creator_idx')
+      && $self->bz_index_info_real('series', 'series_creator_idx'))
+    {
+      foreach my $column (qw(creator category subcategory name)) {
+        $self->bz_drop_related_fks('series', $column);
+      }
+      $self->bz_drop_index_raw('series', 'series_creator_idx');
     }
+  }
 }
 
 # When you import a MySQL 3/4 mysqldump into MySQL 5, columns that
@@ -785,100 +802,109 @@ sub bz_setup_database {
 # looks like. So we remove defaults from columns that aren't supposed
 # to have them
 sub _fix_defaults {
-    my $self = shift;
-    my $maj_version = substr($self->bz_server_version, 0, 1);
-    return if $maj_version < 5;
-
-    # The oldest column that could have this problem is bugs.assigned_to,
-    # so if it doesn't have the problem, we just skip doing this entirely.
-    my $assi_def = $self->_bz_raw_column_info('bugs', 'assigned_to');
-    my $assi_default = $assi_def->{COLUMN_DEF};
-    # This "ne ''" thing is necessary because _raw_column_info seems to
-    # return COLUMN_DEF as an empty string for columns that don't have
-    # a default.
-    return unless (defined $assi_default && $assi_default ne '');
-
-    my %fix_columns;
-    foreach my $table ($self->_bz_real_schema->get_table_list()) {
-        foreach my $column ($self->bz_table_columns($table)) {
-            my $abs_def = $self->bz_column_info($table, $column);
-            # BLOB/TEXT columns never have defaults
-            next if $abs_def->{TYPE} =~ /BLOB|TEXT/i;
-            if (!defined $abs_def->{DEFAULT}) {
-                # Get the exact default from the database without any
-                # "fixing" by bz_column_info_real.
-                my $raw_info = $self->_bz_raw_column_info($table, $column);
-                my $raw_default = $raw_info->{COLUMN_DEF};
-                if (defined $raw_default) {
-                    if ($raw_default eq '') {
-                        # Only (var)char columns can have empty strings as 
-                        # defaults, so if we got an empty string for some
-                        # other default type, then it's bogus.
-                        next unless $abs_def->{TYPE} =~ /char/i;
-                        $raw_default = "''";
-                    }
-                    $fix_columns{$table} ||= [];
-                    push(@{ $fix_columns{$table} }, $column);
-                    print "$table.$column has incorrect DB default: $raw_default\n";
-                }
-            }
-        } # foreach $column
-    } # foreach $table
-
-    print "Fixing defaults...\n";
-    foreach my $table (reverse sort keys %fix_columns) {
-        my @alters = map("ALTER COLUMN $_ DROP DEFAULT", 
-                         @{ $fix_columns{$table} });
-        my $sql = "ALTER TABLE $table " . join(',', @alters);
-        $self->do($sql);
-    }
+  my $self = shift;
+  my $maj_version = substr($self->bz_server_version, 0, 1);
+  return if $maj_version < 5;
+
+  # The oldest column that could have this problem is bugs.assigned_to,
+  # so if it doesn't have the problem, we just skip doing this entirely.
+  my $assi_def = $self->_bz_raw_column_info('bugs', 'assigned_to');
+  my $assi_default = $assi_def->{COLUMN_DEF};
+
+  # This "ne ''" thing is necessary because _raw_column_info seems to
+  # return COLUMN_DEF as an empty string for columns that don't have
+  # a default.
+  return unless (defined $assi_default && $assi_default ne '');
+
+  my %fix_columns;
+  foreach my $table ($self->_bz_real_schema->get_table_list()) {
+    foreach my $column ($self->bz_table_columns($table)) {
+      my $abs_def = $self->bz_column_info($table, $column);
+
+      # BLOB/TEXT columns never have defaults
+      next if $abs_def->{TYPE} =~ /BLOB|TEXT/i;
+      if (!defined $abs_def->{DEFAULT}) {
+
+        # Get the exact default from the database without any
+        # "fixing" by bz_column_info_real.
+        my $raw_info = $self->_bz_raw_column_info($table, $column);
+        my $raw_default = $raw_info->{COLUMN_DEF};
+        if (defined $raw_default) {
+          if ($raw_default eq '') {
+
+            # Only (var)char columns can have empty strings as
+            # defaults, so if we got an empty string for some
+            # other default type, then it's bogus.
+            next unless $abs_def->{TYPE} =~ /char/i;
+            $raw_default = "''";
+          }
+          $fix_columns{$table} ||= [];
+          push(@{$fix_columns{$table}}, $column);
+          print "$table.$column has incorrect DB default: $raw_default\n";
+        }
+      }
+    }    # foreach $column
+  }    # foreach $table
+
+  print "Fixing defaults...\n";
+  foreach my $table (reverse sort keys %fix_columns) {
+    my @alters = map("ALTER COLUMN $_ DROP DEFAULT", @{$fix_columns{$table}});
+    my $sql = "ALTER TABLE $table " . join(',', @alters);
+    $self->do($sql);
+  }
 }
 
 sub _alter_db_charset_to_utf8 {
-    my $self = shift;
-    my $db_name = Bugzilla->localconfig->{db_name};
-    $self->do("ALTER DATABASE `$db_name` CHARACTER SET utf8"); 
+  my $self    = shift;
+  my $db_name = Bugzilla->localconfig->{db_name};
+  $self->do("ALTER DATABASE `$db_name` CHARACTER SET utf8");
 }
 
 sub bz_db_is_utf8 {
-    my $self = shift;
-    my $db_collation = $self->selectrow_arrayref(
-        "SHOW VARIABLES LIKE 'character_set_database'");
-    # First column holds the variable name, second column holds the value.
-    return $db_collation->[1] =~ /utf8/ ? 1 : 0;
+  my $self = shift;
+  my $db_collation
+    = $self->selectrow_arrayref("SHOW VARIABLES LIKE 'character_set_database'");
+
+  # First column holds the variable name, second column holds the value.
+  return $db_collation->[1] =~ /utf8/ ? 1 : 0;
 }
 
 
 sub bz_enum_initial_values {
-    my ($self) = @_;
-    my %enum_values = %{$self->ENUM_DEFAULTS};
-    # Get a complete description of the 'bugs' table; with DBD::MySQL
-    # there isn't a column-by-column way of doing this.  Could use
-    # $dbh->column_info, but it would go slower and we would have to
-    # use the undocumented mysql_type_name accessor to get the type
-    # of each row.
-    my $sth = $self->prepare("DESCRIBE bugs");
-    $sth->execute();
-    # Look for the particular columns we are interested in.
-    while (my ($thiscol, $thistype) = $sth->fetchrow_array()) {
-        if (defined $enum_values{$thiscol}) {
-            # this is a column of interest.
-            my @value_list;
-            if ($thistype and ($thistype =~ /^enum\(/)) {
-                # it has an enum type; get the set of values.
-                while ($thistype =~ /'([^']*)'(.*)/) {
-                    push(@value_list, $1);
-                    $thistype = $2;
-                }
-            }
-            if (@value_list) {
-                # record the enum values found.
-                $enum_values{$thiscol} = \@value_list;
-            }
+  my ($self) = @_;
+  my %enum_values = %{$self->ENUM_DEFAULTS};
+
+  # Get a complete description of the 'bugs' table; with DBD::MySQL
+  # there isn't a column-by-column way of doing this.  Could use
+  # $dbh->column_info, but it would go slower and we would have to
+  # use the undocumented mysql_type_name accessor to get the type
+  # of each row.
+  my $sth = $self->prepare("DESCRIBE bugs");
+  $sth->execute();
+
+  # Look for the particular columns we are interested in.
+  while (my ($thiscol, $thistype) = $sth->fetchrow_array()) {
+    if (defined $enum_values{$thiscol}) {
+
+      # this is a column of interest.
+      my @value_list;
+      if ($thistype and ($thistype =~ /^enum\(/)) {
+
+        # it has an enum type; get the set of values.
+        while ($thistype =~ /'([^']*)'(.*)/) {
+          push(@value_list, $1);
+          $thistype = $2;
         }
+      }
+      if (@value_list) {
+
+        # record the enum values found.
+        $enum_values{$thiscol} = \@value_list;
+      }
     }
+  }
 
-    return \%enum_values;
+  return \%enum_values;
 }
 
 #####################################################################
@@ -909,29 +935,29 @@ backwards-compatibility anyway, for versions of Bugzilla before 2.20.
 =cut
 
 sub bz_column_info_real {
-    my ($self, $table, $column) = @_;
-    my $col_data = $self->_bz_raw_column_info($table, $column);
-    return $self->_bz_schema->column_info_to_column($col_data);
+  my ($self, $table, $column) = @_;
+  my $col_data = $self->_bz_raw_column_info($table, $column);
+  return $self->_bz_schema->column_info_to_column($col_data);
 }
 
 sub _bz_raw_column_info {
-    my ($self, $table, $column) = @_;
-
-    # DBD::mysql does not support selecting a specific column,
-    # so we have to get all the columns on the table and find 
-    # the one we want.
-    my $info_sth = $self->column_info(undef, undef, $table, '%');
-
-    # Don't use fetchall_hashref as there's a Win32 DBI bug (292821)
-    my $col_data;
-    while ($col_data = $info_sth->fetchrow_hashref) {
-        last if $col_data->{'COLUMN_NAME'} eq $column;
-    }
-
-    if (!defined $col_data) {
-        return undef;
-    }
-    return $col_data;
+  my ($self, $table, $column) = @_;
+
+  # DBD::mysql does not support selecting a specific column,
+  # so we have to get all the columns on the table and find
+  # the one we want.
+  my $info_sth = $self->column_info(undef, undef, $table, '%');
+
+  # Don't use fetchall_hashref as there's a Win32 DBI bug (292821)
+  my $col_data;
+  while ($col_data = $info_sth->fetchrow_hashref) {
+    last if $col_data->{'COLUMN_NAME'} eq $column;
+  }
+
+  if (!defined $col_data) {
+    return undef;
+  }
+  return $col_data;
 }
 
 =item C<bz_index_info_real($table, $index)>
@@ -945,42 +971,43 @@ sub _bz_raw_column_info {
 =cut
 
 sub bz_index_info_real {
-    my ($self, $table, $index) = @_;
-
-    my $sth = $self->prepare("SHOW INDEX FROM $table");
-    $sth->execute;
-
-    my @fields;
-    my $index_type;
-    # $raw_def will be an arrayref containing the following information:
-    # 0 = name of the table that the index is on
-    # 1 = 0 if unique, 1 if not unique
-    # 2 = name of the index
-    # 3 = seq_in_index (The order of the current field in the index).
-    # 4 = Name of ONE column that the index is on
-    # 5 = 'Collation' of the index. Usually 'A'.
-    # 6 = Cardinality. Either a number or undef.
-    # 7 = sub_part. Usually undef. Sometimes 1.
-    # 8 = "packed". Usually undef.
-    # 9 = Null. Sometimes undef, sometimes 'YES'.
-    # 10 = Index_type. The type of the index. Usually either 'BTREE' or 'FULLTEXT'
-    # 11 = 'Comment.' Usually undef.
-    while (my $raw_def = $sth->fetchrow_arrayref) {
-        if ($raw_def->[2] eq $index) {
-            push(@fields, $raw_def->[4]);
-            # No index can be both UNIQUE and FULLTEXT, that's why
-            # this is written this way.
-            $index_type = $raw_def->[1] ? '' : 'UNIQUE';
-            $index_type = $raw_def->[10] eq 'FULLTEXT'
-                ? 'FULLTEXT' : $index_type;
-        }
+  my ($self, $table, $index) = @_;
+
+  my $sth = $self->prepare("SHOW INDEX FROM $table");
+  $sth->execute;
+
+  my @fields;
+  my $index_type;
+
+  # $raw_def will be an arrayref containing the following information:
+  # 0 = name of the table that the index is on
+  # 1 = 0 if unique, 1 if not unique
+  # 2 = name of the index
+  # 3 = seq_in_index (The order of the current field in the index).
+  # 4 = Name of ONE column that the index is on
+  # 5 = 'Collation' of the index. Usually 'A'.
+  # 6 = Cardinality. Either a number or undef.
+  # 7 = sub_part. Usually undef. Sometimes 1.
+  # 8 = "packed". Usually undef.
+  # 9 = Null. Sometimes undef, sometimes 'YES'.
+  # 10 = Index_type. The type of the index. Usually either 'BTREE' or 'FULLTEXT'
+  # 11 = 'Comment.' Usually undef.
+  while (my $raw_def = $sth->fetchrow_arrayref) {
+    if ($raw_def->[2] eq $index) {
+      push(@fields, $raw_def->[4]);
+
+      # No index can be both UNIQUE and FULLTEXT, that's why
+      # this is written this way.
+      $index_type = $raw_def->[1] ? '' : 'UNIQUE';
+      $index_type = $raw_def->[10] eq 'FULLTEXT' ? 'FULLTEXT' : $index_type;
     }
+  }
 
-    my $retval;
-    if (scalar(@fields)) {
-        $retval = {FIELDS => \@fields, TYPE => $index_type};
-    }
-    return $retval;
+  my $retval;
+  if (scalar(@fields)) {
+    $retval = {FIELDS => \@fields, TYPE => $index_type};
+  }
+  return $retval;
 }
 
 =item C<bz_index_list_real($table)>
@@ -993,10 +1020,11 @@ sub bz_index_info_real {
 =cut
 
 sub bz_index_list_real {
-    my ($self, $table) = @_;
-    my $sth = $self->prepare("SHOW INDEX FROM $table");
-    # Column 3 of a SHOW INDEX statement contains the name of the index.
-    return @{ $self->selectcol_arrayref($sth, {Columns => [3]}) };
+  my ($self, $table) = @_;
+  my $sth = $self->prepare("SHOW INDEX FROM $table");
+
+  # Column 3 of a SHOW INDEX statement contains the name of the index.
+  return @{$self->selectcol_arrayref($sth, {Columns => [3]})};
 }
 
 #####################################################################
@@ -1020,34 +1048,33 @@ this code does.
 # bz_column_info_real function would be very difficult to create
 # properly for any other DB besides MySQL.
 sub _bz_build_schema_from_disk {
-    my ($self) = @_;
-
-    my $schema = $self->_bz_schema->get_empty_schema();
-
-    my @tables = $self->bz_table_list_real();
-    if (@tables) {
-        print "Building Schema object from database...\n"; 
+  my ($self) = @_;
+
+  my $schema = $self->_bz_schema->get_empty_schema();
+
+  my @tables = $self->bz_table_list_real();
+  if (@tables) {
+    print "Building Schema object from database...\n";
+  }
+  foreach my $table (@tables) {
+    $schema->add_table($table);
+    my @columns = $self->bz_table_columns_real($table);
+    foreach my $column (@columns) {
+      my $type_info = $self->bz_column_info_real($table, $column);
+      $schema->set_column($table, $column, $type_info);
     }
-    foreach my $table (@tables) {
-        $schema->add_table($table);
-        my @columns = $self->bz_table_columns_real($table);
-        foreach my $column (@columns) {
-            my $type_info = $self->bz_column_info_real($table, $column);
-            $schema->set_column($table, $column, $type_info);
-        }
 
-        my @indexes = $self->bz_index_list_real($table);
-        foreach my $index (@indexes) {
-            unless ($index eq 'PRIMARY') {
-                my $index_info = $self->bz_index_info_real($table, $index);
-                ($index_info = $index_info->{FIELDS}) 
-                    if (!$index_info->{TYPE});
-                $schema->set_index($table, $index, $index_info);
-            }
-        }
+    my @indexes = $self->bz_index_list_real($table);
+    foreach my $index (@indexes) {
+      unless ($index eq 'PRIMARY') {
+        my $index_info = $self->bz_index_info_real($table, $index);
+        ($index_info = $index_info->{FIELDS}) if (!$index_info->{TYPE});
+        $schema->set_index($table, $index, $index_info);
+      }
     }
+  }
 
-    return $schema;
+  return $schema;
 }
 
 1;
index 981f1dbf170168aabb3043c39273bec706c8af5b..b888e30fdce81ce0b137dfdcfb3d23b25c350fd4 100644 (file)
@@ -38,460 +38,472 @@ use Bugzilla::Util;
 #####################################################################
 # Constants
 #####################################################################
-use constant EMPTY_STRING  => '__BZ_EMPTY_STR__';
+use constant EMPTY_STRING    => '__BZ_EMPTY_STR__';
 use constant ISOLATION_LEVEL => 'READ COMMITTED';
-use constant BLOB_TYPE => { ora_type => ORA_BLOB };
+use constant BLOB_TYPE       => {ora_type => ORA_BLOB};
+
 # The max size allowed for LOB fields, in kilobytes.
 use constant MIN_LONG_READ_LEN => 32 * 1024;
-use constant FULLTEXT_OR => ' OR ';
+use constant FULLTEXT_OR       => ' OR ';
 
 sub new {
-    my ($class, $params) = @_;
-    my ($user, $pass, $host, $dbname, $port) = 
-        @$params{qw(db_user db_pass db_host db_name db_port)};
-
-    # You can never connect to Oracle without a DB name,
-    # and there is no default DB.
-    $dbname ||= Bugzilla->localconfig->{db_name};
-
-    # Set the language environment
-    $ENV{'NLS_LANG'} = '.AL32UTF8';
-
-    # construct the DSN from the parameters we got
-    my $dsn = "dbi:Oracle:host=$host;sid=$dbname";
-    $dsn .= ";port=$port" if $port;
-    my $attrs = { FetchHashKeyName => 'NAME_lc',  
-                  LongReadLen => max(Bugzilla->params->{'maxattachmentsize'} || 0,
-                                     MIN_LONG_READ_LEN) * 1024,
-                };
-    my $self = $class->db_new({ dsn => $dsn, user => $user, 
-                                pass => $pass, attrs => $attrs });
-    # Needed by TheSchwartz
-    $self->{private_bz_dsn} = $dsn;
-
-    bless ($self, $class);
-
-    # Set the session's default date format to match MySQL
-    $self->do("ALTER SESSION SET NLS_DATE_FORMAT='YYYY-MM-DD HH24:MI:SS'");
-    $self->do("ALTER SESSION SET NLS_TIMESTAMP_FORMAT='YYYY-MM-DD HH24:MI:SS'");
-    $self->do("ALTER SESSION SET NLS_LENGTH_SEMANTICS='CHAR'");
-    # To allow case insensitive query.
-    $self->do("ALTER SESSION SET NLS_COMP='ANSI'");
-    $self->do("ALTER SESSION SET NLS_SORT='BINARY_AI'");
-    return $self;
+  my ($class, $params) = @_;
+  my ($user, $pass, $host, $dbname, $port)
+    = @$params{qw(db_user db_pass db_host db_name db_port)};
+
+  # You can never connect to Oracle without a DB name,
+  # and there is no default DB.
+  $dbname ||= Bugzilla->localconfig->{db_name};
+
+  # Set the language environment
+  $ENV{'NLS_LANG'} = '.AL32UTF8';
+
+  # construct the DSN from the parameters we got
+  my $dsn = "dbi:Oracle:host=$host;sid=$dbname";
+  $dsn .= ";port=$port" if $port;
+  my $attrs = {
+    FetchHashKeyName => 'NAME_lc',
+    LongReadLen =>
+      max(Bugzilla->params->{'maxattachmentsize'} || 0, MIN_LONG_READ_LEN) * 1024,
+  };
+  my $self = $class->db_new(
+    {dsn => $dsn, user => $user, pass => $pass, attrs => $attrs});
+
+  # Needed by TheSchwartz
+  $self->{private_bz_dsn} = $dsn;
+
+  bless($self, $class);
+
+  # Set the session's default date format to match MySQL
+  $self->do("ALTER SESSION SET NLS_DATE_FORMAT='YYYY-MM-DD HH24:MI:SS'");
+  $self->do("ALTER SESSION SET NLS_TIMESTAMP_FORMAT='YYYY-MM-DD HH24:MI:SS'");
+  $self->do("ALTER SESSION SET NLS_LENGTH_SEMANTICS='CHAR'");
+
+  # To allow case insensitive query.
+  $self->do("ALTER SESSION SET NLS_COMP='ANSI'");
+  $self->do("ALTER SESSION SET NLS_SORT='BINARY_AI'");
+  return $self;
 }
 
 sub bz_last_key {
-    my ($self, $table, $column) = @_;
+  my ($self, $table, $column) = @_;
 
-    my $seq = $table . "_" . $column . "_SEQ";
-    my ($last_insert_id) = $self->selectrow_array("SELECT $seq.CURRVAL "
-                                                  . " FROM DUAL");
-    return $last_insert_id;
+  my $seq = $table . "_" . $column . "_SEQ";
+  my ($last_insert_id)
+    = $self->selectrow_array("SELECT $seq.CURRVAL " . " FROM DUAL");
+  return $last_insert_id;
 }
 
 sub bz_check_regexp {
-    my ($self, $pattern) = @_;
+  my ($self, $pattern) = @_;
 
-    eval { $self->do("SELECT 1 FROM DUAL WHERE "
-          . $self->sql_regexp($self->quote("a"), $pattern, 1)) };
+  eval {
+    $self->do("SELECT 1 FROM DUAL WHERE "
+        . $self->sql_regexp($self->quote("a"), $pattern, 1));
+  };
 
-    $@ && ThrowUserError('illegal_regexp',
-        { value => $pattern, dberror => $self->errstr });
+  $@
+    && ThrowUserError('illegal_regexp',
+    {value => $pattern, dberror => $self->errstr});
 }
 
-sub bz_explain { 
-     my ($self, $sql) = @_; 
-     my $sth = $self->prepare("EXPLAIN PLAN FOR $sql"); 
-     $sth->execute();
-     my $explain = $self->selectcol_arrayref(
-         "SELECT PLAN_TABLE_OUTPUT FROM TABLE(DBMS_XPLAN.DISPLAY)");
-     return join("\n", @$explain); 
-} 
+sub bz_explain {
+  my ($self, $sql) = @_;
+  my $sth = $self->prepare("EXPLAIN PLAN FOR $sql");
+  $sth->execute();
+  my $explain = $self->selectcol_arrayref(
+    "SELECT PLAN_TABLE_OUTPUT FROM TABLE(DBMS_XPLAN.DISPLAY)");
+  return join("\n", @$explain);
+}
 
 sub sql_group_concat {
-    my ($self, $text, $separator) = @_;
-    $separator = $self->quote(', ') if !defined $separator;
-    my ($distinct, $rest) = $text =~/^(\s*DISTINCT\s|)(.+)$/i;
-    return "group_concat($distinct T_CLOB_DELIM(NVL($rest, ' '), $separator))";
+  my ($self, $text, $separator) = @_;
+  $separator = $self->quote(', ') if !defined $separator;
+  my ($distinct, $rest) = $text =~ /^(\s*DISTINCT\s|)(.+)$/i;
+  return "group_concat($distinct T_CLOB_DELIM(NVL($rest, ' '), $separator))";
 }
 
 sub sql_regexp {
-    my ($self, $expr, $pattern, $nocheck, $real_pattern) = @_;
-    $real_pattern ||= $pattern;
+  my ($self, $expr, $pattern, $nocheck, $real_pattern) = @_;
+  $real_pattern ||= $pattern;
 
-    $self->bz_check_regexp($real_pattern) if !$nocheck;
+  $self->bz_check_regexp($real_pattern) if !$nocheck;
 
-    return "REGEXP_LIKE($expr, $pattern)";
+  return "REGEXP_LIKE($expr, $pattern)";
 }
 
 sub sql_not_regexp {
-    my ($self, $expr, $pattern, $nocheck, $real_pattern) = @_;
-    $real_pattern ||= $pattern;
+  my ($self, $expr, $pattern, $nocheck, $real_pattern) = @_;
+  $real_pattern ||= $pattern;
 
-    $self->bz_check_regexp($real_pattern) if !$nocheck;
+  $self->bz_check_regexp($real_pattern) if !$nocheck;
 
-    return "NOT REGEXP_LIKE($expr, $pattern)" 
+  return "NOT REGEXP_LIKE($expr, $pattern)";
 }
 
 sub sql_limit {
-    my ($self, $limit, $offset) = @_;
+  my ($self, $limit, $offset) = @_;
 
-    if(defined $offset) {
-        return  "/* LIMIT $limit $offset */";
-    }
-    return "/* LIMIT $limit */";
+  if (defined $offset) {
+    return "/* LIMIT $limit $offset */";
+  }
+  return "/* LIMIT $limit */";
 }
 
 sub sql_string_concat {
-    my ($self, @params) = @_;
+  my ($self, @params) = @_;
 
-    return 'CONCAT(' . join(', ', @params) . ')';
+  return 'CONCAT(' . join(', ', @params) . ')';
 }
 
 sub sql_to_days {
-    my ($self, $date) = @_;
+  my ($self, $date) = @_;
 
-    return " TO_CHAR(TO_DATE($date),'J') ";
+  return " TO_CHAR(TO_DATE($date),'J') ";
 }
-sub sql_from_days{
-    my ($self, $date) = @_;
 
-    return " TO_DATE($date,'J') ";
+sub sql_from_days {
+  my ($self, $date) = @_;
+
+  return " TO_DATE($date,'J') ";
 }
 
 sub sql_fulltext_search {
-    my ($self, $column, $text) = @_;
-    state $label = 0;
-    $text = $self->quote($text);
-    trick_taint($text);
-    $label++;
-    return "CONTAINS($column,$text,$label) > 0", "SCORE($label)";
+  my ($self, $column, $text) = @_;
+  state $label = 0;
+  $text = $self->quote($text);
+  trick_taint($text);
+  $label++;
+  return "CONTAINS($column,$text,$label) > 0", "SCORE($label)";
 }
 
 sub sql_date_format {
-    my ($self, $date, $format) = @_;
-    
-    $format = "%Y.%m.%d %H:%i:%s" if !$format;
+  my ($self, $date, $format) = @_;
+
+  $format = "%Y.%m.%d %H:%i:%s" if !$format;
 
-    $format =~ s/\%Y/YYYY/g;
-    $format =~ s/\%y/YY/g;
-    $format =~ s/\%m/MM/g;
-    $format =~ s/\%d/DD/g;
-    $format =~ s/\%a/Dy/g;
-    $format =~ s/\%H/HH24/g;
-    $format =~ s/\%i/MI/g;
-    $format =~ s/\%s/SS/g;
+  $format =~ s/\%Y/YYYY/g;
+  $format =~ s/\%y/YY/g;
+  $format =~ s/\%m/MM/g;
+  $format =~ s/\%d/DD/g;
+  $format =~ s/\%a/Dy/g;
+  $format =~ s/\%H/HH24/g;
+  $format =~ s/\%i/MI/g;
+  $format =~ s/\%s/SS/g;
 
-    return "TO_CHAR($date, " . $self->quote($format) . ")";
+  return "TO_CHAR($date, " . $self->quote($format) . ")";
 }
 
 sub sql_date_math {
-    my ($self, $date, $operator, $interval, $units) = @_;
-    my $time_sql;
-    if ($units =~ /YEAR|MONTH/i) {
-        $time_sql = "NUMTOYMINTERVAL($interval,'$units')";
-    } else{
-        $time_sql = "NUMTODSINTERVAL($interval,'$units')";
-    }
-   return "$date $operator $time_sql";
+  my ($self, $date, $operator, $interval, $units) = @_;
+  my $time_sql;
+  if ($units =~ /YEAR|MONTH/i) {
+    $time_sql = "NUMTOYMINTERVAL($interval,'$units')";
+  }
+  else {
+    $time_sql = "NUMTODSINTERVAL($interval,'$units')";
+  }
+  return "$date $operator $time_sql";
 }
 
 sub sql_position {
-    my ($self, $fragment, $text) = @_;
-    return "INSTR($text, $fragment)";
+  my ($self, $fragment, $text) = @_;
+  return "INSTR($text, $fragment)";
 }
 
 sub sql_in {
-    my ($self, $column_name, $in_list_ref, $negate) = @_;
-    my @in_list = @$in_list_ref;
-    return $self->SUPER::sql_in($column_name, $in_list_ref, $negate) if $#in_list < 1000;
-    my @in_str;
-    while (@in_list) {
-        my $length = $#in_list + 1;
-        my $splice = $length > 1000 ? 1000 : $length;
-        my @sub_in_list = splice(@in_list, 0, $splice);
-        push(@in_str, 
-             $self->SUPER::sql_in($column_name, \@sub_in_list, $negate));
-    }
-    return "( " . join(" OR ", @in_str) . " )";
+  my ($self, $column_name, $in_list_ref, $negate) = @_;
+  my @in_list = @$in_list_ref;
+  return $self->SUPER::sql_in($column_name, $in_list_ref, $negate)
+    if $#in_list < 1000;
+  my @in_str;
+  while (@in_list) {
+    my $length      = $#in_list + 1;
+    my $splice      = $length > 1000 ? 1000 : $length;
+    my @sub_in_list = splice(@in_list, 0, $splice);
+    push(@in_str, $self->SUPER::sql_in($column_name, \@sub_in_list, $negate));
+  }
+  return "( " . join(" OR ", @in_str) . " )";
 }
 
 sub _bz_add_field_table {
-    my ($self, $name, $schema_ref, $type) = @_;
-    $self->SUPER::_bz_add_field_table($name, $schema_ref);
-    if (defined($type) && $type == FIELD_TYPE_MULTI_SELECT) {
-        my $uk_name = "UK_" . $self->_bz_schema->_hash_identifier($name . '_value');
-        $self->do("ALTER TABLE $name ADD CONSTRAINT $uk_name UNIQUE(value)");
-    }
+  my ($self, $name, $schema_ref, $type) = @_;
+  $self->SUPER::_bz_add_field_table($name, $schema_ref);
+  if (defined($type) && $type == FIELD_TYPE_MULTI_SELECT) {
+    my $uk_name = "UK_" . $self->_bz_schema->_hash_identifier($name . '_value');
+    $self->do("ALTER TABLE $name ADD CONSTRAINT $uk_name UNIQUE(value)");
+  }
 }
 
 sub bz_drop_table {
-     my ($self, $name) = @_;
-     my $table_exists = $self->bz_table_info($name);
-     if ($table_exists) {
-         $self->_bz_drop_fks($name);
-         $self->SUPER::bz_drop_table($name);
-     }
+  my ($self, $name) = @_;
+  my $table_exists = $self->bz_table_info($name);
+  if ($table_exists) {
+    $self->_bz_drop_fks($name);
+    $self->SUPER::bz_drop_table($name);
+  }
 }
 
-# Dropping all FKs for a specified table. 
+# Dropping all FKs for a specified table.
 sub _bz_drop_fks {
-    my ($self, $table) = @_;
-    my @columns = $self->bz_table_columns($table);
-    foreach my $column (@columns) {
-        $self->bz_drop_fk($table, $column);
-    }
+  my ($self, $table) = @_;
+  my @columns = $self->bz_table_columns($table);
+  foreach my $column (@columns) {
+    $self->bz_drop_fk($table, $column);
+  }
 }
 
 sub _fix_empty {
-    my ($string) = @_;
-    $string = '' if $string eq EMPTY_STRING;
-    return $string;
+  my ($string) = @_;
+  $string = '' if $string eq EMPTY_STRING;
+  return $string;
 }
 
 sub _fix_arrayref {
-    my ($row) = @_;
-    return undef if !defined $row;
-    foreach my $field (@$row) {
-        $field = _fix_empty($field) if defined $field;
-    }
-    return $row;
+  my ($row) = @_;
+  return undef if !defined $row;
+  foreach my $field (@$row) {
+    $field = _fix_empty($field) if defined $field;
+  }
+  return $row;
 }
 
 sub _fix_hashref {
-     my ($row) = @_;
-     return undef if !defined $row;
-     foreach my $value (values %$row) {
-       $value = _fix_empty($value) if defined $value;
-     }
-     return $row;
+  my ($row) = @_;
+  return undef if !defined $row;
+  foreach my $value (values %$row) {
+    $value = _fix_empty($value) if defined $value;
+  }
+  return $row;
 }
 
 sub adjust_statement {
-    my ($sql) = @_;
-    
-    if ($sql =~ /^CREATE OR REPLACE.*/i){
-        return $sql;
-    } 
-
-    # We can't just assume any occurrence of "''" in $sql is an empty
-    # string, since "''" can occur inside a string literal as a way of
-    # escaping a single "'" in the literal.  Therefore we must be trickier...
-
-    # split the statement into parts by single-quotes.  The negative value
-    # at the end to the split operator from dropping trailing empty strings
-    # (e.g., when $sql ends in "''")
-    my @parts = split /'/, $sql, -1;
-
-    if( !(@parts % 2) ) {
-        # Either the string is empty or the quotes are mismatched
-        # Returning input unmodified.
-        return $sql;
+  my ($sql) = @_;
+
+  if ($sql =~ /^CREATE OR REPLACE.*/i) {
+    return $sql;
+  }
+
+  # We can't just assume any occurrence of "''" in $sql is an empty
+  # string, since "''" can occur inside a string literal as a way of
+  # escaping a single "'" in the literal.  Therefore we must be trickier...
+
+  # split the statement into parts by single-quotes.  The negative value
+  # at the end to the split operator from dropping trailing empty strings
+  # (e.g., when $sql ends in "''")
+  my @parts = split /'/, $sql, -1;
+
+  if (!(@parts % 2)) {
+
+    # Either the string is empty or the quotes are mismatched
+    # Returning input unmodified.
+    return $sql;
+  }
+
+  # We already verified that we have an odd number of parts.  If we take
+  # the first part off now, we know we're entering the loop with an even
+  # number of parts
+  my @result;
+  my $part = shift @parts;
+
+  # Oracle requires a FROM clause in all SELECT statements, so append
+  # "FROM dual" to queries without one (e.g., "SELECT NOW()")
+  my $is_select = ($part =~ m/^\s*SELECT\b/io);
+  my $has_from = ($part =~ m/\bFROM\b/io) if $is_select;
+
+  # Oracle includes the time in CURRENT_DATE.
+  $part =~ s/\bCURRENT_DATE\b/TRUNC(CURRENT_DATE)/io;
+
+  # Oracle use SUBSTR instead of SUBSTRING
+  $part =~ s/\bSUBSTRING\b/SUBSTR/io;
+
+  # Oracle need no 'AS'
+  $part =~ s/\bAS\b//ig;
+
+  # Oracle doesn't have LIMIT, so if we find the LIMIT comment, wrap the
+  # query with "SELECT * FROM (...) WHERE rownum < $limit"
+  my ($limit, $offset) = ($part =~ m{/\* LIMIT (\d*) (\d*) \*/}o);
+
+  push @result, $part;
+  while (@parts) {
+    my $string    = shift @parts;
+    my $nonstring = shift @parts;
+
+    # if the non-string part is zero-length and there are more parts left,
+    # then this is an escaped quote inside a string literal
+    while (!(length $nonstring) && @parts) {
+
+      # we know it's safe to remove two parts at a time, since we
+      # entered the loop with an even number of parts
+      $string .= "''" . shift @parts;
+      $nonstring = shift @parts;
     }
 
-    # We already verified that we have an odd number of parts.  If we take
-    # the first part off now, we know we're entering the loop with an even
-    # number of parts
-    my @result;
-    my $part = shift @parts;
-    
-    # Oracle requires a FROM clause in all SELECT statements, so append
-    # "FROM dual" to queries without one (e.g., "SELECT NOW()")
-    my $is_select = ($part =~ m/^\s*SELECT\b/io);
-    my $has_from =  ($part =~ m/\bFROM\b/io) if $is_select;
+    # Look for a FROM if this is a SELECT and we haven't found one yet
+    $has_from = ($nonstring =~ m/\bFROM\b/io) if ($is_select and !$has_from);
 
     # Oracle includes the time in CURRENT_DATE.
-    $part =~ s/\bCURRENT_DATE\b/TRUNC(CURRENT_DATE)/io;
+    $nonstring =~ s/\bCURRENT_DATE\b/TRUNC(CURRENT_DATE)/io;
 
     # Oracle use SUBSTR instead of SUBSTRING
-    $part =~ s/\bSUBSTRING\b/SUBSTR/io;
-   
+    $nonstring =~ s/\bSUBSTRING\b/SUBSTR/io;
+
     # Oracle need no 'AS'
-    $part =~ s/\bAS\b//ig;
-    
-    # Oracle doesn't have LIMIT, so if we find the LIMIT comment, wrap the
-    # query with "SELECT * FROM (...) WHERE rownum < $limit"
-    my ($limit,$offset) = ($part =~ m{/\* LIMIT (\d*) (\d*) \*/}o);
-    
-    push @result, $part;
-    while( @parts ) {
-        my $string = shift @parts;
-        my $nonstring = shift @parts;
-    
-        # if the non-string part is zero-length and there are more parts left,
-        # then this is an escaped quote inside a string literal   
-        while( !(length $nonstring) && @parts  ) {
-            # we know it's safe to remove two parts at a time, since we
-            # entered the loop with an even number of parts
-            $string .= "''" . shift @parts;
-            $nonstring = shift @parts;
-        }
+    $nonstring =~ s/\bAS\b//ig;
 
-        # Look for a FROM if this is a SELECT and we haven't found one yet
-        $has_from = ($nonstring =~ m/\bFROM\b/io) 
-                    if ($is_select and !$has_from);
+    # Look for a LIMIT clause
+    ($limit) = ($nonstring =~ m(/\* LIMIT (\d*) \*/)o);
 
-        # Oracle includes the time in CURRENT_DATE.
-        $nonstring =~ s/\bCURRENT_DATE\b/TRUNC(CURRENT_DATE)/io;
+    if (!length($string)) {
+      push @result, EMPTY_STRING;
+      push @result, $nonstring;
+    }
+    else {
+      push @result, $string;
+      push @result, $nonstring;
+    }
+  }
 
-        # Oracle use SUBSTR instead of SUBSTRING
-        $nonstring =~ s/\bSUBSTRING\b/SUBSTR/io;
+  my $new_sql = join "'", @result;
 
-        # Oracle need no 'AS'
-        $nonstring =~ s/\bAS\b//ig;
+  # Append "FROM dual" if this is a SELECT without a FROM clause
+  $new_sql .= " FROM DUAL" if ($is_select and !$has_from);
 
-        # Look for a LIMIT clause
-        ($limit) = ($nonstring =~ m(/\* LIMIT (\d*) \*/)o);
+  # Wrap the query with a "WHERE rownum <= ..." if we found LIMIT
 
-        if(!length($string)){
-           push @result, EMPTY_STRING;
-           push @result, $nonstring;
-        } else {
-           push @result, $string;
-           push @result, $nonstring;
-        }
+  if (defined($limit)) {
+    if ($new_sql !~ /\bWHERE\b/) {
+      $new_sql = $new_sql . " WHERE 1=1";
     }
-
-    my $new_sql = join "'", @result;
-
-    # Append "FROM dual" if this is a SELECT without a FROM clause
-    $new_sql .= " FROM DUAL" if ($is_select and !$has_from);
-
-    # Wrap the query with a "WHERE rownum <= ..." if we found LIMIT
-    
-    if (defined($limit)) {
-        if ($new_sql !~ /\bWHERE\b/) {
-            $new_sql = $new_sql." WHERE 1=1";
-        }
-        my ($before_where, $after_where) = split(/\bWHERE\b/i, $new_sql, 2);
-        if (defined($offset)) {
-            my ($before_from, $after_from) = split(/\bFROM\b/i, $new_sql, 2);
-            $before_where = "$before_from FROM ($before_from,"
-                          . " ROW_NUMBER() OVER (ORDER BY 1) R "
-                          . " FROM $after_from ) "; 
-            $after_where = " R BETWEEN $offset+1 AND $limit+$offset";
-        } else {
-            $after_where = " rownum <=$limit AND ".$after_where;
-        }
-        $new_sql = $before_where." WHERE ".$after_where;
+    my ($before_where, $after_where) = split(/\bWHERE\b/i, $new_sql, 2);
+    if (defined($offset)) {
+      my ($before_from, $after_from) = split(/\bFROM\b/i, $new_sql, 2);
+      $before_where
+        = "$before_from FROM ($before_from,"
+        . " ROW_NUMBER() OVER (ORDER BY 1) R "
+        . " FROM $after_from ) ";
+      $after_where = " R BETWEEN $offset+1 AND $limit+$offset";
+    }
+    else {
+      $after_where = " rownum <=$limit AND " . $after_where;
     }
-    return $new_sql;
+    $new_sql = $before_where . " WHERE " . $after_where;
+  }
+  return $new_sql;
 }
 
 sub do {
-    my $self = shift;
-    my $sql  = shift;
-    $sql = adjust_statement($sql);
-    unshift @_, $sql;
-    return $self->SUPER::do(@_);
+  my $self = shift;
+  my $sql  = shift;
+  $sql = adjust_statement($sql);
+  unshift @_, $sql;
+  return $self->SUPER::do(@_);
 }
 
 sub selectrow_array {
-    my $self = shift;
-    my $stmt = shift;
-    my $new_stmt = (ref $stmt) ? $stmt : adjust_statement($stmt);
-    unshift @_, $new_stmt;
-    if ( wantarray ) {
-        my @row = $self->SUPER::selectrow_array(@_);
-        _fix_arrayref(\@row);
-        return @row;
-    } else {
-        my $row = $self->SUPER::selectrow_array(@_);
-        $row = _fix_empty($row) if defined $row;
-        return $row;
-    }
+  my $self     = shift;
+  my $stmt     = shift;
+  my $new_stmt = (ref $stmt) ? $stmt : adjust_statement($stmt);
+  unshift @_, $new_stmt;
+  if (wantarray) {
+    my @row = $self->SUPER::selectrow_array(@_);
+    _fix_arrayref(\@row);
+    return @row;
+  }
+  else {
+    my $row = $self->SUPER::selectrow_array(@_);
+    $row = _fix_empty($row) if defined $row;
+    return $row;
+  }
 }
 
 sub selectrow_arrayref {
-    my $self = shift;
-    my $stmt = shift;
-    my $new_stmt = (ref $stmt) ? $stmt : adjust_statement($stmt);
-    unshift @_, $new_stmt;
-    my $ref = $self->SUPER::selectrow_arrayref(@_);
-    return undef if !defined $ref;
+  my $self     = shift;
+  my $stmt     = shift;
+  my $new_stmt = (ref $stmt) ? $stmt : adjust_statement($stmt);
+  unshift @_, $new_stmt;
+  my $ref = $self->SUPER::selectrow_arrayref(@_);
+  return undef if !defined $ref;
 
-    _fix_arrayref($ref);
-    return $ref;
+  _fix_arrayref($ref);
+  return $ref;
 }
 
 sub selectrow_hashref {
-    my $self = shift;
-    my $stmt = shift;
-    my $new_stmt = (ref $stmt) ? $stmt : adjust_statement($stmt);
-    unshift @_, $new_stmt;
-    my $ref = $self->SUPER::selectrow_hashref(@_);
-    return undef if !defined $ref;
+  my $self     = shift;
+  my $stmt     = shift;
+  my $new_stmt = (ref $stmt) ? $stmt : adjust_statement($stmt);
+  unshift @_, $new_stmt;
+  my $ref = $self->SUPER::selectrow_hashref(@_);
+  return undef if !defined $ref;
 
-    _fix_hashref($ref);
-    return $ref;
+  _fix_hashref($ref);
+  return $ref;
 }
 
 sub selectall_arrayref {
-    my $self = shift;
-    my $stmt = shift;
-    my $new_stmt = (ref $stmt) ? $stmt : adjust_statement($stmt);
-    unshift @_, $new_stmt;
-    my $ref = $self->SUPER::selectall_arrayref(@_);
-    return undef if !defined $ref;
-    
-    foreach my $row (@$ref) {
-       if (ref($row) eq 'ARRAY') {
-            _fix_arrayref($row);
-       }
-       elsif (ref($row) eq 'HASH') {
-            _fix_hashref($row);
-       }
+  my $self     = shift;
+  my $stmt     = shift;
+  my $new_stmt = (ref $stmt) ? $stmt : adjust_statement($stmt);
+  unshift @_, $new_stmt;
+  my $ref = $self->SUPER::selectall_arrayref(@_);
+  return undef if !defined $ref;
+
+  foreach my $row (@$ref) {
+    if (ref($row) eq 'ARRAY') {
+      _fix_arrayref($row);
     }
+    elsif (ref($row) eq 'HASH') {
+      _fix_hashref($row);
+    }
+  }
 
-    return $ref;
+  return $ref;
 }
 
 sub selectall_hashref {
-    my $self = shift;
-    my $stmt = shift;
-    my $new_stmt = (ref $stmt) ? $stmt : adjust_statement($stmt);
-    unshift @_, $new_stmt;
-    my $rows = $self->SUPER::selectall_hashref(@_);
-    return undef if !defined $rows;
-    foreach my $row (values %$rows) { 
-          _fix_hashref($row);
-    }
-    return $rows;
+  my $self     = shift;
+  my $stmt     = shift;
+  my $new_stmt = (ref $stmt) ? $stmt : adjust_statement($stmt);
+  unshift @_, $new_stmt;
+  my $rows = $self->SUPER::selectall_hashref(@_);
+  return undef if !defined $rows;
+  foreach my $row (values %$rows) {
+    _fix_hashref($row);
+  }
+  return $rows;
 }
 
 sub selectcol_arrayref {
-    my $self = shift;
-    my $stmt = shift;
-    my $new_stmt = (ref $stmt) ? $stmt : adjust_statement($stmt);
-    unshift @_, $new_stmt;
-    my $ref = $self->SUPER::selectcol_arrayref(@_);
-    return undef if !defined $ref;
-    _fix_arrayref($ref);
-    return $ref;
+  my $self     = shift;
+  my $stmt     = shift;
+  my $new_stmt = (ref $stmt) ? $stmt : adjust_statement($stmt);
+  unshift @_, $new_stmt;
+  my $ref = $self->SUPER::selectcol_arrayref(@_);
+  return undef if !defined $ref;
+  _fix_arrayref($ref);
+  return $ref;
 }
 
 sub prepare {
-    my $self = shift;
-    my $sql  = shift;
-    my $new_sql = adjust_statement($sql);
-    unshift @_, $new_sql;
-    return bless $self->SUPER::prepare(@_),
-                        'Bugzilla::DB::Oracle::st';
+  my $self    = shift;
+  my $sql     = shift;
+  my $new_sql = adjust_statement($sql);
+  unshift @_, $new_sql;
+  return bless $self->SUPER::prepare(@_), 'Bugzilla::DB::Oracle::st';
 }
 
 sub prepare_cached {
-    my $self = shift;
-    my $sql  = shift;
-    my $new_sql = adjust_statement($sql);
-    unshift @_, $new_sql;
-    return bless $self->SUPER::prepare_cached(@_),
-                      'Bugzilla::DB::Oracle::st';
+  my $self    = shift;
+  my $sql     = shift;
+  my $new_sql = adjust_statement($sql);
+  unshift @_, $new_sql;
+  return bless $self->SUPER::prepare_cached(@_), 'Bugzilla::DB::Oracle::st';
 }
 
 sub quote_identifier {
-     my ($self,$id) = @_;
-     return $id;
+  my ($self, $id) = @_;
+  return $id;
 }
 
 #####################################################################
@@ -499,21 +511,23 @@ sub quote_identifier {
 #####################################################################
 
 sub bz_table_columns_real {
-    my ($self, $table) = @_;
-    $table = uc($table);
-    my $cols = $self->selectcol_arrayref(
-        "SELECT LOWER(COLUMN_NAME) FROM USER_TAB_COLUMNS WHERE 
-         TABLE_NAME = ?  ORDER BY COLUMN_NAME", undef, $table);
-    return @$cols;
+  my ($self, $table) = @_;
+  $table = uc($table);
+  my $cols = $self->selectcol_arrayref(
+    "SELECT LOWER(COLUMN_NAME) FROM USER_TAB_COLUMNS WHERE 
+         TABLE_NAME = ?  ORDER BY COLUMN_NAME", undef, $table
+  );
+  return @$cols;
 }
 
 sub bz_table_list_real {
-    my ($self) = @_;
-    my $tables = $self->selectcol_arrayref(
-        "SELECT LOWER(TABLE_NAME) FROM USER_TABLES WHERE 
+  my ($self) = @_;
+  my $tables = $self->selectcol_arrayref(
+    "SELECT LOWER(TABLE_NAME) FROM USER_TABLES WHERE 
         TABLE_NAME NOT LIKE ? AND TABLE_NAME NOT LIKE ?
-        ORDER BY TABLE_NAME", undef, 'DR$%', 'APEX$%');
-    return @$tables;
+        ORDER BY TABLE_NAME", undef, 'DR$%', 'APEX$%'
+  );
+  return @$tables;
 }
 
 #####################################################################
@@ -521,32 +535,37 @@ sub bz_table_list_real {
 #####################################################################
 
 sub bz_setup_database {
-    my $self = shift;
-    
-    # Create a function that returns SYSDATE to emulate MySQL's "NOW()".
-    # Function NOW() is used widely in Bugzilla SQLs, but Oracle does not 
-    # have that function, So we have to create one ourself. 
-    $self->do("CREATE OR REPLACE FUNCTION NOW "
-              . " RETURN DATE IS BEGIN RETURN SYSDATE; END;");
-    $self->do("CREATE OR REPLACE FUNCTION CHAR_LENGTH(COLUMN_NAME VARCHAR2)" 
-              . " RETURN NUMBER IS BEGIN RETURN LENGTH(COLUMN_NAME); END;");
-    
-    # Create types for group_concat
-    my $type_exists = $self->selectrow_array("SELECT 1 FROM user_types
-                                              WHERE type_name = 'T_GROUP_CONCAT'");
-    $self->do("DROP TYPE T_GROUP_CONCAT") if $type_exists;
-    $self->do("CREATE OR REPLACE TYPE T_CLOB_DELIM AS OBJECT "
-          . "( p_CONTENT CLOB, p_DELIMITER VARCHAR2(256)"
-          . ", MAP MEMBER FUNCTION T_CLOB_DELIM_ToVarchar return VARCHAR2"
-          . ");");
-    $self->do("CREATE OR REPLACE TYPE BODY T_CLOB_DELIM IS
+  my $self = shift;
+
+  # Create a function that returns SYSDATE to emulate MySQL's "NOW()".
+  # Function NOW() is used widely in Bugzilla SQLs, but Oracle does not
+  # have that function, So we have to create one ourself.
+  $self->do("CREATE OR REPLACE FUNCTION NOW "
+      . " RETURN DATE IS BEGIN RETURN SYSDATE; END;");
+  $self->do("CREATE OR REPLACE FUNCTION CHAR_LENGTH(COLUMN_NAME VARCHAR2)"
+      . " RETURN NUMBER IS BEGIN RETURN LENGTH(COLUMN_NAME); END;");
+
+  # Create types for group_concat
+  my $type_exists = $self->selectrow_array(
+    "SELECT 1 FROM user_types
+                                              WHERE type_name = 'T_GROUP_CONCAT'"
+  );
+  $self->do("DROP TYPE T_GROUP_CONCAT") if $type_exists;
+  $self->do("CREATE OR REPLACE TYPE T_CLOB_DELIM AS OBJECT "
+      . "( p_CONTENT CLOB, p_DELIMITER VARCHAR2(256)"
+      . ", MAP MEMBER FUNCTION T_CLOB_DELIM_ToVarchar return VARCHAR2"
+      . ");");
+  $self->do(
+    "CREATE OR REPLACE TYPE BODY T_CLOB_DELIM IS
                   MAP MEMBER FUNCTION T_CLOB_DELIM_ToVarchar return VARCHAR2 is
                   BEGIN
                       RETURN p_CONTENT;
                   END;
-              END;");
+              END;"
+  );
 
-    $self->do("CREATE OR REPLACE TYPE T_GROUP_CONCAT AS OBJECT 
+  $self->do(
+    "CREATE OR REPLACE TYPE T_GROUP_CONCAT AS OBJECT 
                (  CLOB_CONTENT CLOB,
                   DELIMITER    VARCHAR2(256),
                   STATIC FUNCTION ODCIAGGREGATEINITIALIZE(
@@ -564,9 +583,11 @@ sub bz_setup_database {
                   MEMBER FUNCTION ODCIAGGREGATEMERGE(
                       SELF IN OUT NOCOPY T_GROUP_CONCAT,
                       CTX2 IN T_GROUP_CONCAT) 
-                  RETURN NUMBER);");
+                  RETURN NUMBER);"
+  );
 
-    $self->do("CREATE OR REPLACE TYPE BODY T_GROUP_CONCAT IS
+  $self->do(
+    "CREATE OR REPLACE TYPE BODY T_GROUP_CONCAT IS
                   STATIC FUNCTION ODCIAGGREGATEINITIALIZE(
                   SCTX IN OUT NOCOPY T_GROUP_CONCAT)
                   RETURN NUMBER IS
@@ -610,110 +631,117 @@ sub bz_setup_database {
                       DBMS_LOB.APPEND(SELF.CLOB_CONTENT, CTX2.CLOB_CONTENT);
                       RETURN ODCICONST.SUCCESS;
                   END;
-               END;");
+               END;"
+  );
 
-    # Create user-defined aggregate function group_concat
-    $self->do("CREATE OR REPLACE FUNCTION GROUP_CONCAT(P_INPUT T_CLOB_DELIM) 
+  # Create user-defined aggregate function group_concat
+  $self->do(
+    "CREATE OR REPLACE FUNCTION GROUP_CONCAT(P_INPUT T_CLOB_DELIM) 
                RETURN CLOB 
-               DETERMINISTIC PARALLEL_ENABLE AGGREGATE USING T_GROUP_CONCAT;");
-
-    # Create a WORLD_LEXER named BZ_LEX for multilingual fulltext search
-    my $lexer = $self->selectcol_arrayref(
-       "SELECT pre_name FROM CTXSYS.CTX_PREFERENCES WHERE pre_name = ? AND
-          pre_owner = ?",
-          undef,'BZ_LEX',uc(Bugzilla->localconfig->{db_user}));
-    if(!@$lexer) {
-       $self->do("BEGIN CTX_DDL.CREATE_PREFERENCE
-                        ('BZ_LEX', 'WORLD_LEXER'); END;");
-    }
-
-    $self->SUPER::bz_setup_database(@_);
-
-    my $sth = $self->prepare("SELECT OBJECT_NAME FROM USER_OBJECTS WHERE OBJECT_NAME = ?");
-    my @tables = $self->bz_table_list_real();
-
-    foreach my $table (@tables) {
-        my @columns = $self->bz_table_columns_real($table);
-        foreach my $column (@columns) {
-            my $def = $self->bz_column_info($table, $column);
-            # bz_add_column() before Bugzilla 4.2.3 didn't handle primary keys
-            # correctly (bug 731156). We have to add missing sequences and
-            # triggers ourselves.
-            if ($def->{TYPE} =~ /SERIAL/i) {
-                my $sequence = "${table}_${column}_SEQ";
-                my $exists = $self->selectrow_array($sth, undef, $sequence);
-                if (!$exists) {
-                    my @sql = $self->_get_create_seq_ddl($table, $column);
-                    $self->do($_) foreach @sql;
-                }
-            }
-
-            if ($def->{REFERENCES}) {
-                my $references = $def->{REFERENCES};
-                my $update = $references->{UPDATE} || 'CASCADE';
-                my $to_table  = $references->{TABLE};
-                my $to_column = $references->{COLUMN};
-                my $fk_name = $self->_bz_schema->_get_fk_name($table,
-                                                              $column,
-                                                              $references);
-                # bz_rename_table didn't rename the trigger correctly.
-                if ($table eq 'bug_tag' && $to_table eq 'tags') {
-                    $to_table = 'tag';
-                }
-                if ( $update =~ /CASCADE/i ){
-                    my $trigger_name = uc($fk_name . "_UC");
-                    my $exist_trigger = $self->selectcol_arrayref($sth, undef, $trigger_name);
-                    if(@$exist_trigger) {
-                        $self->do("DROP TRIGGER $trigger_name");
-                    }
-  
-                    my $tr_str = "CREATE OR REPLACE TRIGGER $trigger_name"
-                         . " AFTER UPDATE OF $to_column ON $to_table "
-                         . " REFERENCING "
-                         . " NEW AS NEW "
-                         . " OLD AS OLD "
-                         . " FOR EACH ROW "
-                         . " BEGIN "
-                         . "     UPDATE $table"
-                         . "        SET $column = :NEW.$to_column"
-                         . "      WHERE $column = :OLD.$to_column;"
-                         . " END $trigger_name;";
-                    $self->do($tr_str);
-                }
-            }
+               DETERMINISTIC PARALLEL_ENABLE AGGREGATE USING T_GROUP_CONCAT;"
+  );
+
+  # Create a WORLD_LEXER named BZ_LEX for multilingual fulltext search
+  my $lexer = $self->selectcol_arrayref(
+    "SELECT pre_name FROM CTXSYS.CTX_PREFERENCES WHERE pre_name = ? AND
+          pre_owner = ?", undef, 'BZ_LEX', uc(Bugzilla->localconfig->{db_user})
+  );
+  if (!@$lexer) {
+    $self->do(
+      "BEGIN CTX_DDL.CREATE_PREFERENCE
+                        ('BZ_LEX', 'WORLD_LEXER'); END;"
+    );
+  }
+
+  $self->SUPER::bz_setup_database(@_);
+
+  my $sth = $self->prepare(
+    "SELECT OBJECT_NAME FROM USER_OBJECTS WHERE OBJECT_NAME = ?");
+  my @tables = $self->bz_table_list_real();
+
+  foreach my $table (@tables) {
+    my @columns = $self->bz_table_columns_real($table);
+    foreach my $column (@columns) {
+      my $def = $self->bz_column_info($table, $column);
+
+      # bz_add_column() before Bugzilla 4.2.3 didn't handle primary keys
+      # correctly (bug 731156). We have to add missing sequences and
+      # triggers ourselves.
+      if ($def->{TYPE} =~ /SERIAL/i) {
+        my $sequence = "${table}_${column}_SEQ";
+        my $exists = $self->selectrow_array($sth, undef, $sequence);
+        if (!$exists) {
+          my @sql = $self->_get_create_seq_ddl($table, $column);
+          $self->do($_) foreach @sql;
         }
+      }
+
+      if ($def->{REFERENCES}) {
+        my $references = $def->{REFERENCES};
+        my $update     = $references->{UPDATE} || 'CASCADE';
+        my $to_table   = $references->{TABLE};
+        my $to_column  = $references->{COLUMN};
+        my $fk_name    = $self->_bz_schema->_get_fk_name($table, $column, $references);
+
+        # bz_rename_table didn't rename the trigger correctly.
+        if ($table eq 'bug_tag' && $to_table eq 'tags') {
+          $to_table = 'tag';
+        }
+        if ($update =~ /CASCADE/i) {
+          my $trigger_name = uc($fk_name . "_UC");
+          my $exist_trigger = $self->selectcol_arrayref($sth, undef, $trigger_name);
+          if (@$exist_trigger) {
+            $self->do("DROP TRIGGER $trigger_name");
+          }
+
+          my $tr_str
+            = "CREATE OR REPLACE TRIGGER $trigger_name"
+            . " AFTER UPDATE OF $to_column ON $to_table "
+            . " REFERENCING "
+            . " NEW AS NEW "
+            . " OLD AS OLD "
+            . " FOR EACH ROW "
+            . " BEGIN "
+            . "     UPDATE $table"
+            . "        SET $column = :NEW.$to_column"
+            . "      WHERE $column = :OLD.$to_column;"
+            . " END $trigger_name;";
+          $self->do($tr_str);
+        }
+      }
     }
+  }
 
-   # Drop the trigger which causes bug 541553
-   my $trigger_name = "PRODUCTS_MILESTONEURL";
-   my $exist_trigger = $self->selectcol_arrayref($sth, undef, $trigger_name);
-   if(@$exist_trigger) {
-       $self->do("DROP TRIGGER $trigger_name");
-   }
+  # Drop the trigger which causes bug 541553
+  my $trigger_name = "PRODUCTS_MILESTONEURL";
+  my $exist_trigger = $self->selectcol_arrayref($sth, undef, $trigger_name);
+  if (@$exist_trigger) {
+    $self->do("DROP TRIGGER $trigger_name");
+  }
 }
 
 # These two methods have been copied from Bugzilla::DB::Schema::Oracle.
 sub _get_create_seq_ddl {
-    my ($self, $table, $column) = @_;
+  my ($self, $table, $column) = @_;
 
-    my $seq_name = "${table}_${column}_SEQ";
-    my $seq_sql = "CREATE SEQUENCE $seq_name INCREMENT BY 1 START WITH 1 " .
-                  "NOMAXVALUE NOCYCLE NOCACHE";
-    my $trigger_sql = $self->_get_create_trigger_ddl($table, $column, $seq_name);
-    return ($seq_sql, $trigger_sql);
+  my $seq_name = "${table}_${column}_SEQ";
+  my $seq_sql  = "CREATE SEQUENCE $seq_name INCREMENT BY 1 START WITH 1 "
+    . "NOMAXVALUE NOCYCLE NOCACHE";
+  my $trigger_sql = $self->_get_create_trigger_ddl($table, $column, $seq_name);
+  return ($seq_sql, $trigger_sql);
 }
 
 sub _get_create_trigger_ddl {
-    my ($self, $table, $column, $seq_name) = @_;
+  my ($self, $table, $column, $seq_name) = @_;
 
-    my $trigger_sql = "CREATE OR REPLACE TRIGGER ${table}_${column}_TR "
-                    . " BEFORE INSERT ON $table "
-                    . " FOR EACH ROW "
-                    . " BEGIN "
-                    . "   SELECT ${seq_name}.NEXTVAL "
-                    . "   INTO :NEW.$column FROM DUAL; "
-                    . " END;";
-    return $trigger_sql;
+  my $trigger_sql
+    = "CREATE OR REPLACE TRIGGER ${table}_${column}_TR "
+    . " BEFORE INSERT ON $table "
+    . " FOR EACH ROW "
+    . " BEGIN "
+    . "   SELECT ${seq_name}.NEXTVAL "
+    . "   INTO :NEW.$column FROM DUAL; " . " END;";
+  return $trigger_sql;
 }
 
 ############################################################################
@@ -725,68 +753,69 @@ use strict;
 use warnings;
 
 use parent -norequire, qw(DBI::st);
+
 sub fetchrow_arrayref {
-    my $self = shift;
-    my $ref = $self->SUPER::fetchrow_arrayref(@_);
-    return undef if !defined $ref;
-    Bugzilla::DB::Oracle::_fix_arrayref($ref);
-    return $ref;
+  my $self = shift;
+  my $ref  = $self->SUPER::fetchrow_arrayref(@_);
+  return undef if !defined $ref;
+  Bugzilla::DB::Oracle::_fix_arrayref($ref);
+  return $ref;
 }
 
 sub fetchrow_array {
-    my $self = shift;
-    if ( wantarray ) {
-        my @row = $self->SUPER::fetchrow_array(@_);
-        Bugzilla::DB::Oracle::_fix_arrayref(\@row);
-        return @row;
-    } else {
-        my $row = $self->SUPER::fetchrow_array(@_);
-        $row = Bugzilla::DB::Oracle::_fix_empty($row) if defined $row;
-        return $row;
-    }
+  my $self = shift;
+  if (wantarray) {
+    my @row = $self->SUPER::fetchrow_array(@_);
+    Bugzilla::DB::Oracle::_fix_arrayref(\@row);
+    return @row;
+  }
+  else {
+    my $row = $self->SUPER::fetchrow_array(@_);
+    $row = Bugzilla::DB::Oracle::_fix_empty($row) if defined $row;
+    return $row;
+  }
 }
 
 sub fetchrow_hashref {
-    my $self = shift;
-    my $ref = $self->SUPER::fetchrow_hashref(@_);
-    return undef if !defined $ref;
-    Bugzilla::DB::Oracle::_fix_hashref($ref);
-    return $ref;
+  my $self = shift;
+  my $ref  = $self->SUPER::fetchrow_hashref(@_);
+  return undef if !defined $ref;
+  Bugzilla::DB::Oracle::_fix_hashref($ref);
+  return $ref;
 }
 
 sub fetchall_arrayref {
-    my $self = shift;
-    my $ref = $self->SUPER::fetchall_arrayref(@_);
-    return undef if !defined $ref;
-    foreach my $row (@$ref) {
-        if (ref($row) eq 'ARRAY') {
-             Bugzilla::DB::Oracle::_fix_arrayref($row);
-        }
-        elsif (ref($row) eq 'HASH') {
-            Bugzilla::DB::Oracle::_fix_hashref($row);
-        }
+  my $self = shift;
+  my $ref  = $self->SUPER::fetchall_arrayref(@_);
+  return undef if !defined $ref;
+  foreach my $row (@$ref) {
+    if (ref($row) eq 'ARRAY') {
+      Bugzilla::DB::Oracle::_fix_arrayref($row);
     }
-    return $ref;
+    elsif (ref($row) eq 'HASH') {
+      Bugzilla::DB::Oracle::_fix_hashref($row);
+    }
+  }
+  return $ref;
 }
 
 sub fetchall_hashref {
-    my $self = shift;
-    my $ref = $self->SUPER::fetchall_hashref(@_);
-    return undef if !defined $ref;
-    foreach my $row (values %$ref) {
-         Bugzilla::DB::Oracle::_fix_hashref($row);
-    }
-     return $ref;
+  my $self = shift;
+  my $ref  = $self->SUPER::fetchall_hashref(@_);
+  return undef if !defined $ref;
+  foreach my $row (values %$ref) {
+    Bugzilla::DB::Oracle::_fix_hashref($row);
+  }
+  return $ref;
 }
 
 sub fetch {
-    my $self = shift;
-    my $row = $self->SUPER::fetch(@_);
-    if ($row) {
-      Bugzilla::DB::Oracle::_fix_arrayref($row);
-    }
-   return $row;
+  my $self = shift;
+  my $row  = $self->SUPER::fetch(@_);
+  if ($row) {
+    Bugzilla::DB::Oracle::_fix_arrayref($row);
+  }
+  return $row;
 }
 1;
 
index ed1a4aa7d957f0cecf80373d22828673c6abfeee..bfbf80e76489e6481c8762d056bf3f22970bb4d8 100644 (file)
@@ -32,200 +32,212 @@ use DBD::Pg;
 # This module extends the DB interface via inheritance
 use parent qw(Bugzilla::DB);
 
-use constant BLOB_TYPE => { pg_type => DBD::Pg::PG_BYTEA };
+use constant BLOB_TYPE => {pg_type => DBD::Pg::PG_BYTEA};
 
 sub new {
-    my ($class, $params) = @_;
-    my ($user, $pass, $host, $dbname, $port) =
-        @$params{qw(db_user db_pass db_host db_name db_port)};
+  my ($class, $params) = @_;
+  my ($user, $pass, $host, $dbname, $port)
+    = @$params{qw(db_user db_pass db_host db_name db_port)};
 
-    # construct the DSN from the parameters we got
-    my $dsn = "dbi:Pg:dbname=$dbname";
-    $dsn .= ";host=$host" if $host;
-    $dsn .= ";port=$port" if $port;
+  # construct the DSN from the parameters we got
+  my $dsn = "dbi:Pg:dbname=$dbname";
+  $dsn .= ";host=$host" if $host;
+  $dsn .= ";port=$port" if $port;
 
-    # This stops Pg from printing out lots of "NOTICE" messages when
-    # creating tables.
-    $dsn .= ";options='-c client_min_messages=warning'";
+  # This stops Pg from printing out lots of "NOTICE" messages when
+  # creating tables.
+  $dsn .= ";options='-c client_min_messages=warning'";
 
-    my $attrs = { pg_enable_utf8 => 1 };
+  my $attrs = {pg_enable_utf8 => 1};
 
-    my $self = $class->db_new({ dsn => $dsn, user => $user, 
-                                pass => $pass, attrs => $attrs });
+  my $self = $class->db_new(
+    {dsn => $dsn, user => $user, pass => $pass, attrs => $attrs});
 
-    # all class local variables stored in DBI derived class needs to have
-    # a prefix 'private_'. See DBI documentation.
-    $self->{private_bz_tables_locked} = "";
-    # Needed by TheSchwartz
-    $self->{private_bz_dsn} = $dsn;
+  # all class local variables stored in DBI derived class needs to have
+  # a prefix 'private_'. See DBI documentation.
+  $self->{private_bz_tables_locked} = "";
 
-    bless ($self, $class);
+  # Needed by TheSchwartz
+  $self->{private_bz_dsn} = $dsn;
 
-    return $self;
+  bless($self, $class);
+
+  return $self;
 }
 
 # if last_insert_id is supported on PostgreSQL by lowest DBI/DBD version
 # supported by Bugzilla, this implementation can be removed.
 sub bz_last_key {
-    my ($self, $table, $column) = @_;
+  my ($self, $table, $column) = @_;
 
-    my $seq = $table . "_" . $column . "_seq";
-    my ($last_insert_id) = $self->selectrow_array("SELECT CURRVAL('$seq')");
+  my $seq = $table . "_" . $column . "_seq";
+  my ($last_insert_id) = $self->selectrow_array("SELECT CURRVAL('$seq')");
 
-    return $last_insert_id;
+  return $last_insert_id;
 }
 
 sub sql_group_concat {
-    my ($self, $text, $separator, $sort, $order_by) = @_;
-    $sort = 1 if !defined $sort;
-    $separator = $self->quote(', ') if !defined $separator;
-
-    if ($order_by && $text =~ /^DISTINCT\s*(.+)$/i) {
-        # Since Postgres (quite rightly) doesn't support "SELECT DISTINCT x
-        # ORDER BY y", we need to sort the list, and then get the unique
-        # values
-        return "ARRAY_TO_STRING(ANYARRAY_UNIQ(ARRAY_AGG($1 ORDER BY $order_by)), $separator)";
-    }
-
-    # Determine the ORDER BY clause (if any)
-    if ($order_by) {
-        $order_by = " ORDER BY $order_by";
-    }
-    elsif ($sort) {
-        # We don't include the DISTINCT keyword in an order by
-        $text =~ /^(?:DISTINCT\s*)?(.+)$/i;
-        $order_by = " ORDER BY $1";
-    }
-
-    return "STRING_AGG(${text}::text, $separator${order_by}::text)"
+  my ($self, $text, $separator, $sort, $order_by) = @_;
+  $sort = 1 if !defined $sort;
+  $separator = $self->quote(', ') if !defined $separator;
+
+  if ($order_by && $text =~ /^DISTINCT\s*(.+)$/i) {
+
+    # Since Postgres (quite rightly) doesn't support "SELECT DISTINCT x
+    # ORDER BY y", we need to sort the list, and then get the unique
+    # values
+    return
+      "ARRAY_TO_STRING(ANYARRAY_UNIQ(ARRAY_AGG($1 ORDER BY $order_by)), $separator)";
+  }
+
+  # Determine the ORDER BY clause (if any)
+  if ($order_by) {
+    $order_by = " ORDER BY $order_by";
+  }
+  elsif ($sort) {
+
+    # We don't include the DISTINCT keyword in an order by
+    $text =~ /^(?:DISTINCT\s*)?(.+)$/i;
+    $order_by = " ORDER BY $1";
+  }
+
+  return "STRING_AGG(${text}::text, $separator${order_by}::text)";
 }
 
 sub sql_istring {
-    my ($self, $string) = @_;
+  my ($self, $string) = @_;
 
-    return "LOWER(${string}::text)";
+  return "LOWER(${string}::text)";
 }
 
 sub sql_position {
-    my ($self, $fragment, $text) = @_;
+  my ($self, $fragment, $text) = @_;
 
-    return "POSITION(${fragment}::text IN ${text}::text)";
+  return "POSITION(${fragment}::text IN ${text}::text)";
 }
 
 sub sql_like {
-    my ($self, $fragment, $column, $not) = @_;
-    $not //= '';
+  my ($self, $fragment, $column, $not) = @_;
+  $not //= '';
 
-    return "${column}::text $not LIKE " . $self->sql_like_escape($fragment) . " ESCAPE '|'";
+  return
+      "${column}::text $not LIKE "
+    . $self->sql_like_escape($fragment)
+    . " ESCAPE '|'";
 }
 
 sub sql_ilike {
-    my ($self, $fragment, $column, $not) = @_;
-    $not //= '';
+  my ($self, $fragment, $column, $not) = @_;
+  $not //= '';
 
-    return "${column}::text $not ILIKE " . $self->sql_like_escape($fragment) . " ESCAPE '|'";
+  return
+      "${column}::text $not ILIKE "
+    . $self->sql_like_escape($fragment)
+    . " ESCAPE '|'";
 }
 
 sub sql_not_ilike {
-    return shift->sql_ilike(@_, 'NOT');
+  return shift->sql_ilike(@_, 'NOT');
 }
 
 # Escapes any % or _ characters which are special in a LIKE match.
 # Also performs a $dbh->quote to escape any quote characters.
 sub sql_like_escape {
-    my ($self, $fragment) = @_;
+  my ($self, $fragment) = @_;
 
-    $fragment =~ s/\|/\|\|/g;  # escape the escape character if it appears
-    $fragment =~ s/%/\|%/g;    # percent and underscore are the special match
-    $fragment =~ s/_/\|_/g;    # characters in SQL.
+  $fragment =~ s/\|/\|\|/g;    # escape the escape character if it appears
+  $fragment =~ s/%/\|%/g;      # percent and underscore are the special match
+  $fragment =~ s/_/\|_/g;      # characters in SQL.
 
-    return $self->quote("%$fragment%");
+  return $self->quote("%$fragment%");
 }
 
 sub sql_regexp {
-    my ($self, $expr, $pattern, $nocheck, $real_pattern) = @_;
-    $real_pattern ||= $pattern;
+  my ($self, $expr, $pattern, $nocheck, $real_pattern) = @_;
+  $real_pattern ||= $pattern;
 
-    $self->bz_check_regexp($real_pattern) if !$nocheck;
+  $self->bz_check_regexp($real_pattern) if !$nocheck;
 
-    return "${expr}::text ~* $pattern";
+  return "${expr}::text ~* $pattern";
 }
 
 sub sql_not_regexp {
-    my ($self, $expr, $pattern, $nocheck, $real_pattern) = @_;
-    $real_pattern ||= $pattern;
+  my ($self, $expr, $pattern, $nocheck, $real_pattern) = @_;
+  $real_pattern ||= $pattern;
 
-    $self->bz_check_regexp($real_pattern) if !$nocheck;
+  $self->bz_check_regexp($real_pattern) if !$nocheck;
 
-    return "${expr}::text !~* $pattern" 
+  return "${expr}::text !~* $pattern";
 }
 
 sub sql_limit {
-    my ($self, $limit, $offset) = @_;
-
-    if (defined($offset)) {
-        return "LIMIT $limit OFFSET $offset";
-    } else {
-        return "LIMIT $limit";
-    }
+  my ($self, $limit, $offset) = @_;
+
+  if (defined($offset)) {
+    return "LIMIT $limit OFFSET $offset";
+  }
+  else {
+    return "LIMIT $limit";
+  }
 }
 
 sub sql_from_days {
-    my ($self, $days) = @_;
+  my ($self, $days) = @_;
 
-    return "TO_TIMESTAMP('$days', 'J')::date";
+  return "TO_TIMESTAMP('$days', 'J')::date";
 }
 
 sub sql_to_days {
-    my ($self, $date) = @_;
+  my ($self, $date) = @_;
 
-    return "TO_CHAR(${date}::date, 'J')::int";
+  return "TO_CHAR(${date}::date, 'J')::int";
 }
 
 sub sql_date_format {
-    my ($self, $date, $format) = @_;
-    
-    $format = "%Y.%m.%d %H:%i:%s" if !$format;
-
-    $format =~ s/\%Y/YYYY/g;
-    $format =~ s/\%y/YY/g;
-    $format =~ s/\%m/MM/g;
-    $format =~ s/\%d/DD/g;
-    $format =~ s/\%a/Dy/g;
-    $format =~ s/\%H/HH24/g;
-    $format =~ s/\%i/MI/g;
-    $format =~ s/\%s/SS/g;
-
-    return "TO_CHAR($date, " . $self->quote($format) . ")";
+  my ($self, $date, $format) = @_;
+
+  $format = "%Y.%m.%d %H:%i:%s" if !$format;
+
+  $format =~ s/\%Y/YYYY/g;
+  $format =~ s/\%y/YY/g;
+  $format =~ s/\%m/MM/g;
+  $format =~ s/\%d/DD/g;
+  $format =~ s/\%a/Dy/g;
+  $format =~ s/\%H/HH24/g;
+  $format =~ s/\%i/MI/g;
+  $format =~ s/\%s/SS/g;
+
+  return "TO_CHAR($date, " . $self->quote($format) . ")";
 }
 
 sub sql_date_math {
-    my ($self, $date, $operator, $interval, $units) = @_;
-    
-    return "$date $operator $interval * INTERVAL '1 $units'";
+  my ($self, $date, $operator, $interval, $units) = @_;
+
+  return "$date $operator $interval * INTERVAL '1 $units'";
 }
 
 sub sql_string_concat {
-    my ($self, @params) = @_;
+  my ($self, @params) = @_;
 
-    # PostgreSQL 8.3 and newer require an explicit coercion to text
-    # to support concatenation of different data types.
-    return '(CAST(' . join(' AS text) || CAST(', @params) . ' AS text))';
+  # PostgreSQL 8.3 and newer require an explicit coercion to text
+  # to support concatenation of different data types.
+  return '(CAST(' . join(' AS text) || CAST(', @params) . ' AS text))';
 }
 
 # Tell us whether or not a particular sequence exists in the DB.
 sub bz_sequence_exists {
-    my ($self, $seq_name) = @_;
-    my $exists = $self->selectrow_array(
-        'SELECT 1 FROM pg_statio_user_sequences WHERE relname = ?',
-        undef, $seq_name);
-    return $exists || 0;
+  my ($self, $seq_name) = @_;
+  my $exists
+    = $self->selectrow_array(
+    'SELECT 1 FROM pg_statio_user_sequences WHERE relname = ?',
+    undef, $seq_name);
+  return $exists || 0;
 }
 
 sub bz_explain {
-    my ($self, $sql) = @_;
-    my $explain = $self->selectcol_arrayref("EXPLAIN ANALYZE $sql");
-    return join("\n", @$explain);
+  my ($self, $sql) = @_;
+  my $explain = $self->selectcol_arrayref("EXPLAIN ANALYZE $sql");
+  return join("\n", @$explain);
 }
 
 #####################################################################
@@ -233,35 +245,38 @@ sub bz_explain {
 #####################################################################
 
 sub bz_setup_database {
-    my $self = shift;
-    $self->SUPER::bz_setup_database(@_);
-
-    my ($has_plpgsql) = $self->selectrow_array("SELECT COUNT(*) FROM pg_language WHERE lanname = 'plpgsql'");
-    $self->do('CREATE LANGUAGE plpgsql') unless $has_plpgsql;
-
-    # Custom Functions
-
-    # -Copyright Â© 2013 Joshua D. Burns (JDBurnZ) and Message In Action LLC
-    # JDBurnZ: https://github.com/JDBurnZ
-    # Message In Action: https://www.messageinaction.com
-    #
-    #Permission is hereby granted, free of charge, to any person obtaining a copy of
-    #this software and associated documentation files (the "Software"), to deal in
-    #the Software without restriction, including without limitation the rights to
-    #use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
-    #the Software, and to permit persons to whom the Software is furnished to do so,
-    #subject to the following conditions:
-    #
-    #The above copyright notice and this permission notice shall be included in all
-    #copies or substantial portions of the Software.
-    #
-    #THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-    #IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
-    #FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
-    #COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
-    #IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
-    #CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-    $self->do(q|
+  my $self = shift;
+  $self->SUPER::bz_setup_database(@_);
+
+  my ($has_plpgsql)
+    = $self->selectrow_array(
+    "SELECT COUNT(*) FROM pg_language WHERE lanname = 'plpgsql'");
+  $self->do('CREATE LANGUAGE plpgsql') unless $has_plpgsql;
+
+  # Custom Functions
+
+ # -Copyright Â© 2013 Joshua D. Burns (JDBurnZ) and Message In Action LLC
+ # JDBurnZ: https://github.com/JDBurnZ
+ # Message In Action: https://www.messageinaction.com
+ #
+ #Permission is hereby granted, free of charge, to any person obtaining a copy of
+ #this software and associated documentation files (the "Software"), to deal in
+ #the Software without restriction, including without limitation the rights to
+ #use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+ #the Software, and to permit persons to whom the Software is furnished to do so,
+ #subject to the following conditions:
+ #
+ #The above copyright notice and this permission notice shall be included in all
+ #copies or substantial portions of the Software.
+ #
+ #THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ #IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+ #FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+ #COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+ #IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ #CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+  $self->do(
+    q|
         DROP FUNCTION IF EXISTS anyarray_uniq(anyarray);
         CREATE OR REPLACE FUNCTION anyarray_uniq(with_array anyarray)
         RETURNS anyarray AS $BODY$
@@ -286,134 +301,151 @@ sub bz_setup_database {
             RETURN return_array;
         END;
         $BODY$ LANGUAGE plpgsql;
-    |);
-
-    # PostgreSQL doesn't like having *any* index on the thetext
-    # field, because it can't have index data longer than 2770
-    # characters on that field.
-    $self->bz_drop_index('longdescs', 'longdescs_thetext_idx');
-    # Same for all the comments fields in the fulltext table.
-    $self->bz_drop_index('bugs_fulltext', 'bugs_fulltext_comments_idx');
-    $self->bz_drop_index('bugs_fulltext', 
-                         'bugs_fulltext_comments_noprivate_idx');
-
-    # PostgreSQL also wants an index for calling LOWER on
-    # login_name, which we do with sql_istrcmp all over the place.
-    $self->bz_add_index('profiles', 'profiles_login_name_lower_idx', 
-        {FIELDS => ['LOWER(login_name)'], TYPE => 'UNIQUE'});
-
-    # Now that Bugzilla::Object uses sql_istrcmp, other tables
-    # also need a LOWER() index.
-    _fix_case_differences('fielddefs', 'name');
-    $self->bz_add_index('fielddefs', 'fielddefs_name_lower_idx',
-        {FIELDS => ['LOWER(name)'], TYPE => 'UNIQUE'});
-    _fix_case_differences('keyworddefs', 'name');
-    $self->bz_add_index('keyworddefs', 'keyworddefs_name_lower_idx',
-        {FIELDS => ['LOWER(name)'], TYPE => 'UNIQUE'});
-    _fix_case_differences('products', 'name');
-    $self->bz_add_index('products', 'products_name_lower_idx',
-        {FIELDS => ['LOWER(name)'], TYPE => 'UNIQUE'});
-
-    # bz_rename_column and bz_rename_table didn't correctly rename
-    # the sequence.
-    $self->_fix_bad_sequence('fielddefs', 'id', 'fielddefs_fieldid_seq', 'fielddefs_id_seq');
-    # If the 'tags' table still exists, then bz_rename_table()
-    # will fix the sequence for us.
-    if (!$self->bz_table_info('tags')) {
-        my $res = $self->_fix_bad_sequence('tag', 'id', 'tags_id_seq', 'tag_id_seq');
-        # If $res is true, then the sequence has been renamed, meaning that
-        # the primary key must be renamed too.
-        if ($res) {
-            $self->do('ALTER INDEX tags_pkey RENAME TO tag_pkey');
-        }
+    |
+  );
+
+  # PostgreSQL doesn't like having *any* index on the thetext
+  # field, because it can't have index data longer than 2770
+  # characters on that field.
+  $self->bz_drop_index('longdescs', 'longdescs_thetext_idx');
+
+  # Same for all the comments fields in the fulltext table.
+  $self->bz_drop_index('bugs_fulltext', 'bugs_fulltext_comments_idx');
+  $self->bz_drop_index('bugs_fulltext', 'bugs_fulltext_comments_noprivate_idx');
+
+  # PostgreSQL also wants an index for calling LOWER on
+  # login_name, which we do with sql_istrcmp all over the place.
+  $self->bz_add_index(
+    'profiles',
+    'profiles_login_name_lower_idx',
+    {FIELDS => ['LOWER(login_name)'], TYPE => 'UNIQUE'}
+  );
+
+  # Now that Bugzilla::Object uses sql_istrcmp, other tables
+  # also need a LOWER() index.
+  _fix_case_differences('fielddefs', 'name');
+  $self->bz_add_index('fielddefs', 'fielddefs_name_lower_idx',
+    {FIELDS => ['LOWER(name)'], TYPE => 'UNIQUE'});
+  _fix_case_differences('keyworddefs', 'name');
+  $self->bz_add_index('keyworddefs', 'keyworddefs_name_lower_idx',
+    {FIELDS => ['LOWER(name)'], TYPE => 'UNIQUE'});
+  _fix_case_differences('products', 'name');
+  $self->bz_add_index('products', 'products_name_lower_idx',
+    {FIELDS => ['LOWER(name)'], TYPE => 'UNIQUE'});
+
+  # bz_rename_column and bz_rename_table didn't correctly rename
+  # the sequence.
+  $self->_fix_bad_sequence('fielddefs', 'id', 'fielddefs_fieldid_seq',
+    'fielddefs_id_seq');
+
+  # If the 'tags' table still exists, then bz_rename_table()
+  # will fix the sequence for us.
+  if (!$self->bz_table_info('tags')) {
+    my $res = $self->_fix_bad_sequence('tag', 'id', 'tags_id_seq', 'tag_id_seq');
+
+    # If $res is true, then the sequence has been renamed, meaning that
+    # the primary key must be renamed too.
+    if ($res) {
+      $self->do('ALTER INDEX tags_pkey RENAME TO tag_pkey');
     }
-
-    # Certain sequences got upgraded before we required Pg 8.3, and
-    # so they were not properly associated with their columns.
-    my @tables = $self->bz_table_list_real;
-    foreach my $table (@tables) {
-        my @columns = $self->bz_table_columns_real($table);
-        foreach my $column (@columns) {
-            # All our SERIAL pks have "id" in their name at the end.
-            next unless $column =~ /id$/;
-            my $sequence = "${table}_${column}_seq";
-            if ($self->bz_sequence_exists($sequence)) {
-                my $is_associated = $self->selectrow_array(
-                    'SELECT pg_get_serial_sequence(?,?)',
-                    undef, $table, $column);
-                next if $is_associated;
-                print "Fixing $sequence to be associated"
-                      . " with $table.$column...\n";
-                $self->do("ALTER SEQUENCE $sequence OWNED BY $table.$column");
-                # In order to produce an exactly identical schema to what
-                # a brand-new checksetup.pl run would produce, we also need
-                # to re-set the default on this column.
-                $self->do("ALTER TABLE $table
+  }
+
+  # Certain sequences got upgraded before we required Pg 8.3, and
+  # so they were not properly associated with their columns.
+  my @tables = $self->bz_table_list_real;
+  foreach my $table (@tables) {
+    my @columns = $self->bz_table_columns_real($table);
+    foreach my $column (@columns) {
+
+      # All our SERIAL pks have "id" in their name at the end.
+      next unless $column =~ /id$/;
+      my $sequence = "${table}_${column}_seq";
+      if ($self->bz_sequence_exists($sequence)) {
+        my $is_associated = $self->selectrow_array('SELECT pg_get_serial_sequence(?,?)',
+          undef, $table, $column);
+        next if $is_associated;
+        print "Fixing $sequence to be associated" . " with $table.$column...\n";
+        $self->do("ALTER SEQUENCE $sequence OWNED BY $table.$column");
+
+        # In order to produce an exactly identical schema to what
+        # a brand-new checksetup.pl run would produce, we also need
+        # to re-set the default on this column.
+        $self->do(
+          "ALTER TABLE $table
                           ALTER COLUMN $column
-                           SET DEFAULT nextval('$sequence')");
-            }
-        }
+                           SET DEFAULT nextval('$sequence')"
+        );
+      }
     }
+  }
 }
 
 sub _fix_bad_sequence {
-    my ($self, $table, $column, $old_seq, $new_seq) = @_;
-    if ($self->bz_column_info($table, $column)
-        && $self->bz_sequence_exists($old_seq))
-    {
-        print "Fixing $old_seq sequence...\n";
-        $self->do("ALTER SEQUENCE $old_seq RENAME TO $new_seq");
-        $self->do("ALTER TABLE $table ALTER COLUMN $column
-                    SET DEFAULT NEXTVAL('$new_seq')");
-        return 1;
-    }
-    return 0;
+  my ($self, $table, $column, $old_seq, $new_seq) = @_;
+  if ( $self->bz_column_info($table, $column)
+    && $self->bz_sequence_exists($old_seq))
+  {
+    print "Fixing $old_seq sequence...\n";
+    $self->do("ALTER SEQUENCE $old_seq RENAME TO $new_seq");
+    $self->do(
+      "ALTER TABLE $table ALTER COLUMN $column
+                    SET DEFAULT NEXTVAL('$new_seq')"
+    );
+    return 1;
+  }
+  return 0;
 }
 
 # Renames things that differ only in case.
 sub _fix_case_differences {
-    my ($table, $field) = @_;
-    my $dbh = Bugzilla->dbh;
-
-    my $duplicates = $dbh->selectcol_arrayref(
-          "SELECT DISTINCT LOWER($field) FROM $table 
-        GROUP BY LOWER($field) HAVING COUNT(LOWER($field)) > 1");
-
-    foreach my $name (@$duplicates) {
-        my $dups = $dbh->selectcol_arrayref(
-            "SELECT $field FROM $table WHERE LOWER($field) = ?",
-            undef, $name);
-        my $primary = shift @$dups;
-        foreach my $dup (@$dups) {
-            my $new_name = "${dup}_";
-            # Make sure the new name isn't *also* a duplicate.
-            while (1) {
-                last if (!$dbh->selectrow_array(
-                             "SELECT 1 FROM $table WHERE LOWER($field) = ?",
-                              undef, lc($new_name)));
-                $new_name .= "_";
-            }
-            print "$table '$primary' and '$dup' have names that differ",
-                  " only in case.\nRenaming '$dup' to '$new_name'...\n";
-            $dbh->do("UPDATE $table SET $field = ? WHERE $field = ?",
-                     undef, $new_name, $dup);
-        }
+  my ($table, $field) = @_;
+  my $dbh = Bugzilla->dbh;
+
+  my $duplicates = $dbh->selectcol_arrayref(
+    "SELECT DISTINCT LOWER($field) FROM $table 
+        GROUP BY LOWER($field) HAVING COUNT(LOWER($field)) > 1"
+  );
+
+  foreach my $name (@$duplicates) {
+    my $dups
+      = $dbh->selectcol_arrayref(
+      "SELECT $field FROM $table WHERE LOWER($field) = ?",
+      undef, $name);
+    my $primary = shift @$dups;
+    foreach my $dup (@$dups) {
+      my $new_name = "${dup}_";
+
+      # Make sure the new name isn't *also* a duplicate.
+      while (1) {
+        last
+          if (!$dbh->selectrow_array(
+          "SELECT 1 FROM $table WHERE LOWER($field) = ?",
+          undef, lc($new_name)
+          ));
+        $new_name .= "_";
+      }
+      print "$table '$primary' and '$dup' have names that differ",
+        " only in case.\nRenaming '$dup' to '$new_name'...\n";
+      $dbh->do("UPDATE $table SET $field = ? WHERE $field = ?",
+        undef, $new_name, $dup);
     }
+  }
 }
 
 #####################################################################
 # Custom Schema Information Functions
 #####################################################################
 
-# Pg includes the PostgreSQL system tables in table_list_real, so 
+# Pg includes the PostgreSQL system tables in table_list_real, so
 # we need to remove those.
 sub bz_table_list_real {
-    my $self = shift;
+  my $self = shift;
+
+  my @full_table_list = $self->SUPER::bz_table_list_real(@_);
 
-    my @full_table_list = $self->SUPER::bz_table_list_real(@_);
-    # All PostgreSQL system tables start with "pg_" or "sql_"
-    my @table_list = grep(!/(^pg_)|(^sql_)/, @full_table_list);
-    return @table_list;
+  # All PostgreSQL system tables start with "pg_" or "sql_"
+  my @table_list = grep(!/(^pg_)|(^sql_)/, @full_table_list);
+  return @table_list;
 }
 
 1;
index 0a31c8d1af9fba022fc2e6fb0d68f41fa9c8a027..bacc9dc5dfc18434344512ffe6650b105229aa11 100644 (file)
@@ -28,9 +28,11 @@ use Carp qw(confess);
 use Digest::MD5 qw(md5_hex);
 use Hash::Util qw(lock_value unlock_hash lock_keys unlock_keys);
 use List::MoreUtils qw(firstidx natatime);
+
 # Bug 1270550 - Tie::Hash::NamedCapture must be loaded before Safe.
 use Tie::Hash::NamedCapture;
 use Safe;
+
 # Historical, needed for SCHEMA_VERSION = '1.00'
 use Storable qw(dclone freeze thaw);
 
@@ -199,1618 +201,1560 @@ update this column in this table."
 
 =cut
 
-use constant SCHEMA_VERSION  => 3;
-use constant ADD_COLUMN      => 'ADD COLUMN';
+use constant SCHEMA_VERSION => 3;
+use constant ADD_COLUMN     => 'ADD COLUMN';
+
 # Multiple FKs can be added using ALTER TABLE ADD CONSTRAINT in one
 # SQL statement. This isn't true for all databases.
 use constant MULTIPLE_FKS_IN_ALTER => 1;
+
 # This is a reasonable default that's true for both PostgreSQL and MySQL.
 use constant MAX_IDENTIFIER_LEN => 63;
 
 use constant FIELD_TABLE_SCHEMA => {
+  FIELDS => [
+    id                  => {TYPE => 'SMALLSERIAL', NOTNULL => 1, PRIMARYKEY => 1},
+    value               => {TYPE => 'varchar(64)', NOTNULL => 1},
+    sortkey             => {TYPE => 'INT2',        NOTNULL => 1, DEFAULT => 0},
+    isactive            => {TYPE => 'BOOLEAN',     NOTNULL => 1, DEFAULT => 'TRUE'},
+    visibility_value_id => {TYPE => 'INT2'},
+  ],
+
+  # Note that bz_add_field_table should prepend the table name
+  # to these index names.
+  INDEXES => [
+    value_idx               => {FIELDS => ['value'], TYPE => 'UNIQUE'},
+    sortkey_idx             => ['sortkey',           'value'],
+    visibility_value_id_idx => ['visibility_value_id'],
+  ],
+};
+
+use constant ABSTRACT_SCHEMA => {
+
+  # BUG-RELATED TABLES
+  # ------------------
+
+  # General Bug Information
+  # -----------------------
+  bugs => {
     FIELDS => [
-        id       => {TYPE => 'SMALLSERIAL', NOTNULL => 1,
-                     PRIMARYKEY => 1},
-        value    => {TYPE => 'varchar(64)', NOTNULL => 1},
-        sortkey  => {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0},
-        isactive => {TYPE => 'BOOLEAN', NOTNULL => 1,
-                     DEFAULT => 'TRUE'},
-        visibility_value_id => {TYPE => 'INT2'},
+      bug_id      => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1},
+      assigned_to => {
+        TYPE       => 'INT3',
+        NOTNULL    => 1,
+        REFERENCES => {TABLE => 'profiles', COLUMN => 'userid'}
+      },
+      bug_file_loc => {TYPE => 'MEDIUMTEXT',   NOTNULL => 1, DEFAULT => "''"},
+      bug_severity => {TYPE => 'varchar(64)',  NOTNULL => 1},
+      bug_status   => {TYPE => 'varchar(64)',  NOTNULL => 1},
+      creation_ts  => {TYPE => 'DATETIME'},
+      delta_ts     => {TYPE => 'DATETIME',     NOTNULL => 1},
+      short_desc   => {TYPE => 'varchar(255)', NOTNULL => 1},
+      op_sys       => {TYPE => 'varchar(64)',  NOTNULL => 1},
+      priority     => {TYPE => 'varchar(64)',  NOTNULL => 1},
+      product_id   => {
+        TYPE       => 'INT2',
+        NOTNULL    => 1,
+        REFERENCES => {TABLE => 'products', COLUMN => 'id'}
+      },
+      rep_platform => {TYPE => 'varchar(64)', NOTNULL => 1},
+      reporter     => {
+        TYPE       => 'INT3',
+        NOTNULL    => 1,
+        REFERENCES => {TABLE => 'profiles', COLUMN => 'userid'}
+      },
+      version      => {TYPE => 'varchar(64)', NOTNULL => 1},
+      component_id => {
+        TYPE       => 'INT3',
+        NOTNULL    => 1,
+        REFERENCES => {TABLE => 'components', COLUMN => 'id'}
+      },
+      resolution       => {TYPE => 'varchar(64)', NOTNULL => 1, DEFAULT => "''"},
+      target_milestone => {TYPE => 'varchar(64)', NOTNULL => 1, DEFAULT => "'---'"},
+      qa_contact =>
+        {TYPE => 'INT3', REFERENCES => {TABLE => 'profiles', COLUMN => 'userid'}},
+      status_whiteboard   => {TYPE => 'MEDIUMTEXT', NOTNULL => 1, DEFAULT => "''"},
+      lastdiffed          => {TYPE => 'DATETIME'},
+      everconfirmed       => {TYPE => 'BOOLEAN', NOTNULL => 1},
+      reporter_accessible => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'},
+      cclist_accessible   => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'},
+      estimated_time      => {TYPE => 'decimal(7,2)', NOTNULL => 1, DEFAULT => '0'},
+      remaining_time      => {TYPE => 'decimal(7,2)', NOTNULL => 1, DEFAULT => '0'},
+      deadline            => {TYPE => 'DATETIME'},
     ],
-    # Note that bz_add_field_table should prepend the table name
-    # to these index names.
     INDEXES => [
-        value_idx   => {FIELDS => ['value'], TYPE => 'UNIQUE'},
-        sortkey_idx => ['sortkey', 'value'],
-        visibility_value_id_idx => ['visibility_value_id'],
+      bugs_assigned_to_idx      => ['assigned_to'],
+      bugs_creation_ts_idx      => ['creation_ts'],
+      bugs_delta_ts_idx         => ['delta_ts'],
+      bugs_bug_severity_idx     => ['bug_severity'],
+      bugs_bug_status_idx       => ['bug_status'],
+      bugs_op_sys_idx           => ['op_sys'],
+      bugs_priority_idx         => ['priority'],
+      bugs_product_id_idx       => ['product_id'],
+      bugs_reporter_idx         => ['reporter'],
+      bugs_version_idx          => ['version'],
+      bugs_component_id_idx     => ['component_id'],
+      bugs_resolution_idx       => ['resolution'],
+      bugs_target_milestone_idx => ['target_milestone'],
+      bugs_qa_contact_idx       => ['qa_contact'],
     ],
-};
+  },
 
-use constant ABSTRACT_SCHEMA => {
+  bugs_fulltext => {
+    FIELDS => [
+      bug_id => {
+        TYPE       => 'INT3',
+        NOTNULL    => 1,
+        PRIMARYKEY => 1,
+        REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'}
+      },
+      short_desc => {TYPE => 'varchar(255)', NOTNULL => 1},
+
+      # Comments are stored all together in one column for searching.
+      # This allows us to examine all comments together when deciding
+      # the relevance of a bug in fulltext search.
+      comments           => {TYPE => 'LONGTEXT'},
+      comments_noprivate => {TYPE => 'LONGTEXT'},
+    ],
+    INDEXES => [
+      bugs_fulltext_short_desc_idx => {FIELDS => ['short_desc'], TYPE => 'FULLTEXT'},
+      bugs_fulltext_comments_idx   => {FIELDS => ['comments'],   TYPE => 'FULLTEXT'},
+      bugs_fulltext_comments_noprivate_idx =>
+        {FIELDS => ['comments_noprivate'], TYPE => 'FULLTEXT'},
+    ],
+  },
 
-    # BUG-RELATED TABLES
-    # ------------------
-
-    # General Bug Information
-    # -----------------------
-    bugs => {
-        FIELDS => [
-            bug_id              => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1,
-                                    PRIMARYKEY => 1},
-            assigned_to         => {TYPE => 'INT3', NOTNULL => 1,
-                                    REFERENCES => {TABLE  => 'profiles',
-                                                   COLUMN => 'userid'}},
-            bug_file_loc        => {TYPE => 'MEDIUMTEXT', 
-                                    NOTNULL => 1, DEFAULT => "''"},
-            bug_severity        => {TYPE => 'varchar(64)', NOTNULL => 1},
-            bug_status          => {TYPE => 'varchar(64)', NOTNULL => 1},
-            creation_ts         => {TYPE => 'DATETIME'},
-            delta_ts            => {TYPE => 'DATETIME', NOTNULL => 1},
-            short_desc          => {TYPE => 'varchar(255)', NOTNULL => 1},
-            op_sys              => {TYPE => 'varchar(64)', NOTNULL => 1},
-            priority            => {TYPE => 'varchar(64)', NOTNULL => 1},
-            product_id          => {TYPE => 'INT2', NOTNULL => 1,
-                                    REFERENCES => {TABLE  => 'products',
-                                                   COLUMN => 'id'}},
-            rep_platform        => {TYPE => 'varchar(64)', NOTNULL => 1},
-            reporter            => {TYPE => 'INT3', NOTNULL => 1,
-                                    REFERENCES => {TABLE  => 'profiles',
-                                                   COLUMN => 'userid'}},
-            version             => {TYPE => 'varchar(64)', NOTNULL => 1},
-            component_id        => {TYPE => 'INT3', NOTNULL => 1,
-                                    REFERENCES => {TABLE  => 'components',
-                                                   COLUMN => 'id'}},
-            resolution          => {TYPE => 'varchar(64)',
-                                    NOTNULL => 1, DEFAULT => "''"},
-            target_milestone    => {TYPE => 'varchar(64)',
-                                    NOTNULL => 1, DEFAULT => "'---'"},
-            qa_contact          => {TYPE => 'INT3',
-                                    REFERENCES => {TABLE  => 'profiles',
-                                                   COLUMN => 'userid'}},
-            status_whiteboard   => {TYPE => 'MEDIUMTEXT', NOTNULL => 1,
-                                    DEFAULT => "''"},
-            lastdiffed          => {TYPE => 'DATETIME'},
-            everconfirmed       => {TYPE => 'BOOLEAN', NOTNULL => 1},
-            reporter_accessible => {TYPE => 'BOOLEAN',
-                                    NOTNULL => 1, DEFAULT => 'TRUE'},
-            cclist_accessible   => {TYPE => 'BOOLEAN',
-                                    NOTNULL => 1, DEFAULT => 'TRUE'},
-            estimated_time      => {TYPE => 'decimal(7,2)',
-                                    NOTNULL => 1, DEFAULT => '0'},
-            remaining_time      => {TYPE => 'decimal(7,2)',
-                                    NOTNULL => 1, DEFAULT => '0'},
-            deadline            => {TYPE => 'DATETIME'},
-        ],
-        INDEXES => [
-            bugs_assigned_to_idx      => ['assigned_to'],
-            bugs_creation_ts_idx      => ['creation_ts'],
-            bugs_delta_ts_idx         => ['delta_ts'],
-            bugs_bug_severity_idx     => ['bug_severity'],
-            bugs_bug_status_idx       => ['bug_status'],
-            bugs_op_sys_idx           => ['op_sys'],
-            bugs_priority_idx         => ['priority'],
-            bugs_product_id_idx       => ['product_id'],
-            bugs_reporter_idx         => ['reporter'],
-            bugs_version_idx          => ['version'],
-            bugs_component_id_idx     => ['component_id'],
-            bugs_resolution_idx       => ['resolution'],
-            bugs_target_milestone_idx => ['target_milestone'],
-            bugs_qa_contact_idx       => ['qa_contact'],
-        ],
-    },
-
-    bugs_fulltext => {
-        FIELDS => [
-            bug_id     => {TYPE => 'INT3', NOTNULL => 1, PRIMARYKEY => 1,
-                           REFERENCES => {TABLE  => 'bugs',
-                                          COLUMN => 'bug_id',
-                                          DELETE => 'CASCADE'}},
-            short_desc => {TYPE => 'varchar(255)', NOTNULL => 1},
-            # Comments are stored all together in one column for searching.
-            # This allows us to examine all comments together when deciding
-            # the relevance of a bug in fulltext search.
-            comments   => {TYPE => 'LONGTEXT'},
-            comments_noprivate => {TYPE => 'LONGTEXT'},
-        ],
-        INDEXES => [
-            bugs_fulltext_short_desc_idx => {FIELDS => ['short_desc'],
-                                               TYPE => 'FULLTEXT'},
-            bugs_fulltext_comments_idx   => {FIELDS => ['comments'],
-                                               TYPE => 'FULLTEXT'},
-            bugs_fulltext_comments_noprivate_idx => {
-                FIELDS => ['comments_noprivate'], TYPE => 'FULLTEXT'},
-        ],
-    },
-
-    bugs_activity => {
-        FIELDS => [
-            id        => {TYPE => 'INTSERIAL', NOTNULL => 1, 
-                          PRIMARYKEY => 1}, 
-            bug_id    => {TYPE => 'INT3', NOTNULL => 1,
-                          REFERENCES    =>  {TABLE  =>  'bugs',
-                                             COLUMN =>  'bug_id',
-                                             DELETE => 'CASCADE'}},
-            attach_id => {TYPE => 'INT3',
-                          REFERENCES    =>  {TABLE  =>  'attachments',
-                                            COLUMN  =>  'attach_id',
-                                            DELETE => 'CASCADE'}},
-            who       => {TYPE => 'INT3', NOTNULL => 1,
-                          REFERENCES => {TABLE  => 'profiles',
-                                         COLUMN => 'userid'}},
-            bug_when  => {TYPE => 'DATETIME', NOTNULL => 1},
-            fieldid   => {TYPE => 'INT3', NOTNULL => 1,
-                          REFERENCES    =>  {TABLE  =>  'fielddefs',
-                                             COLUMN =>  'id'}},
-            added     => {TYPE => 'varchar(255)'},
-            removed   => {TYPE => 'varchar(255)'},
-            comment_id => {TYPE => 'INT4', 
-                           REFERENCES => { TABLE  => 'longdescs',
-                                           COLUMN => 'comment_id',
-                                           DELETE => 'CASCADE'}},
-        ],
-        INDEXES => [
-            bugs_activity_bug_id_idx  => ['bug_id'],
-            bugs_activity_who_idx     => ['who'],
-            bugs_activity_bug_when_idx => ['bug_when'],
-            bugs_activity_fieldid_idx => ['fieldid'],
-            bugs_activity_added_idx   => ['added'],
-            bugs_activity_removed_idx => ['removed'], 
-        ],
-    },
-
-    bugs_aliases => {
-        FIELDS => [
-            alias     => {TYPE => 'varchar(40)', NOTNULL => 1},
-            bug_id    => {TYPE => 'INT3',
-                          REFERENCES => {TABLE  => 'bugs',
-                                         COLUMN => 'bug_id',
-                                         DELETE => 'CASCADE'}},
-        ],
-        INDEXES => [
-            bugs_aliases_bug_id_idx => ['bug_id'],
-            bugs_aliases_alias_idx  => {FIELDS => ['alias'],
-                                        TYPE => 'UNIQUE'},
-        ],
-    },
-
-    cc => {
-        FIELDS => [
-            bug_id => {TYPE => 'INT3', NOTNULL => 1,
-                       REFERENCES => {TABLE  => 'bugs',
-                                      COLUMN => 'bug_id',
-                                      DELETE => 'CASCADE'}},
-            who    => {TYPE => 'INT3', NOTNULL => 1,
-                       REFERENCES => {TABLE  => 'profiles',
-                                      COLUMN => 'userid',
-                                      DELETE => 'CASCADE'}},
-        ],
-        INDEXES => [
-            cc_bug_id_idx => {FIELDS => [qw(bug_id who)],
-                              TYPE => 'UNIQUE'},
-            cc_who_idx    => ['who'],
-        ],
-    },
-
-    longdescs => {
-        FIELDS => [
-            comment_id      => {TYPE => 'INTSERIAL',  NOTNULL => 1,
-                                PRIMARYKEY => 1},
-            bug_id          => {TYPE => 'INT3',  NOTNULL => 1,
-                                REFERENCES => {TABLE => 'bugs',
-                                               COLUMN => 'bug_id',
-                                               DELETE => 'CASCADE'}},
-            who             => {TYPE => 'INT3', NOTNULL => 1,
-                                REFERENCES => {TABLE => 'profiles',
-                                               COLUMN => 'userid'}},
-            bug_when        => {TYPE => 'DATETIME', NOTNULL => 1},
-            work_time       => {TYPE => 'decimal(7,2)', NOTNULL => 1,
-                                DEFAULT => '0'},
-            thetext         => {TYPE => 'LONGTEXT', NOTNULL => 1},
-            isprivate       => {TYPE => 'BOOLEAN', NOTNULL => 1,
-                                DEFAULT => 'FALSE'},
-            already_wrapped => {TYPE => 'BOOLEAN', NOTNULL => 1,
-                                DEFAULT => 'FALSE'},
-            type            => {TYPE => 'INT2', NOTNULL => 1,
-                                DEFAULT => '0'},
-            extra_data      => {TYPE => 'varchar(255)'},
-            is_markdown     => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}
-        ],
-        INDEXES => [
-            longdescs_bug_id_idx   => [qw(bug_id work_time)],
-            longdescs_who_idx     => [qw(who bug_id)],
-            longdescs_bug_when_idx => ['bug_when'],
-        ],
-    },
-
-    longdescs_tags => {
-        FIELDS => [
-            id         => { TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1 },
-            comment_id => { TYPE => 'INT4',
-                            REFERENCES => { TABLE  => 'longdescs',
-                                            COLUMN => 'comment_id',
-                                            DELETE => 'CASCADE' }},
-            tag        => { TYPE => 'varchar(24)',  NOTNULL => 1 },
-        ],
-        INDEXES => [
-            longdescs_tags_idx => { FIELDS => ['comment_id', 'tag'], TYPE => 'UNIQUE' },
-        ],
-    },
-
-    longdescs_tags_weights => {
-        FIELDS => [
-            id     => { TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1 },
-            tag    => { TYPE => 'varchar(24)',  NOTNULL => 1 },
-            weight => { TYPE => 'INT3',         NOTNULL => 1 },
-        ],
-        INDEXES => [
-            longdescs_tags_weights_tag_idx => { FIELDS => ['tag'], TYPE => 'UNIQUE' },
-        ],
-    },
-
-    longdescs_tags_activity => {
-        FIELDS => [
-            id         => { TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1 },
-            bug_id     => { TYPE => 'INT3', NOTNULL => 1,
-                            REFERENCES =>  { TABLE  =>  'bugs',
-                                             COLUMN =>  'bug_id',
-                                             DELETE => 'CASCADE' }},
-            comment_id => { TYPE => 'INT4',
-                            REFERENCES => { TABLE  => 'longdescs',
-                                            COLUMN => 'comment_id',
-                                            DELETE => 'CASCADE' }},
-            who        => { TYPE => 'INT3', NOTNULL => 1,
-                            REFERENCES => { TABLE  => 'profiles',
-                                            COLUMN => 'userid' }},
-            bug_when  => { TYPE => 'DATETIME', NOTNULL => 1 },
-            added     => { TYPE => 'varchar(24)' },
-            removed   => { TYPE => 'varchar(24)' },
-        ],
-        INDEXES => [
-            longdescs_tags_activity_bug_id_idx  => ['bug_id'],
-        ],
-    },
-
-    dependencies => {
-        FIELDS => [
-            blocked   => {TYPE => 'INT3', NOTNULL => 1,
-                          REFERENCES    =>  {TABLE  =>   'bugs',
-                                            COLUMN  =>  'bug_id',
-                                            DELETE => 'CASCADE'}},
-            dependson => {TYPE => 'INT3', NOTNULL => 1,
-                          REFERENCES    =>  {TABLE  =>   'bugs',
-                                            COLUMN  =>  'bug_id',
-                                            DELETE => 'CASCADE'}},
-        ],
-        INDEXES => [
-            dependencies_blocked_idx => {FIELDS => [qw(blocked dependson)],
-                                         TYPE   => 'UNIQUE'},
-            dependencies_dependson_idx => ['dependson'],
-        ],
-    },
-
-    attachments => {
-        FIELDS => [
-            attach_id    => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1,
-                             PRIMARYKEY => 1},
-            bug_id       => {TYPE => 'INT3', NOTNULL => 1,
-                             REFERENCES    =>  {TABLE  => 'bugs',
-                                                COLUMN => 'bug_id',
-                                                DELETE => 'CASCADE'}},
-            creation_ts  => {TYPE => 'DATETIME', NOTNULL => 1},
-            modification_time => {TYPE => 'DATETIME', NOTNULL => 1},
-            description  => {TYPE => 'TINYTEXT', NOTNULL => 1},
-            mimetype     => {TYPE => 'TINYTEXT', NOTNULL => 1},
-            ispatch      => {TYPE => 'BOOLEAN', NOTNULL => 1,
-                             DEFAULT => 'FALSE'},
-            filename     => {TYPE => 'varchar(255)', NOTNULL => 1},
-            submitter_id => {TYPE => 'INT3', NOTNULL => 1,
-                             REFERENCES => {TABLE => 'profiles',
-                                            COLUMN => 'userid'}},
-            isobsolete   => {TYPE => 'BOOLEAN', NOTNULL => 1,
-                             DEFAULT => 'FALSE'},
-            isprivate    => {TYPE => 'BOOLEAN', NOTNULL => 1,
-                             DEFAULT => 'FALSE'},
-        ],
-        INDEXES => [
-            attachments_bug_id_idx => ['bug_id'],
-            attachments_creation_ts_idx => ['creation_ts'],
-            attachments_modification_time_idx => ['modification_time'],
-            attachments_submitter_id_idx => ['submitter_id', 'bug_id'],
-        ],
-    },
-    attach_data => {
-        FIELDS => [
-            id      => {TYPE => 'INT3', NOTNULL => 1,
-                        PRIMARYKEY => 1,
-                        REFERENCES  =>  {TABLE  => 'attachments',
-                                         COLUMN => 'attach_id',
-                                         DELETE => 'CASCADE'}},
-            thedata => {TYPE => 'LONGBLOB', NOTNULL => 1},
-        ],
-    },
-
-    duplicates => {
-        FIELDS => [
-            dupe_of => {TYPE => 'INT3', NOTNULL => 1,
-                        REFERENCES => {TABLE  =>  'bugs',
-                                       COLUMN =>  'bug_id',
-                                       DELETE =>  'CASCADE'}},
-            dupe    => {TYPE => 'INT3', NOTNULL => 1,
-                        PRIMARYKEY => 1,
-                        REFERENCES => {TABLE  =>  'bugs',
-                                       COLUMN =>  'bug_id',
-                                       DELETE =>  'CASCADE'}},
-        ],
-    },
-
-    bug_see_also => {
-        FIELDS => [
-            id     => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1,
-                       PRIMARYKEY => 1},
-            bug_id => {TYPE => 'INT3', NOTNULL => 1,
-                       REFERENCES => {TABLE  => 'bugs',
-                                      COLUMN => 'bug_id',
-                                      DELETE => 'CASCADE'}},
-            value  => {TYPE => 'varchar(255)', NOTNULL => 1},
-            class  => {TYPE => 'varchar(255)', NOTNULL => 1, DEFAULT => "''"},
-        ],
-        INDEXES => [
-            bug_see_also_bug_id_idx => {FIELDS => [qw(bug_id value)], 
-                                        TYPE   => 'UNIQUE'},
-        ],
-    },
-
-    # Auditing
-    # --------
-
-    audit_log => {
-        FIELDS => [
-            user_id   => {TYPE => 'INT3',
-                          REFERENCES => {TABLE  => 'profiles',
-                                         COLUMN => 'userid',
-                                         DELETE => 'SET NULL'}},
-            class     => {TYPE => 'varchar(255)', NOTNULL => 1},
-            object_id => {TYPE => 'INT4', NOTNULL => 1},
-            field     => {TYPE => 'varchar(64)', NOTNULL => 1},
-            removed   => {TYPE => 'MEDIUMTEXT'},
-            added     => {TYPE => 'MEDIUMTEXT'},
-            at_time   => {TYPE => 'DATETIME', NOTNULL => 1},
-        ],
-        INDEXES => [
-                    audit_log_class_idx => ['class', 'at_time'],
-        ],
-    },
-
-    # Keywords
-    # --------
-
-    keyworddefs => {
-        FIELDS => [
-            id          => {TYPE => 'SMALLSERIAL', NOTNULL => 1,
-                            PRIMARYKEY => 1},
-            name        => {TYPE => 'varchar(64)', NOTNULL => 1},
-            description => {TYPE => 'MEDIUMTEXT', NOTNULL => 1},
-            is_active   => {TYPE => 'BOOLEAN', NOTNULL => 1,
-                            DEFAULT => 'TRUE'},
-        ],
-        INDEXES => [
-            keyworddefs_name_idx   => {FIELDS => ['name'],
-                                       TYPE => 'UNIQUE'},
-        ],
-    },
-
-    keywords => {
-        FIELDS => [
-            bug_id    => {TYPE => 'INT3', NOTNULL => 1,
-                          REFERENCES => {TABLE  => 'bugs',
-                                         COLUMN => 'bug_id',
-                                         DELETE => 'CASCADE'}},
-            keywordid => {TYPE => 'INT2', NOTNULL => 1,
-                          REFERENCES => {TABLE  => 'keyworddefs',
-                                         COLUMN => 'id',
-                                         DELETE => 'CASCADE'}},
-        ],
-        INDEXES => [
-            keywords_bug_id_idx    => {FIELDS => [qw(bug_id keywordid)],
-                                       TYPE => 'UNIQUE'},
-            keywords_keywordid_idx => ['keywordid'],
-        ],
-    },
-
-    # Flags
-    # -----
-
-    # "flags" stores one record for each flag on each bug/attachment.
-    flags => {
-        FIELDS => [
-            id                => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1,
-                                  PRIMARYKEY => 1},
-            type_id           => {TYPE => 'INT2', NOTNULL => 1,
-                                  REFERENCES => {TABLE  => 'flagtypes',
-                                                 COLUMN => 'id',
-                                                 DELETE => 'CASCADE'}},
-            status            => {TYPE => 'char(1)', NOTNULL => 1},
-            bug_id            => {TYPE => 'INT3', NOTNULL => 1,
-                                  REFERENCES => {TABLE  => 'bugs',
-                                                 COLUMN => 'bug_id',
-                                                 DELETE => 'CASCADE'}},
-            attach_id         => {TYPE => 'INT3',
-                                  REFERENCES => {TABLE  => 'attachments',
-                                                 COLUMN => 'attach_id',
-                                                 DELETE => 'CASCADE'}},
-            creation_date     => {TYPE => 'DATETIME', NOTNULL => 1},
-            modification_date => {TYPE => 'DATETIME'},
-            setter_id         => {TYPE => 'INT3', NOTNULL => 1,
-                                  REFERENCES => {TABLE  => 'profiles',
-                                                 COLUMN => 'userid'}},
-            requestee_id      => {TYPE => 'INT3',
-                                  REFERENCES => {TABLE  => 'profiles',
-                                                 COLUMN => 'userid'}},
-        ],
-        INDEXES => [
-            flags_bug_id_idx       => [qw(bug_id attach_id)],
-            flags_setter_id_idx    => ['setter_id'],
-            flags_requestee_id_idx => ['requestee_id'],
-            flags_type_id_idx      => ['type_id'],
-        ],
-    },
-
-    # "flagtypes" defines the types of flags that can be set.
-    flagtypes => {
-        FIELDS => [
-            id               => {TYPE => 'SMALLSERIAL', NOTNULL => 1,
-                                 PRIMARYKEY => 1},
-            name             => {TYPE => 'varchar(50)', NOTNULL => 1},
-            description      => {TYPE => 'MEDIUMTEXT', NOTNULL => 1},
-            cc_list          => {TYPE => 'varchar(200)'},
-            target_type      => {TYPE => 'char(1)', NOTNULL => 1,
-                                 DEFAULT => "'b'"},
-            is_active        => {TYPE => 'BOOLEAN', NOTNULL => 1,
-                                 DEFAULT => 'TRUE'},
-            is_requestable   => {TYPE => 'BOOLEAN', NOTNULL => 1,
-                                 DEFAULT => 'FALSE'},
-            is_requesteeble  => {TYPE => 'BOOLEAN', NOTNULL => 1,
-                                 DEFAULT => 'FALSE'},
-            is_multiplicable => {TYPE => 'BOOLEAN', NOTNULL => 1,
-                                 DEFAULT => 'FALSE'},
-            sortkey          => {TYPE => 'INT2', NOTNULL => 1,
-                                 DEFAULT => '0'},
-            grant_group_id   => {TYPE => 'INT3',
-                                 REFERENCES => {TABLE  => 'groups',
-                                                COLUMN => 'id',
-                                                DELETE => 'SET NULL'}},
-            request_group_id => {TYPE => 'INT3',
-                                 REFERENCES => {TABLE  => 'groups',
-                                                COLUMN => 'id',
-                                                DELETE => 'SET NULL'}},
-        ],
-    },
-
-    # "flaginclusions" and "flagexclusions" specify the products/components
-    #     a bug/attachment must belong to in order for flags of a given type
-    #     to be set for them.
-    flaginclusions => {
-        FIELDS => [
-            type_id      => {TYPE => 'INT2', NOTNULL => 1,
-                             REFERENCES => {TABLE  => 'flagtypes',
-                                            COLUMN => 'id',
-                                            DELETE => 'CASCADE'}},
-            product_id   => {TYPE => 'INT2',
-                             REFERENCES => {TABLE  => 'products',
-                                            COLUMN => 'id',
-                                            DELETE => 'CASCADE'}},
-            component_id => {TYPE => 'INT3',
-                             REFERENCES => {TABLE  => 'components',
-                                            COLUMN => 'id',
-                                            DELETE => 'CASCADE'}},
-        ],
-        INDEXES => [
-            flaginclusions_type_id_idx => { FIELDS => [qw(type_id product_id component_id)],
-                                            TYPE   => 'UNIQUE' },
-        ],
-    },
-
-    flagexclusions => {
-        FIELDS => [
-            type_id      => {TYPE => 'INT2', NOTNULL => 1,
-                             REFERENCES => {TABLE  => 'flagtypes',
-                                            COLUMN => 'id',
-                                            DELETE => 'CASCADE'}},
-            product_id   => {TYPE => 'INT2',
-                             REFERENCES => {TABLE  => 'products',
-                                            COLUMN => 'id',
-                                            DELETE => 'CASCADE'}},
-            component_id => {TYPE => 'INT3',
-                             REFERENCES => {TABLE  => 'components',
-                                            COLUMN => 'id',
-                                            DELETE => 'CASCADE'}},
-        ],
-        INDEXES => [
-            flagexclusions_type_id_idx => { FIELDS => [qw(type_id product_id component_id)],
-                                            TYPE   => 'UNIQUE' },
-        ],
-    },
-
-    # General Field Information
-    # -------------------------
-
-    fielddefs => {
-        FIELDS => [
-            id          => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1,
-                            PRIMARYKEY => 1},
-            name        => {TYPE => 'varchar(64)', NOTNULL => 1},
-            type        => {TYPE => 'INT2', NOTNULL => 1,
-                            DEFAULT => FIELD_TYPE_UNKNOWN},
-            custom      => {TYPE => 'BOOLEAN', NOTNULL => 1,
-                            DEFAULT => 'FALSE'},
-            description => {TYPE => 'TINYTEXT', NOTNULL => 1},
-            long_desc   => {TYPE => 'varchar(255)', NOTNULL => 1, DEFAULT => "''"},
-            mailhead    => {TYPE => 'BOOLEAN', NOTNULL => 1,
-                            DEFAULT => 'FALSE'},
-            sortkey     => {TYPE => 'INT2', NOTNULL => 1},
-            obsolete    => {TYPE => 'BOOLEAN', NOTNULL => 1,
-                            DEFAULT => 'FALSE'},
-            enter_bug   => {TYPE => 'BOOLEAN', NOTNULL => 1,
-                            DEFAULT => 'FALSE'},
-            buglist     => {TYPE => 'BOOLEAN', NOTNULL => 1,
-                            DEFAULT => 'FALSE'},
-            visibility_field_id => {TYPE => 'INT3', 
-                                    REFERENCES => {TABLE  => 'fielddefs',
-                                                   COLUMN => 'id'}},
-            value_field_id => {TYPE => 'INT3',
-                               REFERENCES => {TABLE  => 'fielddefs',
-                                              COLUMN => 'id'}},
-            reverse_desc => {TYPE => 'TINYTEXT'},
-            is_mandatory => {TYPE => 'BOOLEAN', NOTNULL => 1,
-                             DEFAULT => 'FALSE'},
-            is_numeric    => {TYPE => 'BOOLEAN', NOTNULL => 1,
-                             DEFAULT => 'FALSE'},
-        ],
-        INDEXES => [
-            fielddefs_name_idx    => {FIELDS => ['name'],
-                                      TYPE => 'UNIQUE'},
-            fielddefs_sortkey_idx => ['sortkey'],
-            fielddefs_value_field_id_idx => ['value_field_id'],
-            fielddefs_is_mandatory_idx => ['is_mandatory'],
-        ],
-    },
-
-    # Field Visibility Information
-    # -------------------------
-
-    field_visibility => {
-        FIELDS => [
-            field_id => {TYPE => 'INT3', 
-                         REFERENCES => {TABLE  => 'fielddefs',
-                                        COLUMN => 'id',
-                                        DELETE => 'CASCADE'}},
-            value_id => {TYPE => 'INT2', NOTNULL => 1}
-        ],
-        INDEXES => [
-            field_visibility_field_id_idx => {
-                FIELDS => [qw(field_id value_id)],
-                TYPE   => 'UNIQUE'
-            },
-        ],
-    },
-
-    # Per-product Field Values
-    # ------------------------
-
-    versions => {
-        FIELDS => [
-            id         =>  {TYPE => 'MEDIUMSERIAL', NOTNULL => 1,
-                            PRIMARYKEY => 1},
-            value      =>  {TYPE => 'varchar(64)', NOTNULL => 1},
-            product_id =>  {TYPE => 'INT2', NOTNULL => 1,
-                            REFERENCES => {TABLE  => 'products',
-                                           COLUMN => 'id',
-                                           DELETE => 'CASCADE'}},
-            isactive   =>  {TYPE => 'BOOLEAN', NOTNULL => 1, 
-                            DEFAULT => 'TRUE'},
-        ],
-        INDEXES => [
-            versions_product_id_idx => {FIELDS => [qw(product_id value)],
-                                        TYPE => 'UNIQUE'},
-        ],
-    },
-
-    milestones => {
-        FIELDS => [
-            id         => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, 
-                           PRIMARYKEY => 1},
-            product_id => {TYPE => 'INT2', NOTNULL => 1,
-                           REFERENCES => {TABLE  => 'products',
-                                          COLUMN => 'id',
-                                          DELETE => 'CASCADE'}},
-            value      => {TYPE => 'varchar(64)', NOTNULL => 1},
-            sortkey    => {TYPE => 'INT2', NOTNULL => 1,
-                           DEFAULT => 0},
-            isactive   => {TYPE => 'BOOLEAN', NOTNULL => 1, 
-                           DEFAULT => 'TRUE'},
-        ],
-        INDEXES => [
-            milestones_product_id_idx => {FIELDS => [qw(product_id value)],
-                                          TYPE => 'UNIQUE'},
-        ],
-    },
-
-    # Global Field Values
-    # -------------------
-
-    bug_status => {
-        FIELDS => [
-            @{ dclone(FIELD_TABLE_SCHEMA->{FIELDS}) },
-            is_open  => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'},
-
-        ],
-        INDEXES => [
-            bug_status_value_idx  => {FIELDS => ['value'],
-                                       TYPE => 'UNIQUE'},
-            bug_status_sortkey_idx => ['sortkey', 'value'],
-            bug_status_visibility_value_id_idx => ['visibility_value_id'],
-        ],
-    },
-
-    resolution => {
-        FIELDS => dclone(FIELD_TABLE_SCHEMA->{FIELDS}),
-        INDEXES => [
-            resolution_value_idx   => {FIELDS => ['value'],
-                                       TYPE => 'UNIQUE'},
-            resolution_sortkey_idx => ['sortkey', 'value'],
-            resolution_visibility_value_id_idx => ['visibility_value_id'],
-        ],
-    },
-
-    bug_severity => {
-        FIELDS => dclone(FIELD_TABLE_SCHEMA->{FIELDS}),
-        INDEXES => [
-            bug_severity_value_idx   => {FIELDS => ['value'],
-                                         TYPE => 'UNIQUE'},
-            bug_severity_sortkey_idx => ['sortkey', 'value'],
-            bug_severity_visibility_value_id_idx => ['visibility_value_id'],
-        ],
-    },
-
-    priority => {
-        FIELDS => dclone(FIELD_TABLE_SCHEMA->{FIELDS}),
-        INDEXES => [
-            priority_value_idx   => {FIELDS => ['value'],
-                                     TYPE => 'UNIQUE'},
-            priority_sortkey_idx => ['sortkey', 'value'],
-            priority_visibility_value_id_idx => ['visibility_value_id'],
-        ],
-    },
-
-    rep_platform => {
-        FIELDS => dclone(FIELD_TABLE_SCHEMA->{FIELDS}),
-        INDEXES => [
-            rep_platform_value_idx   => {FIELDS => ['value'],
-                                         TYPE => 'UNIQUE'},
-            rep_platform_sortkey_idx => ['sortkey', 'value'],
-            rep_platform_visibility_value_id_idx => ['visibility_value_id'],
-        ],
-    },
-
-    op_sys => {
-        FIELDS => dclone(FIELD_TABLE_SCHEMA->{FIELDS}),
-        INDEXES => [
-            op_sys_value_idx   => {FIELDS => ['value'],
-                                   TYPE => 'UNIQUE'},
-            op_sys_sortkey_idx => ['sortkey', 'value'],
-            op_sys_visibility_value_id_idx => ['visibility_value_id'],
-        ],
-    },
-
-    status_workflow => {
-        FIELDS => [
-            # On bug creation, there is no old value.
-            old_status      => {TYPE => 'INT2',
-                                REFERENCES => {TABLE  => 'bug_status', 
-                                               COLUMN => 'id',
-                                               DELETE => 'CASCADE'}},
-            new_status      => {TYPE => 'INT2', NOTNULL => 1,
-                                REFERENCES => {TABLE  => 'bug_status', 
-                                               COLUMN => 'id',
-                                               DELETE => 'CASCADE'}},
-            require_comment => {TYPE => 'INT1', NOTNULL => 1, DEFAULT => 0},
-        ],
-        INDEXES => [
-            status_workflow_idx  => {FIELDS => ['old_status', 'new_status'],
-                                     TYPE => 'UNIQUE'},
-        ],
-    },
-
-    # USER INFO
-    # ---------
-
-    # General User Information
-    # ------------------------
-
-    profiles => {
-        FIELDS => [
-            userid         => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1,
-                               PRIMARYKEY => 1},
-            login_name     => {TYPE => 'varchar(255)', NOTNULL => 1},
-            email          => {TYPE => 'varchar(255)', NOTNULL => 1},
-            cryptpassword  => {TYPE => 'varchar(128)'},
-            realname       => {TYPE => 'varchar(255)', NOTNULL => 1,
-                               DEFAULT => "''"},
-            disabledtext   => {TYPE => 'MEDIUMTEXT', NOTNULL => 1,
-                               DEFAULT => "''"},
-            disable_mail   => {TYPE => 'BOOLEAN', NOTNULL => 1,
-                               DEFAULT => 'FALSE'},
-            mybugslink     => {TYPE => 'BOOLEAN', NOTNULL => 1,
-                               DEFAULT => 'TRUE'},
-            extern_id      => {TYPE => 'varchar(64)'},
-            is_enabled     => {TYPE => 'BOOLEAN', NOTNULL => 1, 
-                               DEFAULT => 'TRUE'}, 
-            last_seen_date => {TYPE => 'DATETIME'},
-        ],
-        INDEXES => [
-            profiles_login_name_idx => {FIELDS => ['login_name'],
-                                        TYPE => 'UNIQUE'},
-            profiles_extern_id_idx => {FIELDS => ['extern_id'],
-                                       TYPE   => 'UNIQUE'},
-            profiles_email_idx => {FIELDS => ['email'],
-                                   TYPE => 'UNIQUE'}
-        ],
-    },
-
-    profile_search => {
-        FIELDS => [
-            id         => {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1},
-            user_id    => {TYPE => 'INT3', NOTNULL => 1, 
-                           REFERENCES => {TABLE  => 'profiles', 
-                                          COLUMN => 'userid', 
-                                          DELETE => 'CASCADE'}},
-            bug_list   => {TYPE => 'MEDIUMTEXT', NOTNULL => 1},
-            list_order => {TYPE => 'MEDIUMTEXT'},
-        ],
-        INDEXES => [
-            profile_search_user_id_idx => [qw(user_id)],
-        ],
-    },
-
-    profiles_activity => {
-        FIELDS => [
-            id            => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, 
-                              PRIMARYKEY => 1}, 
-            userid        => {TYPE => 'INT3', NOTNULL => 1,
-                              REFERENCES => {TABLE  => 'profiles', 
-                                             COLUMN => 'userid',
-                                             DELETE => 'CASCADE'}},
-            who           => {TYPE => 'INT3', NOTNULL => 1,
-                              REFERENCES => {TABLE  => 'profiles',
-                                             COLUMN => 'userid'}},
-            profiles_when => {TYPE => 'DATETIME', NOTNULL => 1},
-            fieldid       => {TYPE => 'INT3', NOTNULL => 1,
-                              REFERENCES => {TABLE  => 'fielddefs',
-                                             COLUMN => 'id'}},
-            oldvalue      => {TYPE => 'TINYTEXT'},
-            newvalue      => {TYPE => 'TINYTEXT'},
-        ],
-        INDEXES => [
-            profiles_activity_userid_idx  => ['userid'],
-            profiles_activity_profiles_when_idx => ['profiles_when'],
-            profiles_activity_fieldid_idx => ['fieldid'],
-        ],
-    },
-
-    email_setting => {
-        FIELDS => [
-            user_id      => {TYPE => 'INT3', NOTNULL => 1,
-                             REFERENCES => {TABLE  => 'profiles',
-                                            COLUMN => 'userid',
-                                            DELETE => 'CASCADE'}},
-            relationship => {TYPE => 'INT1', NOTNULL => 1},
-            event        => {TYPE => 'INT1', NOTNULL => 1},
-        ],
-        INDEXES => [
-            email_setting_user_id_idx  =>
-                                    {FIELDS => [qw(user_id relationship event)],
-                                     TYPE => 'UNIQUE'},
-        ],
-    },
-
-    email_bug_ignore => {
-        FIELDS => [
-            user_id => {TYPE => 'INT3', NOTNULL => 1,
-                        REFERENCES => {TABLE  => 'profiles',
-                                       COLUMN => 'userid',
-                                       DELETE => 'CASCADE'}},
-            bug_id  => {TYPE => 'INT3', NOTNULL => 1,
-                        REFERENCES => {TABLE  => 'bugs',
-                                       COLUMN => 'bug_id',
-                                       DELETE => 'CASCADE'}},
-        ],
-        INDEXES => [
-            email_bug_ignore_user_id_idx => {FIELDS => [qw(user_id bug_id)],
-                                             TYPE   => 'UNIQUE'},
-        ],
-    },
-
-    watch => {
-        FIELDS => [
-            watcher => {TYPE => 'INT3', NOTNULL => 1,
-                        REFERENCES => {TABLE  => 'profiles',
-                                       COLUMN => 'userid',
-                                       DELETE => 'CASCADE'}},
-            watched => {TYPE => 'INT3', NOTNULL => 1,
-                        REFERENCES => {TABLE  => 'profiles',
-                                       COLUMN => 'userid',
-                                       DELETE => 'CASCADE'}},
-        ],
-        INDEXES => [
-            watch_watcher_idx => {FIELDS => [qw(watcher watched)],
-                                  TYPE => 'UNIQUE'},
-            watch_watched_idx => ['watched'],
-        ],
-    },
-
-    namedqueries => {
-        FIELDS => [
-            id           => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1,
-                             PRIMARYKEY => 1},
-            userid       => {TYPE => 'INT3', NOTNULL => 1,
-                             REFERENCES => {TABLE  => 'profiles',
-                                            COLUMN => 'userid',
-                                            DELETE => 'CASCADE'}},
-            name         => {TYPE => 'varchar(64)', NOTNULL => 1},
-            query        => {TYPE => 'LONGTEXT', NOTNULL => 1},
-        ],
-        INDEXES => [
-            namedqueries_userid_idx => {FIELDS => [qw(userid name)],
-                                        TYPE => 'UNIQUE'},
-        ],
-    },
-
-    namedqueries_link_in_footer => {
-        FIELDS => [
-            namedquery_id => {TYPE => 'INT3', NOTNULL => 1,
-                              REFERENCES => {TABLE  => 'namedqueries',
-                                             COLUMN => 'id',
-                                             DELETE => 'CASCADE'}},
-            user_id       => {TYPE => 'INT3', NOTNULL => 1,
-                              REFERENCES => {TABLE  => 'profiles',
-                                             COLUMN => 'userid',
-                                             DELETE => 'CASCADE'}},
-        ],
-        INDEXES => [
-            namedqueries_link_in_footer_id_idx => {FIELDS => [qw(namedquery_id user_id)],
-                                                   TYPE => 'UNIQUE'},
-            namedqueries_link_in_footer_userid_idx => ['user_id'],
-        ],
-    },
-
-    tag => {
-        FIELDS => [
-            id   => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1},
-            name => {TYPE => 'varchar(64)', NOTNULL => 1},
-            user_id  => {TYPE => 'INT3', NOTNULL => 1,
-                         REFERENCES => {TABLE  => 'profiles',
-                                        COLUMN => 'userid',
-                                        DELETE => 'CASCADE'}},
-        ],
-        INDEXES => [
-            tag_user_id_idx => {FIELDS => [qw(user_id name)], TYPE => 'UNIQUE'},
-        ],
-    },
-
-    bug_tag => {
-        FIELDS => [
-            bug_id => {TYPE => 'INT3', NOTNULL => 1,
-                       REFERENCES => {TABLE  => 'bugs',
-                                      COLUMN => 'bug_id',
-                                      DELETE => 'CASCADE'}},
-            tag_id => {TYPE => 'INT3', NOTNULL => 1,
-                       REFERENCES => {TABLE  => 'tag',
-                                      COLUMN => 'id',
-                                      DELETE => 'CASCADE'}},
-        ],
-        INDEXES => [
-            bug_tag_bug_id_idx => {FIELDS => [qw(bug_id tag_id)], TYPE => 'UNIQUE'},
-        ],
-    },
-
-    reports => {
-        FIELDS => [
-            id      => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1,
-                        PRIMARYKEY => 1},
-            user_id => {TYPE => 'INT3', NOTNULL => 1,
-                        REFERENCES => {TABLE  => 'profiles',
-                                       COLUMN => 'userid',
-                                       DELETE => 'CASCADE'}},
-            name    => {TYPE => 'varchar(64)', NOTNULL => 1},
-            query   => {TYPE => 'LONGTEXT', NOTNULL => 1},
-        ],
-        INDEXES => [
-            reports_user_id_idx => {FIELDS => [qw(user_id name)],
-                                   TYPE => 'UNIQUE'},
-        ],
-    },
-
-    component_cc => {
-
-        FIELDS => [
-            user_id      => {TYPE => 'INT3', NOTNULL => 1,
-                             REFERENCES => {TABLE  => 'profiles',
-                                            COLUMN => 'userid',
-                                            DELETE => 'CASCADE'}},
-            component_id => {TYPE => 'INT3', NOTNULL => 1,
-                             REFERENCES => {TABLE  => 'components',
-                                            COLUMN => 'id',
-                                            DELETE => 'CASCADE'}},
-        ],
-        INDEXES => [
-            component_cc_user_id_idx => {FIELDS => [qw(component_id user_id)],
-                                         TYPE => 'UNIQUE'},
-        ],
-    },
-
-    # Authentication
-    # --------------
-
-    logincookies => {
-        FIELDS => [
-            cookie   => {TYPE => 'varchar(22)', NOTNULL => 1,
-                         PRIMARYKEY => 1},
-            userid   => {TYPE => 'INT3', NOTNULL => 1,
-                         REFERENCES => {TABLE  => 'profiles',
-                                        COLUMN => 'userid',
-                                        DELETE => 'CASCADE'}},
-            ipaddr   => {TYPE => 'varchar(40)'},
-            lastused => {TYPE => 'DATETIME', NOTNULL => 1},
-        ],
-        INDEXES => [
-            logincookies_lastused_idx => ['lastused'],
-        ],
-    },
-
-    login_failure => {
-        FIELDS => [
-            user_id    => {TYPE => 'INT3', NOTNULL => 1,
-                           REFERENCES => {TABLE  => 'profiles',
-                                          COLUMN => 'userid',
-                                          DELETE => 'CASCADE'}},
-            login_time => {TYPE => 'DATETIME', NOTNULL => 1},
-            ip_addr    => {TYPE => 'varchar(40)', NOTNULL => 1},
-        ],
-        INDEXES => [
-            # We do lookups by every item in the table simultaneously, but 
-            # having an index with all three items would be the same size as
-            # the table. So instead we have an index on just the smallest item, 
-            # to speed lookups.
-            login_failure_user_id_idx => ['user_id'],
-        ],
-    },
-
-
-    # "tokens" stores the tokens users receive when a password or email
-    #     change is requested.  Tokens provide an extra measure of security
-    #     for these changes.
-    tokens => {
-        FIELDS => [
-            userid    => {TYPE => 'INT3', REFERENCES => {TABLE  => 'profiles',
-                                                         COLUMN => 'userid',
-                                                         DELETE => 'CASCADE'}},
-            issuedate => {TYPE => 'DATETIME', NOTNULL => 1} ,
-            token     => {TYPE => 'varchar(22)', NOTNULL => 1,
-                          PRIMARYKEY => 1},
-            tokentype => {TYPE => 'varchar(16)', NOTNULL => 1} ,
-            eventdata => {TYPE => 'TINYTEXT'},
-        ],
-        INDEXES => [
-            tokens_userid_idx => ['userid'],
-        ],
-    },
-
-    # GROUPS
-    # ------
-
-    groups => {
-        FIELDS => [
-            id           => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1,
-                             PRIMARYKEY => 1},
-            name         => {TYPE => 'varchar(255)', NOTNULL => 1},
-            description  => {TYPE => 'MEDIUMTEXT', NOTNULL => 1},
-            isbuggroup   => {TYPE => 'BOOLEAN', NOTNULL => 1},
-            userregexp   => {TYPE => 'TINYTEXT', NOTNULL => 1,
-                             DEFAULT => "''"},
-            isactive     => {TYPE => 'BOOLEAN', NOTNULL => 1,
-                             DEFAULT => 'TRUE'},
-            icon_url     => {TYPE => 'TINYTEXT'},
-        ],
-        INDEXES => [
-            groups_name_idx => {FIELDS => ['name'], TYPE => 'UNIQUE'},
-        ],
-    },
-
-    group_control_map => {
-        FIELDS => [
-            group_id      => {TYPE => 'INT3', NOTNULL => 1,
-                              REFERENCES => {TABLE  => 'groups',
-                                             COLUMN => 'id',
-                                             DELETE => 'CASCADE'}},
-            product_id    => {TYPE => 'INT2', NOTNULL => 1,
-                              REFERENCES => {TABLE  =>  'products',
-                                             COLUMN =>  'id',
-                                             DELETE =>  'CASCADE'}},
-            entry         => {TYPE => 'BOOLEAN', NOTNULL => 1,
-                              DEFAULT => 'FALSE'},
-            membercontrol => {TYPE => 'INT1', NOTNULL => 1,
-                              DEFAULT => CONTROLMAPNA},
-            othercontrol  => {TYPE => 'INT1', NOTNULL => 1,
-                              DEFAULT => CONTROLMAPNA},
-            canedit       => {TYPE => 'BOOLEAN', NOTNULL => 1,
-                              DEFAULT => 'FALSE'},
-            editcomponents => {TYPE => 'BOOLEAN', NOTNULL => 1,
-                               DEFAULT => 'FALSE'},
-            editbugs      => {TYPE => 'BOOLEAN', NOTNULL => 1,
-                              DEFAULT => 'FALSE'},
-            canconfirm    => {TYPE => 'BOOLEAN', NOTNULL => 1,
-                              DEFAULT => 'FALSE'},
-        ],
-        INDEXES => [
-            group_control_map_product_id_idx =>
-            {FIELDS => [qw(product_id group_id)], TYPE => 'UNIQUE'},
-            group_control_map_group_id_idx    => ['group_id'],
-        ],
-    },
-
-    # "user_group_map" determines the groups that a user belongs to
-    # directly or due to regexp and which groups can be blessed by a user.
-    #
-    # grant_type:
-    # if GRANT_DIRECT - record was explicitly granted
-    # if GRANT_DERIVED - record was derived from expanding a group hierarchy
-    # if GRANT_REGEXP - record was created by evaluating a regexp
-    user_group_map => {
-        FIELDS => [
-            user_id    => {TYPE => 'INT3', NOTNULL => 1,
-                           REFERENCES => {TABLE  => 'profiles',
-                                          COLUMN => 'userid',
-                                          DELETE => 'CASCADE'}},
-            group_id   => {TYPE => 'INT3', NOTNULL => 1,
-                           REFERENCES => {TABLE  => 'groups',
-                                          COLUMN => 'id',
-                                          DELETE => 'CASCADE'}},
-            isbless    => {TYPE => 'BOOLEAN', NOTNULL => 1,
-                           DEFAULT => 'FALSE'},
-            grant_type => {TYPE => 'INT1', NOTNULL => 1,
-                           DEFAULT => GRANT_DIRECT},
-        ],
-        INDEXES => [
-            user_group_map_user_id_idx =>
-                {FIELDS => [qw(user_id group_id grant_type isbless)],
-                 TYPE => 'UNIQUE'},
-        ],
-    },
-
-    # This table determines which groups are made a member of another
-    # group, given the ability to bless another group, or given
-    # visibility to another groups existence and membership
-    # grant_type:
-    # if GROUP_MEMBERSHIP - member groups are made members of grantor
-    # if GROUP_BLESS - member groups may grant membership in grantor
-    # if GROUP_VISIBLE - member groups may see grantor group
-    group_group_map => {
-        FIELDS => [
-            member_id  => {TYPE => 'INT3', NOTNULL => 1,
-                           REFERENCES => {TABLE  => 'groups',
-                                          COLUMN => 'id',
-                                          DELETE => 'CASCADE'}},
-            grantor_id => {TYPE => 'INT3', NOTNULL => 1,
-                           REFERENCES => {TABLE  => 'groups',
-                                          COLUMN => 'id',
-                                          DELETE => 'CASCADE'}},
-            grant_type => {TYPE => 'INT1', NOTNULL => 1,
-                           DEFAULT => GROUP_MEMBERSHIP},
-        ],
-        INDEXES => [
-            group_group_map_member_id_idx =>
-                {FIELDS => [qw(member_id grantor_id grant_type)],
-                 TYPE => 'UNIQUE'},
-        ],
-    },
-
-    # This table determines which groups a user must be a member of
-    # in order to see a bug.
-    bug_group_map => {
-        FIELDS => [
-            bug_id   => {TYPE => 'INT3', NOTNULL => 1,
-                         REFERENCES => {TABLE  => 'bugs',
-                                        COLUMN => 'bug_id',
-                                        DELETE => 'CASCADE'}},
-            group_id => {TYPE => 'INT3', NOTNULL => 1,
-                         REFERENCES => {TABLE  => 'groups',
-                                        COLUMN => 'id',
-                                        DELETE => 'CASCADE'}},
-        ],
-        INDEXES => [
-            bug_group_map_bug_id_idx   =>
-                {FIELDS => [qw(bug_id group_id)], TYPE => 'UNIQUE'},
-            bug_group_map_group_id_idx => ['group_id'],
-        ],
-    },
-
-    # This table determines which groups a user must be a member of
-    # in order to see a named query somebody else shares.
-    namedquery_group_map => {
-        FIELDS => [
-            namedquery_id => {TYPE => 'INT3', NOTNULL => 1,
-                              REFERENCES => {TABLE  => 'namedqueries',
-                                             COLUMN => 'id',
-                                             DELETE => 'CASCADE'}},
-            group_id      => {TYPE => 'INT3', NOTNULL => 1,
-                              REFERENCES => {TABLE  => 'groups',
-                                             COLUMN => 'id',
-                                             DELETE => 'CASCADE'}},
-        ],
-        INDEXES => [
-            namedquery_group_map_namedquery_id_idx   =>
-                {FIELDS => [qw(namedquery_id)], TYPE => 'UNIQUE'},
-            namedquery_group_map_group_id_idx => ['group_id'],
-        ],
-    },
-
-    category_group_map => {
-        FIELDS => [
-            category_id => {TYPE => 'INT2', NOTNULL => 1,
-                            REFERENCES => {TABLE  => 'series_categories',
-                                           COLUMN =>  'id',
-                                           DELETE => 'CASCADE'}},
-            group_id    => {TYPE => 'INT3', NOTNULL => 1,
-                            REFERENCES => {TABLE  => 'groups',
-                                           COLUMN => 'id',
-                                           DELETE => 'CASCADE'}},
-        ],
-        INDEXES => [
-            category_group_map_category_id_idx =>
-                {FIELDS => [qw(category_id group_id)], TYPE => 'UNIQUE'},
-        ],
-    },
-
-
-    # PRODUCTS
-    # --------
-
-    classifications => {
-        FIELDS => [
-            id          => {TYPE => 'SMALLSERIAL', NOTNULL => 1,
-                            PRIMARYKEY => 1},
-            name        => {TYPE => 'varchar(64)', NOTNULL => 1},
-            description => {TYPE => 'MEDIUMTEXT'},
-            sortkey     => {TYPE => 'INT2', NOTNULL => 1, DEFAULT => '0'},
-        ],
-        INDEXES => [
-            classifications_name_idx => {FIELDS => ['name'],
-                                           TYPE => 'UNIQUE'},
-        ],
-    },
-
-    products => {
-        FIELDS => [
-            id                => {TYPE => 'SMALLSERIAL', NOTNULL => 1,
-                                  PRIMARYKEY => 1},
-            name              => {TYPE => 'varchar(64)', NOTNULL => 1},
-            classification_id => {TYPE => 'INT2', NOTNULL => 1,
-                                  DEFAULT => '1',
-                                  REFERENCES => {TABLE  => 'classifications',
-                                                 COLUMN => 'id',
-                                                 DELETE => 'CASCADE'}},
-            description       => {TYPE => 'MEDIUMTEXT', NOTNULL => 1},
-            isactive          => {TYPE => 'BOOLEAN', NOTNULL => 1,
-                                  DEFAULT => 1},
-            defaultmilestone  => {TYPE => 'varchar(64)',
-                                  NOTNULL => 1, DEFAULT => "'---'"},
-            allows_unconfirmed => {TYPE => 'BOOLEAN', NOTNULL => 1,
-                                   DEFAULT => 'TRUE'},
-        ],
-        INDEXES => [
-            products_name_idx   => {FIELDS => ['name'],
-                                    TYPE => 'UNIQUE'},
-        ],
-    },
-
-    components => {
-        FIELDS => [
-            id               => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1,
-                                 PRIMARYKEY => 1},
-            name             => {TYPE => 'varchar(64)', NOTNULL => 1},
-            product_id       => {TYPE => 'INT2', NOTNULL => 1,
-                                 REFERENCES => {TABLE  => 'products',
-                                                COLUMN => 'id',
-                                                DELETE => 'CASCADE'}},
-            initialowner     => {TYPE => 'INT3', NOTNULL => 1,
-                                 REFERENCES => {TABLE  => 'profiles',
-                                                COLUMN => 'userid'}},
-            initialqacontact => {TYPE => 'INT3',
-                                 REFERENCES => {TABLE  => 'profiles',
-                                                COLUMN => 'userid',
-                                                DELETE => 'SET NULL'}},
-            description      => {TYPE => 'MEDIUMTEXT', NOTNULL => 1},
-            isactive         => {TYPE => 'BOOLEAN', NOTNULL => 1, 
-                                 DEFAULT => 'TRUE'},
-        ],
-        INDEXES => [
-            components_product_id_idx => {FIELDS => [qw(product_id name)],
-                                          TYPE => 'UNIQUE'},
-            components_name_idx   => ['name'],
-        ],
-    },
-
-    # CHARTS
-    # ------
-
-    series => {
-        FIELDS => [
-            series_id   => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1,
-                            PRIMARYKEY => 1},
-            creator     => {TYPE => 'INT3',
-                            REFERENCES => {TABLE  => 'profiles',
-                                           COLUMN => 'userid',
-                                           DELETE => 'CASCADE'}},
-            category    => {TYPE => 'INT2', NOTNULL => 1,
-                            REFERENCES => {TABLE  => 'series_categories',
-                                           COLUMN => 'id',
-                                           DELETE => 'CASCADE'}},
-            subcategory => {TYPE => 'INT2', NOTNULL => 1,
-                            REFERENCES => {TABLE  => 'series_categories',
-                                           COLUMN => 'id',
-                                           DELETE => 'CASCADE'}},
-            name        => {TYPE => 'varchar(64)', NOTNULL => 1},
-            frequency   => {TYPE => 'INT2', NOTNULL => 1},
-            query       => {TYPE => 'MEDIUMTEXT', NOTNULL => 1},
-            is_public   => {TYPE => 'BOOLEAN', NOTNULL => 1,
-                            DEFAULT => 'FALSE'},
-        ],
-        INDEXES => [
-            series_creator_idx  => ['creator'],
-            series_category_idx => {FIELDS => [qw(category subcategory name)],
-                                    TYPE => 'UNIQUE'},
-        ],
-    },
-
-    series_data => {
-        FIELDS => [
-            series_id    => {TYPE => 'INT3', NOTNULL => 1,
-                             REFERENCES => {TABLE  => 'series',
-                                            COLUMN => 'series_id',
-                                            DELETE => 'CASCADE'}},
-            series_date  => {TYPE => 'DATETIME', NOTNULL => 1},
-            series_value => {TYPE => 'INT3', NOTNULL => 1},
-        ],
-        INDEXES => [
-            series_data_series_id_idx =>
-                {FIELDS => [qw(series_id series_date)],
-                 TYPE => 'UNIQUE'},
-        ],
-    },
-
-    series_categories => {
-        FIELDS => [
-            id   => {TYPE => 'SMALLSERIAL', NOTNULL => 1,
-                     PRIMARYKEY => 1},
-            name => {TYPE => 'varchar(64)', NOTNULL => 1},
-        ],
-        INDEXES => [
-            series_categories_name_idx => {FIELDS => ['name'],
-                                           TYPE => 'UNIQUE'},
-        ],
-    },
-
-    # WHINE SYSTEM
-    # ------------
-
-    whine_queries => {
-        FIELDS => [
-            id            => {TYPE => 'MEDIUMSERIAL', PRIMARYKEY => 1,
-                              NOTNULL => 1},
-            eventid       => {TYPE => 'INT3', NOTNULL => 1,
-                              REFERENCES => {TABLE => 'whine_events',
-                                             COLUMN => 'id',
-                                             DELETE => 'CASCADE'}},
-            query_name    => {TYPE => 'varchar(64)', NOTNULL => 1,
-                              DEFAULT => "''"},
-            sortkey       => {TYPE => 'INT2', NOTNULL => 1,
-                              DEFAULT => '0'},
-            onemailperbug => {TYPE => 'BOOLEAN', NOTNULL => 1,
-                              DEFAULT => 'FALSE'},
-            title         => {TYPE => 'varchar(128)', NOTNULL => 1,
-                              DEFAULT => "''"},
-        ],
-        INDEXES => [
-            whine_queries_eventid_idx => ['eventid'],
-        ],
-    },
-
-    whine_schedules => {
-        FIELDS => [
-            id          => {TYPE => 'MEDIUMSERIAL', PRIMARYKEY => 1,
-                            NOTNULL => 1},
-            eventid     => {TYPE => 'INT3', NOTNULL => 1,
-                            REFERENCES => {TABLE  => 'whine_events',
-                                           COLUMN => 'id',
-                                           DELETE => 'CASCADE'}},
-            run_day     => {TYPE => 'varchar(32)'},
-            run_time    => {TYPE => 'varchar(32)'},
-            run_next    => {TYPE => 'DATETIME'},
-            mailto      => {TYPE => 'INT3', NOTNULL => 1},
-            mailto_type => {TYPE => 'INT2', NOTNULL => 1, DEFAULT => '0'},
-        ],
-        INDEXES => [
-            whine_schedules_run_next_idx => ['run_next'],
-            whine_schedules_eventid_idx  => ['eventid'],
-        ],
-    },
-
-    whine_events => {
-        FIELDS => [
-            id           => {TYPE => 'MEDIUMSERIAL', PRIMARYKEY => 1,
-                             NOTNULL => 1},
-            owner_userid => {TYPE => 'INT3', NOTNULL => 1,
-                             REFERENCES => {TABLE  => 'profiles', 
-                                            COLUMN => 'userid',
-                                            DELETE => 'CASCADE'}},
-            subject      => {TYPE => 'varchar(128)'},
-            body         => {TYPE => 'MEDIUMTEXT'},
-            mailifnobugs => {TYPE => 'BOOLEAN', NOTNULL => 1,
-                             DEFAULT => 'FALSE'},
-        ],
-    },
-
-    # QUIPS
-    # -----
-
-    quips => {
-        FIELDS => [
-            quipid   => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1,
-                         PRIMARYKEY => 1},
-            userid   => {TYPE => 'INT3',
-                         REFERENCES => {TABLE  => 'profiles', 
-                                        COLUMN => 'userid',
-                                        DELETE => 'SET NULL'}},
-            quip     => {TYPE => 'varchar(512)', NOTNULL => 1},
-            approved => {TYPE => 'BOOLEAN', NOTNULL => 1,
-                         DEFAULT => 'TRUE'},
-        ],
-    },
-
-    # SETTINGS
-    # --------
-    # setting          - each global setting will have exactly one entry
-    #                    in this table.
-    # setting_value    - stores the list of acceptable values for each
-    #                    setting, and a sort index that controls the order
-    #                    in which the values are displayed.
-    # profile_setting  - If a user has chosen to use a value other than the
-    #                    global default for a given setting, it will be
-    #                    stored in this table. Note: even if a setting is
-    #                    later changed so is_enabled = false, the stored
-    #                    value will remain in case it is ever enabled again.
-    #
-    setting => {
-        FIELDS => [
-            name          => {TYPE => 'varchar(32)', NOTNULL => 1,
-                              PRIMARYKEY => 1}, 
-            default_value => {TYPE => 'varchar(32)', NOTNULL => 1},
-            is_enabled    => {TYPE => 'BOOLEAN', NOTNULL => 1,
-                              DEFAULT => 'TRUE'},
-            subclass      => {TYPE => 'varchar(32)'},
-        ],
-    },
-
-    setting_value => {
-        FIELDS => [
-            name        => {TYPE => 'varchar(32)', NOTNULL => 1,
-                            REFERENCES => {TABLE  => 'setting', 
-                                           COLUMN => 'name',
-                                           DELETE => 'CASCADE'}},
-            value       => {TYPE => 'varchar(32)', NOTNULL => 1},
-            sortindex   => {TYPE => 'INT2', NOTNULL => 1},
-        ],
-        INDEXES => [
-            setting_value_nv_unique_idx  => {FIELDS => [qw(name value)],
-                                             TYPE => 'UNIQUE'},
-            setting_value_ns_unique_idx  => {FIELDS => [qw(name sortindex)],
-                                             TYPE => 'UNIQUE'},
-        ],
-     },
-
-    profile_setting => {
-        FIELDS => [
-            user_id       => {TYPE => 'INT3', NOTNULL => 1,
-                              REFERENCES => {TABLE  => 'profiles',
-                                             COLUMN => 'userid',
-                                             DELETE => 'CASCADE'}},
-            setting_name  => {TYPE => 'varchar(32)', NOTNULL => 1,
-                              REFERENCES => {TABLE  => 'setting',
-                                             COLUMN => 'name',
-                                             DELETE => 'CASCADE'}},
-            setting_value => {TYPE => 'varchar(32)', NOTNULL => 1},
-        ],
-        INDEXES => [
-            profile_setting_value_unique_idx  => {FIELDS => [qw(user_id setting_name)],
-                                                  TYPE => 'UNIQUE'},
-        ],
-     },
-
-    # BUGMAIL
-    # -------
-
-    mail_staging => {
-        FIELDS => [
-            id      => {TYPE => 'INTSERIAL', PRIMARYKEY => 1, NOTNULL => 1},
-            message => {TYPE => 'LONGBLOB', NOTNULL => 1},
-        ],
-    },
-
-    email_rates => {
-        FIELDS => [
-            id         => {TYPE => 'INTSERIAL', NOTNULL => 1,
-                           PRIMARYKEY => 1},
-            recipient  => {TYPE => 'varchar(255)', NOTNULL => 1},
-            message_ts => {TYPE => 'DATETIME', NOTNULL => 1},
-        ],
-        INDEXES => [
-            email_rates_idx => [qw(recipient message_ts)],
-        ],
-    },
-
-    # THESCHWARTZ TABLES
-    # ------------------
-    # Note: In the standard TheSchwartz schema, most integers are unsigned,
-    # but we didn't implement unsigned ints for Bugzilla schemas, so we
-    # just create signed ints, which should be fine.
-
-    ts_funcmap => {
-        FIELDS => [
-            funcid   => {TYPE => 'INTSERIAL', PRIMARYKEY => 1, NOTNULL => 1},
-            funcname => {TYPE => 'varchar(255)', NOTNULL => 1},
-        ],
-        INDEXES => [
-            ts_funcmap_funcname_idx => {FIELDS => ['funcname'], 
-                                          TYPE => 'UNIQUE'},
-        ],
-    },
-
-    ts_job => {
-        FIELDS => [
-            # In a standard TheSchwartz schema, this is a BIGINT, but we
-            # don't have those and I didn't want to add them just for this.
-            jobid         => {TYPE => 'INTSERIAL', PRIMARYKEY => 1, 
-                              NOTNULL => 1},
-            funcid        => {TYPE => 'INT4', NOTNULL => 1},
-            # In standard TheSchwartz, this is a MEDIUMBLOB.
-            arg           => {TYPE => 'LONGBLOB'},
-            uniqkey       => {TYPE => 'varchar(255)'},
-            insert_time   => {TYPE => 'INT4'},
-            run_after     => {TYPE => 'INT4', NOTNULL => 1},
-            grabbed_until => {TYPE => 'INT4', NOTNULL => 1},
-            priority      => {TYPE => 'INT2'},
-            coalesce      => {TYPE => 'varchar(255)'},
-        ],
-        INDEXES => [
-            ts_job_funcid_idx => {FIELDS => [qw(funcid uniqkey)],
-                                  TYPE   => 'UNIQUE'},
-            # In a standard TheSchewartz schema, these both go in the other
-            # direction, but there's no reason to have three indexes that
-            # all start with the same column, and our naming scheme doesn't
-            # allow it anyhow.
-            ts_job_run_after_idx => [qw(run_after funcid)],
-            ts_job_coalesce_idx  => [qw(coalesce funcid)],
-        ],
-    },
-
-    ts_note => {
-        FIELDS => [
-            # This is a BIGINT in standard TheSchwartz schemas.
-            jobid   => {TYPE => 'INT4', NOTNULL => 1},
-            notekey => {TYPE => 'varchar(255)'},
-            value   => {TYPE => 'LONGBLOB'},
-        ],
-        INDEXES => [
-            ts_note_jobid_idx => {FIELDS => [qw(jobid notekey)], 
-                                    TYPE => 'UNIQUE'},
-        ],
-    },
-
-    ts_error => {
-        FIELDS => [
-            error_time => {TYPE => 'INT4', NOTNULL => 1},
-            jobid      => {TYPE => 'INT4', NOTNULL => 1},
-            message    => {TYPE => 'varchar(255)', NOTNULL => 1},
-            funcid     => {TYPE => 'INT4', NOTNULL => 1, DEFAULT => 0},
-        ],
-        INDEXES => [
-            ts_error_funcid_idx     => [qw(funcid error_time)],
-            ts_error_error_time_idx => ['error_time'],
-            ts_error_jobid_idx      => ['jobid'],
-        ],
-    },
-
-    ts_exitstatus => {
-        FIELDS => [
-            jobid           => {TYPE => 'INTSERIAL', PRIMARYKEY => 1,
-                                NOTNULL => 1},
-            funcid          => {TYPE => 'INT4', NOTNULL => 1, DEFAULT => 0},
-            status          => {TYPE => 'INT2'},
-            completion_time => {TYPE => 'INT4'},
-            delete_after    => {TYPE => 'INT4'},
-        ],
-        INDEXES => [
-            ts_exitstatus_funcid_idx       => ['funcid'],
-            ts_exitstatus_delete_after_idx => ['delete_after'],
-        ],
-    },
-
-    # SCHEMA STORAGE
-    # --------------
-
-    bz_schema => {
-        FIELDS => [
-            schema_data => {TYPE => 'LONGBLOB', NOTNULL => 1},
-            version     => {TYPE => 'decimal(3,2)', NOTNULL => 1},
-        ],
-        INDEXES => [
-            bz_schema_version_idx => {FIELDS => ['version'], TYPE => 'UNIQUE'},
-        ],
-    },
-
-    bug_user_last_visit => {
-        FIELDS => [
-            id            => {TYPE => 'INTSERIAL', NOTNULL => 1,
-                              PRIMARYKEY => 1},
-            user_id       => {TYPE => 'INT3', NOTNULL => 1,
-                              REFERENCES => {TABLE  => 'profiles',
-                                             COLUMN => 'userid',
-                                             DELETE => 'CASCADE'}},
-            bug_id        => {TYPE => 'INT3', NOTNULL => 1,
-                              REFERENCES => {TABLE  => 'bugs',
-                                             COLUMN => 'bug_id',
-                                             DELETE => 'CASCADE'}},
-            last_visit_ts => {TYPE => 'DATETIME', NOTNULL => 1},
-        ],
-        INDEXES => [
-            bug_user_last_visit_idx => {FIELDS => ['user_id', 'bug_id'],
-                                        TYPE => 'UNIQUE'},
-            bug_user_last_visit_last_visit_ts_idx => ['last_visit_ts'],
-        ],
-    },
-
-    user_api_keys => {
-        FIELDS => [
-            id            => {TYPE => 'INTSERIAL', NOTNULL => 1,
-                              PRIMARYKEY => 1},
-            user_id       => {TYPE => 'INT3', NOTNULL => 1,
-                              REFERENCES => {TABLE  => 'profiles',
-                                             COLUMN => 'userid',
-                                             DELETE => 'CASCADE'}},
-            api_key       => {TYPE => 'varchar(40)', NOTNULL => 1},
-            description   => {TYPE => 'varchar(255)'},
-            revoked       => {TYPE => 'BOOLEAN', NOTNULL => 1,
-                              DEFAULT => 'FALSE'},
-            last_used     => {TYPE => 'DATETIME'},
-            app_id        => {TYPE => 'varchar(64)'},
-        ],
-        INDEXES => [
-            user_api_keys_api_key_idx => {FIELDS => ['api_key'], TYPE => 'UNIQUE'},
-            user_api_keys_user_id_idx => ['user_id'],
-            user_api_keys_user_id_app_id_idx  => ['user_id', 'app_id'],
-        ],
-    },
-};
+  bugs_activity => {
+    FIELDS => [
+      id     => {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1},
+      bug_id => {
+        TYPE       => 'INT3',
+        NOTNULL    => 1,
+        REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'}
+      },
+      attach_id => {
+        TYPE => 'INT3',
+        REFERENCES =>
+          {TABLE => 'attachments', COLUMN => 'attach_id', DELETE => 'CASCADE'}
+      },
+      who => {
+        TYPE       => 'INT3',
+        NOTNULL    => 1,
+        REFERENCES => {TABLE => 'profiles', COLUMN => 'userid'}
+      },
+      bug_when => {TYPE => 'DATETIME', NOTNULL => 1},
+      fieldid  => {
+        TYPE       => 'INT3',
+        NOTNULL    => 1,
+        REFERENCES => {TABLE => 'fielddefs', COLUMN => 'id'}
+      },
+      added      => {TYPE => 'varchar(255)'},
+      removed    => {TYPE => 'varchar(255)'},
+      comment_id => {
+        TYPE => 'INT4',
+        REFERENCES =>
+          {TABLE => 'longdescs', COLUMN => 'comment_id', DELETE => 'CASCADE'}
+      },
+    ],
+    INDEXES => [
+      bugs_activity_bug_id_idx   => ['bug_id'],
+      bugs_activity_who_idx      => ['who'],
+      bugs_activity_bug_when_idx => ['bug_when'],
+      bugs_activity_fieldid_idx  => ['fieldid'],
+      bugs_activity_added_idx    => ['added'],
+      bugs_activity_removed_idx  => ['removed'],
+    ],
+  },
 
-# Foreign Keys are added in Bugzilla::DB::bz_add_field_tables
-use constant MULTI_SELECT_VALUE_TABLE => {
+  bugs_aliases => {
+    FIELDS => [
+      alias  => {TYPE => 'varchar(40)', NOTNULL => 1},
+      bug_id => {
+        TYPE       => 'INT3',
+        REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'}
+      },
+    ],
+    INDEXES => [
+      bugs_aliases_bug_id_idx => ['bug_id'],
+      bugs_aliases_alias_idx  => {FIELDS => ['alias'], TYPE => 'UNIQUE'},
+    ],
+  },
+
+  cc => {
+    FIELDS => [
+      bug_id => {
+        TYPE       => 'INT3',
+        NOTNULL    => 1,
+        REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'}
+      },
+      who => {
+        TYPE       => 'INT3',
+        NOTNULL    => 1,
+        REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'}
+      },
+    ],
+    INDEXES => [
+      cc_bug_id_idx => {FIELDS => [qw(bug_id who)], TYPE => 'UNIQUE'},
+      cc_who_idx    => ['who'],
+    ],
+  },
+
+  longdescs => {
+    FIELDS => [
+      comment_id => {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1},
+      bug_id     => {
+        TYPE       => 'INT3',
+        NOTNULL    => 1,
+        REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'}
+      },
+      who => {
+        TYPE       => 'INT3',
+        NOTNULL    => 1,
+        REFERENCES => {TABLE => 'profiles', COLUMN => 'userid'}
+      },
+      bug_when        => {TYPE => 'DATETIME',     NOTNULL => 1},
+      work_time       => {TYPE => 'decimal(7,2)', NOTNULL => 1, DEFAULT => '0'},
+      thetext         => {TYPE => 'LONGTEXT',     NOTNULL => 1},
+      isprivate       => {TYPE => 'BOOLEAN',      NOTNULL => 1, DEFAULT => 'FALSE'},
+      already_wrapped => {TYPE => 'BOOLEAN',      NOTNULL => 1, DEFAULT => 'FALSE'},
+      type            => {TYPE => 'INT2',         NOTNULL => 1, DEFAULT => '0'},
+      extra_data  => {TYPE => 'varchar(255)'},
+      is_markdown => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}
+    ],
+    INDEXES => [
+      longdescs_bug_id_idx   => [qw(bug_id work_time)],
+      longdescs_who_idx      => [qw(who bug_id)],
+      longdescs_bug_when_idx => ['bug_when'],
+    ],
+  },
+
+  longdescs_tags => {
+    FIELDS => [
+      id         => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1},
+      comment_id => {
+        TYPE => 'INT4',
+        REFERENCES =>
+          {TABLE => 'longdescs', COLUMN => 'comment_id', DELETE => 'CASCADE'}
+      },
+      tag => {TYPE => 'varchar(24)', NOTNULL => 1},
+    ],
+    INDEXES =>
+      [longdescs_tags_idx => {FIELDS => ['comment_id', 'tag'], TYPE => 'UNIQUE'},],
+  },
+
+  longdescs_tags_weights => {
+    FIELDS => [
+      id     => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1},
+      tag    => {TYPE => 'varchar(24)',  NOTNULL => 1},
+      weight => {TYPE => 'INT3',         NOTNULL => 1},
+    ],
+    INDEXES =>
+      [longdescs_tags_weights_tag_idx => {FIELDS => ['tag'], TYPE => 'UNIQUE'},],
+  },
+
+  longdescs_tags_activity => {
+    FIELDS => [
+      id     => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1},
+      bug_id => {
+        TYPE       => 'INT3',
+        NOTNULL    => 1,
+        REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'}
+      },
+      comment_id => {
+        TYPE => 'INT4',
+        REFERENCES =>
+          {TABLE => 'longdescs', COLUMN => 'comment_id', DELETE => 'CASCADE'}
+      },
+      who => {
+        TYPE       => 'INT3',
+        NOTNULL    => 1,
+        REFERENCES => {TABLE => 'profiles', COLUMN => 'userid'}
+      },
+      bug_when => {TYPE => 'DATETIME', NOTNULL => 1},
+      added    => {TYPE => 'varchar(24)'},
+      removed  => {TYPE => 'varchar(24)'},
+    ],
+    INDEXES => [longdescs_tags_activity_bug_id_idx => ['bug_id'],],
+  },
+
+  dependencies => {
+    FIELDS => [
+      blocked => {
+        TYPE       => 'INT3',
+        NOTNULL    => 1,
+        REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'}
+      },
+      dependson => {
+        TYPE       => 'INT3',
+        NOTNULL    => 1,
+        REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'}
+      },
+    ],
+    INDEXES => [
+      dependencies_blocked_idx =>
+        {FIELDS => [qw(blocked dependson)], TYPE => 'UNIQUE'},
+      dependencies_dependson_idx => ['dependson'],
+    ],
+  },
+
+  attachments => {
+    FIELDS => [
+      attach_id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1},
+      bug_id    => {
+        TYPE       => 'INT3',
+        NOTNULL    => 1,
+        REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'}
+      },
+      creation_ts       => {TYPE => 'DATETIME',     NOTNULL => 1},
+      modification_time => {TYPE => 'DATETIME',     NOTNULL => 1},
+      description       => {TYPE => 'TINYTEXT',     NOTNULL => 1},
+      mimetype          => {TYPE => 'TINYTEXT',     NOTNULL => 1},
+      ispatch           => {TYPE => 'BOOLEAN',      NOTNULL => 1, DEFAULT => 'FALSE'},
+      filename          => {TYPE => 'varchar(255)', NOTNULL => 1},
+      submitter_id => {
+        TYPE       => 'INT3',
+        NOTNULL    => 1,
+        REFERENCES => {TABLE => 'profiles', COLUMN => 'userid'}
+      },
+      isobsolete => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'},
+      isprivate  => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'},
+    ],
+    INDEXES => [
+      attachments_bug_id_idx            => ['bug_id'],
+      attachments_creation_ts_idx       => ['creation_ts'],
+      attachments_modification_time_idx => ['modification_time'],
+      attachments_submitter_id_idx      => ['submitter_id', 'bug_id'],
+    ],
+  },
+  attach_data => {
+    FIELDS => [
+      id => {
+        TYPE       => 'INT3',
+        NOTNULL    => 1,
+        PRIMARYKEY => 1,
+        REFERENCES =>
+          {TABLE => 'attachments', COLUMN => 'attach_id', DELETE => 'CASCADE'}
+      },
+      thedata => {TYPE => 'LONGBLOB', NOTNULL => 1},
+    ],
+  },
+
+  duplicates => {
+    FIELDS => [
+      dupe_of => {
+        TYPE       => 'INT3',
+        NOTNULL    => 1,
+        REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'}
+      },
+      dupe => {
+        TYPE       => 'INT3',
+        NOTNULL    => 1,
+        PRIMARYKEY => 1,
+        REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'}
+      },
+    ],
+  },
+
+  bug_see_also => {
+    FIELDS => [
+      id     => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1},
+      bug_id => {
+        TYPE       => 'INT3',
+        NOTNULL    => 1,
+        REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'}
+      },
+      value => {TYPE => 'varchar(255)', NOTNULL => 1},
+      class => {TYPE => 'varchar(255)', NOTNULL => 1, DEFAULT => "''"},
+    ],
+    INDEXES => [
+      bug_see_also_bug_id_idx => {FIELDS => [qw(bug_id value)], TYPE => 'UNIQUE'},
+    ],
+  },
+
+  # Auditing
+  # --------
+
+  audit_log => {
+    FIELDS => [
+      user_id => {
+        TYPE       => 'INT3',
+        REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'SET NULL'}
+      },
+      class     => {TYPE => 'varchar(255)', NOTNULL => 1},
+      object_id => {TYPE => 'INT4',         NOTNULL => 1},
+      field     => {TYPE => 'varchar(64)',  NOTNULL => 1},
+      removed   => {TYPE => 'MEDIUMTEXT'},
+      added     => {TYPE => 'MEDIUMTEXT'},
+      at_time   => {TYPE => 'DATETIME',     NOTNULL => 1},
+    ],
+    INDEXES => [audit_log_class_idx => ['class', 'at_time'],],
+  },
+
+  # Keywords
+  # --------
+
+  keyworddefs => {
+    FIELDS => [
+      id          => {TYPE => 'SMALLSERIAL', NOTNULL => 1, PRIMARYKEY => 1},
+      name        => {TYPE => 'varchar(64)', NOTNULL => 1},
+      description => {TYPE => 'MEDIUMTEXT',  NOTNULL => 1},
+      is_active   => {TYPE => 'BOOLEAN',     NOTNULL => 1, DEFAULT => 'TRUE'},
+    ],
+    INDEXES => [keyworddefs_name_idx => {FIELDS => ['name'], TYPE => 'UNIQUE'},],
+  },
+
+  keywords => {
+    FIELDS => [
+      bug_id => {
+        TYPE       => 'INT3',
+        NOTNULL    => 1,
+        REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'}
+      },
+      keywordid => {
+        TYPE       => 'INT2',
+        NOTNULL    => 1,
+        REFERENCES => {TABLE => 'keyworddefs', COLUMN => 'id', DELETE => 'CASCADE'}
+      },
+    ],
+    INDEXES => [
+      keywords_bug_id_idx    => {FIELDS => [qw(bug_id keywordid)], TYPE => 'UNIQUE'},
+      keywords_keywordid_idx => ['keywordid'],
+    ],
+  },
+
+  # Flags
+  # -----
+
+  # "flags" stores one record for each flag on each bug/attachment.
+  flags => {
+    FIELDS => [
+      id      => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1},
+      type_id => {
+        TYPE       => 'INT2',
+        NOTNULL    => 1,
+        REFERENCES => {TABLE => 'flagtypes', COLUMN => 'id', DELETE => 'CASCADE'}
+      },
+      status => {TYPE => 'char(1)', NOTNULL => 1},
+      bug_id => {
+        TYPE       => 'INT3',
+        NOTNULL    => 1,
+        REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'}
+      },
+      attach_id => {
+        TYPE => 'INT3',
+        REFERENCES =>
+          {TABLE => 'attachments', COLUMN => 'attach_id', DELETE => 'CASCADE'}
+      },
+      creation_date     => {TYPE => 'DATETIME', NOTNULL => 1},
+      modification_date => {TYPE => 'DATETIME'},
+      setter_id         => {
+        TYPE       => 'INT3',
+        NOTNULL    => 1,
+        REFERENCES => {TABLE => 'profiles', COLUMN => 'userid'}
+      },
+      requestee_id =>
+        {TYPE => 'INT3', REFERENCES => {TABLE => 'profiles', COLUMN => 'userid'}},
+    ],
+    INDEXES => [
+      flags_bug_id_idx       => [qw(bug_id attach_id)],
+      flags_setter_id_idx    => ['setter_id'],
+      flags_requestee_id_idx => ['requestee_id'],
+      flags_type_id_idx      => ['type_id'],
+    ],
+  },
+
+  # "flagtypes" defines the types of flags that can be set.
+  flagtypes => {
+    FIELDS => [
+      id               => {TYPE => 'SMALLSERIAL', NOTNULL => 1, PRIMARYKEY => 1},
+      name             => {TYPE => 'varchar(50)', NOTNULL => 1},
+      description      => {TYPE => 'MEDIUMTEXT',  NOTNULL => 1},
+      cc_list          => {TYPE => 'varchar(200)'},
+      target_type      => {TYPE => 'char(1)',     NOTNULL => 1, DEFAULT => "'b'"},
+      is_active        => {TYPE => 'BOOLEAN',     NOTNULL => 1, DEFAULT => 'TRUE'},
+      is_requestable   => {TYPE => 'BOOLEAN',     NOTNULL => 1, DEFAULT => 'FALSE'},
+      is_requesteeble  => {TYPE => 'BOOLEAN',     NOTNULL => 1, DEFAULT => 'FALSE'},
+      is_multiplicable => {TYPE => 'BOOLEAN',     NOTNULL => 1, DEFAULT => 'FALSE'},
+      sortkey          => {TYPE => 'INT2',        NOTNULL => 1, DEFAULT => '0'},
+      grant_group_id   => {
+        TYPE       => 'INT3',
+        REFERENCES => {TABLE => 'groups', COLUMN => 'id', DELETE => 'SET NULL'}
+      },
+      request_group_id => {
+        TYPE       => 'INT3',
+        REFERENCES => {TABLE => 'groups', COLUMN => 'id', DELETE => 'SET NULL'}
+      },
+    ],
+  },
+
+  # "flaginclusions" and "flagexclusions" specify the products/components
+  #     a bug/attachment must belong to in order for flags of a given type
+  #     to be set for them.
+  flaginclusions => {
+    FIELDS => [
+      type_id => {
+        TYPE       => 'INT2',
+        NOTNULL    => 1,
+        REFERENCES => {TABLE => 'flagtypes', COLUMN => 'id', DELETE => 'CASCADE'}
+      },
+      product_id => {
+        TYPE       => 'INT2',
+        REFERENCES => {TABLE => 'products', COLUMN => 'id', DELETE => 'CASCADE'}
+      },
+      component_id => {
+        TYPE       => 'INT3',
+        REFERENCES => {TABLE => 'components', COLUMN => 'id', DELETE => 'CASCADE'}
+      },
+    ],
+    INDEXES => [
+      flaginclusions_type_id_idx =>
+        {FIELDS => [qw(type_id product_id component_id)], TYPE => 'UNIQUE'},
+    ],
+  },
+
+  flagexclusions => {
+    FIELDS => [
+      type_id => {
+        TYPE       => 'INT2',
+        NOTNULL    => 1,
+        REFERENCES => {TABLE => 'flagtypes', COLUMN => 'id', DELETE => 'CASCADE'}
+      },
+      product_id => {
+        TYPE       => 'INT2',
+        REFERENCES => {TABLE => 'products', COLUMN => 'id', DELETE => 'CASCADE'}
+      },
+      component_id => {
+        TYPE       => 'INT3',
+        REFERENCES => {TABLE => 'components', COLUMN => 'id', DELETE => 'CASCADE'}
+      },
+    ],
+    INDEXES => [
+      flagexclusions_type_id_idx =>
+        {FIELDS => [qw(type_id product_id component_id)], TYPE => 'UNIQUE'},
+    ],
+  },
+
+  # General Field Information
+  # -------------------------
+
+  fielddefs => {
+    FIELDS => [
+      id     => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1},
+      name   => {TYPE => 'varchar(64)',  NOTNULL => 1},
+      type   => {TYPE => 'INT2',         NOTNULL => 1, DEFAULT => FIELD_TYPE_UNKNOWN},
+      custom => {TYPE => 'BOOLEAN',      NOTNULL => 1, DEFAULT => 'FALSE'},
+      description => {TYPE => 'TINYTEXT',     NOTNULL => 1},
+      long_desc   => {TYPE => 'varchar(255)', NOTNULL => 1, DEFAULT => "''"},
+      mailhead    => {TYPE => 'BOOLEAN',      NOTNULL => 1, DEFAULT => 'FALSE'},
+      sortkey     => {TYPE => 'INT2',         NOTNULL => 1},
+      obsolete    => {TYPE => 'BOOLEAN',      NOTNULL => 1, DEFAULT => 'FALSE'},
+      enter_bug   => {TYPE => 'BOOLEAN',      NOTNULL => 1, DEFAULT => 'FALSE'},
+      buglist     => {TYPE => 'BOOLEAN',      NOTNULL => 1, DEFAULT => 'FALSE'},
+      visibility_field_id =>
+        {TYPE => 'INT3', REFERENCES => {TABLE => 'fielddefs', COLUMN => 'id'}},
+      value_field_id =>
+        {TYPE => 'INT3', REFERENCES => {TABLE => 'fielddefs', COLUMN => 'id'}},
+      reverse_desc => {TYPE => 'TINYTEXT'},
+      is_mandatory => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'},
+      is_numeric   => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'},
+    ],
+    INDEXES => [
+      fielddefs_name_idx           => {FIELDS => ['name'], TYPE => 'UNIQUE'},
+      fielddefs_sortkey_idx        => ['sortkey'],
+      fielddefs_value_field_id_idx => ['value_field_id'],
+      fielddefs_is_mandatory_idx   => ['is_mandatory'],
+    ],
+  },
+
+  # Field Visibility Information
+  # -------------------------
+
+  field_visibility => {
+    FIELDS => [
+      field_id => {
+        TYPE       => 'INT3',
+        REFERENCES => {TABLE => 'fielddefs', COLUMN => 'id', DELETE => 'CASCADE'}
+      },
+      value_id => {TYPE => 'INT2', NOTNULL => 1}
+    ],
+    INDEXES => [
+      field_visibility_field_id_idx =>
+        {FIELDS => [qw(field_id value_id)], TYPE => 'UNIQUE'},
+    ],
+  },
+
+  # Per-product Field Values
+  # ------------------------
+
+  versions => {
+    FIELDS => [
+      id         => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1},
+      value      => {TYPE => 'varchar(64)',  NOTNULL => 1},
+      product_id => {
+        TYPE       => 'INT2',
+        NOTNULL    => 1,
+        REFERENCES => {TABLE => 'products', COLUMN => 'id', DELETE => 'CASCADE'}
+      },
+      isactive => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'},
+    ],
+    INDEXES => [
+      versions_product_id_idx => {FIELDS => [qw(product_id value)], TYPE => 'UNIQUE'},
+    ],
+  },
+
+  milestones => {
+    FIELDS => [
+      id         => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1},
+      product_id => {
+        TYPE       => 'INT2',
+        NOTNULL    => 1,
+        REFERENCES => {TABLE => 'products', COLUMN => 'id', DELETE => 'CASCADE'}
+      },
+      value    => {TYPE => 'varchar(64)', NOTNULL => 1},
+      sortkey  => {TYPE => 'INT2',        NOTNULL => 1, DEFAULT => 0},
+      isactive => {TYPE => 'BOOLEAN',     NOTNULL => 1, DEFAULT => 'TRUE'},
+    ],
+    INDEXES => [
+      milestones_product_id_idx =>
+        {FIELDS => [qw(product_id value)], TYPE => 'UNIQUE'},
+    ],
+  },
+
+  # Global Field Values
+  # -------------------
+
+  bug_status => {
+    FIELDS => [
+      @{dclone(FIELD_TABLE_SCHEMA->{FIELDS})},
+      is_open => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'},
+
+    ],
+    INDEXES => [
+      bug_status_value_idx               => {FIELDS => ['value'], TYPE => 'UNIQUE'},
+      bug_status_sortkey_idx             => ['sortkey',           'value'],
+      bug_status_visibility_value_id_idx => ['visibility_value_id'],
+    ],
+  },
+
+  resolution => {
+    FIELDS  => dclone(FIELD_TABLE_SCHEMA->{FIELDS}),
+    INDEXES => [
+      resolution_value_idx               => {FIELDS => ['value'], TYPE => 'UNIQUE'},
+      resolution_sortkey_idx             => ['sortkey',           'value'],
+      resolution_visibility_value_id_idx => ['visibility_value_id'],
+    ],
+  },
+
+  bug_severity => {
+    FIELDS  => dclone(FIELD_TABLE_SCHEMA->{FIELDS}),
+    INDEXES => [
+      bug_severity_value_idx               => {FIELDS => ['value'], TYPE => 'UNIQUE'},
+      bug_severity_sortkey_idx             => ['sortkey',           'value'],
+      bug_severity_visibility_value_id_idx => ['visibility_value_id'],
+    ],
+  },
+
+  priority => {
+    FIELDS  => dclone(FIELD_TABLE_SCHEMA->{FIELDS}),
+    INDEXES => [
+      priority_value_idx               => {FIELDS => ['value'], TYPE => 'UNIQUE'},
+      priority_sortkey_idx             => ['sortkey',           'value'],
+      priority_visibility_value_id_idx => ['visibility_value_id'],
+    ],
+  },
+
+  rep_platform => {
+    FIELDS  => dclone(FIELD_TABLE_SCHEMA->{FIELDS}),
+    INDEXES => [
+      rep_platform_value_idx               => {FIELDS => ['value'], TYPE => 'UNIQUE'},
+      rep_platform_sortkey_idx             => ['sortkey',           'value'],
+      rep_platform_visibility_value_id_idx => ['visibility_value_id'],
+    ],
+  },
+
+  op_sys => {
+    FIELDS  => dclone(FIELD_TABLE_SCHEMA->{FIELDS}),
+    INDEXES => [
+      op_sys_value_idx               => {FIELDS => ['value'], TYPE => 'UNIQUE'},
+      op_sys_sortkey_idx             => ['sortkey',           'value'],
+      op_sys_visibility_value_id_idx => ['visibility_value_id'],
+    ],
+  },
+
+  status_workflow => {
+    FIELDS => [
+
+      # On bug creation, there is no old value.
+      old_status => {
+        TYPE       => 'INT2',
+        REFERENCES => {TABLE => 'bug_status', COLUMN => 'id', DELETE => 'CASCADE'}
+      },
+      new_status => {
+        TYPE       => 'INT2',
+        NOTNULL    => 1,
+        REFERENCES => {TABLE => 'bug_status', COLUMN => 'id', DELETE => 'CASCADE'}
+      },
+      require_comment => {TYPE => 'INT1', NOTNULL => 1, DEFAULT => 0},
+    ],
+    INDEXES => [
+      status_workflow_idx =>
+        {FIELDS => ['old_status', 'new_status'], TYPE => 'UNIQUE'},
+    ],
+  },
+
+  # USER INFO
+  # ---------
+
+  # General User Information
+  # ------------------------
+
+  profiles => {
+    FIELDS => [
+      userid         => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1},
+      login_name     => {TYPE => 'varchar(255)', NOTNULL => 1},
+      email          => {TYPE => 'varchar(255)', NOTNULL => 1},
+      cryptpassword  => {TYPE => 'varchar(128)'},
+      realname       => {TYPE => 'varchar(255)', NOTNULL => 1, DEFAULT => "''"},
+      disabledtext   => {TYPE => 'MEDIUMTEXT',   NOTNULL => 1, DEFAULT => "''"},
+      disable_mail   => {TYPE => 'BOOLEAN',      NOTNULL => 1, DEFAULT => 'FALSE'},
+      mybugslink     => {TYPE => 'BOOLEAN',      NOTNULL => 1, DEFAULT => 'TRUE'},
+      extern_id      => {TYPE => 'varchar(64)'},
+      is_enabled     => {TYPE => 'BOOLEAN',      NOTNULL => 1, DEFAULT => 'TRUE'},
+      last_seen_date => {TYPE => 'DATETIME'},
+    ],
+    INDEXES => [
+      profiles_login_name_idx => {FIELDS => ['login_name'], TYPE => 'UNIQUE'},
+      profiles_extern_id_idx  => {FIELDS => ['extern_id'],  TYPE => 'UNIQUE'},
+      profiles_email_idx      => {FIELDS => ['email'],      TYPE => 'UNIQUE'}
+    ],
+  },
+
+  profile_search => {
+    FIELDS => [
+      id      => {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1},
+      user_id => {
+        TYPE       => 'INT3',
+        NOTNULL    => 1,
+        REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'}
+      },
+      bug_list   => {TYPE => 'MEDIUMTEXT', NOTNULL => 1},
+      list_order => {TYPE => 'MEDIUMTEXT'},
+    ],
+    INDEXES => [profile_search_user_id_idx => [qw(user_id)],],
+  },
+
+  profiles_activity => {
+    FIELDS => [
+      id     => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1},
+      userid => {
+        TYPE       => 'INT3',
+        NOTNULL    => 1,
+        REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'}
+      },
+      who => {
+        TYPE       => 'INT3',
+        NOTNULL    => 1,
+        REFERENCES => {TABLE => 'profiles', COLUMN => 'userid'}
+      },
+      profiles_when => {TYPE => 'DATETIME', NOTNULL => 1},
+      fieldid       => {
+        TYPE       => 'INT3',
+        NOTNULL    => 1,
+        REFERENCES => {TABLE => 'fielddefs', COLUMN => 'id'}
+      },
+      oldvalue => {TYPE => 'TINYTEXT'},
+      newvalue => {TYPE => 'TINYTEXT'},
+    ],
+    INDEXES => [
+      profiles_activity_userid_idx        => ['userid'],
+      profiles_activity_profiles_when_idx => ['profiles_when'],
+      profiles_activity_fieldid_idx       => ['fieldid'],
+    ],
+  },
+
+  email_setting => {
     FIELDS => [
-        bug_id => {TYPE => 'INT3', NOTNULL => 1},
-        value  => {TYPE => 'varchar(64)', NOTNULL => 1},
+      user_id => {
+        TYPE       => 'INT3',
+        NOTNULL    => 1,
+        REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'}
+      },
+      relationship => {TYPE => 'INT1', NOTNULL => 1},
+      event        => {TYPE => 'INT1', NOTNULL => 1},
     ],
     INDEXES => [
-        bug_id_idx => {FIELDS => [qw( bug_id value)], TYPE => 'UNIQUE'},
+      email_setting_user_id_idx =>
+        {FIELDS => [qw(user_id relationship event)], TYPE => 'UNIQUE'},
+    ],
+  },
+
+  email_bug_ignore => {
+    FIELDS => [
+      user_id => {
+        TYPE       => 'INT3',
+        NOTNULL    => 1,
+        REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'}
+      },
+      bug_id => {
+        TYPE       => 'INT3',
+        NOTNULL    => 1,
+        REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'}
+      },
+    ],
+    INDEXES => [
+      email_bug_ignore_user_id_idx =>
+        {FIELDS => [qw(user_id bug_id)], TYPE => 'UNIQUE'},
+    ],
+  },
+
+  watch => {
+    FIELDS => [
+      watcher => {
+        TYPE       => 'INT3',
+        NOTNULL    => 1,
+        REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'}
+      },
+      watched => {
+        TYPE       => 'INT3',
+        NOTNULL    => 1,
+        REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'}
+      },
+    ],
+    INDEXES => [
+      watch_watcher_idx => {FIELDS => [qw(watcher watched)], TYPE => 'UNIQUE'},
+      watch_watched_idx => ['watched'],
+    ],
+  },
+
+  namedqueries => {
+    FIELDS => [
+      id     => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1},
+      userid => {
+        TYPE       => 'INT3',
+        NOTNULL    => 1,
+        REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'}
+      },
+      name  => {TYPE => 'varchar(64)', NOTNULL => 1},
+      query => {TYPE => 'LONGTEXT',    NOTNULL => 1},
+    ],
+    INDEXES =>
+      [namedqueries_userid_idx => {FIELDS => [qw(userid name)], TYPE => 'UNIQUE'},],
+  },
+
+  namedqueries_link_in_footer => {
+    FIELDS => [
+      namedquery_id => {
+        TYPE       => 'INT3',
+        NOTNULL    => 1,
+        REFERENCES => {TABLE => 'namedqueries', COLUMN => 'id', DELETE => 'CASCADE'}
+      },
+      user_id => {
+        TYPE       => 'INT3',
+        NOTNULL    => 1,
+        REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'}
+      },
+    ],
+    INDEXES => [
+      namedqueries_link_in_footer_id_idx =>
+        {FIELDS => [qw(namedquery_id user_id)], TYPE => 'UNIQUE'},
+      namedqueries_link_in_footer_userid_idx => ['user_id'],
+    ],
+  },
+
+  tag => {
+    FIELDS => [
+      id      => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1},
+      name    => {TYPE => 'varchar(64)',  NOTNULL => 1},
+      user_id => {
+        TYPE       => 'INT3',
+        NOTNULL    => 1,
+        REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'}
+      },
+    ],
+    INDEXES =>
+      [tag_user_id_idx => {FIELDS => [qw(user_id name)], TYPE => 'UNIQUE'},],
+  },
+
+  bug_tag => {
+    FIELDS => [
+      bug_id => {
+        TYPE       => 'INT3',
+        NOTNULL    => 1,
+        REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'}
+      },
+      tag_id => {
+        TYPE       => 'INT3',
+        NOTNULL    => 1,
+        REFERENCES => {TABLE => 'tag', COLUMN => 'id', DELETE => 'CASCADE'}
+      },
+    ],
+    INDEXES =>
+      [bug_tag_bug_id_idx => {FIELDS => [qw(bug_id tag_id)], TYPE => 'UNIQUE'},],
+  },
+
+  reports => {
+    FIELDS => [
+      id      => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1},
+      user_id => {
+        TYPE       => 'INT3',
+        NOTNULL    => 1,
+        REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'}
+      },
+      name  => {TYPE => 'varchar(64)', NOTNULL => 1},
+      query => {TYPE => 'LONGTEXT',    NOTNULL => 1},
+    ],
+    INDEXES =>
+      [reports_user_id_idx => {FIELDS => [qw(user_id name)], TYPE => 'UNIQUE'},],
+  },
+
+  component_cc => {
+
+    FIELDS => [
+      user_id => {
+        TYPE       => 'INT3',
+        NOTNULL    => 1,
+        REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'}
+      },
+      component_id => {
+        TYPE       => 'INT3',
+        NOTNULL    => 1,
+        REFERENCES => {TABLE => 'components', COLUMN => 'id', DELETE => 'CASCADE'}
+      },
+    ],
+    INDEXES => [
+      component_cc_user_id_idx =>
+        {FIELDS => [qw(component_id user_id)], TYPE => 'UNIQUE'},
+    ],
+  },
+
+  # Authentication
+  # --------------
+
+  logincookies => {
+    FIELDS => [
+      cookie => {TYPE => 'varchar(22)', NOTNULL => 1, PRIMARYKEY => 1},
+      userid => {
+        TYPE       => 'INT3',
+        NOTNULL    => 1,
+        REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'}
+      },
+      ipaddr   => {TYPE => 'varchar(40)'},
+      lastused => {TYPE => 'DATETIME', NOTNULL => 1},
+    ],
+    INDEXES => [logincookies_lastused_idx => ['lastused'],],
+  },
+
+  login_failure => {
+    FIELDS => [
+      user_id => {
+        TYPE       => 'INT3',
+        NOTNULL    => 1,
+        REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'}
+      },
+      login_time => {TYPE => 'DATETIME',    NOTNULL => 1},
+      ip_addr    => {TYPE => 'varchar(40)', NOTNULL => 1},
+    ],
+    INDEXES => [
+
+      # We do lookups by every item in the table simultaneously, but
+      # having an index with all three items would be the same size as
+      # the table. So instead we have an index on just the smallest item,
+      # to speed lookups.
+      login_failure_user_id_idx => ['user_id'],
+    ],
+  },
+
+
+  # "tokens" stores the tokens users receive when a password or email
+  #     change is requested.  Tokens provide an extra measure of security
+  #     for these changes.
+  tokens => {
+    FIELDS => [
+      userid => {
+        TYPE       => 'INT3',
+        REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'}
+      },
+      issuedate => {TYPE => 'DATETIME',    NOTNULL => 1},
+      token     => {TYPE => 'varchar(22)', NOTNULL => 1, PRIMARYKEY => 1},
+      tokentype => {TYPE => 'varchar(16)', NOTNULL => 1},
+      eventdata => {TYPE => 'TINYTEXT'},
+    ],
+    INDEXES => [tokens_userid_idx => ['userid'],],
+  },
+
+  # GROUPS
+  # ------
+
+  groups => {
+    FIELDS => [
+      id          => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1},
+      name        => {TYPE => 'varchar(255)', NOTNULL => 1},
+      description => {TYPE => 'MEDIUMTEXT',   NOTNULL => 1},
+      isbuggroup  => {TYPE => 'BOOLEAN',      NOTNULL => 1},
+      userregexp  => {TYPE => 'TINYTEXT',     NOTNULL => 1, DEFAULT => "''"},
+      isactive    => {TYPE => 'BOOLEAN',      NOTNULL => 1, DEFAULT => 'TRUE'},
+      icon_url    => {TYPE => 'TINYTEXT'},
+    ],
+    INDEXES => [groups_name_idx => {FIELDS => ['name'], TYPE => 'UNIQUE'},],
+  },
+
+  group_control_map => {
+    FIELDS => [
+      group_id => {
+        TYPE       => 'INT3',
+        NOTNULL    => 1,
+        REFERENCES => {TABLE => 'groups', COLUMN => 'id', DELETE => 'CASCADE'}
+      },
+      product_id => {
+        TYPE       => 'INT2',
+        NOTNULL    => 1,
+        REFERENCES => {TABLE => 'products', COLUMN => 'id', DELETE => 'CASCADE'}
+      },
+      entry          => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'},
+      membercontrol  => {TYPE => 'INT1',    NOTNULL => 1, DEFAULT => CONTROLMAPNA},
+      othercontrol   => {TYPE => 'INT1',    NOTNULL => 1, DEFAULT => CONTROLMAPNA},
+      canedit        => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'},
+      editcomponents => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'},
+      editbugs       => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'},
+      canconfirm     => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'},
     ],
+    INDEXES => [
+      group_control_map_product_id_idx =>
+        {FIELDS => [qw(product_id group_id)], TYPE => 'UNIQUE'},
+      group_control_map_group_id_idx => ['group_id'],
+    ],
+  },
+
+  # "user_group_map" determines the groups that a user belongs to
+  # directly or due to regexp and which groups can be blessed by a user.
+  #
+  # grant_type:
+  # if GRANT_DIRECT - record was explicitly granted
+  # if GRANT_DERIVED - record was derived from expanding a group hierarchy
+  # if GRANT_REGEXP - record was created by evaluating a regexp
+  user_group_map => {
+    FIELDS => [
+      user_id => {
+        TYPE       => 'INT3',
+        NOTNULL    => 1,
+        REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'}
+      },
+      group_id => {
+        TYPE       => 'INT3',
+        NOTNULL    => 1,
+        REFERENCES => {TABLE => 'groups', COLUMN => 'id', DELETE => 'CASCADE'}
+      },
+      isbless    => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'},
+      grant_type => {TYPE => 'INT1',    NOTNULL => 1, DEFAULT => GRANT_DIRECT},
+    ],
+    INDEXES => [
+      user_group_map_user_id_idx =>
+        {FIELDS => [qw(user_id group_id grant_type isbless)], TYPE => 'UNIQUE'},
+    ],
+  },
+
+  # This table determines which groups are made a member of another
+  # group, given the ability to bless another group, or given
+  # visibility to another groups existence and membership
+  # grant_type:
+  # if GROUP_MEMBERSHIP - member groups are made members of grantor
+  # if GROUP_BLESS - member groups may grant membership in grantor
+  # if GROUP_VISIBLE - member groups may see grantor group
+  group_group_map => {
+    FIELDS => [
+      member_id => {
+        TYPE       => 'INT3',
+        NOTNULL    => 1,
+        REFERENCES => {TABLE => 'groups', COLUMN => 'id', DELETE => 'CASCADE'}
+      },
+      grantor_id => {
+        TYPE       => 'INT3',
+        NOTNULL    => 1,
+        REFERENCES => {TABLE => 'groups', COLUMN => 'id', DELETE => 'CASCADE'}
+      },
+      grant_type => {TYPE => 'INT1', NOTNULL => 1, DEFAULT => GROUP_MEMBERSHIP},
+    ],
+    INDEXES => [
+      group_group_map_member_id_idx =>
+        {FIELDS => [qw(member_id grantor_id grant_type)], TYPE => 'UNIQUE'},
+    ],
+  },
+
+  # This table determines which groups a user must be a member of
+  # in order to see a bug.
+  bug_group_map => {
+    FIELDS => [
+      bug_id => {
+        TYPE       => 'INT3',
+        NOTNULL    => 1,
+        REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'}
+      },
+      group_id => {
+        TYPE       => 'INT3',
+        NOTNULL    => 1,
+        REFERENCES => {TABLE => 'groups', COLUMN => 'id', DELETE => 'CASCADE'}
+      },
+    ],
+    INDEXES => [
+      bug_group_map_bug_id_idx => {FIELDS => [qw(bug_id group_id)], TYPE => 'UNIQUE'},
+      bug_group_map_group_id_idx => ['group_id'],
+    ],
+  },
+
+  # This table determines which groups a user must be a member of
+  # in order to see a named query somebody else shares.
+  namedquery_group_map => {
+    FIELDS => [
+      namedquery_id => {
+        TYPE       => 'INT3',
+        NOTNULL    => 1,
+        REFERENCES => {TABLE => 'namedqueries', COLUMN => 'id', DELETE => 'CASCADE'}
+      },
+      group_id => {
+        TYPE       => 'INT3',
+        NOTNULL    => 1,
+        REFERENCES => {TABLE => 'groups', COLUMN => 'id', DELETE => 'CASCADE'}
+      },
+    ],
+    INDEXES => [
+      namedquery_group_map_namedquery_id_idx =>
+        {FIELDS => [qw(namedquery_id)], TYPE => 'UNIQUE'},
+      namedquery_group_map_group_id_idx => ['group_id'],
+    ],
+  },
+
+  category_group_map => {
+    FIELDS => [
+      category_id => {
+        TYPE    => 'INT2',
+        NOTNULL => 1,
+        REFERENCES =>
+          {TABLE => 'series_categories', COLUMN => 'id', DELETE => 'CASCADE'}
+      },
+      group_id => {
+        TYPE       => 'INT3',
+        NOTNULL    => 1,
+        REFERENCES => {TABLE => 'groups', COLUMN => 'id', DELETE => 'CASCADE'}
+      },
+    ],
+    INDEXES => [
+      category_group_map_category_id_idx =>
+        {FIELDS => [qw(category_id group_id)], TYPE => 'UNIQUE'},
+    ],
+  },
+
+
+  # PRODUCTS
+  # --------
+
+  classifications => {
+    FIELDS => [
+      id          => {TYPE => 'SMALLSERIAL', NOTNULL => 1, PRIMARYKEY => 1},
+      name        => {TYPE => 'varchar(64)', NOTNULL => 1},
+      description => {TYPE => 'MEDIUMTEXT'},
+      sortkey     => {TYPE => 'INT2',        NOTNULL => 1, DEFAULT => '0'},
+    ],
+    INDEXES =>
+      [classifications_name_idx => {FIELDS => ['name'], TYPE => 'UNIQUE'},],
+  },
+
+  products => {
+    FIELDS => [
+      id                => {TYPE => 'SMALLSERIAL', NOTNULL => 1, PRIMARYKEY => 1},
+      name              => {TYPE => 'varchar(64)', NOTNULL => 1},
+      classification_id => {
+        TYPE       => 'INT2',
+        NOTNULL    => 1,
+        DEFAULT    => '1',
+        REFERENCES => {TABLE => 'classifications', COLUMN => 'id', DELETE => 'CASCADE'}
+      },
+      description        => {TYPE => 'MEDIUMTEXT',  NOTNULL => 1},
+      isactive           => {TYPE => 'BOOLEAN',     NOTNULL => 1, DEFAULT => 1},
+      defaultmilestone   => {TYPE => 'varchar(64)', NOTNULL => 1, DEFAULT => "'---'"},
+      allows_unconfirmed => {TYPE => 'BOOLEAN',     NOTNULL => 1, DEFAULT => 'TRUE'},
+    ],
+    INDEXES => [products_name_idx => {FIELDS => ['name'], TYPE => 'UNIQUE'},],
+  },
+
+  components => {
+    FIELDS => [
+      id         => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1},
+      name       => {TYPE => 'varchar(64)',  NOTNULL => 1},
+      product_id => {
+        TYPE       => 'INT2',
+        NOTNULL    => 1,
+        REFERENCES => {TABLE => 'products', COLUMN => 'id', DELETE => 'CASCADE'}
+      },
+      initialowner => {
+        TYPE       => 'INT3',
+        NOTNULL    => 1,
+        REFERENCES => {TABLE => 'profiles', COLUMN => 'userid'}
+      },
+      initialqacontact => {
+        TYPE       => 'INT3',
+        REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'SET NULL'}
+      },
+      description => {TYPE => 'MEDIUMTEXT', NOTNULL => 1},
+      isactive    => {TYPE => 'BOOLEAN',    NOTNULL => 1, DEFAULT => 'TRUE'},
+    ],
+    INDEXES => [
+      components_product_id_idx =>
+        {FIELDS => [qw(product_id name)], TYPE => 'UNIQUE'},
+      components_name_idx => ['name'],
+    ],
+  },
+
+  # CHARTS
+  # ------
+
+  series => {
+    FIELDS => [
+      series_id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1},
+      creator   => {
+        TYPE       => 'INT3',
+        REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'}
+      },
+      category => {
+        TYPE    => 'INT2',
+        NOTNULL => 1,
+        REFERENCES =>
+          {TABLE => 'series_categories', COLUMN => 'id', DELETE => 'CASCADE'}
+      },
+      subcategory => {
+        TYPE    => 'INT2',
+        NOTNULL => 1,
+        REFERENCES =>
+          {TABLE => 'series_categories', COLUMN => 'id', DELETE => 'CASCADE'}
+      },
+      name      => {TYPE => 'varchar(64)', NOTNULL => 1},
+      frequency => {TYPE => 'INT2',        NOTNULL => 1},
+      query     => {TYPE => 'MEDIUMTEXT',  NOTNULL => 1},
+      is_public => {TYPE => 'BOOLEAN',     NOTNULL => 1, DEFAULT => 'FALSE'},
+    ],
+    INDEXES => [
+      series_creator_idx => ['creator'],
+      series_category_idx =>
+        {FIELDS => [qw(category subcategory name)], TYPE => 'UNIQUE'},
+    ],
+  },
+
+  series_data => {
+    FIELDS => [
+      series_id => {
+        TYPE       => 'INT3',
+        NOTNULL    => 1,
+        REFERENCES => {TABLE => 'series', COLUMN => 'series_id', DELETE => 'CASCADE'}
+      },
+      series_date  => {TYPE => 'DATETIME', NOTNULL => 1},
+      series_value => {TYPE => 'INT3',     NOTNULL => 1},
+    ],
+    INDEXES => [
+      series_data_series_id_idx =>
+        {FIELDS => [qw(series_id series_date)], TYPE => 'UNIQUE'},
+    ],
+  },
+
+  series_categories => {
+    FIELDS => [
+      id   => {TYPE => 'SMALLSERIAL', NOTNULL => 1, PRIMARYKEY => 1},
+      name => {TYPE => 'varchar(64)', NOTNULL => 1},
+    ],
+    INDEXES =>
+      [series_categories_name_idx => {FIELDS => ['name'], TYPE => 'UNIQUE'},],
+  },
+
+  # WHINE SYSTEM
+  # ------------
+
+  whine_queries => {
+    FIELDS => [
+      id      => {TYPE => 'MEDIUMSERIAL', PRIMARYKEY => 1, NOTNULL => 1},
+      eventid => {
+        TYPE       => 'INT3',
+        NOTNULL    => 1,
+        REFERENCES => {TABLE => 'whine_events', COLUMN => 'id', DELETE => 'CASCADE'}
+      },
+      query_name    => {TYPE => 'varchar(64)',  NOTNULL => 1, DEFAULT => "''"},
+      sortkey       => {TYPE => 'INT2',         NOTNULL => 1, DEFAULT => '0'},
+      onemailperbug => {TYPE => 'BOOLEAN',      NOTNULL => 1, DEFAULT => 'FALSE'},
+      title         => {TYPE => 'varchar(128)', NOTNULL => 1, DEFAULT => "''"},
+    ],
+    INDEXES => [whine_queries_eventid_idx => ['eventid'],],
+  },
+
+  whine_schedules => {
+    FIELDS => [
+      id      => {TYPE => 'MEDIUMSERIAL', PRIMARYKEY => 1, NOTNULL => 1},
+      eventid => {
+        TYPE       => 'INT3',
+        NOTNULL    => 1,
+        REFERENCES => {TABLE => 'whine_events', COLUMN => 'id', DELETE => 'CASCADE'}
+      },
+      run_day     => {TYPE => 'varchar(32)'},
+      run_time    => {TYPE => 'varchar(32)'},
+      run_next    => {TYPE => 'DATETIME'},
+      mailto      => {TYPE => 'INT3', NOTNULL => 1},
+      mailto_type => {TYPE => 'INT2', NOTNULL => 1, DEFAULT => '0'},
+    ],
+    INDEXES => [
+      whine_schedules_run_next_idx => ['run_next'],
+      whine_schedules_eventid_idx  => ['eventid'],
+    ],
+  },
+
+  whine_events => {
+    FIELDS => [
+      id           => {TYPE => 'MEDIUMSERIAL', PRIMARYKEY => 1, NOTNULL => 1},
+      owner_userid => {
+        TYPE       => 'INT3',
+        NOTNULL    => 1,
+        REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'}
+      },
+      subject      => {TYPE => 'varchar(128)'},
+      body         => {TYPE => 'MEDIUMTEXT'},
+      mailifnobugs => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'},
+    ],
+  },
+
+  # QUIPS
+  # -----
+
+  quips => {
+    FIELDS => [
+      quipid => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1},
+      userid => {
+        TYPE       => 'INT3',
+        REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'SET NULL'}
+      },
+      quip     => {TYPE => 'varchar(512)', NOTNULL => 1},
+      approved => {TYPE => 'BOOLEAN',      NOTNULL => 1, DEFAULT => 'TRUE'},
+    ],
+  },
+
+  # SETTINGS
+  # --------
+  # setting          - each global setting will have exactly one entry
+  #                    in this table.
+  # setting_value    - stores the list of acceptable values for each
+  #                    setting, and a sort index that controls the order
+  #                    in which the values are displayed.
+  # profile_setting  - If a user has chosen to use a value other than the
+  #                    global default for a given setting, it will be
+  #                    stored in this table. Note: even if a setting is
+  #                    later changed so is_enabled = false, the stored
+  #                    value will remain in case it is ever enabled again.
+  #
+  setting => {
+    FIELDS => [
+      name          => {TYPE => 'varchar(32)', NOTNULL => 1, PRIMARYKEY => 1},
+      default_value => {TYPE => 'varchar(32)', NOTNULL => 1},
+      is_enabled    => {TYPE => 'BOOLEAN',     NOTNULL => 1, DEFAULT => 'TRUE'},
+      subclass      => {TYPE => 'varchar(32)'},
+    ],
+  },
+
+  setting_value => {
+    FIELDS => [
+      name => {
+        TYPE       => 'varchar(32)',
+        NOTNULL    => 1,
+        REFERENCES => {TABLE => 'setting', COLUMN => 'name', DELETE => 'CASCADE'}
+      },
+      value     => {TYPE => 'varchar(32)', NOTNULL => 1},
+      sortindex => {TYPE => 'INT2',        NOTNULL => 1},
+    ],
+    INDEXES => [
+      setting_value_nv_unique_idx => {FIELDS => [qw(name value)], TYPE => 'UNIQUE'},
+      setting_value_ns_unique_idx =>
+        {FIELDS => [qw(name sortindex)], TYPE => 'UNIQUE'},
+    ],
+  },
+
+  profile_setting => {
+    FIELDS => [
+      user_id => {
+        TYPE       => 'INT3',
+        NOTNULL    => 1,
+        REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'}
+      },
+      setting_name => {
+        TYPE       => 'varchar(32)',
+        NOTNULL    => 1,
+        REFERENCES => {TABLE => 'setting', COLUMN => 'name', DELETE => 'CASCADE'}
+      },
+      setting_value => {TYPE => 'varchar(32)', NOTNULL => 1},
+    ],
+    INDEXES => [
+      profile_setting_value_unique_idx =>
+        {FIELDS => [qw(user_id setting_name)], TYPE => 'UNIQUE'},
+    ],
+  },
+
+  # BUGMAIL
+  # -------
+
+  mail_staging => {
+    FIELDS => [
+      id      => {TYPE => 'INTSERIAL', PRIMARYKEY => 1, NOTNULL => 1},
+      message => {TYPE => 'LONGBLOB',  NOTNULL    => 1},
+    ],
+  },
+
+  email_rates => {
+    FIELDS => [
+      id         => {TYPE => 'INTSERIAL',    NOTNULL => 1, PRIMARYKEY => 1},
+      recipient  => {TYPE => 'varchar(255)', NOTNULL => 1},
+      message_ts => {TYPE => 'DATETIME',     NOTNULL => 1},
+    ],
+    INDEXES => [email_rates_idx => [qw(recipient message_ts)],],
+  },
+
+  # THESCHWARTZ TABLES
+  # ------------------
+  # Note: In the standard TheSchwartz schema, most integers are unsigned,
+  # but we didn't implement unsigned ints for Bugzilla schemas, so we
+  # just create signed ints, which should be fine.
+
+  ts_funcmap => {
+    FIELDS => [
+      funcid   => {TYPE => 'INTSERIAL',    PRIMARYKEY => 1, NOTNULL => 1},
+      funcname => {TYPE => 'varchar(255)', NOTNULL    => 1},
+    ],
+    INDEXES =>
+      [ts_funcmap_funcname_idx => {FIELDS => ['funcname'], TYPE => 'UNIQUE'},],
+  },
+
+  ts_job => {
+    FIELDS => [
+
+      # In a standard TheSchwartz schema, this is a BIGINT, but we
+      # don't have those and I didn't want to add them just for this.
+      jobid  => {TYPE => 'INTSERIAL', PRIMARYKEY => 1, NOTNULL => 1},
+      funcid => {TYPE => 'INT4',      NOTNULL    => 1},
+
+      # In standard TheSchwartz, this is a MEDIUMBLOB.
+      arg           => {TYPE => 'LONGBLOB'},
+      uniqkey       => {TYPE => 'varchar(255)'},
+      insert_time   => {TYPE => 'INT4'},
+      run_after     => {TYPE => 'INT4', NOTNULL => 1},
+      grabbed_until => {TYPE => 'INT4', NOTNULL => 1},
+      priority      => {TYPE => 'INT2'},
+      coalesce      => {TYPE => 'varchar(255)'},
+    ],
+    INDEXES => [
+      ts_job_funcid_idx => {FIELDS => [qw(funcid uniqkey)], TYPE => 'UNIQUE'},
+
+      # In a standard TheSchewartz schema, these both go in the other
+      # direction, but there's no reason to have three indexes that
+      # all start with the same column, and our naming scheme doesn't
+      # allow it anyhow.
+      ts_job_run_after_idx => [qw(run_after funcid)],
+      ts_job_coalesce_idx  => [qw(coalesce funcid)],
+    ],
+  },
+
+  ts_note => {
+    FIELDS => [
+
+      # This is a BIGINT in standard TheSchwartz schemas.
+      jobid   => {TYPE => 'INT4', NOTNULL => 1},
+      notekey => {TYPE => 'varchar(255)'},
+      value   => {TYPE => 'LONGBLOB'},
+    ],
+    INDEXES =>
+      [ts_note_jobid_idx => {FIELDS => [qw(jobid notekey)], TYPE => 'UNIQUE'},],
+  },
+
+  ts_error => {
+    FIELDS => [
+      error_time => {TYPE => 'INT4',         NOTNULL => 1},
+      jobid      => {TYPE => 'INT4',         NOTNULL => 1},
+      message    => {TYPE => 'varchar(255)', NOTNULL => 1},
+      funcid     => {TYPE => 'INT4',         NOTNULL => 1, DEFAULT => 0},
+    ],
+    INDEXES => [
+      ts_error_funcid_idx     => [qw(funcid error_time)],
+      ts_error_error_time_idx => ['error_time'],
+      ts_error_jobid_idx      => ['jobid'],
+    ],
+  },
+
+  ts_exitstatus => {
+    FIELDS => [
+      jobid           => {TYPE => 'INTSERIAL', PRIMARYKEY => 1, NOTNULL => 1},
+      funcid          => {TYPE => 'INT4',      NOTNULL    => 1, DEFAULT => 0},
+      status          => {TYPE => 'INT2'},
+      completion_time => {TYPE => 'INT4'},
+      delete_after    => {TYPE => 'INT4'},
+    ],
+    INDEXES => [
+      ts_exitstatus_funcid_idx       => ['funcid'],
+      ts_exitstatus_delete_after_idx => ['delete_after'],
+    ],
+  },
+
+  # SCHEMA STORAGE
+  # --------------
+
+  bz_schema => {
+    FIELDS => [
+      schema_data => {TYPE => 'LONGBLOB',     NOTNULL => 1},
+      version     => {TYPE => 'decimal(3,2)', NOTNULL => 1},
+    ],
+    INDEXES =>
+      [bz_schema_version_idx => {FIELDS => ['version'], TYPE => 'UNIQUE'},],
+  },
+
+  bug_user_last_visit => {
+    FIELDS => [
+      id      => {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1},
+      user_id => {
+        TYPE       => 'INT3',
+        NOTNULL    => 1,
+        REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'}
+      },
+      bug_id => {
+        TYPE       => 'INT3',
+        NOTNULL    => 1,
+        REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'}
+      },
+      last_visit_ts => {TYPE => 'DATETIME', NOTNULL => 1},
+    ],
+    INDEXES => [
+      bug_user_last_visit_idx => {FIELDS => ['user_id', 'bug_id'], TYPE => 'UNIQUE'},
+      bug_user_last_visit_last_visit_ts_idx => ['last_visit_ts'],
+    ],
+  },
+
+  user_api_keys => {
+    FIELDS => [
+      id      => {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1},
+      user_id => {
+        TYPE       => 'INT3',
+        NOTNULL    => 1,
+        REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'}
+      },
+      api_key     => {TYPE => 'varchar(40)', NOTNULL => 1},
+      description => {TYPE => 'varchar(255)'},
+      revoked     => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'},
+      last_used   => {TYPE => 'DATETIME'},
+      app_id      => {TYPE => 'varchar(64)'},
+    ],
+    INDEXES => [
+      user_api_keys_api_key_idx        => {FIELDS => ['api_key'], TYPE => 'UNIQUE'},
+      user_api_keys_user_id_idx        => ['user_id'],
+      user_api_keys_user_id_app_id_idx => ['user_id', 'app_id'],
+    ],
+  },
+};
+
+# Foreign Keys are added in Bugzilla::DB::bz_add_field_tables
+use constant MULTI_SELECT_VALUE_TABLE => {
+  FIELDS => [
+    bug_id => {TYPE => 'INT3',        NOTNULL => 1},
+    value  => {TYPE => 'varchar(64)', NOTNULL => 1},
+  ],
+  INDEXES => [bug_id_idx => {FIELDS => [qw( bug_id value)], TYPE => 'UNIQUE'},],
 };
 
 #--------------------------------------------------------------------------
@@ -1845,27 +1789,28 @@ sub new {
 
 =cut
 
-    my $this = shift;
-    my $class = ref($this) || $this;
-    my $driver = shift;
+  my $this   = shift;
+  my $class  = ref($this) || $this;
+  my $driver = shift;
 
-    if ($driver) {
-        (my $subclass = $driver) =~ s/^(\S)/\U$1/;
-        $class .= '::' . $subclass;
-        eval "require $class;";
-        die "The $class class could not be found ($subclass " .
-            "not supported?): $@" if ($@);
-    }
-    die "$class is an abstract base class. Instantiate a subclass instead."
-      if ($class eq __PACKAGE__);
+  if ($driver) {
+    (my $subclass = $driver) =~ s/^(\S)/\U$1/;
+    $class .= '::' . $subclass;
+    eval "require $class;";
+    die "The $class class could not be found ($subclass " . "not supported?): $@"
+      if ($@);
+  }
+  die "$class is an abstract base class. Instantiate a subclass instead."
+    if ($class eq __PACKAGE__);
 
-    my $self = {};
-    bless $self, $class;
-    $self = $self->_initialize(@_);
+  my $self = {};
+  bless $self, $class;
+  $self = $self->_initialize(@_);
 
-    return($self);
+  return ($self);
+
+}    #eosub--new
 
-} #eosub--new
 #--------------------------------------------------------------------------
 sub _initialize {
 
@@ -1888,33 +1833,34 @@ sub _initialize {
 
 =cut
 
-    my $self = shift;
-    my $abstract_schema = shift;
+  my $self            = shift;
+  my $abstract_schema = shift;
 
-    if (!$abstract_schema) {
-        # While ABSTRACT_SCHEMA cannot be modified, $abstract_schema can be.
-        # So, we dclone it to prevent anything from mucking with the constant.
-        $abstract_schema = dclone(ABSTRACT_SCHEMA);
+  if (!$abstract_schema) {
 
-        # Let extensions add tables, but make sure they can't modify existing
-        # tables. If we don't lock/unlock keys, lock_value complains.
-        lock_keys(%$abstract_schema);
-        foreach my $table (keys %{ABSTRACT_SCHEMA()}) {
-            lock_value(%$abstract_schema, $table) 
-                if exists $abstract_schema->{$table};
-        }
-        unlock_keys(%$abstract_schema);
-        Bugzilla::Hook::process('db_schema_abstract_schema', 
-                                { schema => $abstract_schema });
-        unlock_hash(%$abstract_schema);
+    # While ABSTRACT_SCHEMA cannot be modified, $abstract_schema can be.
+    # So, we dclone it to prevent anything from mucking with the constant.
+    $abstract_schema = dclone(ABSTRACT_SCHEMA);
+
+    # Let extensions add tables, but make sure they can't modify existing
+    # tables. If we don't lock/unlock keys, lock_value complains.
+    lock_keys(%$abstract_schema);
+    foreach my $table (keys %{ABSTRACT_SCHEMA()}) {
+      lock_value(%$abstract_schema, $table) if exists $abstract_schema->{$table};
     }
+    unlock_keys(%$abstract_schema);
+    Bugzilla::Hook::process('db_schema_abstract_schema',
+      {schema => $abstract_schema});
+    unlock_hash(%$abstract_schema);
+  }
 
-    $self->{schema} = dclone($abstract_schema);
-    $self->{abstract_schema} = $abstract_schema;
+  $self->{schema}          = dclone($abstract_schema);
+  $self->{abstract_schema} = $abstract_schema;
 
-    return $self;
+  return $self;
+
+}    #eosub--_initialize
 
-} #eosub--_initialize
 #--------------------------------------------------------------------------
 sub _adjust_schema {
 
@@ -1930,36 +1876,41 @@ sub _adjust_schema {
 
 =cut
 
-    my $self = shift;
-
-    # The _initialize method has already set up the db_specific hash with
-    # the information on how to implement the abstract data types for the
-    # instantiated DBMS-specific subclass.
-    my $db_specific = $self->{db_specific};
-
-    # Loop over each table in the abstract database schema.
-    foreach my $table (keys %{ $self->{schema} }) {
-        my %fields = (@{ $self->{schema}{$table}{FIELDS} });
-        # Loop over the field definitions in each table.
-        foreach my $field_def (values %fields) {
-            # If the field type is an abstract data type defined in the
-            # $db_specific hash, replace it with the DBMS-specific data type
-            # that implements it.
-            if (exists($db_specific->{$field_def->{TYPE}})) {
-                $field_def->{TYPE} = $db_specific->{$field_def->{TYPE}};
-            }
-            # Replace abstract default values (such as 'TRUE' and 'FALSE')
-            # with their database-specific implementations.
-            if (exists($field_def->{DEFAULT})
-                && exists($db_specific->{$field_def->{DEFAULT}})) {
-                $field_def->{DEFAULT} = $db_specific->{$field_def->{DEFAULT}};
-            }
-        }
+  my $self = shift;
+
+  # The _initialize method has already set up the db_specific hash with
+  # the information on how to implement the abstract data types for the
+  # instantiated DBMS-specific subclass.
+  my $db_specific = $self->{db_specific};
+
+  # Loop over each table in the abstract database schema.
+  foreach my $table (keys %{$self->{schema}}) {
+    my %fields = (@{$self->{schema}{$table}{FIELDS}});
+
+    # Loop over the field definitions in each table.
+    foreach my $field_def (values %fields) {
+
+      # If the field type is an abstract data type defined in the
+      # $db_specific hash, replace it with the DBMS-specific data type
+      # that implements it.
+      if (exists($db_specific->{$field_def->{TYPE}})) {
+        $field_def->{TYPE} = $db_specific->{$field_def->{TYPE}};
+      }
+
+      # Replace abstract default values (such as 'TRUE' and 'FALSE')
+      # with their database-specific implementations.
+      if ( exists($field_def->{DEFAULT})
+        && exists($db_specific->{$field_def->{DEFAULT}}))
+      {
+        $field_def->{DEFAULT} = $db_specific->{$field_def->{DEFAULT}};
+      }
     }
+  }
+
+  return $self;
 
-    return $self;
+}    #eosub--_adjust_schema
 
-} #eosub--_adjust_schema
 #--------------------------------------------------------------------------
 sub get_type_ddl {
 
@@ -1993,30 +1944,34 @@ C<ALTER TABLE> SQL statement
 
 =cut
 
-    my $self = shift;
-    my $finfo = (@_ == 1 && ref($_[0]) eq 'HASH') ? $_[0] : { @_ };
-    my $type = $finfo->{TYPE};
-    confess "A valid TYPE was not specified for this column (got " 
-            . Dumper($finfo) . ")" unless ($type);
-
-    my $default = $finfo->{DEFAULT};
-    # Replace any abstract default value (such as 'TRUE' or 'FALSE')
-    # with its database-specific implementation.
-    if ( defined $default && exists($self->{db_specific}->{$default}) ) {
-        $default = $self->{db_specific}->{$default};
-    }
+  my $self  = shift;
+  my $finfo = (@_ == 1 && ref($_[0]) eq 'HASH') ? $_[0] : {@_};
+  my $type  = $finfo->{TYPE};
+  confess "A valid TYPE was not specified for this column (got "
+    . Dumper($finfo) . ")"
+    unless ($type);
+
+  my $default = $finfo->{DEFAULT};
+
+  # Replace any abstract default value (such as 'TRUE' or 'FALSE')
+  # with its database-specific implementation.
+  if (defined $default && exists($self->{db_specific}->{$default})) {
+    $default = $self->{db_specific}->{$default};
+  }
+
+  my $type_ddl = $self->convert_type($type);
 
-    my $type_ddl = $self->convert_type($type);
-    # DEFAULT attribute must appear before any column constraints
-    # (e.g., NOT NULL), for Oracle
-    $type_ddl .= " DEFAULT $default" if (defined($default));
-    # PRIMARY KEY must appear before NOT NULL for SQLite.
-    $type_ddl .= " PRIMARY KEY" if ($finfo->{PRIMARYKEY});
-    $type_ddl .= " NOT NULL" if ($finfo->{NOTNULL});
+  # DEFAULT attribute must appear before any column constraints
+  # (e.g., NOT NULL), for Oracle
+  $type_ddl .= " DEFAULT $default" if (defined($default));
 
-    return($type_ddl);
+  # PRIMARY KEY must appear before NOT NULL for SQLite.
+  $type_ddl .= " PRIMARY KEY" if ($finfo->{PRIMARYKEY});
+  $type_ddl .= " NOT NULL"    if ($finfo->{NOTNULL});
 
-} #eosub--get_type_ddl
+  return ($type_ddl);
+
+}    #eosub--get_type_ddl
 
 
 sub get_fk_ddl {
@@ -2050,78 +2005,80 @@ is undefined.
 
 =cut
 
-    my ($self, $table, $column, $references) = @_;
-    return "" if !$references;
+  my ($self, $table, $column, $references) = @_;
+  return "" if !$references;
 
-    my $update    = $references->{UPDATE} || 'CASCADE';
-    my $delete    = $references->{DELETE} || 'RESTRICT';
-    my $to_table  = $references->{TABLE}  || confess "No table in reference";
-    my $to_column = $references->{COLUMN} || confess "No column in reference";
-    my $fk_name   = $self->_get_fk_name($table, $column, $references);
+  my $update    = $references->{UPDATE} || 'CASCADE';
+  my $delete    = $references->{DELETE} || 'RESTRICT';
+  my $to_table  = $references->{TABLE}  || confess "No table in reference";
+  my $to_column = $references->{COLUMN} || confess "No column in reference";
+  my $fk_name = $self->_get_fk_name($table, $column, $references);
 
-    return "\n     CONSTRAINT $fk_name FOREIGN KEY ($column)\n"
-         . "     REFERENCES $to_table($to_column)\n"
-         . "      ON UPDATE $update ON DELETE $delete";
+  return
+      "\n     CONSTRAINT $fk_name FOREIGN KEY ($column)\n"
+    . "     REFERENCES $to_table($to_column)\n"
+    . "      ON UPDATE $update ON DELETE $delete";
 }
 
 # Generates a name for a Foreign Key. It's separate from get_fk_ddl
 # so that certain databases can override it (for shorter identifiers or
 # other reasons).
 sub _get_fk_name {
-    my ($self, $table, $column, $references) = @_;
-    my $to_table  = $references->{TABLE}; 
-    my $to_column = $references->{COLUMN};
-    my $name = "fk_${table}_${column}_${to_table}_${to_column}";
+  my ($self, $table, $column, $references) = @_;
+  my $to_table  = $references->{TABLE};
+  my $to_column = $references->{COLUMN};
+  my $name      = "fk_${table}_${column}_${to_table}_${to_column}";
 
-    if (length($name) > $self->MAX_IDENTIFIER_LEN) {
-        $name = 'fk_' . $self->_hash_identifier($name);
-    }
+  if (length($name) > $self->MAX_IDENTIFIER_LEN) {
+    $name = 'fk_' . $self->_hash_identifier($name);
+  }
 
-    return $name;
+  return $name;
 }
 
 sub _hash_identifier {
-    my ($invocant, $value) = @_;
-    # We do -7 to allow prefixes like "idx_" or "fk_", or perhaps something
-    # longer in the future.
-    return substr(md5_hex($value), 0, $invocant->MAX_IDENTIFIER_LEN - 7);
+  my ($invocant, $value) = @_;
+
+  # We do -7 to allow prefixes like "idx_" or "fk_", or perhaps something
+  # longer in the future.
+  return substr(md5_hex($value), 0, $invocant->MAX_IDENTIFIER_LEN - 7);
 }
 
 
 sub get_add_fks_sql {
-    my ($self, $table, $column_fks) = @_;
-
-    my @add = $self->_column_fks_to_ddl($table, $column_fks);
-
-    my @sql;
-    if ($self->MULTIPLE_FKS_IN_ALTER) {
-        my $alter = "ALTER TABLE $table ADD " . join(', ADD ', @add);
-        push(@sql, $alter);
-    }
-    else {
-        foreach my $fk_string (@add) {
-            push(@sql, "ALTER TABLE $table ADD $fk_string");
-        }
+  my ($self, $table, $column_fks) = @_;
+
+  my @add = $self->_column_fks_to_ddl($table, $column_fks);
+
+  my @sql;
+  if ($self->MULTIPLE_FKS_IN_ALTER) {
+    my $alter = "ALTER TABLE $table ADD " . join(', ADD ', @add);
+    push(@sql, $alter);
+  }
+  else {
+    foreach my $fk_string (@add) {
+      push(@sql, "ALTER TABLE $table ADD $fk_string");
     }
-    return @sql;
+  }
+  return @sql;
 }
 
 sub _column_fks_to_ddl {
-    my ($self, $table, $column_fks) = @_;
-    my @ddl;
-    foreach my $column (keys %$column_fks) {
-        my $def = $column_fks->{$column};
-        my $fk_string = $self->get_fk_ddl($table, $column, $def);
-        push(@ddl, $fk_string);
-    }
-    return @ddl;
+  my ($self, $table, $column_fks) = @_;
+  my @ddl;
+  foreach my $column (keys %$column_fks) {
+    my $def = $column_fks->{$column};
+    my $fk_string = $self->get_fk_ddl($table, $column, $def);
+    push(@ddl, $fk_string);
+  }
+  return @ddl;
 }
 
-sub get_drop_fk_sql { 
-    my ($self, $table, $column, $references) = @_;
-    my $fk_name = $self->_get_fk_name($table, $column, $references);
+sub get_drop_fk_sql {
+  my ($self, $table, $column, $references) = @_;
+  my $fk_name = $self->_get_fk_name($table, $column, $references);
 
-    return ("ALTER TABLE $table DROP CONSTRAINT $fk_name");
+  return ("ALTER TABLE $table DROP CONSTRAINT $fk_name");
 }
 
 sub convert_type {
@@ -2132,8 +2089,8 @@ Converts a TYPE from the L</ABSTRACT_SCHEMA> format into the real SQL type.
 
 =cut
 
-    my ($self, $type) = @_;
-    return $self->{db_specific}->{$type} || $type;
+  my ($self, $type) = @_;
+  return $self->{db_specific}->{$type} || $type;
 }
 
 sub get_column {
@@ -2150,16 +2107,16 @@ sub get_column {
 
 =cut
 
-    my($self, $table, $column) = @_;
+  my ($self, $table, $column) = @_;
 
-    # Prevent a possible dereferencing of an undef hash, if the
-    # table doesn't exist.
-    if (exists $self->{schema}->{$table}) {
-        my %fields = (@{ $self->{schema}{$table}{FIELDS} });
-        return $fields{$column};
-    }
-    return undef;
-} #eosub--get_column
+  # Prevent a possible dereferencing of an undef hash, if the
+  # table doesn't exist.
+  if (exists $self->{schema}->{$table}) {
+    my %fields = (@{$self->{schema}{$table}{FIELDS}});
+    return $fields{$column};
+  }
+  return undef;
+}    #eosub--get_column
 
 sub get_table_list {
 
@@ -2174,8 +2131,8 @@ sub get_table_list {
 
 =cut
 
-    my $self = shift;
-    return sort keys %{$self->{schema}};   
+  my $self = shift;
+  return sort keys %{$self->{schema}};
 }
 
 sub get_table_columns {
@@ -2189,34 +2146,33 @@ sub get_table_columns {
 
 =cut
 
-    my($self, $table) = @_;
-    my @ddl = ();
+  my ($self, $table) = @_;
+  my @ddl = ();
 
-    my $thash = $self->{schema}{$table};
-    die "Table $table does not exist in the database schema."
-        unless (ref($thash));
+  my $thash = $self->{schema}{$table};
+  die "Table $table does not exist in the database schema." unless (ref($thash));
 
-    my @columns = ();
-    my @fields = @{ $thash->{FIELDS} };
-    while (@fields) {
-        push(@columns, shift(@fields));
-        shift(@fields);
-    }
+  my @columns = ();
+  my @fields  = @{$thash->{FIELDS}};
+  while (@fields) {
+    push(@columns, shift(@fields));
+    shift(@fields);
+  }
 
-    return @columns;
+  return @columns;
 
-} #eosub--get_table_columns
+}    #eosub--get_table_columns
 
 sub get_table_indexes_abstract {
-    my ($self, $table) = @_;
-    my $table_def = $self->get_table_abstract($table);
-    my %indexes = @{$table_def->{INDEXES} || []};
-    return \%indexes;
+  my ($self, $table) = @_;
+  my $table_def = $self->get_table_abstract($table);
+  my %indexes = @{$table_def->{INDEXES} || []};
+  return \%indexes;
 }
 
 sub get_create_database_sql {
-    my ($self, $name) = @_;
-    return ("CREATE DATABASE \"$name\"");
+  my ($self, $name) = @_;
+  return ("CREATE DATABASE \"$name\"");
 }
 
 sub get_table_ddl {
@@ -2233,30 +2189,29 @@ sub get_table_ddl {
 
 =cut
 
-    my($self, $table) = @_;
-    my @ddl = ();
+  my ($self, $table) = @_;
+  my @ddl = ();
 
-    die "Table $table does not exist in the database schema."
-        unless (ref($self->{schema}{$table}));
+  die "Table $table does not exist in the database schema."
+    unless (ref($self->{schema}{$table}));
 
-    my $create_table = $self->_get_create_table_ddl($table);
-    push(@ddl, $create_table) if $create_table;
+  my $create_table = $self->_get_create_table_ddl($table);
+  push(@ddl, $create_table) if $create_table;
 
-    my @indexes = @{ $self->{schema}{$table}{INDEXES} || [] };
-    while (@indexes) {
-        my $index_name = shift(@indexes);
-        my $index_info = shift(@indexes);
-        my $index_sql  = $self->get_add_index_ddl($table, $index_name, 
-                                                  $index_info);
-        push(@ddl, $index_sql) if $index_sql;
-    }
+  my @indexes = @{$self->{schema}{$table}{INDEXES} || []};
+  while (@indexes) {
+    my $index_name = shift(@indexes);
+    my $index_info = shift(@indexes);
+    my $index_sql  = $self->get_add_index_ddl($table, $index_name, $index_info);
+    push(@ddl, $index_sql) if $index_sql;
+  }
 
-    push(@ddl, @{ $self->{schema}{$table}{DB_EXTRAS} })
-      if (ref($self->{schema}{$table}{DB_EXTRAS}));
+  push(@ddl, @{$self->{schema}{$table}{DB_EXTRAS}})
+    if (ref($self->{schema}{$table}{DB_EXTRAS}));
 
-    return @ddl;
+  return @ddl;
 
-} #eosub--get_table_ddl
+}    #eosub--get_table_ddl
 
 sub _get_create_table_ddl {
 
@@ -2269,30 +2224,29 @@ sub _get_create_table_ddl {
 
 =cut
 
-    my($self, $table) = @_;
-
-    my $thash = $self->{schema}{$table};
-    die "Table $table does not exist in the database schema."
-        unless ref $thash;
-
-    my (@col_lines, @fk_lines);
-    my @fields = @{ $thash->{FIELDS} };
-    while (@fields) {
-        my $field = shift(@fields);
-        my $finfo = shift(@fields);
-        push(@col_lines, "\t$field\t" . $self->get_type_ddl($finfo));
-        if ($self->FK_ON_CREATE and $finfo->{REFERENCES}) {
-            my $fk = $finfo->{REFERENCES};
-            my $fk_ddl = $self->get_fk_ddl($table, $field, $fk);
-            push(@fk_lines, $fk_ddl);
-        }
+  my ($self, $table) = @_;
+
+  my $thash = $self->{schema}{$table};
+  die "Table $table does not exist in the database schema." unless ref $thash;
+
+  my (@col_lines, @fk_lines);
+  my @fields = @{$thash->{FIELDS}};
+  while (@fields) {
+    my $field = shift(@fields);
+    my $finfo = shift(@fields);
+    push(@col_lines, "\t$field\t" . $self->get_type_ddl($finfo));
+    if ($self->FK_ON_CREATE and $finfo->{REFERENCES}) {
+      my $fk = $finfo->{REFERENCES};
+      my $fk_ddl = $self->get_fk_ddl($table, $field, $fk);
+      push(@fk_lines, $fk_ddl);
     }
-    
-    my $sql = "CREATE TABLE $table (\n" . join(",\n", @col_lines, @fk_lines)
-              . "\n)";
-    return $sql
+  }
 
-} 
+  my $sql
+    = "CREATE TABLE $table (\n" . join(",\n", @col_lines, @fk_lines) . "\n)";
+  return $sql;
+
+}
 
 sub _get_create_index_ddl {
 
@@ -2308,16 +2262,17 @@ sub _get_create_index_ddl {
 
 =cut
 
-    my ($self, $table_name, $index_name, $index_fields, $index_type) = @_;
+  my ($self, $table_name, $index_name, $index_fields, $index_type) = @_;
 
-    my $sql = "CREATE ";
-    $sql .= "$index_type " if ($index_type && $index_type eq 'UNIQUE');
-    $sql .= "INDEX $index_name ON $table_name \(" .
-      join(", ", @$index_fields) . "\)";
+  my $sql = "CREATE ";
+  $sql .= "$index_type " if ($index_type && $index_type eq 'UNIQUE');
+  $sql
+    .= "INDEX $index_name ON $table_name \(" . join(", ", @$index_fields) . "\)";
 
-    return($sql);
+  return ($sql);
+
+}    #eosub--_get_create_index_ddl
 
-} #eosub--_get_create_index_ddl
 #--------------------------------------------------------------------------
 
 sub get_add_column_ddl {
@@ -2336,30 +2291,37 @@ sub get_add_column_ddl {
 
 =cut
 
-    my ($self, $table, $column, $definition, $init_value) = @_;
-    my @statements;
-    # If DEFAULT is undefined and the column is enforced to be NOT NULL,
-    # then we use the init value as a temporary default value.
-    my $temp_default = 0;
-    if ($definition->{NOTNULL} && !exists $definition->{DEFAULT} && defined $init_value) {
-        $temp_default = 1;
-        $definition->{DEFAULT} = $init_value;
-    }
-
-    push(@statements, "ALTER TABLE $table ". $self->ADD_COLUMN ." $column " .
-        $self->get_type_ddl($definition));
-
-    if ($temp_default) {
-        push(@statements, $self->get_drop_default_ddl($table, $column));
-        delete $definition->{DEFAULT};
-    }
-
-    if (defined $definition->{REFERENCES}) {
-        push(@statements, $self->get_add_fks_sql($table, { $column =>
-                                                           $definition->{REFERENCES} }));
-    }
-
-    return (@statements);
+  my ($self, $table, $column, $definition, $init_value) = @_;
+  my @statements;
+
+  # If DEFAULT is undefined and the column is enforced to be NOT NULL,
+  # then we use the init value as a temporary default value.
+  my $temp_default = 0;
+  if ( $definition->{NOTNULL}
+    && !exists $definition->{DEFAULT}
+    && defined $init_value)
+  {
+    $temp_default = 1;
+    $definition->{DEFAULT} = $init_value;
+  }
+
+  push(@statements,
+        "ALTER TABLE $table "
+      . $self->ADD_COLUMN
+      . " $column "
+      . $self->get_type_ddl($definition));
+
+  if ($temp_default) {
+    push(@statements, $self->get_drop_default_ddl($table, $column));
+    delete $definition->{DEFAULT};
+  }
+
+  if (defined $definition->{REFERENCES}) {
+    push(@statements,
+      $self->get_add_fks_sql($table, {$column => $definition->{REFERENCES}}));
+  }
+
+  return (@statements);
 }
 
 sub get_drop_default_ddl {
@@ -2373,8 +2335,8 @@ sub get_drop_default_ddl {
 
 =cut
 
-    my ($self, $table, $column) = @_;
-    return "ALTER TABLE $table ALTER COLUMN $column DROP DEFAULT";
+  my ($self, $table, $column) = @_;
+  return "ALTER TABLE $table ALTER COLUMN $column DROP DEFAULT";
 }
 
 sub get_add_index_ddl {
@@ -2395,20 +2357,21 @@ sub get_add_index_ddl {
 
 =cut
 
-    my ($self, $table, $name, $definition) = @_;
+  my ($self, $table, $name, $definition) = @_;
 
-    my ($index_fields, $index_type);
-    # Index defs can be arrays or hashes
-    if (ref($definition) eq 'HASH') {
-        $index_fields = $definition->{FIELDS};
-        $index_type = $definition->{TYPE};
-    } else {
-        $index_fields = $definition;
-        $index_type = '';
-    }
-    
-    return $self->_get_create_index_ddl($table, $name, $index_fields, 
-                                        $index_type);
+  my ($index_fields, $index_type);
+
+  # Index defs can be arrays or hashes
+  if (ref($definition) eq 'HASH') {
+    $index_fields = $definition->{FIELDS};
+    $index_type   = $definition->{TYPE};
+  }
+  else {
+    $index_fields = $definition;
+    $index_type   = '';
+  }
+
+  return $self->_get_create_index_ddl($table, $name, $index_fields, $index_type);
 }
 
 sub get_alter_column_ddl {
@@ -2431,84 +2394,88 @@ sub get_alter_column_ddl {
 
 =cut
 
-    my $self = shift;
-    my ($table, $column, $new_def, $set_nulls_to) = @_;
-
-    my @statements;
-    my $old_def = $self->get_column_abstract($table, $column);
-    my $specific = $self->{db_specific};
-
-    # If the types have changed, we have to deal with that.
-    if (uc(trim($old_def->{TYPE})) ne uc(trim($new_def->{TYPE}))) {
-        push(@statements, $self->_get_alter_type_sql($table, $column, 
-                                                     $new_def, $old_def));
-    }
-
-    my $default = $new_def->{DEFAULT};
-    my $default_old = $old_def->{DEFAULT};
-
-    if (defined $default) {
-        $default = $specific->{$default} if exists $specific->{$default};
-    }
-    # This first condition prevents "uninitialized value" errors.
-    if (!defined $default && !defined $default_old) {
-        # Do Nothing
-    }
-    # If we went from having a default to not having one
-    elsif (!defined $default && defined $default_old) {
-        push(@statements, $self->get_drop_default_ddl($table, $column));
-    }
-    # If we went from no default to a default, or we changed the default.
-    elsif ( (defined $default && !defined $default_old) || 
-            ($default ne $default_old) ) 
-    {
-        push(@statements, "ALTER TABLE $table ALTER COLUMN $column "
-                         . " SET DEFAULT $default");
-    }
-
-    # If we went from NULL to NOT NULL.
-    if (!$old_def->{NOTNULL} && $new_def->{NOTNULL}) {
-        push(@statements, $self->_set_nulls_sql(@_));
-        push(@statements, "ALTER TABLE $table ALTER COLUMN $column"
-                        . " SET NOT NULL");
-    }
-    # If we went from NOT NULL to NULL
-    elsif ($old_def->{NOTNULL} && !$new_def->{NOTNULL}) {
-        push(@statements, "ALTER TABLE $table ALTER COLUMN $column"
-                        . " DROP NOT NULL");
-    }
-
-    # If we went from not being a PRIMARY KEY to being a PRIMARY KEY.
-    if (!$old_def->{PRIMARYKEY} && $new_def->{PRIMARYKEY}) {
-        push(@statements, "ALTER TABLE $table ADD PRIMARY KEY ($column)");
-    }
-    # If we went from being a PK to not being a PK
-    elsif ( $old_def->{PRIMARYKEY} && !$new_def->{PRIMARYKEY} ) {
-        push(@statements, "ALTER TABLE $table DROP PRIMARY KEY");
-    }
-
-    return @statements;
+  my $self = shift;
+  my ($table, $column, $new_def, $set_nulls_to) = @_;
+
+  my @statements;
+  my $old_def = $self->get_column_abstract($table, $column);
+  my $specific = $self->{db_specific};
+
+  # If the types have changed, we have to deal with that.
+  if (uc(trim($old_def->{TYPE})) ne uc(trim($new_def->{TYPE}))) {
+    push(@statements,
+      $self->_get_alter_type_sql($table, $column, $new_def, $old_def));
+  }
+
+  my $default     = $new_def->{DEFAULT};
+  my $default_old = $old_def->{DEFAULT};
+
+  if (defined $default) {
+    $default = $specific->{$default} if exists $specific->{$default};
+  }
+
+  # This first condition prevents "uninitialized value" errors.
+  if (!defined $default && !defined $default_old) {
+
+    # Do Nothing
+  }
+
+  # If we went from having a default to not having one
+  elsif (!defined $default && defined $default_old) {
+    push(@statements, $self->get_drop_default_ddl($table, $column));
+  }
+
+  # If we went from no default to a default, or we changed the default.
+  elsif ((defined $default && !defined $default_old)
+    || ($default ne $default_old))
+  {
+    push(@statements,
+      "ALTER TABLE $table ALTER COLUMN $column " . " SET DEFAULT $default");
+  }
+
+  # If we went from NULL to NOT NULL.
+  if (!$old_def->{NOTNULL} && $new_def->{NOTNULL}) {
+    push(@statements, $self->_set_nulls_sql(@_));
+    push(@statements, "ALTER TABLE $table ALTER COLUMN $column" . " SET NOT NULL");
+  }
+
+  # If we went from NOT NULL to NULL
+  elsif ($old_def->{NOTNULL} && !$new_def->{NOTNULL}) {
+    push(@statements, "ALTER TABLE $table ALTER COLUMN $column" . " DROP NOT NULL");
+  }
+
+  # If we went from not being a PRIMARY KEY to being a PRIMARY KEY.
+  if (!$old_def->{PRIMARYKEY} && $new_def->{PRIMARYKEY}) {
+    push(@statements, "ALTER TABLE $table ADD PRIMARY KEY ($column)");
+  }
+
+  # If we went from being a PK to not being a PK
+  elsif ($old_def->{PRIMARYKEY} && !$new_def->{PRIMARYKEY}) {
+    push(@statements, "ALTER TABLE $table DROP PRIMARY KEY");
+  }
+
+  return @statements;
 }
 
 # Helps handle any fields that were NULL before, if we have a default,
 # when doing an ALTER COLUMN.
 sub _set_nulls_sql {
-    my ($self, $table, $column, $new_def, $set_nulls_to) = @_;
-    my $default = $new_def->{DEFAULT};
-    # If we have a set_nulls_to, that overrides the DEFAULT 
-    # (although nobody would usually specify both a default and 
-    # a set_nulls_to.)
-    $default = $set_nulls_to if defined $set_nulls_to;
-    if (defined $default) {
-         my $specific = $self->{db_specific};
-         $default = $specific->{$default} if exists $specific->{$default};
-    }
-    my @sql;
-    if (defined $default) {
-        push(@sql, "UPDATE $table SET $column = $default"
-                . "  WHERE $column IS NULL");
-    }
-    return @sql;
+  my ($self, $table, $column, $new_def, $set_nulls_to) = @_;
+  my $default = $new_def->{DEFAULT};
+
+  # If we have a set_nulls_to, that overrides the DEFAULT
+  # (although nobody would usually specify both a default and
+  # a set_nulls_to.)
+  $default = $set_nulls_to if defined $set_nulls_to;
+  if (defined $default) {
+    my $specific = $self->{db_specific};
+    $default = $specific->{$default} if exists $specific->{$default};
+  }
+  my @sql;
+  if (defined $default) {
+    push(@sql, "UPDATE $table SET $column = $default" . "  WHERE $column IS NULL");
+  }
+  return @sql;
 }
 
 sub get_drop_index_ddl {
@@ -2522,11 +2489,11 @@ sub get_drop_index_ddl {
 
 =cut
 
-    my ($self, $table, $name) = @_;
+  my ($self, $table, $name) = @_;
 
-    # Although ANSI SQL-92 doesn't specify a method of dropping an index,
-    # many DBs support this syntax.
-    return ("DROP INDEX $name");
+  # Although ANSI SQL-92 doesn't specify a method of dropping an index,
+  # many DBs support this syntax.
+  return ("DROP INDEX $name");
 }
 
 sub get_drop_column_ddl {
@@ -2540,8 +2507,8 @@ sub get_drop_column_ddl {
 
 =cut
 
-    my ($self, $table, $column) = @_;
-    return ("ALTER TABLE $table DROP COLUMN $column");
+  my ($self, $table, $column) = @_;
+  return ("ALTER TABLE $table DROP COLUMN $column");
 }
 
 =item C<get_drop_table_ddl($table)>
@@ -2553,8 +2520,8 @@ sub get_drop_column_ddl {
 =cut
 
 sub get_drop_table_ddl {
-    my ($self, $table) = @_;
-    return ("DROP TABLE $table");
+  my ($self, $table) = @_;
+  return ("DROP TABLE $table");
 }
 
 sub get_rename_column_ddl {
@@ -2572,8 +2539,8 @@ sub get_rename_column_ddl {
 
 =cut
 
-    die "ANSI SQL has no way to rename a column, and your database driver\n"
-        . " has not implemented a method.";
+  die "ANSI SQL has no way to rename a column, and your database driver\n"
+    . " has not implemented a method.";
 }
 
 
@@ -2603,8 +2570,8 @@ Gets SQL to rename a table in the database.
 
 =cut
 
-    my ($self, $old_name, $new_name) = @_;
-    return ("ALTER TABLE $old_name RENAME TO $new_name");
+  my ($self, $old_name, $new_name) = @_;
+  return ("ALTER TABLE $old_name RENAME TO $new_name");
 }
 
 =item C<delete_table($name)>
@@ -2617,13 +2584,13 @@ Gets SQL to rename a table in the database.
 =cut
 
 sub delete_table {
-    my ($self, $name) = @_;
+  my ($self, $name) = @_;
 
-    die "Attempted to delete nonexistent table '$name'." unless
-        $self->get_table_abstract($name);
+  die "Attempted to delete nonexistent table '$name'."
+    unless $self->get_table_abstract($name);
 
-    delete $self->{abstract_schema}->{$name};
-    delete $self->{schema}->{$name};
+  delete $self->{abstract_schema}->{$name};
+  delete $self->{schema}->{$name};
 }
 
 sub get_column_abstract {
@@ -2640,15 +2607,15 @@ sub get_column_abstract {
 
 =cut
 
-    my ($self, $table, $column) = @_;
+  my ($self, $table, $column) = @_;
 
-    # Prevent a possible dereferencing of an undef hash, if the
-    # table doesn't exist.
-    if ($self->get_table_abstract($table)) {
-        my %fields = (@{ $self->{abstract_schema}{$table}{FIELDS} });
-        return $fields{$column};
-    }
-    return undef;
+  # Prevent a possible dereferencing of an undef hash, if the
+  # table doesn't exist.
+  if ($self->get_table_abstract($table)) {
+    my %fields = (@{$self->{abstract_schema}{$table}{FIELDS}});
+    return $fields{$column};
+  }
+  return undef;
 }
 
 =item C<get_indexes_on_column_abstract($table, $column)>
@@ -2666,29 +2633,31 @@ sub get_column_abstract {
 =cut
 
 sub get_indexes_on_column_abstract {
-    my ($self, $table, $column) = @_;
-    my %ret_hash;
-
-    my $table_def = $self->get_table_abstract($table);
-    if ($table_def && exists $table_def->{INDEXES}) {
-        my %indexes = (@{ $table_def->{INDEXES} });
-        foreach my $index_name (keys %indexes) {
-            my $col_list;
-            # Get the column list, depending on whether the index
-            # is in hashref or arrayref format.
-            if (ref($indexes{$index_name}) eq 'HASH') {
-                $col_list = $indexes{$index_name}->{FIELDS};
-            } else {
-                $col_list = $indexes{$index_name};
-            }
-
-            if(grep($_ eq $column, @$col_list)) {
-                $ret_hash{$index_name} = dclone($indexes{$index_name});
-            }
-        }
+  my ($self, $table, $column) = @_;
+  my %ret_hash;
+
+  my $table_def = $self->get_table_abstract($table);
+  if ($table_def && exists $table_def->{INDEXES}) {
+    my %indexes = (@{$table_def->{INDEXES}});
+    foreach my $index_name (keys %indexes) {
+      my $col_list;
+
+      # Get the column list, depending on whether the index
+      # is in hashref or arrayref format.
+      if (ref($indexes{$index_name}) eq 'HASH') {
+        $col_list = $indexes{$index_name}->{FIELDS};
+      }
+      else {
+        $col_list = $indexes{$index_name};
+      }
+
+      if (grep($_ eq $column, @$col_list)) {
+        $ret_hash{$index_name} = dclone($indexes{$index_name});
+      }
     }
+  }
 
-    return %ret_hash;
+  return %ret_hash;
 }
 
 sub get_index_abstract {
@@ -2704,16 +2673,16 @@ sub get_index_abstract {
 
 =cut
 
-    my ($self, $table, $index) = @_;
+  my ($self, $table, $index) = @_;
 
-    # Prevent a possible dereferencing of an undef hash, if the
-    # table doesn't exist.
-    my $index_table = $self->get_table_abstract($table);
-    if ($index_table && exists $index_table->{INDEXES}) {
-        my %indexes = (@{ $index_table->{INDEXES} });
-        return $indexes{$index};
-    }
-    return undef;
+  # Prevent a possible dereferencing of an undef hash, if the
+  # table doesn't exist.
+  my $index_table = $self->get_table_abstract($table);
+  if ($index_table && exists $index_table->{INDEXES}) {
+    my %indexes = (@{$index_table->{INDEXES}});
+    return $indexes{$index};
+  }
+  return undef;
 }
 
 =item C<get_table_abstract($table)>
@@ -2727,8 +2696,8 @@ sub get_index_abstract {
 =cut
 
 sub get_table_abstract {
-    my ($self, $table) = @_;
-    return $self->{abstract_schema}->{$table};
+  my ($self, $table) = @_;
+  return $self->{abstract_schema}->{$table};
 }
 
 =item C<add_table($name, \%definition)>
@@ -2744,22 +2713,20 @@ sub get_table_abstract {
 =cut
 
 sub add_table {
-    my ($self, $name, $definition) = @_;
-    (die "Table already exists: $name")
-        if exists $self->{abstract_schema}->{$name};
-    if ($definition) {
-        $self->{abstract_schema}->{$name} = dclone($definition);
-        $self->{schema} = dclone($self->{abstract_schema});
-        $self->_adjust_schema();
-    }
-    else {
-        $self->{abstract_schema}->{$name} = {FIELDS => []};
-        $self->{schema}->{$name}          = {FIELDS => []};
-    }
+  my ($self, $name, $definition) = @_;
+  (die "Table already exists: $name") if exists $self->{abstract_schema}->{$name};
+  if ($definition) {
+    $self->{abstract_schema}->{$name} = dclone($definition);
+    $self->{schema} = dclone($self->{abstract_schema});
+    $self->_adjust_schema();
+  }
+  else {
+    $self->{abstract_schema}->{$name} = {FIELDS => []};
+    $self->{schema}->{$name}          = {FIELDS => []};
+  }
 }
 
 
-
 sub rename_table {
 
 =item C<rename_table>
@@ -2769,10 +2736,10 @@ Renames a table from C<$old_name> to C<$new_name> in this Schema object.
 =cut
 
 
-    my ($self, $old_name, $new_name) = @_;
-    my $table = $self->get_table_abstract($old_name);
-    $self->delete_table($old_name);
-    $self->add_table($new_name, $table);
+  my ($self, $old_name, $new_name) = @_;
+  my $table = $self->get_table_abstract($old_name);
+  $self->delete_table($old_name);
+  $self->add_table($new_name, $table);
 }
 
 sub delete_column {
@@ -2787,17 +2754,18 @@ sub delete_column {
 
 =cut
 
-    my ($self, $table, $column) = @_;
+  my ($self, $table, $column) = @_;
 
-    my $abstract_fields = $self->{abstract_schema}{$table}{FIELDS};
-    my $name_position = firstidx { $_ eq $column } @$abstract_fields;
-    die "Attempted to delete nonexistent column ${table}.${column}" 
-        if $name_position == -1;
-    # Delete the key/value pair from the array.
-    splice(@$abstract_fields, $name_position, 2);
+  my $abstract_fields = $self->{abstract_schema}{$table}{FIELDS};
+  my $name_position = firstidx { $_ eq $column } @$abstract_fields;
+  die "Attempted to delete nonexistent column ${table}.${column}"
+    if $name_position == -1;
 
-    $self->{schema} = dclone($self->{abstract_schema});
-    $self->_adjust_schema();
+  # Delete the key/value pair from the array.
+  splice(@$abstract_fields, $name_position, 2);
+
+  $self->{schema} = dclone($self->{abstract_schema});
+  $self->_adjust_schema();
 }
 
 sub rename_column {
@@ -2813,11 +2781,11 @@ sub rename_column {
 
 =cut
 
-    my ($self, $table, $old_name, $new_name) = @_;
-    my $def = $self->get_column_abstract($table, $old_name);
-    die "Renaming a column that doesn't exist" if !$def;
-    $self->delete_column($table, $old_name);
-    $self->set_column($table, $new_name, $def);
+  my ($self, $table, $old_name, $new_name) = @_;
+  my $def = $self->get_column_abstract($table, $old_name);
+  die "Renaming a column that doesn't exist" if !$def;
+  $self->delete_column($table, $old_name);
+  $self->set_column($table, $new_name, $def);
 }
 
 sub set_column {
@@ -2838,10 +2806,10 @@ sub set_column {
 
 =cut
 
-    my ($self, $table, $column, $new_def) = @_;
+  my ($self, $table, $column, $new_def) = @_;
 
-    my $fields = $self->{abstract_schema}{$table}{FIELDS};
-    $self->_set_object($table, $column, $new_def, $fields);
+  my $fields = $self->{abstract_schema}{$table}{FIELDS};
+  $self->_set_object($table, $column, $new_def, $fields);
 }
 
 =item C<set_fk($table, $column \%fk_def)>
@@ -2851,19 +2819,20 @@ Sets the C<REFERENCES> item on the specified column.
 =cut
 
 sub set_fk {
-    my ($self, $table, $column, $fk_def) = @_;
-    # Don't want to modify the source def before we explicitly set it below.
-    # This is just us being extra-cautious.
-    my $column_def = dclone($self->get_column_abstract($table, $column));
-    die "Tried to set an fk on $table.$column, but that column doesn't exist"
-        if !$column_def;
-    if ($fk_def) {
-        $column_def->{REFERENCES} = $fk_def;
-    }
-    else {
-        delete $column_def->{REFERENCES};
-    }
-    $self->set_column($table, $column, $column_def);
+  my ($self, $table, $column, $fk_def) = @_;
+
+  # Don't want to modify the source def before we explicitly set it below.
+  # This is just us being extra-cautious.
+  my $column_def = dclone($self->get_column_abstract($table, $column));
+  die "Tried to set an fk on $table.$column, but that column doesn't exist"
+    if !$column_def;
+  if ($fk_def) {
+    $column_def->{REFERENCES} = $fk_def;
+  }
+  else {
+    delete $column_def->{REFERENCES};
+  }
+  $self->set_column($table, $column, $column_def);
 }
 
 sub set_index {
@@ -2884,36 +2853,39 @@ sub set_index {
 
 =cut
 
-    my ($self, $table, $name, $definition) = @_;
+  my ($self, $table, $name, $definition) = @_;
 
-    if ( exists $self->{abstract_schema}{$table}
-         && !exists $self->{abstract_schema}{$table}{INDEXES} ) {
-        $self->{abstract_schema}{$table}{INDEXES} = [];
-    }
+  if (exists $self->{abstract_schema}{$table}
+    && !exists $self->{abstract_schema}{$table}{INDEXES})
+  {
+    $self->{abstract_schema}{$table}{INDEXES} = [];
+  }
 
-    my $indexes = $self->{abstract_schema}{$table}{INDEXES};
-    $self->_set_object($table, $name, $definition, $indexes);
+  my $indexes = $self->{abstract_schema}{$table}{INDEXES};
+  $self->_set_object($table, $name, $definition, $indexes);
 }
 
 # A private helper for set_index and set_column.
 # This does the actual "work" of those two functions.
 # $array_to_change is an arrayref.
 sub _set_object {
-    my ($self, $table, $name, $definition, $array_to_change) = @_;
+  my ($self, $table, $name, $definition, $array_to_change) = @_;
 
-    my $obj_position = (firstidx { $_ eq $name } @$array_to_change) + 1;
-    # If the object doesn't exist, then add it.
-    if (!$obj_position) {
-        push(@$array_to_change, $name);
-        push(@$array_to_change, $definition);
-    }
-    # We're modifying an existing object in the Schema.
-    else {
-        splice(@$array_to_change, $obj_position, 1, $definition);
-    }
+  my $obj_position = (firstidx { $_ eq $name } @$array_to_change) + 1;
 
-    $self->{schema} = dclone($self->{abstract_schema});
-    $self->_adjust_schema();
+  # If the object doesn't exist, then add it.
+  if (!$obj_position) {
+    push(@$array_to_change, $name);
+    push(@$array_to_change, $definition);
+  }
+
+  # We're modifying an existing object in the Schema.
+  else {
+    splice(@$array_to_change, $obj_position, 1, $definition);
+  }
+
+  $self->{schema} = dclone($self->{abstract_schema});
+  $self->_adjust_schema();
 }
 
 =item C<delete_index($table, $name)>
@@ -2931,16 +2903,17 @@ sub _set_object {
 =cut
 
 sub delete_index {
-    my ($self, $table, $name) = @_;
-
-    my $indexes = $self->{abstract_schema}{$table}{INDEXES};
-    my $name_position = firstidx { $_ eq $name } @$indexes;
-    die "Attempted to delete nonexistent index $name on the $table table" 
-        if $name_position == -1;
-    # Delete the key/value pair from the array.
-    splice(@$indexes, $name_position, 2);
-    $self->{schema} = dclone($self->{abstract_schema});
-    $self->_adjust_schema();
+  my ($self, $table, $name) = @_;
+
+  my $indexes = $self->{abstract_schema}{$table}{INDEXES};
+  my $name_position = firstidx { $_ eq $name } @$indexes;
+  die "Attempted to delete nonexistent index $name on the $table table"
+    if $name_position == -1;
+
+  # Delete the key/value pair from the array.
+  splice(@$indexes, $name_position, 2);
+  $self->{schema} = dclone($self->{abstract_schema});
+  $self->_adjust_schema();
 }
 
 sub columns_equal {
@@ -2958,24 +2931,24 @@ sub columns_equal {
 
 =cut
 
-    my $self = shift;
-    my $col_one = dclone(shift);
-    my $col_two = dclone(shift);
+  my $self    = shift;
+  my $col_one = dclone(shift);
+  my $col_two = dclone(shift);
 
-    $col_one->{TYPE} = uc($col_one->{TYPE});
-    $col_two->{TYPE} = uc($col_two->{TYPE});
+  $col_one->{TYPE} = uc($col_one->{TYPE});
+  $col_two->{TYPE} = uc($col_two->{TYPE});
 
-    # We don't care about foreign keys when comparing column definitions.
-    delete $col_one->{REFERENCES};
-    delete $col_two->{REFERENCES};
+  # We don't care about foreign keys when comparing column definitions.
+  delete $col_one->{REFERENCES};
+  delete $col_two->{REFERENCES};
 
-    my @col_one_array = %$col_one;
-    my @col_two_array = %$col_two;
+  my @col_one_array = %$col_one;
+  my @col_two_array = %$col_two;
 
-    my ($removed, $added) = diff_arrays(\@col_one_array, \@col_two_array);
+  my ($removed, $added) = diff_arrays(\@col_one_array, \@col_two_array);
 
-    # If there are no differences between the arrays, then they are equal.
-    return !scalar(@$removed) && !scalar(@$added) ? 1 : 0;
+  # If there are no differences between the arrays, then they are equal.
+  return !scalar(@$removed) && !scalar(@$added) ? 1 : 0;
 }
 
 
@@ -2999,18 +2972,18 @@ sub columns_equal {
 =cut
 
 sub serialize_abstract {
-    my ($self) = @_;
-    
-    # Make it ok to eval
-    local $Data::Dumper::Purity = 1;
-    
-    # Avoid cross-refs
-    local $Data::Dumper::Deepcopy = 1;
-    
-    # Always sort keys to allow textual compare
-    local $Data::Dumper::Sortkeys = 1;
-    
-    return Dumper($self->{abstract_schema});
+  my ($self) = @_;
+
+  # Make it ok to eval
+  local $Data::Dumper::Purity = 1;
+
+  # Avoid cross-refs
+  local $Data::Dumper::Deepcopy = 1;
+
+  # Always sort keys to allow textual compare
+  local $Data::Dumper::Sortkeys = 1;
+
+  return Dumper($self->{abstract_schema});
 }
 
 =item C<deserialize_abstract($serialized, $version)>
@@ -3029,36 +3002,34 @@ sub serialize_abstract {
 =cut
 
 sub deserialize_abstract {
-    my ($class, $serialized, $version) = @_;
-
-    my $thawed_hash;
-    if ($version < 2) {
-        $thawed_hash = thaw($serialized);
-    }
-    else {
-        my $cpt = new Safe;
-        $cpt->reval($serialized) ||
-            die "Unable to restore cached schema: " . $@;
-        $thawed_hash = ${$cpt->varglob('VAR1')};
-    }
-
-    # Version 2 didn't have the "created" key for REFERENCES items.
-    if ($version < 3) {
-        my $standard = $class->new()->{abstract_schema};
-        foreach my $table_name (keys %$thawed_hash) {
-            my %standard_fields = 
-                @{ $standard->{$table_name}->{FIELDS} || [] };
-            my $table = $thawed_hash->{$table_name};
-            my %fields = @{ $table->{FIELDS} || [] };
-            while (my ($field, $def) = each %fields) {
-                if (exists $def->{REFERENCES}) {
-                    $def->{REFERENCES}->{created} = 1;
-                }
-            }
+  my ($class, $serialized, $version) = @_;
+
+  my $thawed_hash;
+  if ($version < 2) {
+    $thawed_hash = thaw($serialized);
+  }
+  else {
+    my $cpt = new Safe;
+    $cpt->reval($serialized) || die "Unable to restore cached schema: " . $@;
+    $thawed_hash = ${$cpt->varglob('VAR1')};
+  }
+
+  # Version 2 didn't have the "created" key for REFERENCES items.
+  if ($version < 3) {
+    my $standard = $class->new()->{abstract_schema};
+    foreach my $table_name (keys %$thawed_hash) {
+      my %standard_fields = @{$standard->{$table_name}->{FIELDS} || []};
+      my $table           = $thawed_hash->{$table_name};
+      my %fields          = @{$table->{FIELDS} || []};
+      while (my ($field, $def) = each %fields) {
+        if (exists $def->{REFERENCES}) {
+          $def->{REFERENCES}->{created} = 1;
         }
+      }
     }
+  }
 
-    return $class->new(undef, $thawed_hash);
+  return $class->new(undef, $thawed_hash);
 }
 
 #####################################################################
@@ -3086,8 +3057,8 @@ object.
 =cut
 
 sub get_empty_schema {
-    my ($class) = @_;
-    return $class->deserialize_abstract(Dumper({}), SCHEMA_VERSION);
+  my ($class) = @_;
+  return $class->deserialize_abstract(Dumper({}), SCHEMA_VERSION);
 }
 
 1;
index 59066bdc492c67d4730f32c5c8149ef76103eda6..a6399430d3aee1c8ace5cc49caf12be703695faf 100644 (file)
@@ -21,7 +21,7 @@ use Bugzilla::Error;
 
 use parent qw(Bugzilla::DB::Schema);
 
-# This is for column_info_to_column, to know when a tinyint is a 
+# This is for column_info_to_column, to know when a tinyint is a
 # boolean and when it's really a tinyint. This only has to be accurate
 # up to and through 2.19.3, because that's the only time we need
 # column_info_to_column.
@@ -30,50 +30,59 @@ use parent qw(Bugzilla::DB::Schema);
 # that should be interpreted as a BOOLEAN instead of as an INT1 when
 # reading in the Schema from the disk. The values are discarded; I just
 # used "1" for simplicity.
-# 
+#
 # THIS CONSTANT IS ONLY USED FOR UPGRADES FROM 2.18 OR EARLIER. DON'T
 # UPDATE IT TO MODERN COLUMN NAMES OR DEFINITIONS.
 use constant BOOLEAN_MAP => {
-    bugs           => {everconfirmed => 1, reporter_accessible => 1,
-                       cclist_accessible => 1, qacontact_accessible => 1,
-                       assignee_accessible => 1},
-    longdescs      => {isprivate => 1, already_wrapped => 1},
-    attachments    => {ispatch => 1, isobsolete => 1, isprivate => 1},
-    flags          => {is_active => 1},
-    flagtypes      => {is_active => 1, is_requestable => 1, 
-                       is_requesteeble => 1, is_multiplicable => 1},
-    fielddefs      => {mailhead => 1, obsolete => 1},
-    bug_status     => {isactive => 1},
-    resolution     => {isactive => 1},
-    bug_severity   => {isactive => 1},
-    priority       => {isactive => 1},
-    rep_platform   => {isactive => 1},
-    op_sys         => {isactive => 1},
-    profiles       => {mybugslink => 1, newemailtech => 1},
-    namedqueries   => {linkinfooter => 1, watchfordiffs => 1},
-    groups         => {isbuggroup => 1, isactive => 1},
-    group_control_map => {entry => 1, membercontrol => 1, othercontrol => 1,
-                          canedit => 1},
-    group_group_map => {isbless => 1},
-    user_group_map => {isbless => 1, isderived => 1},
-    products       => {disallownew => 1},
-    series         => {public => 1},
-    whine_queries  => {onemailperbug => 1},
-    quips          => {approved => 1},
-    setting        => {is_enabled => 1}
+  bugs => {
+    everconfirmed        => 1,
+    reporter_accessible  => 1,
+    cclist_accessible    => 1,
+    qacontact_accessible => 1,
+    assignee_accessible  => 1
+  },
+  longdescs   => {isprivate => 1, already_wrapped => 1},
+  attachments => {ispatch   => 1, isobsolete      => 1, isprivate => 1},
+  flags     => {is_active => 1},
+  flagtypes => {
+    is_active        => 1,
+    is_requestable   => 1,
+    is_requesteeble  => 1,
+    is_multiplicable => 1
+  },
+  fielddefs    => {mailhead     => 1, obsolete => 1},
+  bug_status   => {isactive     => 1},
+  resolution   => {isactive     => 1},
+  bug_severity => {isactive     => 1},
+  priority     => {isactive     => 1},
+  rep_platform => {isactive     => 1},
+  op_sys       => {isactive     => 1},
+  profiles     => {mybugslink   => 1, newemailtech => 1},
+  namedqueries => {linkinfooter => 1, watchfordiffs => 1},
+  groups       => {isbuggroup   => 1, isactive => 1},
+  group_control_map =>
+    {entry => 1, membercontrol => 1, othercontrol => 1, canedit => 1},
+  group_group_map => {isbless       => 1},
+  user_group_map  => {isbless       => 1, isderived => 1},
+  products        => {disallownew   => 1},
+  series          => {public        => 1},
+  whine_queries   => {onemailperbug => 1},
+  quips           => {approved      => 1},
+  setting         => {is_enabled    => 1}
 };
 
 # Maps the db_specific hash backwards, for use in column_info_to_column.
 use constant REVERSE_MAPPING => {
-    # Boolean and the SERIAL fields are handled in column_info_to_column,
-    # and so don't have an entry here.
-    TINYINT   => 'INT1',
-    SMALLINT  => 'INT2',
-    MEDIUMINT => 'INT3',
-    INTEGER   => 'INT4',
-
-    # All the other types have the same name in their abstract version
-    # as in their db-specific version, so no reverse mapping is needed.
+
+  # Boolean and the SERIAL fields are handled in column_info_to_column,
+  # and so don't have an entry here.
+  TINYINT   => 'INT1',
+  SMALLINT  => 'INT2',
+  MEDIUMINT => 'INT3',
+  INTEGER   => 'INT4',
+
+  # All the other types have the same name in their abstract version
+  # as in their db-specific version, so no reverse mapping is needed.
 };
 
 use constant MYISAM_TABLES => qw(bugs_fulltext);
@@ -81,176 +90,190 @@ use constant MYISAM_TABLES => qw(bugs_fulltext);
 #------------------------------------------------------------------------------
 sub _initialize {
 
-    my $self = shift;
+  my $self = shift;
+
+  $self = $self->SUPER::_initialize(@_);
 
-    $self = $self->SUPER::_initialize(@_);
+  $self->{db_specific} = {
 
-    $self->{db_specific} = {
+    BOOLEAN => 'tinyint',
+    FALSE   => '0',
+    TRUE    => '1',
 
-        BOOLEAN =>      'tinyint',
-        FALSE =>        '0', 
-        TRUE =>         '1',
+    INT1 => 'tinyint',
+    INT2 => 'smallint',
+    INT3 => 'mediumint',
+    INT4 => 'integer',
 
-        INT1 =>         'tinyint',
-        INT2 =>         'smallint',
-        INT3 =>         'mediumint',
-        INT4 =>         'integer',
+    SMALLSERIAL  => 'smallint auto_increment',
+    MEDIUMSERIAL => 'mediumint auto_increment',
+    INTSERIAL    => 'integer auto_increment',
 
-        SMALLSERIAL =>  'smallint auto_increment',
-        MEDIUMSERIAL => 'mediumint auto_increment',
-        INTSERIAL =>    'integer auto_increment',
+    TINYTEXT   => 'tinytext',
+    MEDIUMTEXT => 'mediumtext',
+    LONGTEXT   => 'mediumtext',
 
-        TINYTEXT =>     'tinytext',
-        MEDIUMTEXT =>   'mediumtext',
-        LONGTEXT =>     'mediumtext',
+    LONGBLOB => 'longblob',
 
-        LONGBLOB =>     'longblob',
+    DATETIME => 'datetime',
+    DATE     => 'date',
+  };
 
-        DATETIME =>     'datetime',
-        DATE     =>     'date',
-    };
+  $self->_adjust_schema;
 
-    $self->_adjust_schema;
+  return $self;
 
-    return $self;
+}    #eosub--_initialize
 
-} #eosub--_initialize
 #------------------------------------------------------------------------------
 sub _get_create_table_ddl {
-    # Extend superclass method to specify the MYISAM storage engine.
-    # Returns a "create table" SQL statement.
 
-    my($self, $table) = @_;
+  # Extend superclass method to specify the MYISAM storage engine.
+  # Returns a "create table" SQL statement.
 
-    my $charset = Bugzilla->dbh->bz_db_is_utf8 ? "CHARACTER SET utf8" : '';
-    my $type    = grep($_ eq $table, MYISAM_TABLES) ? 'MYISAM' : 'InnoDB';
-    return($self->SUPER::_get_create_table_ddl($table) 
-           . " ENGINE = $type $charset");
+  my ($self, $table) = @_;
+
+  my $charset = Bugzilla->dbh->bz_db_is_utf8 ? "CHARACTER SET utf8" : '';
+  my $type = grep($_ eq $table, MYISAM_TABLES) ? 'MYISAM' : 'InnoDB';
+  return (
+    $self->SUPER::_get_create_table_ddl($table) . " ENGINE = $type $charset");
+
+}    #eosub--_get_create_table_ddl
 
-} #eosub--_get_create_table_ddl
 #------------------------------------------------------------------------------
 sub _get_create_index_ddl {
-    # Extend superclass method to create FULLTEXT indexes on text fields.
-    # Returns a "create index" SQL statement.
 
-    my($self, $table_name, $index_name, $index_fields, $index_type) = @_;
+  # Extend superclass method to create FULLTEXT indexes on text fields.
+  # Returns a "create index" SQL statement.
+
+  my ($self, $table_name, $index_name, $index_fields, $index_type) = @_;
+
+  my $sql = "CREATE ";
+  $sql .= "$index_type "
+    if ($index_type eq 'UNIQUE' || $index_type eq 'FULLTEXT');
+  $sql .= "INDEX \`$index_name\` ON $table_name \("
+    . join(", ", @$index_fields) . "\)";
 
-    my $sql = "CREATE ";
-    $sql .= "$index_type " if ($index_type eq 'UNIQUE'
-                               || $index_type eq 'FULLTEXT');
-    $sql .= "INDEX \`$index_name\` ON $table_name \(" .
-      join(", ", @$index_fields) . "\)";
+  return ($sql);
 
-    return($sql);
+}    #eosub--_get_create_index_ddl
 
-} #eosub--_get_create_index_ddl
 #--------------------------------------------------------------------
 
 sub get_create_database_sql {
-    my ($self, $name) = @_;
-    return ("CREATE DATABASE `$name` CHARACTER SET utf8");
+  my ($self, $name) = @_;
+  return ("CREATE DATABASE `$name` CHARACTER SET utf8");
 }
 
 # MySQL has a simpler ALTER TABLE syntax than ANSI.
 sub get_alter_column_ddl {
-    my ($self, $table, $column, $new_def, $set_nulls_to) = @_;
-    my $old_def = $self->get_column($table, $column);
-    my %new_def_copy = %$new_def;
-    if ($old_def->{PRIMARYKEY} && $new_def->{PRIMARYKEY}) {
-        # If a column stays a primary key do NOT specify PRIMARY KEY in the
-        # ALTER TABLE statement. This avoids a MySQL error that two primary
-        # keys are not allowed.
-        delete $new_def_copy{PRIMARYKEY};
-    }
-
-    my @statements;
-
-    push(@statements, "UPDATE $table SET $column = $set_nulls_to
-                        WHERE $column IS NULL") if defined $set_nulls_to;
-
-    # Calling SET DEFAULT or DROP DEFAULT is *way* faster than calling
-    # CHANGE COLUMN, so just do that if we're just changing the default.
-    my %old_defaultless = %$old_def;
-    my %new_defaultless = %$new_def;
-    delete $old_defaultless{DEFAULT};
-    delete $new_defaultless{DEFAULT};
-    if (!$self->columns_equal($old_def, $new_def)
-        && $self->columns_equal(\%new_defaultless, \%old_defaultless)) 
-    {
-        if (!defined $new_def->{DEFAULT}) {
-            push(@statements,
-                 "ALTER TABLE $table ALTER COLUMN $column DROP DEFAULT");
-        }
-        else {
-            push(@statements, "ALTER TABLE $table ALTER COLUMN $column
-                               SET DEFAULT " . $new_def->{DEFAULT});
-        }
+  my ($self, $table, $column, $new_def, $set_nulls_to) = @_;
+  my $old_def = $self->get_column($table, $column);
+  my %new_def_copy = %$new_def;
+  if ($old_def->{PRIMARYKEY} && $new_def->{PRIMARYKEY}) {
+
+    # If a column stays a primary key do NOT specify PRIMARY KEY in the
+    # ALTER TABLE statement. This avoids a MySQL error that two primary
+    # keys are not allowed.
+    delete $new_def_copy{PRIMARYKEY};
+  }
+
+  my @statements;
+
+  push(
+    @statements, "UPDATE $table SET $column = $set_nulls_to
+                        WHERE $column IS NULL"
+  ) if defined $set_nulls_to;
+
+  # Calling SET DEFAULT or DROP DEFAULT is *way* faster than calling
+  # CHANGE COLUMN, so just do that if we're just changing the default.
+  my %old_defaultless = %$old_def;
+  my %new_defaultless = %$new_def;
+  delete $old_defaultless{DEFAULT};
+  delete $new_defaultless{DEFAULT};
+  if (!$self->columns_equal($old_def, $new_def)
+    && $self->columns_equal(\%new_defaultless, \%old_defaultless))
+  {
+    if (!defined $new_def->{DEFAULT}) {
+      push(@statements, "ALTER TABLE $table ALTER COLUMN $column DROP DEFAULT");
     }
     else {
-        my $new_ddl = $self->get_type_ddl(\%new_def_copy);
-        push(@statements, "ALTER TABLE $table CHANGE COLUMN 
-                       $column $column $new_ddl");
-    }
-
-    if ($old_def->{PRIMARYKEY} && !$new_def->{PRIMARYKEY}) {
-        # Dropping a PRIMARY KEY needs an explicit DROP PRIMARY KEY
-        push(@statements, "ALTER TABLE $table DROP PRIMARY KEY");
+      push(
+        @statements, "ALTER TABLE $table ALTER COLUMN $column
+                               SET DEFAULT " . $new_def->{DEFAULT}
+      );
     }
-
-    return @statements;
+  }
+  else {
+    my $new_ddl = $self->get_type_ddl(\%new_def_copy);
+    push(
+      @statements, "ALTER TABLE $table CHANGE COLUMN 
+                       $column $column $new_ddl"
+    );
+  }
+
+  if ($old_def->{PRIMARYKEY} && !$new_def->{PRIMARYKEY}) {
+
+    # Dropping a PRIMARY KEY needs an explicit DROP PRIMARY KEY
+    push(@statements, "ALTER TABLE $table DROP PRIMARY KEY");
+  }
+
+  return @statements;
 }
 
 sub get_drop_fk_sql {
-    my ($self, $table, $column, $references) = @_;
-    my $fk_name = $self->_get_fk_name($table, $column, $references);
-    my @sql = ("ALTER TABLE $table DROP FOREIGN KEY $fk_name");
-    my $dbh = Bugzilla->dbh;
-
-    # MySQL requires, and will create, an index on any column with
-    # an FK. It will name it after the fk, which we never do.
-    # So if there's an index named after the fk, we also have to delete it. 
-    if ($dbh->bz_index_info_real($table, $fk_name)) {
-        push(@sql, $self->get_drop_index_ddl($table, $fk_name));
-    }
-
-    return @sql;
+  my ($self, $table, $column, $references) = @_;
+  my $fk_name = $self->_get_fk_name($table, $column, $references);
+  my @sql     = ("ALTER TABLE $table DROP FOREIGN KEY $fk_name");
+  my $dbh     = Bugzilla->dbh;
+
+  # MySQL requires, and will create, an index on any column with
+  # an FK. It will name it after the fk, which we never do.
+  # So if there's an index named after the fk, we also have to delete it.
+  if ($dbh->bz_index_info_real($table, $fk_name)) {
+    push(@sql, $self->get_drop_index_ddl($table, $fk_name));
+  }
+
+  return @sql;
 }
 
 sub get_drop_index_ddl {
-    my ($self, $table, $name) = @_;
-    return ("DROP INDEX \`$name\` ON $table");
+  my ($self, $table, $name) = @_;
+  return ("DROP INDEX \`$name\` ON $table");
 }
 
 # A special function for MySQL, for renaming a lot of indexes.
-# Index renames is a hash, where the key is a string - the 
+# Index renames is a hash, where the key is a string - the
 # old names of the index, and the value is a hash - the index
 # definition that we're renaming to, with an extra key of "NAME"
 # that contains the new index name.
 # The indexes in %indexes must be in hashref format.
 sub get_rename_indexes_ddl {
-    my ($self, $table, %indexes) = @_;
-    my @keys = keys %indexes or return ();
-
-    my $sql = "ALTER TABLE $table ";
-
-    foreach my $old_name (@keys) {
-        my $name = $indexes{$old_name}->{NAME};
-        my $type = $indexes{$old_name}->{TYPE};
-        $type ||= 'INDEX';
-        my $fields = join(',', @{$indexes{$old_name}->{FIELDS}});
-        # $old_name needs to be escaped, sometimes, because it was
-        # a reserved word.
-        $old_name = '`' . $old_name . '`';
-        $sql .= " ADD $type $name ($fields), DROP INDEX $old_name,";
-    }
-    # Remove the last comma.
-    chop($sql);
-    return ($sql);
+  my ($self, $table, %indexes) = @_;
+  my @keys = keys %indexes or return ();
+
+  my $sql = "ALTER TABLE $table ";
+
+  foreach my $old_name (@keys) {
+    my $name = $indexes{$old_name}->{NAME};
+    my $type = $indexes{$old_name}->{TYPE};
+    $type ||= 'INDEX';
+    my $fields = join(',', @{$indexes{$old_name}->{FIELDS}});
+
+    # $old_name needs to be escaped, sometimes, because it was
+    # a reserved word.
+    $old_name = '`' . $old_name . '`';
+    $sql .= " ADD $type $name ($fields), DROP INDEX $old_name,";
+  }
+
+  # Remove the last comma.
+  chop($sql);
+  return ($sql);
 }
 
 sub get_set_serial_sql {
-    my ($self, $table, $column, $value) = @_;
-    return ("ALTER TABLE $table AUTO_INCREMENT = $value");
+  my ($self, $table, $column, $value) = @_;
+  return ("ALTER TABLE $table AUTO_INCREMENT = $value");
 }
 
 # Converts a DBI column_info output to an abstract column definition.
@@ -258,124 +281,137 @@ sub get_set_serial_sql {
 # although there's a chance that it will also work properly if called
 # elsewhere.
 sub column_info_to_column {
-    my ($self, $column_info) = @_;
-
-    # Unfortunately, we have to break Schema's normal "no database"
-    # barrier a few times in this function.
-    my $dbh = Bugzilla->dbh;
-
-    my $table = $column_info->{TABLE_NAME};
-    my $col_name = $column_info->{COLUMN_NAME};
-
-    my $column = {};
-
-    ($column->{NOTNULL} = 1) if $column_info->{NULLABLE} == 0;
-
-    if ($column_info->{mysql_is_pri_key}) {
-        # In MySQL, if a table has no PK, but it has a UNIQUE index,
-        # that index will show up as the PK. So we have to eliminate
-        # that possibility.
-        # Unfortunately, the only way to definitely solve this is
-        # to break Schema's standard of not touching the live database
-        # and check if the index called PRIMARY is on that field.
-        my $pri_index = $dbh->bz_index_info_real($table, 'PRIMARY');
-        if ( $pri_index && grep($_ eq $col_name, @{$pri_index->{FIELDS}}) ) {
-            $column->{PRIMARYKEY} = 1;
-        }
-    }
+  my ($self, $column_info) = @_;
 
-    # MySQL frequently defines a default for a field even when we
-    # didn't explicitly set one. So we have to have some special
-    # hacks to determine whether or not we should actually put
-    # a default in the abstract schema for this field.
-    if (defined $column_info->{COLUMN_DEF}) {
-        # The defaults that MySQL inputs automatically are usually
-        # something that would be considered "false" by perl, either
-        # a 0 or an empty string. (Except for datetime and decimal
-        # fields, which have their own special auto-defaults.)
-        #
-        # Here's how we handle this: If it exists in the schema
-        # without a default, then we don't use the default. If it
-        # doesn't exist in the schema, then we're either going to
-        # be dropping it soon, or it's a custom end-user column, in which
-        # case having a bogus default won't harm anything.
-        my $schema_column = $self->get_column($table, $col_name);
-        unless ( (!$column_info->{COLUMN_DEF} 
-                  || $column_info->{COLUMN_DEF} eq '0000-00-00 00:00:00'
-                  || $column_info->{COLUMN_DEF} eq '0.00')
-                && $schema_column 
-                && !exists $schema_column->{DEFAULT}) {
-            
-            my $default = $column_info->{COLUMN_DEF};
-            # Schema uses '0' for the defaults for decimal fields. 
-            $default = 0 if $default =~ /^0\.0+$/;
-            # If we're not a number, we're a string and need to be
-            # quoted.
-            $default = $dbh->quote($default) if !($default =~ /^(-)?(\d+)(\.\d+)?$/a);
-            $column->{DEFAULT} = $default;
-        }
-    }
+  # Unfortunately, we have to break Schema's normal "no database"
+  # barrier a few times in this function.
+  my $dbh = Bugzilla->dbh;
 
-    my $type = $column_info->{TYPE_NAME};
+  my $table    = $column_info->{TABLE_NAME};
+  my $col_name = $column_info->{COLUMN_NAME};
 
-    # Certain types of columns need the size/precision appended.
-    if ($type =~ /CHAR$/ || $type eq 'DECIMAL') {
-        # This is nicely lowercase and has the size/precision appended.
-        $type = $column_info->{mysql_type_name};
-    }
+  my $column = {};
 
-    # If we're a tinyint, we could be either a BOOLEAN or an INT1.
-    # Only the BOOLEAN_MAP knows the difference.
-    elsif ($type eq 'TINYINT' && exists BOOLEAN_MAP->{$table}
-           && exists BOOLEAN_MAP->{$table}->{$col_name}) {
-        $type = 'BOOLEAN';
-        if (exists $column->{DEFAULT}) {
-            $column->{DEFAULT} = $column->{DEFAULT} ? 'TRUE' : 'FALSE';
-        }
-    }
+  ($column->{NOTNULL} = 1) if $column_info->{NULLABLE} == 0;
 
-    # We also need to check if we're an auto_increment field.
-    elsif ($type =~ /INT/) {
-        # Unfortunately, the only way to do this in DBI is to query the
-        # database, so we have to break the rule here that Schema normally
-        # doesn't touch the live DB.
-        my $ref_sth = $dbh->prepare(
-            "SELECT $col_name FROM $table LIMIT 1");
-        $ref_sth->execute;
-        if ($ref_sth->{mysql_is_auto_increment}->[0]) {
-            if ($type eq 'MEDIUMINT') {
-                $type = 'MEDIUMSERIAL';
-            }
-            elsif ($type eq 'SMALLINT') {
-                $type = 'SMALLSERIAL';
-            } 
-            else {
-                $type = 'INTSERIAL';
-            }
-        }
-        $ref_sth->finish;
+  if ($column_info->{mysql_is_pri_key}) {
 
+    # In MySQL, if a table has no PK, but it has a UNIQUE index,
+    # that index will show up as the PK. So we have to eliminate
+    # that possibility.
+    # Unfortunately, the only way to definitely solve this is
+    # to break Schema's standard of not touching the live database
+    # and check if the index called PRIMARY is on that field.
+    my $pri_index = $dbh->bz_index_info_real($table, 'PRIMARY');
+    if ($pri_index && grep($_ eq $col_name, @{$pri_index->{FIELDS}})) {
+      $column->{PRIMARYKEY} = 1;
     }
+  }
+
+  # MySQL frequently defines a default for a field even when we
+  # didn't explicitly set one. So we have to have some special
+  # hacks to determine whether or not we should actually put
+  # a default in the abstract schema for this field.
+  if (defined $column_info->{COLUMN_DEF}) {
+
+    # The defaults that MySQL inputs automatically are usually
+    # something that would be considered "false" by perl, either
+    # a 0 or an empty string. (Except for datetime and decimal
+    # fields, which have their own special auto-defaults.)
+    #
+    # Here's how we handle this: If it exists in the schema
+    # without a default, then we don't use the default. If it
+    # doesn't exist in the schema, then we're either going to
+    # be dropping it soon, or it's a custom end-user column, in which
+    # case having a bogus default won't harm anything.
+    my $schema_column = $self->get_column($table, $col_name);
+    unless (
+      (
+          !$column_info->{COLUMN_DEF}
+        || $column_info->{COLUMN_DEF} eq '0000-00-00 00:00:00'
+        || $column_info->{COLUMN_DEF} eq '0.00'
+      )
+      && $schema_column
+      && !exists $schema_column->{DEFAULT}
+      )
+    {
 
-    # For all other db-specific types, check if they exist in 
-    # REVERSE_MAPPING and use the type found there.
-    if (exists REVERSE_MAPPING->{$type}) {
-        $type = REVERSE_MAPPING->{$type};
+      my $default = $column_info->{COLUMN_DEF};
+
+      # Schema uses '0' for the defaults for decimal fields.
+      $default = 0 if $default =~ /^0\.0+$/;
+
+      # If we're not a number, we're a string and need to be
+      # quoted.
+      $default = $dbh->quote($default) if !($default =~ /^(-)?(\d+)(\.\d+)?$/a);
+      $column->{DEFAULT} = $default;
+    }
+  }
+
+  my $type = $column_info->{TYPE_NAME};
+
+  # Certain types of columns need the size/precision appended.
+  if ($type =~ /CHAR$/ || $type eq 'DECIMAL') {
+
+    # This is nicely lowercase and has the size/precision appended.
+    $type = $column_info->{mysql_type_name};
+  }
+
+  # If we're a tinyint, we could be either a BOOLEAN or an INT1.
+  # Only the BOOLEAN_MAP knows the difference.
+  elsif ($type eq 'TINYINT'
+    && exists BOOLEAN_MAP->{$table}
+    && exists BOOLEAN_MAP->{$table}->{$col_name})
+  {
+    $type = 'BOOLEAN';
+    if (exists $column->{DEFAULT}) {
+      $column->{DEFAULT} = $column->{DEFAULT} ? 'TRUE' : 'FALSE';
+    }
+  }
+
+  # We also need to check if we're an auto_increment field.
+  elsif ($type =~ /INT/) {
+
+    # Unfortunately, the only way to do this in DBI is to query the
+    # database, so we have to break the rule here that Schema normally
+    # doesn't touch the live DB.
+    my $ref_sth = $dbh->prepare("SELECT $col_name FROM $table LIMIT 1");
+    $ref_sth->execute;
+    if ($ref_sth->{mysql_is_auto_increment}->[0]) {
+      if ($type eq 'MEDIUMINT') {
+        $type = 'MEDIUMSERIAL';
+      }
+      elsif ($type eq 'SMALLINT') {
+        $type = 'SMALLSERIAL';
+      }
+      else {
+        $type = 'INTSERIAL';
+      }
     }
+    $ref_sth->finish;
 
-    $column->{TYPE} = $type;
+  }
 
-    #print "$table.$col_name: " . Data::Dumper->Dump([$column]) . "\n";
+  # For all other db-specific types, check if they exist in
+  # REVERSE_MAPPING and use the type found there.
+  if (exists REVERSE_MAPPING->{$type}) {
+    $type = REVERSE_MAPPING->{$type};
+  }
 
-    return $column;
+  $column->{TYPE} = $type;
+
+  #print "$table.$col_name: " . Data::Dumper->Dump([$column]) . "\n";
+
+  return $column;
 }
 
 sub get_rename_column_ddl {
-    my ($self, $table, $old_name, $new_name) = @_;
-    my $def = $self->get_type_ddl($self->get_column($table, $old_name));
-    # MySQL doesn't like having the PRIMARY KEY statement in a rename.
-    $def =~ s/PRIMARY KEY//i;
-    return ("ALTER TABLE $table CHANGE COLUMN $old_name $new_name $def");
+  my ($self, $table, $old_name, $new_name) = @_;
+  my $def = $self->get_type_ddl($self->get_column($table, $old_name));
+
+  # MySQL doesn't like having the PRIMARY KEY statement in a rename.
+  $def =~ s/PRIMARY KEY//i;
+  return ("ALTER TABLE $table CHANGE COLUMN $old_name $new_name $def");
 }
 
 1;
index 8d237d27e8a4aa7e4c585775f6e771aceb26e27a..92ffb65c2c0c1ef18957d4b0e95500e804f05060 100644 (file)
@@ -21,8 +21,9 @@ use parent qw(Bugzilla::DB::Schema);
 use Carp qw(confess);
 use Bugzilla::Util;
 
-use constant ADD_COLUMN => 'ADD';
+use constant ADD_COLUMN            => 'ADD';
 use constant MULTIPLE_FKS_IN_ALTER => 0;
+
 # Whether this is true or not, this is what it needs to be in order for
 # hash_identifier to maintain backwards compatibility with versions before
 # 3.2rc2.
@@ -31,123 +32,128 @@ use constant MAX_IDENTIFIER_LEN => 27;
 #------------------------------------------------------------------------------
 sub _initialize {
 
-    my $self = shift;
+  my $self = shift;
+
+  $self = $self->SUPER::_initialize(@_);
 
-    $self = $self->SUPER::_initialize(@_);
+  $self->{db_specific} = {
 
-    $self->{db_specific} = {
+    BOOLEAN => 'integer',
+    FALSE   => '0',
+    TRUE    => '1',
 
-        BOOLEAN =>      'integer',
-        FALSE =>        '0', 
-        TRUE =>         '1',
+    INT1 => 'integer',
+    INT2 => 'integer',
+    INT3 => 'integer',
+    INT4 => 'integer',
 
-        INT1 =>         'integer',
-        INT2 =>         'integer',
-        INT3 =>         'integer',
-        INT4 =>         'integer',
+    SMALLSERIAL  => 'integer',
+    MEDIUMSERIAL => 'integer',
+    INTSERIAL    => 'integer',
 
-        SMALLSERIAL  => 'integer',
-        MEDIUMSERIAL => 'integer',
-        INTSERIAL    => 'integer',
+    TINYTEXT   => 'varchar(255)',
+    MEDIUMTEXT => 'varchar(4000)',
+    LONGTEXT   => 'clob',
 
-        TINYTEXT   =>   'varchar(255)',
-        MEDIUMTEXT =>   'varchar(4000)',
-        LONGTEXT   =>   'clob',
+    LONGBLOB => 'blob',
 
-        LONGBLOB =>     'blob',
+    DATETIME => 'date',
+    DATE     => 'date',
+  };
 
-        DATETIME =>     'date',
-        DATE     =>     'date',
-    };
+  $self->_adjust_schema;
 
-    $self->_adjust_schema;
+  return $self;
 
-    return $self;
+}    #eosub--_initialize
 
-} #eosub--_initialize
 #--------------------------------------------------------------------
 
 sub get_table_ddl {
-    my $self = shift;
-    my $table = shift;
-    unshift @_, $table;
-    my @ddl = $self->SUPER::get_table_ddl(@_);
-
-    my @fields = @{ $self->{abstract_schema}{$table}{FIELDS} || [] };
-    while (@fields) {
-        my $field_name = shift @fields;
-        my $field_info = shift @fields;
-        # Create triggers to deal with empty string. 
-        if ( $field_info->{TYPE} =~ /varchar|TEXT/i 
-                && $field_info->{NOTNULL} ) {
-             push (@ddl, _get_notnull_trigger_ddl($table, $field_name));
-        }
-        # Create sequences and triggers to emulate SERIAL datatypes.
-        if ( $field_info->{TYPE} =~ /SERIAL/i ) {
-            push (@ddl, $self->_get_create_seq_ddl($table, $field_name));
-        }
+  my $self  = shift;
+  my $table = shift;
+  unshift @_, $table;
+  my @ddl = $self->SUPER::get_table_ddl(@_);
+
+  my @fields = @{$self->{abstract_schema}{$table}{FIELDS} || []};
+  while (@fields) {
+    my $field_name = shift @fields;
+    my $field_info = shift @fields;
+
+    # Create triggers to deal with empty string.
+    if ($field_info->{TYPE} =~ /varchar|TEXT/i && $field_info->{NOTNULL}) {
+      push(@ddl, _get_notnull_trigger_ddl($table, $field_name));
     }
-    return @ddl;
 
-} #eosub--get_table_ddl
+    # Create sequences and triggers to emulate SERIAL datatypes.
+    if ($field_info->{TYPE} =~ /SERIAL/i) {
+      push(@ddl, $self->_get_create_seq_ddl($table, $field_name));
+    }
+  }
+  return @ddl;
 
-# Extend superclass method to create Oracle Text indexes if index type 
+}    #eosub--get_table_ddl
+
+# Extend superclass method to create Oracle Text indexes if index type
 # is FULLTEXT from schema. Returns a "create index" SQL statement.
 sub _get_create_index_ddl {
 
-    my ($self, $table_name, $index_name, $index_fields, $index_type) = @_;
-    $index_name = "idx_" . $self->_hash_identifier($index_name);
-    if ($index_type eq 'FULLTEXT') {
-        my $sql = "CREATE INDEX $index_name ON $table_name (" 
-                  . join(',',@$index_fields)
-                  . ") INDEXTYPE IS CTXSYS.CONTEXT "
-                  . " PARAMETERS('LEXER BZ_LEX SYNC(ON COMMIT)')" ;
-        return $sql;
-    }
-
-    return($self->SUPER::_get_create_index_ddl($table_name, $index_name, 
-                                               $index_fields, $index_type));
+  my ($self, $table_name, $index_name, $index_fields, $index_type) = @_;
+  $index_name = "idx_" . $self->_hash_identifier($index_name);
+  if ($index_type eq 'FULLTEXT') {
+    my $sql
+      = "CREATE INDEX $index_name ON $table_name ("
+      . join(',', @$index_fields)
+      . ") INDEXTYPE IS CTXSYS.CONTEXT "
+      . " PARAMETERS('LEXER BZ_LEX SYNC(ON COMMIT)')";
+    return $sql;
+  }
+
+  return ($self->SUPER::_get_create_index_ddl(
+    $table_name, $index_name, $index_fields, $index_type
+  ));
 
 }
 
 sub get_drop_index_ddl {
-    my $self = shift;
-    my ($table, $name) = @_;
+  my $self = shift;
+  my ($table, $name) = @_;
 
-    $name = 'idx_' . $self->_hash_identifier($name);
-    return $self->SUPER::get_drop_index_ddl($table, $name);
+  $name = 'idx_' . $self->_hash_identifier($name);
+  return $self->SUPER::get_drop_index_ddl($table, $name);
 }
 
-# Oracle supports the use of FOREIGN KEY integrity constraints 
+# Oracle supports the use of FOREIGN KEY integrity constraints
 # to define the referential integrity actions, including:
 # - Update and delete No Action (default)
 # - Delete CASCADE
 # - Delete SET NULL
 sub get_fk_ddl {
-    my $self = shift;
-    my $ddl = $self->SUPER::get_fk_ddl(@_);
+  my $self = shift;
+  my $ddl  = $self->SUPER::get_fk_ddl(@_);
 
-    # iThe Bugzilla Oracle driver implements UPDATE via a trigger.
-    $ddl =~ s/ON UPDATE \S+//i;
-    # RESTRICT is the default for DELETE on Oracle and may not be specified.
-    $ddl =~ s/ON DELETE RESTRICT//i;
+  # iThe Bugzilla Oracle driver implements UPDATE via a trigger.
+  $ddl =~ s/ON UPDATE \S+//i;
 
-    return $ddl;
+  # RESTRICT is the default for DELETE on Oracle and may not be specified.
+  $ddl =~ s/ON DELETE RESTRICT//i;
+
+  return $ddl;
 }
 
 sub get_add_fks_sql {
-    my $self = shift;
-    my ($table, $column_fks) = @_;
-    my @sql = $self->SUPER::get_add_fks_sql(@_);
-
-    foreach my $column (keys %$column_fks) {
-        my $fk = $column_fks->{$column};
-        next if $fk->{UPDATE} && uc($fk->{UPDATE}) ne 'CASCADE';
-        my $fk_name   = $self->_get_fk_name($table, $column, $fk);
-        my $to_column = $fk->{COLUMN};
-        my $to_table  = $fk->{TABLE};
-
-        my $trigger = <<END;
+  my $self = shift;
+  my ($table, $column_fks) = @_;
+  my @sql = $self->SUPER::get_add_fks_sql(@_);
+
+  foreach my $column (keys %$column_fks) {
+    my $fk = $column_fks->{$column};
+    next if $fk->{UPDATE} && uc($fk->{UPDATE}) ne 'CASCADE';
+    my $fk_name   = $self->_get_fk_name($table, $column, $fk);
+    my $to_column = $fk->{COLUMN};
+    my $to_table  = $fk->{TABLE};
+
+    my $trigger = <<END;
 CREATE OR REPLACE TRIGGER ${fk_name}_UC
           AFTER UPDATE OF $to_column ON $to_table
               REFERENCING NEW AS NEW OLD AS OLD
@@ -158,355 +164,376 @@ CREATE OR REPLACE TRIGGER ${fk_name}_UC
                     WHERE $column = :OLD.$to_column;
                       END ${fk_name}_UC;
 END
-        push(@sql, $trigger);
-    }
+    push(@sql, $trigger);
+  }
 
-    return @sql;
+  return @sql;
 }
 
 sub get_drop_fk_sql {
-    my $self = shift;
-    my ($table, $column, $references) = @_;
-    my $fk_name = $self->_get_fk_name(@_);
-    my @sql;
-    if (!$references->{UPDATE} || $references->{UPDATE} =~ /CASCADE/i) {
-        push(@sql, "DROP TRIGGER ${fk_name}_uc");
-    }
-    push(@sql, $self->SUPER::get_drop_fk_sql(@_));
-    return @sql;
+  my $self = shift;
+  my ($table, $column, $references) = @_;
+  my $fk_name = $self->_get_fk_name(@_);
+  my @sql;
+  if (!$references->{UPDATE} || $references->{UPDATE} =~ /CASCADE/i) {
+    push(@sql, "DROP TRIGGER ${fk_name}_uc");
+  }
+  push(@sql, $self->SUPER::get_drop_fk_sql(@_));
+  return @sql;
 }
 
 sub _get_fk_name {
-    my ($self, $table, $column, $references) = @_;
-    my $to_table  = $references->{TABLE};
-    my $to_column = $references->{COLUMN};
-    my $fk_name   = "${table}_${column}_${to_table}_${to_column}";
-    $fk_name      = "fk_" . $self->_hash_identifier($fk_name);
-    
-    return $fk_name;
+  my ($self, $table, $column, $references) = @_;
+  my $to_table  = $references->{TABLE};
+  my $to_column = $references->{COLUMN};
+  my $fk_name   = "${table}_${column}_${to_table}_${to_column}";
+  $fk_name = "fk_" . $self->_hash_identifier($fk_name);
+
+  return $fk_name;
 }
 
 sub get_add_column_ddl {
-    my $self = shift;
-    my ($table, $column, $definition, $init_value) = @_;
-    my @sql;
-
-    # Create sequences and triggers to emulate SERIAL datatypes.
-    if ($definition->{TYPE} =~ /SERIAL/i) {
-        # Clone the definition to not alter the original one.
-        my %def = %$definition;
-        # Oracle requires to define the column is several steps.
-        my $pk = delete $def{PRIMARYKEY};
-        my $notnull = delete $def{NOTNULL};
-        @sql = $self->SUPER::get_add_column_ddl($table, $column, \%def, $init_value);
-        push(@sql, $self->_get_create_seq_ddl($table, $column));
-        push(@sql, "UPDATE $table SET $column = ${table}_${column}_SEQ.NEXTVAL");
-        push(@sql, "ALTER TABLE $table MODIFY $column NOT NULL") if $notnull;
-        push(@sql, "ALTER TABLE $table ADD PRIMARY KEY ($column)") if $pk;
-    }
-    else {
-        @sql = $self->SUPER::get_add_column_ddl(@_);
-        # Create triggers to deal with empty string. 
-        if ($definition->{TYPE} =~ /varchar|TEXT/i && $definition->{NOTNULL}) {
-            push(@sql, _get_notnull_trigger_ddl($table, $column));
-        }
+  my $self = shift;
+  my ($table, $column, $definition, $init_value) = @_;
+  my @sql;
+
+  # Create sequences and triggers to emulate SERIAL datatypes.
+  if ($definition->{TYPE} =~ /SERIAL/i) {
+
+    # Clone the definition to not alter the original one.
+    my %def = %$definition;
+
+    # Oracle requires to define the column is several steps.
+    my $pk      = delete $def{PRIMARYKEY};
+    my $notnull = delete $def{NOTNULL};
+    @sql = $self->SUPER::get_add_column_ddl($table, $column, \%def, $init_value);
+    push(@sql, $self->_get_create_seq_ddl($table, $column));
+    push(@sql, "UPDATE $table SET $column = ${table}_${column}_SEQ.NEXTVAL");
+    push(@sql, "ALTER TABLE $table MODIFY $column NOT NULL") if $notnull;
+    push(@sql, "ALTER TABLE $table ADD PRIMARY KEY ($column)") if $pk;
+  }
+  else {
+    @sql = $self->SUPER::get_add_column_ddl(@_);
+
+    # Create triggers to deal with empty string.
+    if ($definition->{TYPE} =~ /varchar|TEXT/i && $definition->{NOTNULL}) {
+      push(@sql, _get_notnull_trigger_ddl($table, $column));
     }
+  }
 
-    return @sql;
+  return @sql;
 }
 
 sub get_drop_default_ddl {
-    my ($self, $table, $column) = @_;
-    return "ALTER TABLE $table MODIFY $column DEFAULT NULL";
+  my ($self, $table, $column) = @_;
+  return "ALTER TABLE $table MODIFY $column DEFAULT NULL";
 }
 
 sub get_alter_column_ddl {
-    my ($self, $table, $column, $new_def, $set_nulls_to) = @_;
-
-    my @statements;
-    my $old_def = $self->get_column_abstract($table, $column);
-    my $specific = $self->{db_specific};
-
-    # If the types have changed, we have to deal with that.
-    if (uc(trim($old_def->{TYPE})) ne uc(trim($new_def->{TYPE}))) {
-        push(@statements, $self->_get_alter_type_sql($table, $column, 
-                                                     $new_def, $old_def));
-    }
-
-    my $default = $new_def->{DEFAULT};
-    my $default_old = $old_def->{DEFAULT};
-
-    if (defined $default) {
-        $default = $specific->{$default} if exists $specific->{$default};
-    }
-    # This first condition prevents "uninitialized value" errors.
-    if (!defined $default && !defined $default_old) {
-        # Do Nothing
-    }
-    # If we went from having a default to not having one
-    elsif (!defined $default && defined $default_old) {
-        push(@statements, $self->get_drop_default_ddl($table, $column));
-    }
-    # If we went from no default to a default, or we changed the default.
-    elsif ( (defined $default && !defined $default_old) || 
-            ($default ne $default_old) ) 
-    {
-        push(@statements, "ALTER TABLE $table MODIFY $column "
-                         . " DEFAULT $default");
-    }
-
-    # If we went from NULL to NOT NULL.
-    if (!$old_def->{NOTNULL} && $new_def->{NOTNULL}) {
-        my $setdefault;
-        # Handle any fields that were NULL before, if we have a default,
-        $setdefault = $default if defined $default;
-        # But if we have a set_nulls_to, that overrides the DEFAULT 
-        # (although nobody would usually specify both a default and 
-        # a set_nulls_to.)
-        $setdefault = $set_nulls_to if defined $set_nulls_to;
-        if (defined $setdefault) {
-            push(@statements, "UPDATE $table SET $column = $setdefault"
-                            . "  WHERE $column IS NULL");
-        }
-        push(@statements, "ALTER TABLE $table MODIFY $column"
-                        . " NOT NULL");
-        push (@statements, _get_notnull_trigger_ddl($table, $column))
-                                   if $old_def->{TYPE} =~ /varchar|text/i
-                                     && $new_def->{TYPE} =~ /varchar|text/i;
-    }
-    # If we went from NOT NULL to NULL
-    elsif ($old_def->{NOTNULL} && !$new_def->{NOTNULL}) {
-        push(@statements, "ALTER TABLE $table MODIFY $column"
-                        . " NULL");
-        push(@statements, "DROP TRIGGER ${table}_${column}")
-                           if $new_def->{TYPE} =~ /varchar|text/i 
-                             && $old_def->{TYPE} =~ /varchar|text/i;
-    }
-
-    # If we went from not being a PRIMARY KEY to being a PRIMARY KEY.
-    if (!$old_def->{PRIMARYKEY} && $new_def->{PRIMARYKEY}) {
-        push(@statements, "ALTER TABLE $table ADD PRIMARY KEY ($column)");
+  my ($self, $table, $column, $new_def, $set_nulls_to) = @_;
+
+  my @statements;
+  my $old_def = $self->get_column_abstract($table, $column);
+  my $specific = $self->{db_specific};
+
+  # If the types have changed, we have to deal with that.
+  if (uc(trim($old_def->{TYPE})) ne uc(trim($new_def->{TYPE}))) {
+    push(@statements,
+      $self->_get_alter_type_sql($table, $column, $new_def, $old_def));
+  }
+
+  my $default     = $new_def->{DEFAULT};
+  my $default_old = $old_def->{DEFAULT};
+
+  if (defined $default) {
+    $default = $specific->{$default} if exists $specific->{$default};
+  }
+
+  # This first condition prevents "uninitialized value" errors.
+  if (!defined $default && !defined $default_old) {
+
+    # Do Nothing
+  }
+
+  # If we went from having a default to not having one
+  elsif (!defined $default && defined $default_old) {
+    push(@statements, $self->get_drop_default_ddl($table, $column));
+  }
+
+  # If we went from no default to a default, or we changed the default.
+  elsif ((defined $default && !defined $default_old)
+    || ($default ne $default_old))
+  {
+    push(@statements, "ALTER TABLE $table MODIFY $column " . " DEFAULT $default");
+  }
+
+  # If we went from NULL to NOT NULL.
+  if (!$old_def->{NOTNULL} && $new_def->{NOTNULL}) {
+    my $setdefault;
+
+    # Handle any fields that were NULL before, if we have a default,
+    $setdefault = $default if defined $default;
+
+    # But if we have a set_nulls_to, that overrides the DEFAULT
+    # (although nobody would usually specify both a default and
+    # a set_nulls_to.)
+    $setdefault = $set_nulls_to if defined $set_nulls_to;
+    if (defined $setdefault) {
+      push(@statements,
+        "UPDATE $table SET $column = $setdefault" . "  WHERE $column IS NULL");
     }
-    # If we went from being a PK to not being a PK
-    elsif ( $old_def->{PRIMARYKEY} && !$new_def->{PRIMARYKEY} ) {
-        push(@statements, "ALTER TABLE $table DROP PRIMARY KEY");
-    }
-
-    return @statements;
+    push(@statements, "ALTER TABLE $table MODIFY $column" . " NOT NULL");
+    push(@statements, _get_notnull_trigger_ddl($table, $column))
+      if $old_def->{TYPE} =~ /varchar|text/i && $new_def->{TYPE} =~ /varchar|text/i;
+  }
+
+  # If we went from NOT NULL to NULL
+  elsif ($old_def->{NOTNULL} && !$new_def->{NOTNULL}) {
+    push(@statements, "ALTER TABLE $table MODIFY $column" . " NULL");
+    push(@statements, "DROP TRIGGER ${table}_${column}")
+      if $new_def->{TYPE} =~ /varchar|text/i && $old_def->{TYPE} =~ /varchar|text/i;
+  }
+
+  # If we went from not being a PRIMARY KEY to being a PRIMARY KEY.
+  if (!$old_def->{PRIMARYKEY} && $new_def->{PRIMARYKEY}) {
+    push(@statements, "ALTER TABLE $table ADD PRIMARY KEY ($column)");
+  }
+
+  # If we went from being a PK to not being a PK
+  elsif ($old_def->{PRIMARYKEY} && !$new_def->{PRIMARYKEY}) {
+    push(@statements, "ALTER TABLE $table DROP PRIMARY KEY");
+  }
+
+  return @statements;
 }
 
 sub _get_alter_type_sql {
-    my ($self, $table, $column, $new_def, $old_def) = @_;
-    my @statements;
-
-    my $type = $new_def->{TYPE};
-    $type = $self->{db_specific}->{$type} 
-        if exists $self->{db_specific}->{$type};
-
-    if ($type =~ /serial/i && $old_def->{TYPE} !~ /serial/i) {
-        die("You cannot specify a DEFAULT on a SERIAL-type column.") 
-            if $new_def->{DEFAULT};
+  my ($self, $table, $column, $new_def, $old_def) = @_;
+  my @statements;
+
+  my $type = $new_def->{TYPE};
+  $type = $self->{db_specific}->{$type} if exists $self->{db_specific}->{$type};
+
+  if ($type =~ /serial/i && $old_def->{TYPE} !~ /serial/i) {
+    die("You cannot specify a DEFAULT on a SERIAL-type column.")
+      if $new_def->{DEFAULT};
+  }
+
+  if ( ($old_def->{TYPE} =~ /LONGTEXT/i && $new_def->{TYPE} !~ /LONGTEXT/i)
+    || ($old_def->{TYPE} !~ /LONGTEXT/i && $new_def->{TYPE} =~ /LONGTEXT/i))
+  {
+    # LONG to VARCHAR or VARCHAR to LONG is not allowed in Oracle,
+    # just a way to work around.
+    # Determine whether column_temp is already exist.
+    my $dbh          = Bugzilla->dbh;
+    my $column_exist = $dbh->selectcol_arrayref(
+      "SELECT CNAME FROM COL WHERE TNAME = UPPER(?) AND 
+                             CNAME = UPPER(?)", undef, $table, $column . "_temp"
+    );
+    if (!@$column_exist) {
+      push(@statements, "ALTER TABLE $table ADD ${column}_temp $type");
     }
-
-    if ( ($old_def->{TYPE} =~ /LONGTEXT/i && $new_def->{TYPE} !~ /LONGTEXT/i) 
-         || ($old_def->{TYPE} !~ /LONGTEXT/i && $new_def->{TYPE} =~ /LONGTEXT/i)
-       ) {
-        # LONG to VARCHAR or VARCHAR to LONG is not allowed in Oracle, 
-        # just a way to work around.
-        # Determine whether column_temp is already exist.
-        my $dbh=Bugzilla->dbh;
-        my $column_exist = $dbh->selectcol_arrayref(
-                          "SELECT CNAME FROM COL WHERE TNAME = UPPER(?) AND 
-                             CNAME = UPPER(?)", undef,$table,$column . "_temp");
-        if(!@$column_exist) {
-        push(@statements, 
-            "ALTER TABLE $table ADD ${column}_temp $type");  
-        }
-        push(@statements, "UPDATE $table SET ${column}_temp = $column");
-        push(@statements, "COMMIT");
-        push(@statements, "ALTER TABLE $table DROP COLUMN $column");
-        push(@statements, 
-            "ALTER TABLE $table RENAME COLUMN ${column}_temp TO $column");
-    } else {  
-        push(@statements, "ALTER TABLE $table MODIFY $column $type");
-    }
-
-    if ($new_def->{TYPE} =~ /serial/i && $old_def->{TYPE} !~ /serial/i) {
-         push(@statements, _get_create_seq_ddl($table, $column));
-    }
-
-    # If this column is no longer SERIAL, we need to drop the sequence
-    # that went along with it.
-    if ($old_def->{TYPE} =~ /serial/i && $new_def->{TYPE} !~ /serial/i) {
-        push(@statements, "DROP SEQUENCE ${table}_${column}_SEQ");
-        push(@statements, "DROP TRIGGER ${table}_${column}_TR");
-    }
-    
-    # If this column is changed to type TEXT/VARCHAR, we need to deal with
-    # empty string.
-    if ( $old_def->{TYPE} !~ /varchar|text/i 
-            && $new_def->{TYPE} =~ /varchar|text/i 
-            && $new_def->{NOTNULL} )
-    {
-             push (@statements, _get_notnull_trigger_ddl($table, $column));
-    } 
-    # If this column is no longer TEXT/VARCHAR, we need to drop the trigger
-    # that went along with it.
-    if ( $old_def->{TYPE} =~ /varchar|text/i
-            && $old_def->{NOTNULL}
-            && $new_def->{TYPE} !~ /varchar|text/i )
-    {
-        push(@statements, "DROP TRIGGER ${table}_${column}");
-    } 
-    return @statements;
+    push(@statements, "UPDATE $table SET ${column}_temp = $column");
+    push(@statements, "COMMIT");
+    push(@statements, "ALTER TABLE $table DROP COLUMN $column");
+    push(@statements, "ALTER TABLE $table RENAME COLUMN ${column}_temp TO $column");
+  }
+  else {
+    push(@statements, "ALTER TABLE $table MODIFY $column $type");
+  }
+
+  if ($new_def->{TYPE} =~ /serial/i && $old_def->{TYPE} !~ /serial/i) {
+    push(@statements, _get_create_seq_ddl($table, $column));
+  }
+
+  # If this column is no longer SERIAL, we need to drop the sequence
+  # that went along with it.
+  if ($old_def->{TYPE} =~ /serial/i && $new_def->{TYPE} !~ /serial/i) {
+    push(@statements, "DROP SEQUENCE ${table}_${column}_SEQ");
+    push(@statements, "DROP TRIGGER ${table}_${column}_TR");
+  }
+
+  # If this column is changed to type TEXT/VARCHAR, we need to deal with
+  # empty string.
+  if ( $old_def->{TYPE} !~ /varchar|text/i
+    && $new_def->{TYPE} =~ /varchar|text/i
+    && $new_def->{NOTNULL})
+  {
+    push(@statements, _get_notnull_trigger_ddl($table, $column));
+  }
+
+  # If this column is no longer TEXT/VARCHAR, we need to drop the trigger
+  # that went along with it.
+  if ( $old_def->{TYPE} =~ /varchar|text/i
+    && $old_def->{NOTNULL}
+    && $new_def->{TYPE} !~ /varchar|text/i)
+  {
+    push(@statements, "DROP TRIGGER ${table}_${column}");
+  }
+  return @statements;
 }
 
 sub get_rename_column_ddl {
-    my ($self, $table, $old_name, $new_name) = @_;
-    if (lc($old_name) eq lc($new_name)) {
-        # if the only change is a case change, return an empty list.
-        return ();
-    }
-    my @sql = ("ALTER TABLE $table RENAME COLUMN $old_name TO $new_name");
-    my $def = $self->get_column_abstract($table, $old_name);
-    if ($def->{TYPE} =~ /SERIAL/i) {
-        # We have to rename the series also, and fix the default of the series.
-        my $old_seq = "${table}_${old_name}_SEQ";
-        my $new_seq = "${table}_${new_name}_SEQ";
-        push(@sql, "RENAME $old_seq TO $new_seq");
-        push(@sql, $self->_get_create_trigger_ddl($table, $new_name, $new_seq));
-        push(@sql, "DROP TRIGGER ${table}_${old_name}_TR");
-    }
-    if ($def->{TYPE} =~ /varchar|text/i && $def->{NOTNULL} ) {
-        push(@sql, _get_notnull_trigger_ddl($table,$new_name));
-        push(@sql, "DROP TRIGGER ${table}_${old_name}");
-    }
-    return @sql;
+  my ($self, $table, $old_name, $new_name) = @_;
+  if (lc($old_name) eq lc($new_name)) {
+
+    # if the only change is a case change, return an empty list.
+    return ();
+  }
+  my @sql = ("ALTER TABLE $table RENAME COLUMN $old_name TO $new_name");
+  my $def = $self->get_column_abstract($table, $old_name);
+  if ($def->{TYPE} =~ /SERIAL/i) {
+
+    # We have to rename the series also, and fix the default of the series.
+    my $old_seq = "${table}_${old_name}_SEQ";
+    my $new_seq = "${table}_${new_name}_SEQ";
+    push(@sql, "RENAME $old_seq TO $new_seq");
+    push(@sql, $self->_get_create_trigger_ddl($table, $new_name, $new_seq));
+    push(@sql, "DROP TRIGGER ${table}_${old_name}_TR");
+  }
+  if ($def->{TYPE} =~ /varchar|text/i && $def->{NOTNULL}) {
+    push(@sql, _get_notnull_trigger_ddl($table, $new_name));
+    push(@sql, "DROP TRIGGER ${table}_${old_name}");
+  }
+  return @sql;
 }
 
 sub get_drop_column_ddl {
-    my $self = shift;
-    my ($table, $column) = @_;
-    my @sql;
-    push(@sql, $self->SUPER::get_drop_column_ddl(@_));
-    my $dbh=Bugzilla->dbh;
-    my $trigger_name = uc($table . "_" . $column);
-    my $exist_trigger = $dbh->selectcol_arrayref(
-        "SELECT OBJECT_NAME FROM USER_OBJECTS
-         WHERE OBJECT_NAME = ?", undef, $trigger_name);
-    if(@$exist_trigger) {
-        push(@sql, "DROP TRIGGER $trigger_name");
-    }
-    # If this column is of type SERIAL, we need to drop the sequence
-    # and trigger that went along with it.
-    my $def = $self->get_column_abstract($table, $column);
-    if ($def->{TYPE} =~ /SERIAL/i) {
-        push(@sql, "DROP SEQUENCE ${table}_${column}_SEQ");
-        push(@sql, "DROP TRIGGER ${table}_${column}_TR");
-    }
-    return @sql;
+  my $self = shift;
+  my ($table, $column) = @_;
+  my @sql;
+  push(@sql, $self->SUPER::get_drop_column_ddl(@_));
+  my $dbh           = Bugzilla->dbh;
+  my $trigger_name  = uc($table . "_" . $column);
+  my $exist_trigger = $dbh->selectcol_arrayref(
+    "SELECT OBJECT_NAME FROM USER_OBJECTS
+         WHERE OBJECT_NAME = ?", undef, $trigger_name
+  );
+  if (@$exist_trigger) {
+    push(@sql, "DROP TRIGGER $trigger_name");
+  }
+
+  # If this column is of type SERIAL, we need to drop the sequence
+  # and trigger that went along with it.
+  my $def = $self->get_column_abstract($table, $column);
+  if ($def->{TYPE} =~ /SERIAL/i) {
+    push(@sql, "DROP SEQUENCE ${table}_${column}_SEQ");
+    push(@sql, "DROP TRIGGER ${table}_${column}_TR");
+  }
+  return @sql;
 }
 
 sub get_rename_table_sql {
-    my ($self, $old_name, $new_name) = @_;
-    if (lc($old_name) eq lc($new_name)) {
-        # if the only change is a case change, return an empty list.
-        return ();
-    }
+  my ($self, $old_name, $new_name) = @_;
+  if (lc($old_name) eq lc($new_name)) {
+
+    # if the only change is a case change, return an empty list.
+    return ();
+  }
 
-    my @sql = ("ALTER TABLE $old_name RENAME TO $new_name");
-    my @columns = $self->get_table_columns($old_name);
-    foreach my $column (@columns) {
-        my $def = $self->get_column_abstract($old_name, $column);
-        if ($def->{TYPE} =~ /SERIAL/i) {
-            # If there's a SERIAL column on this table, we also need
-            # to rename the sequence.
-            my $old_seq = "${old_name}_${column}_SEQ";
-            my $new_seq = "${new_name}_${column}_SEQ";
-            push(@sql, "RENAME $old_seq TO $new_seq");
-            push(@sql, $self->_get_create_trigger_ddl($new_name, $column, $new_seq));
-            push(@sql, "DROP TRIGGER ${old_name}_${column}_TR");
-        }
-        if ($def->{TYPE} =~ /varchar|text/i && $def->{NOTNULL}) {
-            push(@sql, _get_notnull_trigger_ddl($new_name, $column));
-            push(@sql, "DROP TRIGGER ${old_name}_${column}");
-        }
+  my @sql     = ("ALTER TABLE $old_name RENAME TO $new_name");
+  my @columns = $self->get_table_columns($old_name);
+  foreach my $column (@columns) {
+    my $def = $self->get_column_abstract($old_name, $column);
+    if ($def->{TYPE} =~ /SERIAL/i) {
+
+      # If there's a SERIAL column on this table, we also need
+      # to rename the sequence.
+      my $old_seq = "${old_name}_${column}_SEQ";
+      my $new_seq = "${new_name}_${column}_SEQ";
+      push(@sql, "RENAME $old_seq TO $new_seq");
+      push(@sql, $self->_get_create_trigger_ddl($new_name, $column, $new_seq));
+      push(@sql, "DROP TRIGGER ${old_name}_${column}_TR");
+    }
+    if ($def->{TYPE} =~ /varchar|text/i && $def->{NOTNULL}) {
+      push(@sql, _get_notnull_trigger_ddl($new_name, $column));
+      push(@sql, "DROP TRIGGER ${old_name}_${column}");
     }
+  }
 
-    return @sql;
+  return @sql;
 }
 
 sub get_drop_table_ddl {
-    my ($self, $name) = @_;
-    my @sql;
-
-    my @columns = $self->get_table_columns($name);
-    foreach my $column (@columns) {
-        my $def = $self->get_column_abstract($name, $column);
-        if ($def->{TYPE} =~ /SERIAL/i) {
-            # If there's a SERIAL column on this table, we also need
-            # to remove the sequence.
-            push(@sql, "DROP SEQUENCE ${name}_${column}_SEQ");
-        }
+  my ($self, $name) = @_;
+  my @sql;
+
+  my @columns = $self->get_table_columns($name);
+  foreach my $column (@columns) {
+    my $def = $self->get_column_abstract($name, $column);
+    if ($def->{TYPE} =~ /SERIAL/i) {
+
+      # If there's a SERIAL column on this table, we also need
+      # to remove the sequence.
+      push(@sql, "DROP SEQUENCE ${name}_${column}_SEQ");
     }
-    push(@sql, "DROP TABLE $name CASCADE CONSTRAINTS PURGE");
+  }
+  push(@sql, "DROP TABLE $name CASCADE CONSTRAINTS PURGE");
 
-    return @sql;
+  return @sql;
 }
 
 sub _get_notnull_trigger_ddl {
-      my ($table, $column) = @_;
-
-      my $notnull_sql = "CREATE OR REPLACE TRIGGER "
-                        . " ${table}_${column}"
-                        . " BEFORE INSERT OR UPDATE ON ". $table
-                        . " FOR EACH ROW"
-                        . " BEGIN "
-                        . " IF :NEW.". $column ." IS NULL THEN  "
-                        . " SELECT '" . Bugzilla::DB::Oracle->EMPTY_STRING
-                        . "' INTO :NEW.". $column ." FROM DUAL; "
-                        . " END IF; "
-                        . " END ".$table.";";
-     return $notnull_sql;
+  my ($table, $column) = @_;
+
+  my $notnull_sql
+    = "CREATE OR REPLACE TRIGGER "
+    . " ${table}_${column}"
+    . " BEFORE INSERT OR UPDATE ON "
+    . $table
+    . " FOR EACH ROW"
+    . " BEGIN "
+    . " IF :NEW."
+    . $column
+    . " IS NULL THEN  "
+    . " SELECT '"
+    . Bugzilla::DB::Oracle->EMPTY_STRING
+    . "' INTO :NEW."
+    . $column
+    . " FROM DUAL; "
+    . " END IF; " . " END "
+    . $table . ";";
+  return $notnull_sql;
 }
 
 sub _get_create_seq_ddl {
-     my ($self, $table, $column, $start_with) = @_;
-     $start_with ||= 1;
-     my @ddl;
-     my $seq_name = "${table}_${column}_SEQ";
-     my $seq_sql = "CREATE SEQUENCE $seq_name "
-                   . " INCREMENT BY 1 "
-                   . " START WITH $start_with "
-                   . " NOMAXVALUE "
-                   . " NOCYCLE "
-                   . " NOCACHE";
-    push (@ddl, $seq_sql);
-    push(@ddl, $self->_get_create_trigger_ddl($table, $column, $seq_name));
-
-    return @ddl;
+  my ($self, $table, $column, $start_with) = @_;
+  $start_with ||= 1;
+  my @ddl;
+  my $seq_name = "${table}_${column}_SEQ";
+  my $seq_sql
+    = "CREATE SEQUENCE $seq_name "
+    . " INCREMENT BY 1 "
+    . " START WITH $start_with "
+    . " NOMAXVALUE "
+    . " NOCYCLE "
+    . " NOCACHE";
+  push(@ddl, $seq_sql);
+  push(@ddl, $self->_get_create_trigger_ddl($table, $column, $seq_name));
+
+  return @ddl;
 }
 
 sub _get_create_trigger_ddl {
-    my ($self, $table, $column, $seq_name) = @_;
-    my $serial_sql = "CREATE OR REPLACE TRIGGER ${table}_${column}_TR "
-                   . " BEFORE INSERT ON $table "
-                   . " FOR EACH ROW "
-                   . " BEGIN "
-                   . "   SELECT ${seq_name}.NEXTVAL "
-                   . "   INTO :NEW.$column FROM DUAL; "
-                   . " END;";
-    return $serial_sql;
+  my ($self, $table, $column, $seq_name) = @_;
+  my $serial_sql
+    = "CREATE OR REPLACE TRIGGER ${table}_${column}_TR "
+    . " BEFORE INSERT ON $table "
+    . " FOR EACH ROW "
+    . " BEGIN "
+    . "   SELECT ${seq_name}.NEXTVAL "
+    . "   INTO :NEW.$column FROM DUAL; " . " END;";
+  return $serial_sql;
 }
 
-sub get_set_serial_sql { 
-    my ($self, $table, $column, $value) = @_; 
-    my @sql;
-    my $seq_name = "${table}_${column}_SEQ";
-    push(@sql, "DROP SEQUENCE ${seq_name}");
-    push(@sql, $self->_get_create_seq_ddl($table, $column, $value));       
-    return @sql;
-} 
+sub get_set_serial_sql {
+  my ($self, $table, $column, $value) = @_;
+  my @sql;
+  my $seq_name = "${table}_${column}_SEQ";
+  push(@sql, "DROP SEQUENCE ${seq_name}");
+  push(@sql, $self->_get_create_seq_ddl($table, $column, $value));
+  return @sql;
+}
 
 1;
 
index c82c373b7705485438654acf176b315b72b7fae3..c933b2a893e677e056d4e5c36211602cccf96bf7 100644 (file)
@@ -23,164 +23,185 @@ use Storable qw(dclone);
 #------------------------------------------------------------------------------
 sub _initialize {
 
-    my $self = shift;
-
-    $self = $self->SUPER::_initialize(@_);
-
-    # Remove FULLTEXT index types from the schemas.
-    foreach my $table (keys %{ $self->{schema} }) {
-        if ($self->{schema}{$table}{INDEXES}) {
-            foreach my $index (@{ $self->{schema}{$table}{INDEXES} }) {
-                if (ref($index) eq 'HASH') {
-                    delete($index->{TYPE}) if (exists $index->{TYPE} 
-                        && $index->{TYPE} eq 'FULLTEXT');
-                }
-            }
-            foreach my $index (@{ $self->{abstract_schema}{$table}{INDEXES} }) {
-                if (ref($index) eq 'HASH') {
-                    delete($index->{TYPE}) if (exists $index->{TYPE} 
-                        && $index->{TYPE} eq 'FULLTEXT');
-                }
-            }
+  my $self = shift;
+
+  $self = $self->SUPER::_initialize(@_);
+
+  # Remove FULLTEXT index types from the schemas.
+  foreach my $table (keys %{$self->{schema}}) {
+    if ($self->{schema}{$table}{INDEXES}) {
+      foreach my $index (@{$self->{schema}{$table}{INDEXES}}) {
+        if (ref($index) eq 'HASH') {
+          delete($index->{TYPE})
+            if (exists $index->{TYPE} && $index->{TYPE} eq 'FULLTEXT');
+        }
+      }
+      foreach my $index (@{$self->{abstract_schema}{$table}{INDEXES}}) {
+        if (ref($index) eq 'HASH') {
+          delete($index->{TYPE})
+            if (exists $index->{TYPE} && $index->{TYPE} eq 'FULLTEXT');
         }
+      }
     }
+  }
 
-    $self->{db_specific} = {
+  $self->{db_specific} = {
 
-        BOOLEAN =>      'smallint',
-        FALSE =>        '0', 
-        TRUE =>         '1',
+    BOOLEAN => 'smallint',
+    FALSE   => '0',
+    TRUE    => '1',
 
-        INT1 =>         'integer',
-        INT2 =>         'integer',
-        INT3 =>         'integer',
-        INT4 =>         'integer',
+    INT1 => 'integer',
+    INT2 => 'integer',
+    INT3 => 'integer',
+    INT4 => 'integer',
 
-        SMALLSERIAL =>  'serial unique',
-        MEDIUMSERIAL => 'serial unique',
-        INTSERIAL =>    'serial unique',
+    SMALLSERIAL  => 'serial unique',
+    MEDIUMSERIAL => 'serial unique',
+    INTSERIAL    => 'serial unique',
 
-        TINYTEXT =>     'varchar(255)',
-        MEDIUMTEXT =>   'text',
-        LONGTEXT =>     'text',
+    TINYTEXT   => 'varchar(255)',
+    MEDIUMTEXT => 'text',
+    LONGTEXT   => 'text',
 
-        LONGBLOB =>     'bytea',
+    LONGBLOB => 'bytea',
 
-        DATETIME =>     'timestamp(0) without time zone',
-        DATE     =>     'date',
-    };
+    DATETIME => 'timestamp(0) without time zone',
+    DATE     => 'date',
+  };
 
-    $self->_adjust_schema;
+  $self->_adjust_schema;
 
-    return $self;
+  return $self;
+
+}    #eosub--_initialize
 
-} #eosub--_initialize
 #--------------------------------------------------------------------
 
 sub get_create_database_sql {
-    my ($self, $name) = @_;
-    return ("CREATE DATABASE \"$name\" ENCODING 'UTF8' TEMPLATE template0");
+  my ($self, $name) = @_;
+  return ("CREATE DATABASE \"$name\" ENCODING 'UTF8' TEMPLATE template0");
 }
 
 sub get_rename_column_ddl {
-    my ($self, $table, $old_name, $new_name) = @_;
-    if (lc($old_name) eq lc($new_name)) {
-        # if the only change is a case change, return an empty list, since Pg
-        # is case-insensitive and will return an error about a duplicate name
-        return ();
-    }
-    my @sql = ("ALTER TABLE $table RENAME COLUMN $old_name TO $new_name");
-    my $def = $self->get_column_abstract($table, $old_name);
-    if ($def->{TYPE} =~ /SERIAL/i) {
-        # We have to rename the series also.
-        push(@sql, "ALTER SEQUENCE ${table}_${old_name}_seq 
-                         RENAME TO ${table}_${new_name}_seq");
-    }
-    return @sql;
+  my ($self, $table, $old_name, $new_name) = @_;
+  if (lc($old_name) eq lc($new_name)) {
+
+    # if the only change is a case change, return an empty list, since Pg
+    # is case-insensitive and will return an error about a duplicate name
+    return ();
+  }
+  my @sql = ("ALTER TABLE $table RENAME COLUMN $old_name TO $new_name");
+  my $def = $self->get_column_abstract($table, $old_name);
+  if ($def->{TYPE} =~ /SERIAL/i) {
+
+    # We have to rename the series also.
+    push(
+      @sql, "ALTER SEQUENCE ${table}_${old_name}_seq 
+                         RENAME TO ${table}_${new_name}_seq"
+    );
+  }
+  return @sql;
 }
 
 sub get_rename_table_sql {
-    my ($self, $old_name, $new_name) = @_;
-    if (lc($old_name) eq lc($new_name)) {
-        # if the only change is a case change, return an empty list, since Pg
-        # is case-insensitive and will return an error about a duplicate name
-        return ();
+  my ($self, $old_name, $new_name) = @_;
+  if (lc($old_name) eq lc($new_name)) {
+
+    # if the only change is a case change, return an empty list, since Pg
+    # is case-insensitive and will return an error about a duplicate name
+    return ();
+  }
+
+  my @sql = ("ALTER TABLE $old_name RENAME TO $new_name");
+
+  # If there's a SERIAL column on this table, we also need to rename the
+  # sequence.
+  # If there is a PRIMARY KEY, we need to rename it too.
+  my @columns = $self->get_table_columns($old_name);
+  foreach my $column (@columns) {
+    my $def = $self->get_column_abstract($old_name, $column);
+    if ($def->{TYPE} =~ /SERIAL/i) {
+      my $old_seq = "${old_name}_${column}_seq";
+      my $new_seq = "${new_name}_${column}_seq";
+      push(@sql, "ALTER SEQUENCE $old_seq RENAME TO $new_seq");
+      push(
+        @sql, "ALTER TABLE $new_name ALTER COLUMN $column
+                             SET DEFAULT NEXTVAL('$new_seq')"
+      );
     }
-
-    my @sql = ("ALTER TABLE $old_name RENAME TO $new_name");
-
-    # If there's a SERIAL column on this table, we also need to rename the
-    # sequence.
-    # If there is a PRIMARY KEY, we need to rename it too.
-    my @columns = $self->get_table_columns($old_name);
-    foreach my $column (@columns) {
-        my $def = $self->get_column_abstract($old_name, $column);
-        if ($def->{TYPE} =~ /SERIAL/i) {
-            my $old_seq = "${old_name}_${column}_seq";
-            my $new_seq = "${new_name}_${column}_seq";
-            push(@sql, "ALTER SEQUENCE $old_seq RENAME TO $new_seq");
-            push(@sql, "ALTER TABLE $new_name ALTER COLUMN $column
-                             SET DEFAULT NEXTVAL('$new_seq')");
-        }
-        if ($def->{PRIMARYKEY}) {
-            my $old_pk = "${old_name}_pkey";
-            my $new_pk = "${new_name}_pkey";
-            push(@sql, "ALTER INDEX $old_pk RENAME to $new_pk");
-        }
+    if ($def->{PRIMARYKEY}) {
+      my $old_pk = "${old_name}_pkey";
+      my $new_pk = "${new_name}_pkey";
+      push(@sql, "ALTER INDEX $old_pk RENAME to $new_pk");
     }
+  }
 
-    return @sql;
+  return @sql;
 }
 
 sub get_set_serial_sql {
-    my ($self, $table, $column, $value) = @_;
-    return ("SELECT setval('${table}_${column}_seq', $value, false)
-               FROM $table");
+  my ($self, $table, $column, $value) = @_;
+  return (
+    "SELECT setval('${table}_${column}_seq', $value, false)
+               FROM $table"
+  );
 }
 
 sub _get_alter_type_sql {
-    my ($self, $table, $column, $new_def, $old_def) = @_;
-    my @statements;
-
-    my $type = $new_def->{TYPE};
-    $type = $self->{db_specific}->{$type} 
-        if exists $self->{db_specific}->{$type};
-
-    if ($type =~ /serial/i && $old_def->{TYPE} !~ /serial/i) {
-        die("You cannot specify a DEFAULT on a SERIAL-type column.") 
-            if $new_def->{DEFAULT};
-    }
-
-    $type =~ s/\bserial\b/integer/i;
-
-    # On Pg, you don't need UNIQUE if you're a PK--it creates
-    # two identical indexes otherwise.
-    $type =~ s/unique//i if $new_def->{PRIMARYKEY};
-
-    push(@statements, "ALTER TABLE $table ALTER COLUMN $column
-                              TYPE $type");
-
-    if ($new_def->{TYPE} =~ /serial/i && $old_def->{TYPE} !~ /serial/i) {
-        push(@statements, "CREATE SEQUENCE ${table}_${column}_seq
-                                  OWNED BY $table.$column");
-        push(@statements, "SELECT setval('${table}_${column}_seq',
+  my ($self, $table, $column, $new_def, $old_def) = @_;
+  my @statements;
+
+  my $type = $new_def->{TYPE};
+  $type = $self->{db_specific}->{$type} if exists $self->{db_specific}->{$type};
+
+  if ($type =~ /serial/i && $old_def->{TYPE} !~ /serial/i) {
+    die("You cannot specify a DEFAULT on a SERIAL-type column.")
+      if $new_def->{DEFAULT};
+  }
+
+  $type =~ s/\bserial\b/integer/i;
+
+  # On Pg, you don't need UNIQUE if you're a PK--it creates
+  # two identical indexes otherwise.
+  $type =~ s/unique//i if $new_def->{PRIMARYKEY};
+
+  push(
+    @statements, "ALTER TABLE $table ALTER COLUMN $column
+                              TYPE $type"
+  );
+
+  if ($new_def->{TYPE} =~ /serial/i && $old_def->{TYPE} !~ /serial/i) {
+    push(
+      @statements, "CREATE SEQUENCE ${table}_${column}_seq
+                                  OWNED BY $table.$column"
+    );
+    push(
+      @statements, "SELECT setval('${table}_${column}_seq',
                                          MAX($table.$column))
-                             FROM $table");
-        push(@statements, "ALTER TABLE $table ALTER COLUMN $column 
-                           SET DEFAULT nextval('${table}_${column}_seq')");
-    }
-
-    # If this column is no longer SERIAL, we need to drop the sequence
-    # that went along with it.
-    if ($old_def->{TYPE} =~ /serial/i && $new_def->{TYPE} !~ /serial/i) {
-        push(@statements, "ALTER TABLE $table ALTER COLUMN $column 
-                           DROP DEFAULT");
-        push(@statements, "ALTER SEQUENCE ${table}_${column}_seq 
-                           OWNED BY NONE");
-        push(@statements, "DROP SEQUENCE ${table}_${column}_seq");
-    }
-
-    return @statements;
+                             FROM $table"
+    );
+    push(
+      @statements, "ALTER TABLE $table ALTER COLUMN $column 
+                           SET DEFAULT nextval('${table}_${column}_seq')"
+    );
+  }
+
+  # If this column is no longer SERIAL, we need to drop the sequence
+  # that went along with it.
+  if ($old_def->{TYPE} =~ /serial/i && $new_def->{TYPE} !~ /serial/i) {
+    push(
+      @statements, "ALTER TABLE $table ALTER COLUMN $column 
+                           DROP DEFAULT"
+    );
+    push(
+      @statements, "ALTER SEQUENCE ${table}_${column}_seq 
+                           OWNED BY NONE"
+    );
+    push(@statements, "DROP SEQUENCE ${table}_${column}_seq");
+  }
+
+  return @statements;
 }
 
 1;
index 935bf8e3759d3cf4e81f4d09e1406fbb1ac6c3da..e7f805d0c56721e6127ee1fdcd089dd2bb8e1c27 100644 (file)
@@ -22,37 +22,37 @@ use constant FK_ON_CREATE => 1;
 
 sub _initialize {
 
-    my $self = shift;
+  my $self = shift;
 
-    $self = $self->SUPER::_initialize(@_);
+  $self = $self->SUPER::_initialize(@_);
 
-    $self->{db_specific} = {
-        BOOLEAN =>      'integer',
-        FALSE =>        '0', 
-        TRUE =>         '1',
+  $self->{db_specific} = {
+    BOOLEAN => 'integer',
+    FALSE   => '0',
+    TRUE    => '1',
 
-        INT1 =>         'integer',
-        INT2 =>         'integer',
-        INT3 =>         'integer',
-        INT4 =>         'integer',
+    INT1 => 'integer',
+    INT2 => 'integer',
+    INT3 => 'integer',
+    INT4 => 'integer',
 
-        SMALLSERIAL =>  'SERIAL',
-        MEDIUMSERIAL => 'SERIAL',
-        INTSERIAL =>    'SERIAL',
+    SMALLSERIAL  => 'SERIAL',
+    MEDIUMSERIAL => 'SERIAL',
+    INTSERIAL    => 'SERIAL',
 
-        TINYTEXT =>     'text',
-        MEDIUMTEXT =>   'text',
-        LONGTEXT =>     'text',
+    TINYTEXT   => 'text',
+    MEDIUMTEXT => 'text',
+    LONGTEXT   => 'text',
 
-        LONGBLOB =>     'blob',
+    LONGBLOB => 'blob',
 
-        DATETIME =>     'DATETIME',
-        DATE     =>     'DATETIME',
-    };
+    DATETIME => 'DATETIME',
+    DATE     => 'DATETIME',
+  };
 
-    $self->_adjust_schema;
+  $self->_adjust_schema;
 
-    return $self;
+  return $self;
 
 }
 
@@ -61,83 +61,86 @@ sub _initialize {
 #################################
 
 sub _sqlite_create_table {
-    my ($self, $table) = @_;
-    return scalar Bugzilla->dbh->selectrow_array(
-        "SELECT sql FROM sqlite_master WHERE name = ? AND type = 'table'",
-        undef, $table);
+  my ($self, $table) = @_;
+  return
+    scalar Bugzilla->dbh->selectrow_array(
+    "SELECT sql FROM sqlite_master WHERE name = ? AND type = 'table'",
+    undef, $table);
 }
 
 sub _sqlite_table_lines {
-    my $self = shift;
-    my $table_sql = $self->_sqlite_create_table(@_);
-    $table_sql =~ s/\n*\)$//s;
-    # The $ makes this work even if people some day add crazy stuff to their
-    # schema like multi-column foreign keys.
-    return split(/,\s*$/m, $table_sql);
+  my $self      = shift;
+  my $table_sql = $self->_sqlite_create_table(@_);
+  $table_sql =~ s/\n*\)$//s;
+
+  # The $ makes this work even if people some day add crazy stuff to their
+  # schema like multi-column foreign keys.
+  return split(/,\s*$/m, $table_sql);
 }
 
 # This does most of the "heavy lifting" of the schema-altering functions.
 sub _sqlite_alter_schema {
-    my ($self, $table, $create_table, $options) = @_;
-    
-    # $create_table is sometimes an array in the form that _sqlite_table_lines
-    # returns.
-    if (ref $create_table) {
-        $create_table = join(',', @$create_table) . "\n)";
-    }
-    
-    my $dbh = Bugzilla->dbh;
-    
-    my $random = generate_random_password(5);
-    my $rename_to = "${table}_$random";
-
-    my @columns = $dbh->bz_table_columns_real($table);
-    push(@columns, $options->{extra_column}) if $options->{extra_column};
-    if (my $exclude = $options->{exclude_column}) {
-        @columns = grep { $_ ne $exclude } @columns;
-    }
-    my @insert_cols = @columns;
-    my @select_cols = @columns;
-    if (my $rename = $options->{rename}) {
-        foreach my $from (keys %$rename) {
-            my $to = $rename->{$from};
-            @insert_cols = map { $_ eq $from ? $to : $_ } @insert_cols;
-        }
+  my ($self, $table, $create_table, $options) = @_;
+
+  # $create_table is sometimes an array in the form that _sqlite_table_lines
+  # returns.
+  if (ref $create_table) {
+    $create_table = join(',', @$create_table) . "\n)";
+  }
+
+  my $dbh = Bugzilla->dbh;
+
+  my $random    = generate_random_password(5);
+  my $rename_to = "${table}_$random";
+
+  my @columns = $dbh->bz_table_columns_real($table);
+  push(@columns, $options->{extra_column}) if $options->{extra_column};
+  if (my $exclude = $options->{exclude_column}) {
+    @columns = grep { $_ ne $exclude } @columns;
+  }
+  my @insert_cols = @columns;
+  my @select_cols = @columns;
+  if (my $rename = $options->{rename}) {
+    foreach my $from (keys %$rename) {
+      my $to = $rename->{$from};
+      @insert_cols = map { $_ eq $from ? $to : $_ } @insert_cols;
     }
-    
-    my $insert_str = join(',', @insert_cols);
-    my $select_str = join(',', @select_cols);
-    my $copy_sql = "INSERT INTO $table ($insert_str)"
-                   . " SELECT $select_str FROM $rename_to";
-                   
-    # We have to turn FKs off before doing this. Otherwise, when we rename
-    # the table, all of the FKs in the other tables will be automatically
-    # updated to point to the renamed table. Note that PRAGMA foreign_keys
-    # can only be set outside of a transaction--otherwise it is a no-op.
-    if ($dbh->bz_in_transaction) {
-        die "can't alter the schema inside of a transaction";
-    }
-    my @sql = (
-        'PRAGMA foreign_keys = OFF',
-        'BEGIN EXCLUSIVE TRANSACTION',
-        @{ $options->{pre_sql} || [] },
-        "ALTER TABLE $table RENAME TO $rename_to",
-        $create_table,
-        $copy_sql,
-        "DROP TABLE $rename_to",
-        'COMMIT TRANSACTION',
-        'PRAGMA foreign_keys = ON',
-    );    
+  }
+
+  my $insert_str = join(',', @insert_cols);
+  my $select_str = join(',', @select_cols);
+  my $copy_sql
+    = "INSERT INTO $table ($insert_str)" . " SELECT $select_str FROM $rename_to";
+
+  # We have to turn FKs off before doing this. Otherwise, when we rename
+  # the table, all of the FKs in the other tables will be automatically
+  # updated to point to the renamed table. Note that PRAGMA foreign_keys
+  # can only be set outside of a transaction--otherwise it is a no-op.
+  if ($dbh->bz_in_transaction) {
+    die "can't alter the schema inside of a transaction";
+  }
+  my @sql = (
+    'PRAGMA foreign_keys = OFF',
+    'BEGIN EXCLUSIVE TRANSACTION',
+    @{$options->{pre_sql} || []},
+    "ALTER TABLE $table RENAME TO $rename_to",
+    $create_table,
+    $copy_sql,
+    "DROP TABLE $rename_to",
+    'COMMIT TRANSACTION',
+    'PRAGMA foreign_keys = ON',
+  );
 }
 
 # For finding a particular column's definition in a CREATE TABLE statement.
 sub _sqlite_column_regex {
-    my ($column) = @_;
-    # 1 = Comma at start
-    # 2 = Column name + Space
-    # 3 = Definition
-    # 4 = Ending comma
-    return qr/(^|,)(\s\Q$column\E\s+)(.*?)(,|$)/m;
+  my ($column) = @_;
+
+  # 1 = Comma at start
+  # 2 = Column name + Space
+  # 3 = Definition
+  # 4 = Ending comma
+  return qr/(^|,)(\s\Q$column\E\s+)(.*?)(,|$)/m;
 }
 
 #############################
@@ -145,133 +148,137 @@ sub _sqlite_column_regex {
 #############################
 
 sub get_create_database_sql {
-    # If we get here, it means there was some error creating the
-    # database file during bz_create_database in Bugzilla::DB,
-    # and we just want to display that error instead of doing
-    # anything else.
-    Bugzilla->dbh;
-    die "Reached an unreachable point";
+
+  # If we get here, it means there was some error creating the
+  # database file during bz_create_database in Bugzilla::DB,
+  # and we just want to display that error instead of doing
+  # anything else.
+  Bugzilla->dbh;
+  die "Reached an unreachable point";
 }
 
 sub _get_create_table_ddl {
-    my $self = shift;
-    my ($table) = @_;
-    my $ddl = $self->SUPER::_get_create_table_ddl(@_);
-
-    # TheSchwartz uses its own driver to access its tables, meaning
-    # that it doesn't understand "COLLATE bugzilla" and in fact
-    # SQLite throws an error when TheSchwartz tries to access its
-    # own tables, if COLLATE bugzilla is on them. We don't have
-    # to fix this elsewhere currently, because we only create
-    # TheSchwartz's tables, we never modify them.
-    if ($table =~ /^ts_/) {
-        $ddl =~ s/ COLLATE bugzilla//g;
-    }
-    return $ddl;
+  my $self    = shift;
+  my ($table) = @_;
+  my $ddl     = $self->SUPER::_get_create_table_ddl(@_);
+
+  # TheSchwartz uses its own driver to access its tables, meaning
+  # that it doesn't understand "COLLATE bugzilla" and in fact
+  # SQLite throws an error when TheSchwartz tries to access its
+  # own tables, if COLLATE bugzilla is on them. We don't have
+  # to fix this elsewhere currently, because we only create
+  # TheSchwartz's tables, we never modify them.
+  if ($table =~ /^ts_/) {
+    $ddl =~ s/ COLLATE bugzilla//g;
+  }
+  return $ddl;
 }
 
 sub get_type_ddl {
-    my $self = shift;
-    my $def = dclone($_[0]);
-    
-    my $ddl = $self->SUPER::get_type_ddl(@_);
-    if ($def->{PRIMARYKEY} and $def->{TYPE} =~ /SERIAL/i) {
-        $ddl =~ s/\bSERIAL\b/integer/;
-        $ddl =~ s/\bPRIMARY KEY\b/PRIMARY KEY AUTOINCREMENT/;
-    }
-    if ($def->{TYPE} =~ /text/i or $def->{TYPE} =~ /char/i) {
-        $ddl .= " COLLATE bugzilla";
-    }
-    # Don't collate DATETIME fields.
-    if ($def->{TYPE} eq 'DATETIME') {
-        $ddl =~ s/\bDATETIME\b/text COLLATE BINARY/;
-    }
-    return $ddl;
+  my $self = shift;
+  my $def  = dclone($_[0]);
+
+  my $ddl = $self->SUPER::get_type_ddl(@_);
+  if ($def->{PRIMARYKEY} and $def->{TYPE} =~ /SERIAL/i) {
+    $ddl =~ s/\bSERIAL\b/integer/;
+    $ddl =~ s/\bPRIMARY KEY\b/PRIMARY KEY AUTOINCREMENT/;
+  }
+  if ($def->{TYPE} =~ /text/i or $def->{TYPE} =~ /char/i) {
+    $ddl .= " COLLATE bugzilla";
+  }
+
+  # Don't collate DATETIME fields.
+  if ($def->{TYPE} eq 'DATETIME') {
+    $ddl =~ s/\bDATETIME\b/text COLLATE BINARY/;
+  }
+  return $ddl;
 }
 
 sub get_alter_column_ddl {
-    my $self = shift;
-    my ($table, $column, $new_def, $set_nulls_to) = @_;
-    my $dbh = Bugzilla->dbh;
-    
-    my $table_sql = $self->_sqlite_create_table($table);
-    my $new_ddl = $self->get_type_ddl($new_def);
-    # When we do ADD COLUMN, columns can show up all on one line separated
-    # by commas, so we have to account for that.
-    my $column_regex = _sqlite_column_regex($column);
-    $table_sql =~ s/$column_regex/$1$2$new_ddl$4/
-        || die "couldn't find $column in $table:\n$table_sql";
-    my @pre_sql = $self->_set_nulls_sql(@_);
-    return $self->_sqlite_alter_schema($table, $table_sql,
-                                       { pre_sql => \@pre_sql });
+  my $self = shift;
+  my ($table, $column, $new_def, $set_nulls_to) = @_;
+  my $dbh = Bugzilla->dbh;
+
+  my $table_sql = $self->_sqlite_create_table($table);
+  my $new_ddl   = $self->get_type_ddl($new_def);
+
+  # When we do ADD COLUMN, columns can show up all on one line separated
+  # by commas, so we have to account for that.
+  my $column_regex = _sqlite_column_regex($column);
+  $table_sql =~ s/$column_regex/$1$2$new_ddl$4/
+    || die "couldn't find $column in $table:\n$table_sql";
+  my @pre_sql = $self->_set_nulls_sql(@_);
+  return $self->_sqlite_alter_schema($table, $table_sql, {pre_sql => \@pre_sql});
 }
 
 sub get_add_column_ddl {
-    my $self = shift;
-    my ($table, $column, $definition, $init_value) = @_;
-    # SQLite can use the normal ADD COLUMN when:
-    # * The column isn't a PK
-    if ($definition->{PRIMARYKEY}) {
-        if ($definition->{NOTNULL} and $definition->{TYPE} !~ /SERIAL/i) {
-            die "You can only add new SERIAL type PKs with SQLite";
-        }
-        my $table_sql = $self->_sqlite_new_column_sql(@_);
-        # This works because _sqlite_alter_schema will exclude the new column
-        # in its INSERT ... SELECT statement, meaning that when the "new"
-        # table is populated, it will have AUTOINCREMENT values generated
-        # for it.
-        return $self->_sqlite_alter_schema($table, $table_sql);
-    }
-    # * The column has a default one way or another. Either it
-    #   defaults to NULL (it lacks NOT NULL) or it has a DEFAULT
-    #   clause. Since we also require this when doing bz_add_column (in
-    #   the way of forcing an init_value for NOT NULL columns with no
-    #   default), we first set the init_value as the default and then
-    #   alter the column.
-    if ($definition->{NOTNULL} and !defined $definition->{DEFAULT}) {
-        my %with_default = %$definition;
-        $with_default{DEFAULT} = $init_value;
-        my @pre_sql =
-            $self->SUPER::get_add_column_ddl($table, $column, \%with_default);
-        my $table_sql = $self->_sqlite_new_column_sql(@_);
-        return $self->_sqlite_alter_schema($table, $table_sql,
-            { pre_sql => \@pre_sql, extra_column => $column });
+  my $self = shift;
+  my ($table, $column, $definition, $init_value) = @_;
+
+  # SQLite can use the normal ADD COLUMN when:
+  # * The column isn't a PK
+  if ($definition->{PRIMARYKEY}) {
+    if ($definition->{NOTNULL} and $definition->{TYPE} !~ /SERIAL/i) {
+      die "You can only add new SERIAL type PKs with SQLite";
     }
-    
-    return $self->SUPER::get_add_column_ddl(@_);
+    my $table_sql = $self->_sqlite_new_column_sql(@_);
+
+    # This works because _sqlite_alter_schema will exclude the new column
+    # in its INSERT ... SELECT statement, meaning that when the "new"
+    # table is populated, it will have AUTOINCREMENT values generated
+    # for it.
+    return $self->_sqlite_alter_schema($table, $table_sql);
+  }
+
+  # * The column has a default one way or another. Either it
+  #   defaults to NULL (it lacks NOT NULL) or it has a DEFAULT
+  #   clause. Since we also require this when doing bz_add_column (in
+  #   the way of forcing an init_value for NOT NULL columns with no
+  #   default), we first set the init_value as the default and then
+  #   alter the column.
+  if ($definition->{NOTNULL} and !defined $definition->{DEFAULT}) {
+    my %with_default = %$definition;
+    $with_default{DEFAULT} = $init_value;
+    my @pre_sql = $self->SUPER::get_add_column_ddl($table, $column, \%with_default);
+    my $table_sql = $self->_sqlite_new_column_sql(@_);
+    return $self->_sqlite_alter_schema($table, $table_sql,
+      {pre_sql => \@pre_sql, extra_column => $column});
+  }
+
+  return $self->SUPER::get_add_column_ddl(@_);
 }
 
 sub _sqlite_new_column_sql {
-    my ($self, $table, $column, $def) = @_;
-    my $table_sql = $self->_sqlite_create_table($table);
-    my $new_ddl = $self->get_type_ddl($def);
-    my $new_line = "\t$column\t$new_ddl";
-    $table_sql =~ s/^(CREATE TABLE \w+ \()/$1\n$new_line,/s
-        || die "Can't find start of CREATE TABLE:\n$table_sql";
-    return $table_sql;
+  my ($self, $table, $column, $def) = @_;
+  my $table_sql = $self->_sqlite_create_table($table);
+  my $new_ddl   = $self->get_type_ddl($def);
+  my $new_line  = "\t$column\t$new_ddl";
+  $table_sql =~ s/^(CREATE TABLE \w+ \()/$1\n$new_line,/s
+    || die "Can't find start of CREATE TABLE:\n$table_sql";
+  return $table_sql;
 }
 
 sub get_drop_column_ddl {
-    my ($self, $table, $column) = @_;
-    my $table_sql = $self->_sqlite_create_table($table);
-    my $column_regex = _sqlite_column_regex($column);
-    $table_sql =~ s/$column_regex/$1/
-        || die "Can't find column $column: $table_sql";
-    # Make sure we don't end up with a comma at the end of the definition.
-    $table_sql =~ s/,\s+\)$/\n)/s;
-    return $self->_sqlite_alter_schema($table, $table_sql,
-                                       { exclude_column => $column });
+  my ($self, $table, $column) = @_;
+  my $table_sql    = $self->_sqlite_create_table($table);
+  my $column_regex = _sqlite_column_regex($column);
+  $table_sql =~ s/$column_regex/$1/
+    || die "Can't find column $column: $table_sql";
+
+  # Make sure we don't end up with a comma at the end of the definition.
+  $table_sql =~ s/,\s+\)$/\n)/s;
+  return $self->_sqlite_alter_schema($table, $table_sql,
+    {exclude_column => $column});
 }
 
 sub get_rename_column_ddl {
-    my ($self, $table, $old_name, $new_name) = @_;
-    my $table_sql = $self->_sqlite_create_table($table);
-    my $column_regex = _sqlite_column_regex($old_name);
-    $table_sql =~ s/$column_regex/$1\t$new_name\t$3$4/
-        || die "Can't find $old_name: $table_sql";
-    my %rename = ($old_name => $new_name);
-    return $self->_sqlite_alter_schema($table, $table_sql,
-                                       { rename => \%rename });
+  my ($self, $table, $old_name, $new_name) = @_;
+  my $table_sql    = $self->_sqlite_create_table($table);
+  my $column_regex = _sqlite_column_regex($old_name);
+  $table_sql =~ s/$column_regex/$1\t$new_name\t$3$4/
+    || die "Can't find $old_name: $table_sql";
+  my %rename = ($old_name => $new_name);
+  return $self->_sqlite_alter_schema($table, $table_sql, {rename => \%rename});
 }
 
 ################
@@ -279,24 +286,23 @@ sub get_rename_column_ddl {
 ################
 
 sub get_add_fks_sql {
-    my ($self, $table, $column_fks) = @_;
-    my @clauses = $self->_sqlite_table_lines($table);
-    my @add = $self->_column_fks_to_ddl($table, $column_fks);
-    push(@clauses, @add);
-    return $self->_sqlite_alter_schema($table, \@clauses);
+  my ($self, $table, $column_fks) = @_;
+  my @clauses = $self->_sqlite_table_lines($table);
+  my @add = $self->_column_fks_to_ddl($table, $column_fks);
+  push(@clauses, @add);
+  return $self->_sqlite_alter_schema($table, \@clauses);
 }
 
 sub get_drop_fk_sql {
-    my ($self, $table, $column, $references) = @_;
-    my @clauses = $self->_sqlite_table_lines($table);
-    my $fk_name = $self->_get_fk_name($table, $column, $references);
-    
-    my $line_re = qr/^\s+CONSTRAINT $fk_name /s;
-    grep { $line_re } @clauses
-        or die "Can't find $fk_name: " . join(',', @clauses);
-    @clauses = grep { $_ !~ $line_re } @clauses;
-    
-    return $self->_sqlite_alter_schema($table, \@clauses);
+  my ($self, $table, $column, $references) = @_;
+  my @clauses = $self->_sqlite_table_lines($table);
+  my $fk_name = $self->_get_fk_name($table, $column, $references);
+
+  my $line_re = qr/^\s+CONSTRAINT $fk_name /s;
+  grep {$line_re} @clauses or die "Can't find $fk_name: " . join(',', @clauses);
+  @clauses = grep { $_ !~ $line_re } @clauses;
+
+  return $self->_sqlite_alter_schema($table, \@clauses);
 }
 
 
index cd31065be5d9e7ea8ff3d804749212ab193da353..e05476b768624c42cd04f6ab68bbc0e234a07710 100644 (file)
@@ -46,23 +46,23 @@ sub _sqlite_collate_ci { lc($_[0]) cmp lc($_[1]) }
 sub _sqlite_mod { $_[0] % $_[1] }
 
 sub _sqlite_now {
-    my $now = DateTime->now(time_zone => Bugzilla->local_timezone);
-    return $now->ymd . ' ' . $now->hms;
+  my $now = DateTime->now(time_zone => Bugzilla->local_timezone);
+  return $now->ymd . ' ' . $now->hms;
 }
 
 # SQL's POSITION starts its values from 1 instead of 0 (so we add 1).
 sub _sqlite_position {
-    my ($text, $fragment) = @_;
-    if (!defined $text or !defined $fragment) {
-        return undef;
-    }
-    my $pos = index $text, $fragment;
-    return $pos + 1;
+  my ($text, $fragment) = @_;
+  if (!defined $text or !defined $fragment) {
+    return undef;
+  }
+  my $pos = index $text, $fragment;
+  return $pos + 1;
 }
 
 sub _sqlite_position_ci {
-    my ($text, $fragment) = @_;
-    return _sqlite_position(lc($text), lc($fragment));
+  my ($text, $fragment) = @_;
+  return _sqlite_position(lc($text), lc($fragment));
 }
 
 ###############
@@ -70,75 +70,80 @@ sub _sqlite_position_ci {
 ###############
 
 sub new {
-    my ($class, $params) = @_;
-    my $db_name = $params->{db_name};
-    
-    # Let people specify paths instead of data/ for the DB.
-    if ($db_name and $db_name !~ m{[\\/]}) {
-        # When the DB is first created, there's a chance that the
-        # data directory doesn't exist at all, because the Install::Filesystem
-        # code happens after DB creation. So we create the directory ourselves
-        # if it doesn't exist.
-        my $datadir = bz_locations()->{datadir};
-        if (!-d $datadir) {
-            mkdir $datadir or warn "$datadir: $!";
-        }
-        if (!-d "$datadir/db/") {
-            mkdir "$datadir/db/" or warn "$datadir/db: $!";
-        }
-        $db_name = bz_locations()->{datadir} . "/db/$db_name";
+  my ($class, $params) = @_;
+  my $db_name = $params->{db_name};
+
+  # Let people specify paths instead of data/ for the DB.
+  if ($db_name and $db_name !~ m{[\\/]}) {
+
+    # When the DB is first created, there's a chance that the
+    # data directory doesn't exist at all, because the Install::Filesystem
+    # code happens after DB creation. So we create the directory ourselves
+    # if it doesn't exist.
+    my $datadir = bz_locations()->{datadir};
+    if (!-d $datadir) {
+      mkdir $datadir or warn "$datadir: $!";
     }
-
-    # construct the DSN from the parameters we got
-    my $dsn = "dbi:SQLite:dbname=$db_name";
-
-    my $attrs = {
-        sqlite_unicode => 1,
-    };
-
-    my $self = $class->db_new({ dsn => $dsn, user => '', 
-                                pass => '', attrs => $attrs });
-    # Needed by TheSchwartz
-    $self->{private_bz_dsn} = $dsn;
-    
-    my %pragmas = (
-        # Make sure that the sqlite file doesn't grow without bound.
-        auto_vacuum => 1,
-        encoding => "'UTF-8'",
-        foreign_keys => 'ON',
-        # We want the latest file format.
-        legacy_file_format => 'OFF',
-        # This guarantees that we get column names like "foo"
-        # instead of "table.foo" in selectrow_hashref.
-        short_column_names => 'ON',
-        # The write-ahead log mode in SQLite 3.7 gets us better concurrency,
-        # but breaks backwards-compatibility with older versions of
-        # SQLite. (Which is important because people may also want to use
-        # command-line clients to access and back up their DB.) If you need
-        # better concurrency and don't need 3.6 compatibility, then you can
-        # uncomment this line.
-        #journal_mode => "'WAL'",
-    );
-    
-    while (my ($name, $value) = each %pragmas) {
-        $self->do("PRAGMA $name = $value");
+    if (!-d "$datadir/db/") {
+      mkdir "$datadir/db/" or warn "$datadir/db: $!";
     }
-    
-    $self->sqlite_create_collation('bugzilla', \&_sqlite_collate_ci);
-    $self->sqlite_create_function('position', 2, \&_sqlite_position);
-    $self->sqlite_create_function('iposition', 2, \&_sqlite_position_ci);
-    # SQLite has a "substr" function, but other DBs call it "SUBSTRING"
-    # so that's what we use, and I don't know of any way in SQLite to
-    # alias the SQL "substr" function to be called "SUBSTRING".
-    $self->sqlite_create_function('substring', 3, \&CORE::substr);
-    $self->sqlite_create_function('char_length', 1, sub { length($_[0]) });
-    $self->sqlite_create_function('mod', 2, \&_sqlite_mod);
-    $self->sqlite_create_function('now', 0, \&_sqlite_now);
-    $self->sqlite_create_function('localtimestamp', 1, \&_sqlite_now);
-    $self->sqlite_create_function('floor', 1, \&POSIX::floor);
-
-    bless ($self, $class);
-    return $self;
+    $db_name = bz_locations()->{datadir} . "/db/$db_name";
+  }
+
+  # construct the DSN from the parameters we got
+  my $dsn = "dbi:SQLite:dbname=$db_name";
+
+  my $attrs = {sqlite_unicode => 1,};
+
+  my $self
+    = $class->db_new({dsn => $dsn, user => '', pass => '', attrs => $attrs});
+
+  # Needed by TheSchwartz
+  $self->{private_bz_dsn} = $dsn;
+
+  my %pragmas = (
+
+    # Make sure that the sqlite file doesn't grow without bound.
+    auto_vacuum  => 1,
+    encoding     => "'UTF-8'",
+    foreign_keys => 'ON',
+
+    # We want the latest file format.
+    legacy_file_format => 'OFF',
+
+    # This guarantees that we get column names like "foo"
+    # instead of "table.foo" in selectrow_hashref.
+    short_column_names => 'ON',
+
+    # The write-ahead log mode in SQLite 3.7 gets us better concurrency,
+    # but breaks backwards-compatibility with older versions of
+    # SQLite. (Which is important because people may also want to use
+    # command-line clients to access and back up their DB.) If you need
+    # better concurrency and don't need 3.6 compatibility, then you can
+    # uncomment this line.
+    #journal_mode => "'WAL'",
+  );
+
+  while (my ($name, $value) = each %pragmas) {
+    $self->do("PRAGMA $name = $value");
+  }
+
+  $self->sqlite_create_collation('bugzilla', \&_sqlite_collate_ci);
+  $self->sqlite_create_function('position',  2, \&_sqlite_position);
+  $self->sqlite_create_function('iposition', 2, \&_sqlite_position_ci);
+
+  # SQLite has a "substr" function, but other DBs call it "SUBSTRING"
+  # so that's what we use, and I don't know of any way in SQLite to
+  # alias the SQL "substr" function to be called "SUBSTRING".
+  $self->sqlite_create_function('substring',      3, \&CORE::substr);
+  $self->sqlite_create_function('char_length',    1, sub { length($_[0]) });
+  $self->sqlite_create_function('mod',            2, \&_sqlite_mod);
+  $self->sqlite_create_function('now',            0, \&_sqlite_now);
+  $self->sqlite_create_function('localtimestamp', 1, \&_sqlite_now);
+  $self->sqlite_create_function('floor',          1, \&POSIX::floor);
+
+  bless($self, $class);
+  return $self;
 }
 
 ###############
@@ -146,86 +151,89 @@ sub new {
 ###############
 
 sub sql_position {
-    my ($self, $fragment, $text) = @_;
-    return "POSITION($text, $fragment)";
+  my ($self, $fragment, $text) = @_;
+  return "POSITION($text, $fragment)";
 }
 
 sub sql_iposition {
-    my ($self, $fragment, $text) = @_;
-    return "IPOSITION($text, $fragment)";
+  my ($self, $fragment, $text) = @_;
+  return "IPOSITION($text, $fragment)";
 }
 
 # SQLite does not have to GROUP BY the optional columns.
 sub sql_group_by {
-    my ($self, $needed_columns, $optional_columns) = @_;
-    my $expression = "GROUP BY $needed_columns";
-    return $expression;
+  my ($self, $needed_columns, $optional_columns) = @_;
+  my $expression = "GROUP BY $needed_columns";
+  return $expression;
 }
 
 # XXX SQLite does not support sorting a GROUP_CONCAT, so $sort is unimplemented.
 sub sql_group_concat {
-    my ($self, $column, $separator, $sort) = @_;
-    $separator = $self->quote(', ') if !defined $separator;
-    # In SQLite, a GROUP_CONCAT call with a DISTINCT argument can't
-    # specify its separator, and has to accept the default of ",".
-    if ($column =~ /^DISTINCT/) {
-        return "GROUP_CONCAT($column)";
-    }
-    return "GROUP_CONCAT($column, $separator)";
+  my ($self, $column, $separator, $sort) = @_;
+  $separator = $self->quote(', ') if !defined $separator;
+
+  # In SQLite, a GROUP_CONCAT call with a DISTINCT argument can't
+  # specify its separator, and has to accept the default of ",".
+  if ($column =~ /^DISTINCT/) {
+    return "GROUP_CONCAT($column)";
+  }
+  return "GROUP_CONCAT($column, $separator)";
 }
 
 sub sql_istring {
-    my ($self, $string) = @_;
-    return $string;
+  my ($self, $string) = @_;
+  return $string;
 }
 
 sub sql_regexp {
-    my ($self, $expr, $pattern, $nocheck, $real_pattern) = @_;
-    $real_pattern ||= $pattern;
+  my ($self, $expr, $pattern, $nocheck, $real_pattern) = @_;
+  $real_pattern ||= $pattern;
 
-    $self->bz_check_regexp($real_pattern) if !$nocheck;
+  $self->bz_check_regexp($real_pattern) if !$nocheck;
 
-    return "$expr REGEXP $pattern";
+  return "$expr REGEXP $pattern";
 }
 
 sub sql_not_regexp {
-    my $self = shift;
-    my $re_expression = $self->sql_regexp(@_);
-    return "NOT($re_expression)";
+  my $self          = shift;
+  my $re_expression = $self->sql_regexp(@_);
+  return "NOT($re_expression)";
 }
 
 sub sql_limit {
-    my ($self, $limit, $offset) = @_;
-
-    if (defined($offset)) {
-        return "LIMIT $limit OFFSET $offset";
-    } else {
-        return "LIMIT $limit";
-    }
+  my ($self, $limit, $offset) = @_;
+
+  if (defined($offset)) {
+    return "LIMIT $limit OFFSET $offset";
+  }
+  else {
+    return "LIMIT $limit";
+  }
 }
 
 sub sql_from_days {
-    my ($self, $days) = @_;
-    return "DATETIME($days)";
+  my ($self, $days) = @_;
+  return "DATETIME($days)";
 }
 
 sub sql_to_days {
-    my ($self, $date) = @_;
-    return "JULIANDAY($date)";
+  my ($self, $date) = @_;
+  return "JULIANDAY($date)";
 }
 
 sub sql_date_format {
-    my ($self, $date, $format) = @_;
-    $format = "%Y.%m.%d %H:%M:%S" if !$format;
-    $format =~ s/\%i/\%M/g;
-    $format =~ s/\%s/\%S/g;
-    return "STRFTIME(" . $self->quote($format) . ", $date)";
+  my ($self, $date, $format) = @_;
+  $format = "%Y.%m.%d %H:%M:%S" if !$format;
+  $format =~ s/\%i/\%M/g;
+  $format =~ s/\%s/\%S/g;
+  return "STRFTIME(" . $self->quote($format) . ", $date)";
 }
 
 sub sql_date_math {
-    my ($self, $date, $operator, $interval, $units) = @_;
-    # We do the || thing (concatenation) so that placeholders work properly.
-    return "DATETIME($date, '$operator' || $interval || ' $units')";
+  my ($self, $date, $operator, $interval, $units) = @_;
+
+  # We do the || thing (concatenation) so that placeholders work properly.
+  return "DATETIME($date, '$operator' || $interval || ' $units')";
 }
 
 ###############
@@ -233,56 +241,57 @@ sub sql_date_math {
 ###############
 
 sub bz_setup_database {
-    my $self = shift;
-    $self->SUPER::bz_setup_database(@_);
-
-    # If we created TheSchwartz tables with COLLATE bugzilla (during the
-    # 4.1.x development series) re-create them without it.
-    my @tables = $self->bz_table_list();
-    my @ts_tables = grep { /^ts_/ } @tables;
-    my $drop_ok;
-    foreach my $table (@ts_tables) {
-        my $create_table =
-            $self->_bz_real_schema->_sqlite_create_table($table);
-        if ($create_table =~ /COLLATE bugzilla/) {
-            if (!$drop_ok) {
-                _sqlite_jobqueue_drop_message();
-                $drop_ok = 1;
-            }
-            $self->bz_drop_table($table);
-            $self->bz_add_table($table);
-        }
+  my $self = shift;
+  $self->SUPER::bz_setup_database(@_);
+
+  # If we created TheSchwartz tables with COLLATE bugzilla (during the
+  # 4.1.x development series) re-create them without it.
+  my @tables = $self->bz_table_list();
+  my @ts_tables = grep {/^ts_/} @tables;
+  my $drop_ok;
+  foreach my $table (@ts_tables) {
+    my $create_table = $self->_bz_real_schema->_sqlite_create_table($table);
+    if ($create_table =~ /COLLATE bugzilla/) {
+      if (!$drop_ok) {
+        _sqlite_jobqueue_drop_message();
+        $drop_ok = 1;
+      }
+      $self->bz_drop_table($table);
+      $self->bz_add_table($table);
     }
+  }
 }
 
 sub _sqlite_jobqueue_drop_message {
-    # This is not translated because this situation will only happen if
-    # you are updating from a 4.1.x development version of Bugzilla using
-    # SQLite, and we don't want to maintain this string in strings.txt.pl
-    # forever for just this one uncommon circumstance.
-    print <<END;
+
+  # This is not translated because this situation will only happen if
+  # you are updating from a 4.1.x development version of Bugzilla using
+  # SQLite, and we don't want to maintain this string in strings.txt.pl
+  # forever for just this one uncommon circumstance.
+  print <<END;
 WARNING: We have to re-create all the database tables used by jobqueue.pl.
 If there are any pending jobs in the database (that is, emails that
 haven't been sent), they will be deleted.
 
 END
-    unless (Bugzilla->installation_answers->{NO_PAUSE}) {
-        print install_string('enter_or_ctrl_c');
-        getc;
-    }
+  unless (Bugzilla->installation_answers->{NO_PAUSE}) {
+    print install_string('enter_or_ctrl_c');
+    getc;
+  }
 }
 
 # XXX This needs to be implemented.
 sub bz_explain { }
 
 sub bz_table_list_real {
-    my $self = shift;
-    my @tables = $self->SUPER::bz_table_list_real(@_);
-    # SQLite includes a sqlite_sequence table in every database that isn't
-    # one of our real tables. We exclude any table that starts with sqlite_,
-    # just to be safe.
-    @tables = grep { $_ !~ /^sqlite_/ } @tables;
-    return @tables;
+  my $self   = shift;
+  my @tables = $self->SUPER::bz_table_list_real(@_);
+
+  # SQLite includes a sqlite_sequence table in every database that isn't
+  # one of our real tables. We exclude any table that starts with sqlite_,
+  # just to be safe.
+  @tables = grep { $_ !~ /^sqlite_/ } @tables;
+  return @tables;
 }
 
 1;
index 198460354b93a37b8c34a97c01335bb495277250..50e53f5e030510a5c53f3b26c7149eab18c7e24e 100644 (file)
@@ -25,178 +25,188 @@ use Date::Format;
 use Scalar::Util qw(blessed);
 
 sub _throw_error {
-    my ($name, $error, $vars) = @_;
-    my $cache = Bugzilla->request_cache;
-    $vars ||= {};
-
-    $vars->{error} = $error;
-
-    my $datadir = bz_locations()->{'datadir'};
-    # If a writable $datadir/errorlog exists, log error details there.
-    if (-w "$datadir/errorlog") {
-        require Bugzilla::Util;
-        require Data::Dumper;
-        my $mesg = "";
-        for (1..75) { $mesg .= "-"; };
-        $mesg .= "\n[$$] " . time2str("%D %H:%M:%S ", time());
-        $mesg .= "$name $error ";
-        $mesg .= Bugzilla::Util::remote_ip();
-        $mesg .= Bugzilla->user->login;
-        $mesg .= (' actually ' . Bugzilla->sudoer->login) if Bugzilla->sudoer;
-        $mesg .= "\n";
-        my %params = Bugzilla->cgi->Vars;
-        $Data::Dumper::Useqq = 1;
-        for my $param (sort keys %params) {
-            my $val = $params{$param};
-            # obscure passwords
-            $val = "*****" if $param =~ /password/i;
-            # limit line length
-            $val =~ s/^(.{512}).*$/$1\[CHOP\]/;
-            $mesg .= "[$$] " . Data::Dumper->Dump([$val],["param($param)"]);
-        }
-        for my $var (sort keys %ENV) {
-            my $val = $ENV{$var};
-            $val = "*****" if $val =~ /password|http_pass/i;
-            $mesg .= "[$$] " . Data::Dumper->Dump([$val],["env($var)"]);
-        }
-        open(ERRORLOGFID, ">>", "$datadir/errorlog");
-        print ERRORLOGFID "$mesg\n";
-        close ERRORLOGFID;
+  my ($name, $error, $vars) = @_;
+  my $cache = Bugzilla->request_cache;
+  $vars ||= {};
+
+  $vars->{error} = $error;
+
+  my $datadir = bz_locations()->{'datadir'};
+
+  # If a writable $datadir/errorlog exists, log error details there.
+  if (-w "$datadir/errorlog") {
+    require Bugzilla::Util;
+    require Data::Dumper;
+    my $mesg = "";
+    for (1 .. 75) { $mesg .= "-"; }
+    $mesg .= "\n[$$] " . time2str("%D %H:%M:%S ", time());
+    $mesg .= "$name $error ";
+    $mesg .= Bugzilla::Util::remote_ip();
+    $mesg .= Bugzilla->user->login;
+    $mesg .= (' actually ' . Bugzilla->sudoer->login) if Bugzilla->sudoer;
+    $mesg .= "\n";
+    my %params = Bugzilla->cgi->Vars;
+    $Data::Dumper::Useqq = 1;
+
+    for my $param (sort keys %params) {
+      my $val = $params{$param};
+
+      # obscure passwords
+      $val = "*****" if $param =~ /password/i;
+
+      # limit line length
+      $val =~ s/^(.{512}).*$/$1\[CHOP\]/;
+      $mesg .= "[$$] " . Data::Dumper->Dump([$val], ["param($param)"]);
     }
-
-    my $template = Bugzilla->template;
-    my $message;
-    # There are some tests that throw and catch a lot of errors,
-    # and calling $template->process over and over for those errors
-    # is too slow. So instead, we just "die" with a dump of the arguments.
-    if (Bugzilla->error_mode != ERROR_MODE_TEST && !$Bugzilla::Template::is_processing) {
-        $template->process($name, $vars, \$message)
-          || ThrowTemplateError($template->error());
+    for my $var (sort keys %ENV) {
+      my $val = $ENV{$var};
+      $val = "*****" if $val =~ /password|http_pass/i;
+      $mesg .= "[$$] " . Data::Dumper->Dump([$val], ["env($var)"]);
     }
-
-    # Let's call the hook first, so that extensions can override
-    # or extend the default behavior, or add their own error codes.
-    Bugzilla::Hook::process('error_catch', { error => $error, vars => $vars,
-                                             message => \$message });
-
-    if ($Bugzilla::Template::is_processing) {
-        $name =~ /^global\/(user|code)-error/;
-        my $type = $1 // 'unknown';
-        die Template::Exception->new("bugzilla.$type.$error", $vars);
+    open(ERRORLOGFID, ">>", "$datadir/errorlog");
+    print ERRORLOGFID "$mesg\n";
+    close ERRORLOGFID;
+  }
+
+  my $template = Bugzilla->template;
+  my $message;
+
+  # There are some tests that throw and catch a lot of errors,
+  # and calling $template->process over and over for those errors
+  # is too slow. So instead, we just "die" with a dump of the arguments.
+  if (Bugzilla->error_mode != ERROR_MODE_TEST
+    && !$Bugzilla::Template::is_processing)
+  {
+    $template->process($name, $vars, \$message)
+      || ThrowTemplateError($template->error());
+  }
+
+  # Let's call the hook first, so that extensions can override
+  # or extend the default behavior, or add their own error codes.
+  Bugzilla::Hook::process('error_catch',
+    {error => $error, vars => $vars, message => \$message});
+
+  if ($Bugzilla::Template::is_processing) {
+    $name =~ /^global\/(user|code)-error/;
+    my $type = $1 // 'unknown';
+    die Template::Exception->new("bugzilla.$type.$error", $vars);
+  }
+
+  if (Bugzilla->error_mode == ERROR_MODE_WEBPAGE) {
+    my $cgi = Bugzilla->cgi;
+    $cgi->close_standby_message('text/html', 'inline', 'error', 'html');
+    print $message;
+    print $cgi->multipart_final() if $cgi->{_multipart_in_progress};
+  }
+  elsif (Bugzilla->error_mode == ERROR_MODE_TEST) {
+    die Dumper($vars);
+  }
+  elsif (Bugzilla->error_mode == ERROR_MODE_DIE) {
+    die("$message\n");
+  }
+  elsif (Bugzilla->error_mode == ERROR_MODE_DIE_SOAP_FAULT
+    || Bugzilla->error_mode == ERROR_MODE_JSON_RPC
+    || Bugzilla->error_mode == ERROR_MODE_REST)
+  {
+    # Clone the hash so we aren't modifying the constant.
+    my %error_map = %{WS_ERROR_CODE()};
+    Bugzilla::Hook::process('webservice_error_codes', {error_map => \%error_map});
+    my $code = $error_map{$error};
+    if (!$code) {
+      $code = ERROR_UNKNOWN_FATAL     if $name =~ /code/i;
+      $code = ERROR_UNKNOWN_TRANSIENT if $name =~ /user/i;
     }
 
-    if (Bugzilla->error_mode == ERROR_MODE_WEBPAGE) {
-        my $cgi = Bugzilla->cgi;
-        $cgi->close_standby_message('text/html', 'inline', 'error', 'html');
-        print $message;
-        print $cgi->multipart_final() if $cgi->{_multipart_in_progress};
+    if (Bugzilla->error_mode == ERROR_MODE_DIE_SOAP_FAULT) {
+      die SOAP::Fault->faultcode($code)->faultstring($message);
     }
-    elsif (Bugzilla->error_mode == ERROR_MODE_TEST) {
-        die Dumper($vars);
+    elsif (Bugzilla->error_mode == ERROR_MODE_JSON_RPC) {
+      my $server = Bugzilla->_json_server;
+
+      # Technically JSON-RPC isn't allowed to have error numbers
+      # higher than 999, but we do this to avoid conflicts with
+      # the internal JSON::RPC error codes.
+      $server->raise_error(
+        code    => 100000 + $code,
+        message => $message,
+        id      => $server->{_bz_request_id},
+        version => $server->version
+      );
+
+      # Most JSON-RPC Throw*Error calls happen within an eval inside
+      # of JSON::RPC. So, in that circumstance, instead of exiting,
+      # we die with no message. JSON::RPC checks raise_error before
+      # it checks $@, so it returns the proper error.
+      die if $cache->{in_eval};
+      $server->response($server->error_response_header);
     }
-    elsif (Bugzilla->error_mode == ERROR_MODE_DIE) {
-        die("$message\n");
+    else {
+      my $server          = Bugzilla->api_server;
+      my %status_code_map = %{$server->constants->{REST_STATUS_CODE_MAP}};
+      my $status_code     = $status_code_map{$code} || $status_code_map{'_default'};
+      $server->return_error($status_code, $message, $code);
+      die if $cache->{in_eval};
+      $server->response;
     }
-    elsif (Bugzilla->error_mode == ERROR_MODE_DIE_SOAP_FAULT
-           || Bugzilla->error_mode == ERROR_MODE_JSON_RPC
-           || Bugzilla->error_mode == ERROR_MODE_REST)
-    {
-        # Clone the hash so we aren't modifying the constant.
-        my %error_map = %{ WS_ERROR_CODE() };
-        Bugzilla::Hook::process('webservice_error_codes',
-                                { error_map => \%error_map });
-        my $code = $error_map{$error};
-        if (!$code) {
-            $code = ERROR_UNKNOWN_FATAL if $name =~ /code/i;
-            $code = ERROR_UNKNOWN_TRANSIENT if $name =~ /user/i;
-        }
-
-        if (Bugzilla->error_mode == ERROR_MODE_DIE_SOAP_FAULT) {
-            die SOAP::Fault->faultcode($code)->faultstring($message);
-        }
-        elsif (Bugzilla->error_mode == ERROR_MODE_JSON_RPC) {
-            my $server = Bugzilla->_json_server;
-
-            # Technically JSON-RPC isn't allowed to have error numbers
-            # higher than 999, but we do this to avoid conflicts with
-            # the internal JSON::RPC error codes.
-            $server->raise_error(code        => 100000 + $code,
-                                 message     => $message,
-                                 id          => $server->{_bz_request_id},
-                                 version     => $server->version);
-            # Most JSON-RPC Throw*Error calls happen within an eval inside
-            # of JSON::RPC. So, in that circumstance, instead of exiting,
-            # we die with no message. JSON::RPC checks raise_error before
-            # it checks $@, so it returns the proper error.
-            die if $cache->{in_eval};
-            $server->response($server->error_response_header);
-        }
-        else {
-            my $server = Bugzilla->api_server;
-            my %status_code_map = %{ $server->constants->{REST_STATUS_CODE_MAP} };
-            my $status_code = $status_code_map{$code} || $status_code_map{'_default'};
-            $server->return_error($status_code, $message, $code);
-            die if $cache->{in_eval};
-            $server->response;
-        }
-    }
-    exit;
+  }
+  exit;
 }
 
 sub ThrowUserError {
-    _throw_error("global/user-error.html.tmpl", @_);
+  _throw_error("global/user-error.html.tmpl", @_);
 }
 
 sub ThrowCodeError {
-    my (undef, $vars) = @_;
+  my (undef, $vars) = @_;
+
+  # Don't show function arguments, in case they contain
+  # confidential data.
+  local $Carp::MaxArgNums = -1;
 
-    # Don't show function arguments, in case they contain
-    # confidential data.
-    local $Carp::MaxArgNums = -1;
-    # Don't show the error as coming from Bugzilla::Error, show it
-    # as coming from the caller.
-    local $Carp::CarpInternal{'Bugzilla::Error'} = 1;
-    $vars->{traceback} //= Carp::longmess();
+  # Don't show the error as coming from Bugzilla::Error, show it
+  # as coming from the caller.
+  local $Carp::CarpInternal{'Bugzilla::Error'} = 1;
+  $vars->{traceback} //= Carp::longmess();
 
-    _throw_error("global/code-error.html.tmpl", @_);
+  _throw_error("global/code-error.html.tmpl", @_);
 }
 
 sub ThrowTemplateError {
-    my ($template_err) = @_;
-    my $dbh = Bugzilla->dbh;
-
-    # Make sure the transaction is rolled back (if supported).
-    $dbh->bz_rollback_transaction() if $dbh->bz_in_transaction();
-
-    if (blessed($template_err) && $template_err->isa('Template::Exception')) {
-        my $type = $template_err->type;
-        if ($type =~ /^bugzilla\.(code|user)\.(.+)/) {
-            _throw_error("global/$1-error.html.tmpl", $2, $template_err->info);
-            return;
-        }
-    }
+  my ($template_err) = @_;
+  my $dbh = Bugzilla->dbh;
+
+  # Make sure the transaction is rolled back (if supported).
+  $dbh->bz_rollback_transaction() if $dbh->bz_in_transaction();
 
-    my $vars = {};
-    if (Bugzilla->error_mode == ERROR_MODE_DIE) {
-        die("error: template error: $template_err");
+  if (blessed($template_err) && $template_err->isa('Template::Exception')) {
+    my $type = $template_err->type;
+    if ($type =~ /^bugzilla\.(code|user)\.(.+)/) {
+      _throw_error("global/$1-error.html.tmpl", $2, $template_err->info);
+      return;
     }
+  }
 
-    $vars->{'template_error_msg'} = $template_err;
-    $vars->{'error'} = "template_error";
+  my $vars = {};
+  if (Bugzilla->error_mode == ERROR_MODE_DIE) {
+    die("error: template error: $template_err");
+  }
 
-    my $template = Bugzilla->template;
+  $vars->{'template_error_msg'} = $template_err;
+  $vars->{'error'}              = "template_error";
 
-    # Try a template first; but if this one fails too, fall back
-    # on plain old print statements.
-    if (!$template->process("global/code-error.html.tmpl", $vars)) {
-        require Bugzilla::Util;
-        import Bugzilla::Util qw(html_quote);
-        my $maintainer = Bugzilla->params->{'maintainer'};
-        my $error = html_quote($vars->{'template_error_msg'});
-        my $error2 = html_quote($template->error());
-        my $url = html_quote(Bugzilla->cgi->self_url);
+  my $template = Bugzilla->template;
 
-        print <<END;
+  # Try a template first; but if this one fails too, fall back
+  # on plain old print statements.
+  if (!$template->process("global/code-error.html.tmpl", $vars)) {
+    require Bugzilla::Util;
+    import Bugzilla::Util qw(html_quote);
+    my $maintainer = Bugzilla->params->{'maintainer'};
+    my $error      = html_quote($vars->{'template_error_msg'});
+    my $error2     = html_quote($template->error());
+    my $url        = html_quote(Bugzilla->cgi->self_url);
+
+    print <<END;
           <p>
             Bugzilla has suffered an internal error. Please save this page and 
             send it to $maintainer with details of what you were doing at the 
@@ -207,8 +217,8 @@ sub ThrowTemplateError {
           First error: $error<br>
           Second error: $error2</p>
 END
-    }
-    exit;
+  }
+  exit;
 }
 
 1;
index eb17b9f1a71f4f3b7f277e408cfccd6445497968..c60a3aa768294d4ecb357d3e544382b1bd6459c4 100644 (file)
@@ -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'};
-    push(@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'};
+  push(@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;
index a43fb55e58e7b997f7a16a701308444c816aa48a..8567b6dfcf71f73e8628f43c8d02b53adfcd9c00 100644 (file)
@@ -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,111 +162,233 @@ 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 => 'dupe_of',               desc => 'Duplicate of'},
+  {
+    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 => 'dupe_of',     desc => 'Duplicate of'},
 );
 
 ################
@@ -277,12 +397,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(@_);
 }
 
 ##############
@@ -292,151 +412,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; }
@@ -584,11 +706,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
@@ -609,19 +733,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
@@ -638,8 +762,8 @@ in the C<timetrackinggroup>.
 =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
@@ -658,12 +782,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
@@ -681,22 +804,23 @@ or undef if there is no C<visibility_field> 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
@@ -713,10 +837,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
@@ -734,11 +858,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
@@ -755,10 +879,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
@@ -772,15 +896,15 @@ See L<Bugzilla::Field::ChoiceInterface>.
 =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
@@ -796,13 +920,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
@@ -889,29 +1013,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]); }
 
@@ -935,69 +1062,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 separate 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 separate 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
@@ -1043,90 +1174,95 @@ C<is_mandatory> - 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
@@ -1149,13 +1285,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
@@ -1174,107 +1311,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
@@ -1306,32 +1451,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
@@ -1353,10 +1498,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;
index 3081fb29a1bf635e791e1c2f8a37524956dc77ec..cf045613afff6030304c442d0e9c76d3e3e36902 100644 (file)
@@ -28,43 +28,43 @@ 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',
-    target_milestone => 'Bugzilla::Milestone',
+  bug_status       => 'Bugzilla::Status',
+  classification   => 'Bugzilla::Classification',
+  component        => 'Bugzilla::Component',
+  product          => 'Bugzilla::Product',
+  target_milestone => 'Bugzilla::Milestone',
 };
 
 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',
 };
 
 #################
@@ -77,49 +77,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 <<EOC;
+    # Callers expect the module to be already loaded.
+    eval "require $package";
+    return $package;
+  }
+
+  # 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 <<EOC;
             package $package;
             use parent qw(Bugzilla::Field::Choice);
             use constant DB_TABLE => '$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(@_);
 }
 
 #########################
@@ -131,64 +132,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();
 }
 
 ############
@@ -196,12 +199,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};
 }
 
 ##############
@@ -209,73 +213,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;
index bae62c4591e4fd8b2f25525094b20ad43a120397..9d83bb42ad4f9eeaa56924a4b0b514dc49dd707a 100644 (file)
@@ -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,144 +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 $self->name eq '' || $self->name eq 'DUPLICATE';
-    }
-    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 $self->name eq '' || $self->name eq 'DUPLICATE';
+  }
+  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;
index 6c8dab377a4762ab7a3a5b58c095e4b118652dcc..6e5b05293866c609769b65f472f7becb400006e7 100644 (file)
@@ -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;
-    }
-    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);
-        }
-        ($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);
+  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);
     }
-    # 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' });
+    ($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});
+      }
     }
 
-    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;
 }
 
 ######################################################################
@@ -813,146 +850,165 @@ It can contain both a product_id AND a component id.
 =cut
 
 sub extract_flags_from_cgi {
-    my ($class, $vars, $skip, $args) = @_;
-
-    my $cgi = Bugzilla->cgi;
-
-    my ($bug, $attachment, $component_id, $product_id);
+  my ($class, $vars, $skip, $args) = @_;
+
+  my $cgi = Bugzilla->cgi;
+
+  my ($bug, $attachment, $component_id, $product_id);
+
+  if (defined($args->{bug})) {
+    $bug          = $args->{bug};
+    $component_id = $bug->component_id;
+    $product_id   = $bug->product_id;
+    $attachment   = $args->{attachment} if defined $args->{attachment};
+  }
+  elsif (defined($args->{product_id})) {
+    $product_id   = $args->{product_id};
+    $component_id = $args->{component_id};
+  }
+
+  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+)$/a ? $1 : () } $cgi->multi_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+)$/a ? $1 : () } $cgi->multi_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->multi_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 (defined($args->{bug})) {
-        $bug = $args->{bug};
-        $component_id = $bug->component_id;
-        $product_id = $bug->product_id;
-        $attachment = $args->{attachment} if defined $args->{attachment};
+      # 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") || '');
     }
-    elsif (defined($args->{product_id})) {
-        $product_id = $args->{product_id};
-        $component_id = $args->{component_id};
+
+    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'   => $product_id,
+    'component_id' => $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;
     }
+  }
 
-    my $match_status = Bugzilla::User::match_field({
-        '^requestee(_type)?-(\d+)$' => { 'type' => 'multi' },
-    }, undef, $skip);
+  foreach my $flag_type (@$flag_types) {
+    my $type_id = $flag_type->id;
 
-    $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';
-    }
+    # 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);
 
-    # Extract a list of flag type IDs from field names.
-    my @flagtype_ids = map { /^flag_type-(\d+)$/a ? $1 : () } $cgi->multi_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+)$/a ? $1 : () } $cgi->multi_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->multi_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") || '');
-        }
+    # We are only interested in flags the user tries to create.
+    next unless scalar(grep { $_ == $type_id } @flagtype_ids);
 
-        push(@flags, { id        => $flag_id,
-                       status    => $status,
-                       requestee => $requestee_email,
-                       skip_roe  => $skip });
-    }
+    # If $bug is not defined, then we are creating a flag for an as
+    # yet uncreated bug.
+    if (defined $bug) {
 
-    # Get a list of active flag types available for this product/component.
-    my $flag_types = Bugzilla::FlagType::match(
-        { 'product_id'   => $product_id,
-          'component_id' => $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;
-        }
-    }
+      # 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
+      });
 
-    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);
-
-        # If $bug is not defined, then we are creating a flag for an as
-        # yet uncreated bug.
-        if (defined $bug) {
-            # 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);
-        }
+      # 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->multi_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 });
-        }
+    my $status = $cgi->param("flag_type-$type_id");
+    trick_taint($status);
+
+    my @logins = $cgi->multi_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
@@ -970,100 +1026,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+)$/a ? $1 : () } $cgi->multi_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+)$/a ? $1 : () } $cgi->multi_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->multi_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->multi_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
@@ -1080,106 +1147,113 @@ 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;
+  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 (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 $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;
-    }
-
-    # 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) {
-        my $user = $recipients{$to};
-        # Add threadingmarker to allow flag notification emails to be the
-        # threaded similar to normal bug change emails.
-        my $thread_user_id = $user ? $user->id : 0;
-
-        # We only want to show private comments to users in the is_insider group
-        my $comments = $user && $user->is_insider
-            ? $all_comments : $public_comments;
-
-        my $vars = {
-            flag            => $flag,
-            old_flag        => $old_flag,
-            to              => $to,
-            to_user         => $user,
-            date            => $timestamp,
-            bug             => $bug,
-            attachment      => $attachment,
-            threadingmarker => build_thread_marker($bug->id, $thread_user_id),
-            new_comments    => $comments,
-        };
-
-        my $templates = {
-            header => "email/flagmail-header.txt.tmpl",
-            text   => "email/flagmail.txt.tmpl",
-            html   => "email/flagmail.html.tmpl",
-        };
-
-        MessageToMTA(generate_email($vars, $templates));
+  }
+  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;
+  }
+
+  # 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) {
+    my $user = $recipients{$to};
+
+    # Add threadingmarker to allow flag notification emails to be the
+    # threaded similar to normal bug change emails.
+    my $thread_user_id = $user ? $user->id : 0;
+
+    # We only want to show private comments to users in the is_insider group
+    my $comments = $user && $user->is_insider ? $all_comments : $public_comments;
+
+    my $vars = {
+      flag            => $flag,
+      old_flag        => $old_flag,
+      to              => $to,
+      to_user         => $user,
+      date            => $timestamp,
+      bug             => $bug,
+      attachment      => $attachment,
+      threadingmarker => build_thread_marker($bug->id, $thread_user_id),
+      new_comments    => $comments,
+    };
+
+    my $templates = {
+      header => "email/flagmail-header.txt.tmpl",
+      text   => "email/flagmail.txt.tmpl",
+      html   => "email/flagmail.html.tmpl",
+    };
+
+    MessageToMTA(generate_email($vars, $templates));
+  }
 }
 
 # This is an internal function used by $bug->flag_types
@@ -1187,39 +1261,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;
index 644a02e7fd3c2293ce0e61094e1468ccc1e5aee4..b39fe9d1bec3b30987322464520417d7d058c4bd 100644 (file)
@@ -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 are not
-    # required to match 'emailregexp'.
-    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 are not
+  # required to match 'emailregexp'.
+  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;
index 481987488156c0c66f3f2a198b4a42cae85f04ba..8e83293049913b9ff07ab7d88887270eac9b485e 100644 (file)
@@ -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, email, group_id
+  my $dbh = Bugzilla->dbh;
+  my $sth = $dbh->prepare(
+    "SELECT userid, email, 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, $email, $present) = $sth->fetchrow_array) {
-        if ($regexp ne '' and $email =~ /$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, $email, $present) = $sth->fetchrow_array) {
+    if ($regexp ne '' and $email =~ /$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,40 +477,42 @@ 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) || '';
+  my ($invocant, $regex) = @_;
+  $regex = trim($regex) || '';
 
-    my $max_length = Bugzilla->dbh->bz_column_length( 'groups', 'userregexp' );
-    ThrowUserError( "group_regexp_too_long",
-        { text => $regex, max_length => $max_length } )
-      if length($regex) > $max_length;
+  my $max_length = Bugzilla->dbh->bz_column_length('groups', 'userregexp');
+  ThrowUserError("group_regexp_too_long",
+    {text => $regex, max_length => $max_length})
+    if length($regex) > $max_length;
 
-    ThrowUserError("invalid_regexp") unless (eval {qr/$regex/});
-    return $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; }
index 82a342767c14e8478c571438851820b94e50a9ef..047ea4259938c56623f5c2cd13986f6b1537d50f 100644 (file)
@@ -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;
index e90d545a8a078c90b32b9f5f19609545390f6ab4..3cb64067ef166d89b58e190ad469ee7cb07b6f04 100644 (file)
@@ -31,435 +31,450 @@ 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 => 'Classic' },
+    skin => {subclass => 'Skin', default => 'Classic'},
+
     # 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'},
+
     # 2014-05-24 koosha.khajeh@gmail.com -- Bug 1014164
-    use_markdown       => { options => ['on', 'off'], default => 'on' },
-    }
-};
+    use_markdown => {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 queries and schedules for periodic'
-            . ' reports to be run and sent via email to other users and groups',
-    },
-    {
-        name         => 'bz_canusewhines',
-        description  => 'Can configure queries and schedules for periodic'
-            . ' reports to be run and sent via email to themselves',
-        # 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 queries and schedules for periodic'
+      . ' reports to be run and sent via email to other users and groups',
+  },
+  {
+    name        => 'bz_canusewhines',
+    description => 'Can configure queries and schedules for periodic'
+      . ' reports to be run and sent via email to themselves',
+
+    # 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 $group = new Bugzilla::Group({ name => $definition->{name} });
-        if (!$group) {
-            $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);
-                }
-            }
-        }
-        elsif ($group->description ne $definition->{description}) {
-            $group->set_description($definition->{description});
-            $group->update();
+  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 $group = new Bugzilla::Group({name => $definition->{name}});
+    if (!$group) {
+      $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
+          );
         }
+      }
+    }
+    elsif ($group->description ne $definition->{description}) {
+      $group->set_description($definition->{description});
+      $group->update();
     }
+  }
 
-    $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;
-
-    # We must ensure $/ is set to \r\n on windows.
-    # CGI.pm unsets.
-    local $/ = ON_WINDOWS ? "\x0d\x0a" : "\x0a";
-
-    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_LOGIN'};
-    my $email     = $answer{'ADMIN_EMAIL'};
-    my $password  = $answer{'ADMIN_PASSWORD'};
-    my $full_name = $answer{'ADMIN_REALNAME'};
-
-    if (!($login || Bugzilla->params->{'use_email_as_login'})
-        || !$email
-        || !$password)
-    {
-        say "\n" . get_text('install_admin_setup') . "\n";
-    }
-    if (not $email) {
-        print get_text('install_admin_get_email') . ' ';
-        $email = <STDIN>;
-        chomp $email if defined $email;
-    }
-    Bugzilla::User->check_email($email);
-    # Make sure the email address is used as login when required.
-    if (Bugzilla->params->{'use_email_as_login'}) {
-        $login = $email;
-    }
-
-    if (not $login) {
-        print get_text('install_admin_get_login') . ' ';
-        $login = <STDIN>;
-        chomp $login if defined $login;
-    }
-    Bugzilla::User->check_login_name($login, undef, {email => $email});
-
-    if (not defined $full_name) {
-        print get_text('install_admin_get_name') . ' ';
-        $full_name = <STDIN>;
-        chomp $full_name if defined $full_name;
-    }
-
-    if (not $password) {
-        $password = _prompt_for_password(
-            get_text('install_admin_get_password'));
-    }
-
-    my $admin = Bugzilla::User->create({ login_name    => $login,
-                                         email         => $email,
-                                         realname      => $full_name,
-                                         cryptpassword => $password });
-    make_admin($admin);
+  my ($params) = @_;
+  my $dbh      = Bugzilla->dbh;
+  my $template = Bugzilla->template;
+
+  # We must ensure $/ is set to \r\n on windows.
+  # CGI.pm unsets.
+  local $/ = ON_WINDOWS ? "\x0d\x0a" : "\x0a";
+
+  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_LOGIN'};
+  my $email     = $answer{'ADMIN_EMAIL'};
+  my $password  = $answer{'ADMIN_PASSWORD'};
+  my $full_name = $answer{'ADMIN_REALNAME'};
+
+  if ( !($login || Bugzilla->params->{'use_email_as_login'})
+    || !$email
+    || !$password)
+  {
+    say "\n" . get_text('install_admin_setup') . "\n";
+  }
+  if (not $email) {
+    print get_text('install_admin_get_email') . ' ';
+    $email = <STDIN>;
+    chomp $email if defined $email;
+  }
+  Bugzilla::User->check_email($email);
+
+  # Make sure the email address is used as login when required.
+  if (Bugzilla->params->{'use_email_as_login'}) {
+    $login = $email;
+  }
+
+  if (not $login) {
+    print get_text('install_admin_get_login') . ' ';
+    $login = <STDIN>;
+    chomp $login if defined $login;
+  }
+  Bugzilla::User->check_login_name($login, undef, {email => $email});
+
+  if (not defined $full_name) {
+    print get_text('install_admin_get_name') . ' ';
+    $full_name = <STDIN>;
+    chomp $full_name if defined $full_name;
+  }
+
+  if (not $password) {
+    $password = _prompt_for_password(get_text('install_admin_get_password'));
+  }
+
+  my $admin = Bugzilla::User->create({
+    login_name    => $login,
+    email         => $email,
+    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 = <STDIN>;
-        chomp $password;
-        print "\n", get_text('install_confirm_password'), ' ';
-        my $pass2 = <STDIN>;
-        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 = <STDIN>;
+    chomp $password;
+    print "\n", get_text('install_confirm_password'), ' ';
+    my $pass2 = <STDIN>;
+    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;
index 5ac8bc5aba937c632805efc1bf64c0e7120e0782..772e96e854f5ccbd5ff60b49e68b6526990b2b34 100644 (file)
@@ -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.14.0;
 use strict;
@@ -34,104 +34,108 @@ 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 <myk@mozilla.org> 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 <myk@mozilla.org> 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);
 
-    $dbh->bz_add_column('user_api_keys', 'app_id',
-                        {TYPE => 'varchar(64)'});
-    $dbh->bz_add_index('user_api_keys', 'user_api_keys_user_id_app_id_idx',
-                       [qw(user_id app_id)]);
+  # 2012-04-12 aliustek@gmail.com - Bug 728138
+  $dbh->bz_add_column('fielddefs', 'long_desc',
+    {TYPE => 'varchar(255)', NOTNULL => 1, DEFAULT => "''"}, '');
 
-    Bugzilla::Hook::process('install_update_db_fielddefs');
+  $dbh->bz_add_column('user_api_keys', 'app_id', {TYPE => 'varchar(64)'});
+  $dbh->bz_add_index('user_api_keys', 'user_api_keys_user_id_app_id_idx',
+    [qw(user_id app_id)]);
 
-    # 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.
@@ -141,14 +145,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/
 #
@@ -157,1077 +161,1106 @@ 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();
+  my $old_params = shift;
+  my $dbh        = Bugzilla->dbh;
+  _update_pre_checksetup_bugzillas();
 
-    $dbh->bz_add_column('profiles', 'disabledtext',
-                        {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, '');
+  $dbh->bz_add_column('attachments', 'submitter_id',
+    {TYPE => 'INT3', NOTNULL => 1}, 0);
 
-    _populate_longdescs();
-    _update_bugs_activity_field_to_fieldid();
+  $dbh->bz_rename_column('bugs_activity', 'when', 'bug_when');
 
-    if (!$dbh->bz_column_info('bugs', 'lastdiffed')) {
-        $dbh->bz_add_column('bugs', 'lastdiffed', {TYPE =>'DATETIME'});
-        $dbh->do('UPDATE bugs SET lastdiffed = NOW()');
-    }
+  _add_bug_vote_cache();
+  _update_product_name_definition();
 
-    _add_unique_login_name_index_to_profiles();
+  $dbh->bz_add_column('profiles', 'disabledtext',
+    {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, '');
 
-    $dbh->bz_add_column('profiles', 'mybugslink', 
-                        {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'});
+  _populate_longdescs();
+  _update_bugs_activity_field_to_fieldid();
 
-    _update_component_user_fields_to_ids();
+  if (!$dbh->bz_column_info('bugs', 'lastdiffed')) {
+    $dbh->bz_add_column('bugs', 'lastdiffed', {TYPE => 'DATETIME'});
+    $dbh->do('UPDATE bugs SET lastdiffed = NOW()');
+  }
 
-    $dbh->bz_add_column('bugs', 'everconfirmed',
-                        {TYPE => 'BOOLEAN', NOTNULL => 1}, 1);
+  _add_unique_login_name_index_to_profiles();
 
-    _populate_milestones_table();
+  $dbh->bz_add_column('profiles', 'mybugslink',
+    {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'});
 
-    _add_products_defaultmilestone();
+  _update_component_user_fields_to_ids();
 
-    # 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', 'everconfirmed', {TYPE => 'BOOLEAN', NOTNULL => 1},
+    1);
 
-    _copy_from_comments_to_longdescs();
-    _populate_duplicates_table();
+  _populate_milestones_table();
 
-    if (!$dbh->bz_column_info('email_setting', 'user_id')) {
-        $dbh->bz_add_column('profiles', 'emailflags', {TYPE => 'MEDIUMTEXT'});
-    }
+  _add_products_defaultmilestone();
 
-    $dbh->bz_add_column('groups', 'isactive',
-                        {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('attachments', 'isobsolete',
-                        {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'});
+  _copy_from_comments_to_longdescs();
+  _populate_duplicates_table();
 
-    $dbh->bz_drop_column("profiles", "emailnotification");
-    $dbh->bz_drop_column("profiles", "newemailtech");
+  if (!$dbh->bz_column_info('email_setting', 'user_id')) {
+    $dbh->bz_add_column('profiles', 'emailflags', {TYPE => 'MEDIUMTEXT'});
+  }
 
-    # 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('groups', 'isactive',
+    {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'});
 
-    _recrypt_plaintext_passwords();
+  $dbh->bz_add_column('attachments', 'isobsolete',
+    {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'});
 
-    # 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_drop_column("profiles", "emailnotification");
+  $dbh->bz_drop_column("profiles", "newemailtech");
 
-    _update_bugs_activity_to_only_record_changes();
+  # 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});
 
-    # bug 90933: Make disabledtext NOT NULL
-    if (!$dbh->bz_column_info('profiles', 'disabledtext')->{NOTNULL}) {
-        $dbh->bz_alter_column("profiles", "disabledtext",
-                              {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, '');
-    }
+  _update_bugs_activity_to_only_record_changes();
 
-    $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'});
+  # bug 90933: Make disabledtext NOT NULL
+  if (!$dbh->bz_column_info('profiles', 'disabledtext')->{NOTNULL}) {
+    $dbh->bz_alter_column("profiles", "disabledtext",
+      {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, '');
+  }
 
-    $dbh->bz_add_column("bugs_activity", "attach_id", {TYPE => 'INT3'});
+  $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'});
 
-    _delete_logincookies_cryptpassword_and_handle_invalid_cookies();
+  $dbh->bz_add_column("bugs_activity", "attach_id", {TYPE => 'INT3'});
 
-    # 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");
+  _delete_logincookies_cryptpassword_and_handle_invalid_cookies();
 
-    # 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'});
+  # 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");
 
-    _use_ip_instead_of_hostname_in_logincookies();
+  # 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('longdescs', 'isprivate',
-                        {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'});
-    $dbh->bz_add_column('attachments', 'isprivate',
-                        {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'});
+  _use_ip_instead_of_hostname_in_logincookies();
 
-    _move_quips_into_db();
+  $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();
 
-    $dbh->bz_drop_column("namedqueries", "watchfordiffs");
+  # 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();
+
+  # 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}, '');
+  }
 
-    _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('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 => "''"});
+
+  _clean_control_characters_from_short_desc();
 
-    # 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");
+  # 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});
     }
+  }
 
-    _copy_old_charts_into_database();
-
-    _add_user_group_map_grant_type();
-    _add_group_group_map_grant_type();
+  # 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("profiles", "extern_id", {TYPE => 'varchar(64)'});
+  # 2006-08-06 LpSolit@gmail.com - Bug 347521
+  $dbh->bz_alter_column('flagtypes', 'id',
+    {TYPE => 'SMALLSERIAL', NOTNULL => 1, PRIMARYKEY => 1});
 
-    $dbh->bz_add_column('flagtypes', 'grant_group_id', {TYPE => 'INT3'});
-    $dbh->bz_add_column('flagtypes', 'request_group_id', {TYPE => 'INT3'});
+  $dbh->bz_alter_column('keyworddefs', 'id',
+    {TYPE => 'SMALLSERIAL', NOTNULL => 1, PRIMARYKEY => 1});
 
-    # 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'});
+  # 2006-08-19 LpSolit@gmail.com - Bug 87795
+  $dbh->bz_alter_column('tokens', 'userid', {TYPE => 'INT3'});
 
-    _add_longdescs_already_wrapped();
+  $dbh->bz_drop_index('bugs', 'bugs_short_desc_idx');
 
-    # 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});
+  # 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 => "''"});
 
-    # 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();
-
-    # 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_add_column('products', 'classification_id',
-                        {TYPE => 'INT2', NOTNULL => 1, DEFAULT => '1'});
+  _update_longdescs_who_index();
 
-    _fix_group_with_empty_name();
+  $dbh->bz_add_column('setting', 'subclass', {TYPE => 'varchar(32)'});
 
-    $dbh->bz_add_index('bugs_activity', 'bugs_activity_who_idx', [qw(who)]);
+  $dbh->bz_alter_column('longdescs', 'thetext',
+    {TYPE => 'LONGTEXT', NOTNULL => 1}, '');
 
-    # 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'});
-    }
+  # 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'});
 
-    $dbh->bz_alter_column('bugs', 'lastdiffed', {TYPE => 'DATETIME'});
+  # 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-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");
-    }
+  $dbh->bz_add_column('versions', 'id',
+    {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1});
+  $dbh->bz_add_column('milestones', 'id',
+    {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1});
 
-    # 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");
+  _fix_uppercase_custom_field_names();
+  _fix_uppercase_index_names();
 
-    _migrate_email_prefs_to_new_table();
-    _initialize_new_email_prefs();
-    _change_all_mysql_booleans_to_tinyint();
+  # 2007-05-17 LpSolit@gmail.com - Bug 344965
+  _initialize_workflow_for_upgrade($old_params);
 
-    # make classification_id field type be consistent with DB:Schema
-    $dbh->bz_alter_column('products', 'classification_id',
-                          {TYPE => 'INT2', NOTNULL => 1, DEFAULT => '1'});
+  # 2007-08-08 LpSolit@gmail.com - Bug 332149
+  $dbh->bz_add_column('groups', 'icon_url', {TYPE => 'TINYTEXT'});
 
-    # 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-21 wurblzap@gmail.com - Bug 365378
+  _make_lang_setting_dynamic();
 
-    # 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-11-29 xiaoou.wu@oracle.com - Bug 153129
+  _change_text_types();
 
-    # For a short time, the flags_type_id_idx was misnamed in upgraded installs.
-    $dbh->bz_drop_index('flags', 'type_id');
+  # 2007-09-09 LpSolit@gmail.com - Bug 99215
+  _fix_attachment_modification_date();
 
-    # 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();
+  $dbh->bz_drop_index('longdescs', 'longdescs_thetext_idx');
+  _populate_bugs_fulltext();
 
-    if (!exists $dbh->bz_column_info('milestones', 'sortkey')->{DEFAULT}) {
-        $dbh->bz_alter_column('milestones', 'sortkey',
-                              {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0});
-    }
+  # 2008-01-18 xiaoou.wu@oracle.com - Bug 414292
+  $dbh->bz_alter_column('series', 'query', {TYPE => 'MEDIUMTEXT', NOTNULL => 1});
 
-    # 2005-06-14 - LpSolit@gmail.com - Bug 292544
-    $dbh->bz_alter_column('bugs', 'creation_ts', {TYPE => 'DATETIME'});
+  # Add FK to multi select field tables
+  _add_foreign_keys_to_multiselects();
 
-    _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-07-28 tfu@redhat.com - Bug 431669
+  $dbh->bz_alter_column('group_control_map', 'product_id',
+    {TYPE => 'INT2', NOTNULL => 1});
 
-    _rederive_regex_groups();
+  # 2008-09-07 LpSolit@gmail.com - Bug 452893
+  _fix_illegal_flag_modification_dates();
 
-    # PUBLIC is a reserved word in Oracle.
-    $dbh->bz_rename_column('series', 'public', 'is_public');
+  _add_visiblity_value_to_value_tables();
 
-    # 2005-11-04 LpSolit@gmail.com - Bug 305927
-    $dbh->bz_alter_column('groups', 'userregexp',
-                          {TYPE => 'TINYTEXT', NOTNULL => 1, DEFAULT => "''"});
+  # 2009-03-02 arbingersys@gmail.com - Bug 423613
+  _add_extern_id_index();
 
-    _clean_control_characters_from_short_desc();
+  # 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'});
 
-    # 2005-12-07 altlst@sonic.net -- Bug 225221
-    $dbh->bz_add_column('longdescs', 'comment_id',
-        {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1});
+  # 2009-01-16 oreomike@gmail.com - Bug 302420
+  $dbh->bz_add_column('whine_events', 'mailifnobugs',
+    {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'});
 
-    _stop_storing_inactive_flags();
-    _change_short_desc_from_mediumtext_to_varchar();
+  _convert_disallownew_to_isactive();
 
-    # 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();
+  $dbh->bz_alter_column('bugs_activity', 'added', {TYPE => 'varchar(255)'});
+  $dbh->bz_add_index('bugs_activity', 'bugs_activity_added_idx', ['added']);
 
-    _add_classifications_sortkey();
-    _move_data_nomail_into_db();
+  # 2009-09-28 LpSolit@gmail.com - Bug 519032
+  $dbh->bz_drop_column('series', 'last_viewed');
 
-    # 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});
-        }
-    }
+  # 2009-09-28 LpSolit@gmail.com - Bug 399073
+  _fix_logincookies_ipaddr();
 
-    # 2006-08-04 LpSolit@gmail.com - Bug 305941
-    $dbh->bz_drop_column('profiles', 'refreshed_when');
-    $dbh->bz_drop_column('groups', 'last_changed');
+  # 2009-11-01 LpSolit@gmail.com - Bug 525025
+  _fix_invalid_custom_field_names();
 
-    # 2006-08-06 LpSolit@gmail.com - Bug 347521
-    $dbh->bz_alter_column('flagtypes', 'id',
-          {TYPE => 'SMALLSERIAL', NOTNULL => 1, PRIMARYKEY => 1});
+  _set_attachment_comment_types();
 
-    $dbh->bz_alter_column('keyworddefs', 'id',
-        {TYPE => 'SMALLSERIAL', NOTNULL => 1, PRIMARYKEY => 1});
+  $dbh->bz_drop_column('products', 'milestoneurl');
 
-    # 2006-08-19 LpSolit@gmail.com - Bug 87795
-    $dbh->bz_alter_column('tokens', 'userid', {TYPE => 'INT3'});
+  _add_allows_unconfirmed_to_product_table();
+  _convert_flagtypes_fks_to_set_null();
+  _fix_decimal_types();
+  _fix_series_creator_fk();
 
-    $dbh->bz_drop_index('bugs', 'bugs_short_desc_idx');
+  # 2009-11-14 dkl@redhat.com - Bug 310450
+  $dbh->bz_add_column('bugs_activity', 'comment_id', {TYPE => 'INT4'});
 
-    # 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 => "''"});
+  # 2010-04-07 LpSolit@gmail.com - Bug 69621
+  $dbh->bz_drop_column('bugs', 'keywords');
 
-    _update_longdescs_who_index();
+  # 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});
 
-    $dbh->bz_add_column('setting', 'subclass', {TYPE => 'varchar(32)'});
+  # 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}, '');
 
-    $dbh->bz_alter_column('longdescs', 'thetext', 
-        {TYPE => 'LONGTEXT', NOTNULL => 1}, '');
+  # 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'});
 
-    # 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'});
+  # 2010-07-18 LpSolit@gmail.com - Bug 119703
+  _remove_attachment_isurl();
 
-    # 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)'});
+  # 2009-05-07 ghendricks@novell.com - Bug 77193
+  _add_isactive_to_product_fields();
 
-    $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-10-09 LpSolit@gmail.com - Bug 505165
+  $dbh->bz_alter_column('flags', 'setter_id', {TYPE => 'INT3', NOTNULL => 1});
 
-    _fix_uppercase_custom_field_names();
-    _fix_uppercase_index_names();
+  # 2010-10-09 LpSolit@gmail.com - Bug 451735
+  _fix_series_indexes();
 
-    # 2007-05-17 LpSolit@gmail.com - Bug 344965
-    _initialize_workflow_for_upgrade($old_params);
+  $dbh->bz_add_column('bug_see_also', 'id',
+    {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1});
 
-    # 2007-08-08 LpSolit@gmail.com - Bug 332149
-    $dbh->bz_add_column('groups', 'icon_url', {TYPE => 'TINYTEXT'});
+  _rename_tags_to_tag();
 
-    # 2007-08-21 wurblzap@gmail.com - Bug 365378
-    _make_lang_setting_dynamic();
-    
-    # 2007-11-29 xiaoou.wu@oracle.com - Bug 153129
-    _change_text_types();
+  # 2011-01-29 LpSolit@gmail.com - Bug 616185
+  _migrate_user_tags();
 
-    # 2007-09-09 LpSolit@gmail.com - Bug 99215
-    _fix_attachment_modification_date();
+  _populate_bug_see_also_class();
 
-    $dbh->bz_drop_index('longdescs', 'longdescs_thetext_idx');
-    _populate_bugs_fulltext();
+  # 2011-06-15 dkl@mozilla.com - Bug 658929
+  _migrate_disabledtext_boolean();
 
-    # 2008-01-18 xiaoou.wu@oracle.com - Bug 414292
-    $dbh->bz_alter_column('series', 'query',
-        { TYPE => 'MEDIUMTEXT', NOTNULL => 1 });
+  # 2011-11-01 glob@mozilla.com - Bug 240437
+  $dbh->bz_add_column('profiles', 'last_seen_date', {TYPE => 'DATETIME'});
 
-    # Add FK to multi select field tables
-    _add_foreign_keys_to_multiselects();
+  # 2011-10-11 miketosh - Bug 690173
+  _on_delete_set_null_for_audit_log_userid();
 
-    # 2008-07-28 tfu@redhat.com - Bug 431669
-    $dbh->bz_alter_column('group_control_map', 'product_id',
-        { TYPE => 'INT2', NOTNULL => 1 });
+  # 2011-11-23 gerv@gerv.net - Bug 705058 - make filenames longer
+  $dbh->bz_alter_column('attachments', 'filename',
+    {TYPE => 'varchar(255)', NOTNULL => 1});
 
-    # 2008-09-07 LpSolit@gmail.com - Bug 452893
-    _fix_illegal_flag_modification_dates();
+  # 2011-11-28 dkl@mozilla.com - Bug 685611
+  _fix_notnull_defaults();
 
-    _add_visiblity_value_to_value_tables();
+  # 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-03-02 arbingersys@gmail.com - Bug 423613
-    _add_extern_id_index();
+  # 2012-03-23 LpSolit@gmail.com - Bug 448551
+  $dbh->bz_alter_column('bugs', 'target_milestone',
+    {TYPE => 'varchar(64)', NOTNULL => 1, DEFAULT => "'---'"});
 
-    # 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'});
+  $dbh->bz_alter_column('milestones', 'value',
+    {TYPE => 'varchar(64)', NOTNULL => 1});
 
-    # 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();
+  $dbh->bz_alter_column('products', 'defaultmilestone',
+    {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']);
+  # 2012-04-15 Frank@Frank-Becker.de - Bug 740536
+  $dbh->bz_add_index('audit_log', 'audit_log_class_idx', ['class', 'at_time']);
 
-    # 2009-09-28 LpSolit@gmail.com - Bug 519032
-    $dbh->bz_drop_column('series', 'last_viewed');
+  # 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']);
 
-    # 2009-09-28 LpSolit@gmail.com - Bug 399073
-    _fix_logincookies_ipaddr();
+  # 2012-06-13 dkl@mozilla.com - Bug 764457
+  $dbh->bz_add_column('bugs_activity', 'id',
+    {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1});
 
-    # 2009-11-01 LpSolit@gmail.com - Bug 525025
-    _fix_invalid_custom_field_names();
+  # 2012-06-13 dkl@mozilla.com - Bug 764466
+  $dbh->bz_add_column('profiles_activity', 'id',
+    {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1});
 
-    _set_attachment_comment_types();
+  # 2012-07-24 dkl@mozilla.com - Bug 776972
+  $dbh->bz_alter_column('bugs_activity', 'id',
+    {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1});
 
-    $dbh->bz_drop_column('products', 'milestoneurl');
 
-    _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 776982
+  _fix_longdescs_primary_key();
 
-    # 2009-11-14 dkl@redhat.com - Bug 310450
-    $dbh->bz_add_column('bugs_activity', 'comment_id', {TYPE => 'INT4'});
+  # 2012-08-02 dkl@mozilla.com - Bug 756953
+  _fix_dependencies_dupes();
 
-    # 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-08-01 koosha.khajeh@gmail.com - Bug 187753
+  _shorten_long_quips();
 
-    # 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-12-29 reed@reedloden.com - Bug 785283
+  _add_password_salt_separator();
 
-    # 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' });
+  # 2013-01-02 LpSolit@gmail.com - Bug 824361
+  _fix_longdescs_indexes();
 
-    # 2010-07-18 LpSolit@gmail.com - Bug 119703
-    _remove_attachment_isurl();
+  # 2013-02-04 dkl@mozilla.com - Bug 824346
+  _fix_flagclusions_indexes();
 
-    # 2009-05-07 ghendricks@novell.com - Bug 77193
-    _add_isactive_to_product_fields();
+  # 2013-08-26 sgreen@redhat.com - Bug 903895
+  _fix_components_primary_key();
 
-    # 2010-10-09 LpSolit@gmail.com - Bug 505165
-    $dbh->bz_alter_column('flags', 'setter_id', {TYPE => 'INT3', NOTNULL => 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']);
 
-    # 2010-10-09 LpSolit@gmail.com - Bug 451735
-    _fix_series_indexes();
+  # 2014-07-14 sgreen@redhat.com - Bug 726696
+  $dbh->bz_alter_column('tokens', 'tokentype',
+    {TYPE => 'varchar(16)', NOTNULL => 1});
 
-    $dbh->bz_add_column('bug_see_also', 'id',
-        {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1});
+  # 2014-07-27 LpSolit@gmail.com - Bug 1044561
+  _fix_user_api_keys_indexes();
 
-    _rename_tags_to_tag();
+  # 2014-08-11 sgreen@redhat.com - Bug 1012506
+  _update_alias();
 
-    # 2011-01-29 LpSolit@gmail.com - Bug 616185
-    _migrate_user_tags();
+  # 2014-08-14 koosha.khajeh@gmail.com - Bug 330707
+  $dbh->bz_add_column('longdescs', 'is_markdown',
+    {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'});
 
-    _populate_bug_see_also_class();
+  # 2014-11-18 dylan@mozilla.com - Bug 69267
+  $dbh->bz_add_column('keyworddefs', 'is_active',
+    {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'});
 
-    # 2011-06-15 dkl@mozilla.com - Bug 658929
-    _migrate_disabledtext_boolean();
+  # 2015-07-25 dylan@mozilla.com - Bug 1179856
+  $dbh->bz_alter_column('tokens', 'token',
+    {TYPE => 'varchar(22)', NOTNULL => 1, PRIMARYKEY => 1});
+  $dbh->bz_alter_column('logincookies', 'cookie',
+    {TYPE => 'varchar(22)', NOTNULL => 1, PRIMARYKEY => 1});
 
-    # 2011-11-01 glob@mozilla.com - Bug 240437
-    $dbh->bz_add_column('profiles', 'last_seen_date', {TYPE => 'DATETIME'});
+  # 2015-07-16 LpSolit@gmail.com - Bug 946780
+  $dbh->bz_add_index('bz_schema', 'bz_schema_version_idx',
+    {FIELDS => ['version'], TYPE => 'UNIQUE'});
 
-    # 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 });
+  # 2015-12-16 LpSolit@gmail.com - Bug 1232578
+  _sanitize_audit_log_table();
 
-    # 2011-11-28 dkl@mozilla.com - Bug 685611
-    _fix_notnull_defaults();
+  # 2016-04-27 wurblzap@gmail.com and gerv@gerv.net - Bug 218917
+  # Split login_name into login_name and email columns
+  _split_login_and_email($old_params);
 
-    # 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)]);
-    }
+  ################################################################
+  # New --TABLE-- changes should go *** A B O V E *** this point #
+  ################################################################
 
-    # 2012-03-23 LpSolit@gmail.com - Bug 448551
-    $dbh->bz_alter_column('bugs', 'target_milestone',
-                          {TYPE => 'varchar(64)', NOTNULL => 1, DEFAULT => "'---'"});
+  Bugzilla::Hook::process('install_update_db');
 
-    $dbh->bz_alter_column('milestones', 'value', {TYPE => 'varchar(64)', NOTNULL => 1});
+  # 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_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-08-14 koosha.khajeh@gmail.com - Bug 330707
-    $dbh->bz_add_column('longdescs', 'is_markdown',
-                        {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'});
-
-    # 2014-11-18 dylan@mozilla.com - Bug 69267
-    $dbh->bz_add_column('keyworddefs', 'is_active',
-                        {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'});
-
-    # 2015-07-25 dylan@mozilla.com - Bug 1179856
-    $dbh->bz_alter_column('tokens', 'token',
-                          {TYPE => 'varchar(22)', NOTNULL => 1, PRIMARYKEY => 1});
-    $dbh->bz_alter_column('logincookies', 'cookie',
-                          {TYPE => 'varchar(22)', NOTNULL => 1, PRIMARYKEY => 1});
-
-    # 2015-07-16 LpSolit@gmail.com - Bug 946780
-    $dbh->bz_add_index('bz_schema', 'bz_schema_version_idx',
-                       {FIELDS => ['version'], TYPE => 'UNIQUE'});
-
-    # 2015-12-16 LpSolit@gmail.com - Bug 1232578
-    _sanitize_audit_log_table();
-
-    # 2016-04-27 wurblzap@gmail.com and gerv@gerv.net - Bug 218917
-    # Split login_name into login_name and email columns
-    _split_login_and_email($old_params);
-
-    ################################################################
-    # 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;
 
-        print <<ENDTEXT;
+  # 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 <<ENDTEXT;
 Your current installation of Bugzilla stores passwords in plaintext
 in the database and uses mysql's encrypt function instead of Perl's
 crypt function to crypt passwords.  Passwords are now going to be
@@ -1236,299 +1269,315 @@ deleted from the database.  This could take a while if your
 installation has many users.
 ENDTEXT
 
-        # 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();
-
-        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.
@@ -1539,1211 +1588,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_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
+    $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
                               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_drop_table("user_series_map");
+    $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");
     }
+
+    $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);
 
-            # 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);
-                }
+    # 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) . "&";
+      }
+
+      # 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) {
 
-        $dbh->bz_commit_transaction();
+          # 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});
+        }
+      }
+
+      # 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 <nb@ravenbrook.com> 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 _<n> 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 <nb@ravenbrook.com> 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 _<n> 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 <<EOT;
+  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 <<EOT;
 
 WARNING - During repairs of series $broken_series_id, the irreparable data
 entry for date $date was encountered and is being deleted.
 
 Continuing repairs...
 EOT
-                            $sth_delete_broken_nonopen_data->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 <<EOT;
+              $sth_delete_broken_nonopen_data->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 <<EOT;
 
 WARNING - Series $broken_series_id was meant to collect non-open bug 
 counts, but it has counted all bugs instead. It cannot be repaired
@@ -2753,518 +2932,576 @@ series $broken_series_id manually
 
 Continuing repairs...
 EOT
-                } #  if ($found_open_series_id)
-            } #  if ($nonopen_bugs_query =~
-        } # foreach (@$broken_nonopen_series)
-        print " done.\n";
-    } # if (@$broken_nonopen_series)
+        }    #  if ($found_open_series_id)
+      }    #  if ($nonopen_bugs_query =~
+    }    # foreach (@$broken_nonopen_series)
+    print " done.\n";
+  }    # if (@$broken_nonopen_series)
 }
 
-# This needs to happen at two times: when we upgrade from 2.16 (thus creating 
+# This needs to happen at two times: when we upgrade from 2.16 (thus creating
 # user_group_map), and when we kill derived gruops in the DB.
 sub _rederive_regex_groups {
-    my $dbh = Bugzilla->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
@@ -3272,715 +3509,760 @@ 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');
-    # 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+$/);
+  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+$/);
 }
 
 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;
-
-    if ($dbh->bz_column_info('fielddefs', 'visibility_value_id')) {
-        print "Populating new field_visibility table...\n";
+  my $dbh = Bugzilla->dbh;
 
-        $dbh->bz_start_transaction();
+  if ($dbh->bz_column_info('fielddefs', 'visibility_value_id')) {
+    print "Populating new field_visibility table...\n";
 
-        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);
+    # Make sure that things like "001" get converted to "1"
+    @bug_ids = map { int($_) } @bug_ids;
 
-        my $tag_id = $dbh->selectrow_array($sth_tag_id,
-            undef, $user_id, $tag_name);
+    # And remove duplicates
+    @bug_ids = uniq @bug_ids;
+    foreach my $bug_id (@bug_ids) {
 
-        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);
-        # 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_drop_column('namedqueries', 'query_type');
+  $dbh->bz_commit_transaction();
+
+  $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);
     }
+  }
 }
 
 sub _split_login_and_email {
-    my ($old_params) = @_;
-    my $dbh = Bugzilla->dbh;
+  my ($old_params) = @_;
+  my $dbh = Bugzilla->dbh;
 
-    return if $dbh->bz_column_info('profiles', 'email');
+  return if $dbh->bz_column_info('profiles', 'email');
 
-    $dbh->bz_add_column('profiles', 'email',
-                        {TYPE => 'varchar(255)', NOTNULL => 1}, '');
-    $dbh->do('UPDATE profiles SET email = login_name');
+  $dbh->bz_add_column('profiles', 'email',
+    {TYPE => 'varchar(255)', NOTNULL => 1}, '');
+  $dbh->do('UPDATE profiles SET email = login_name');
 
-    # This change obsoletes the 'emailsuffix' parameter. If it is in use,
-    # append it to all the values in the 'email' column.
-    my $suffix = $old_params->{'emailsuffix'};
-    if ($suffix) {
-        $dbh->do('UPDATE profiles SET email = ' . $dbh->sql_string_concat('email', '?'),
-                 undef, $suffix);
-    }
+  # This change obsoletes the 'emailsuffix' parameter. If it is in use,
+  # append it to all the values in the 'email' column.
+  my $suffix = $old_params->{'emailsuffix'};
+  if ($suffix) {
+    $dbh->do('UPDATE profiles SET email = ' . $dbh->sql_string_concat('email', '?'),
+      undef, $suffix);
+  }
 
-    $dbh->bz_add_index('profiles', 'profiles_email_idx',
-                       {TYPE => 'UNIQUE', FIELDS => ['email']});
+  $dbh->bz_add_index('profiles', 'profiles_email_idx',
+    {TYPE => 'UNIQUE', FIELDS => ['email']});
 }
 
 
index 4c08cc5380f3e2a1c8e741e8defef376afbe632b..167218a5e3f93b5d31d8c639d508c2b5a30d76ef 100644 (file)
@@ -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 => <<EOT;
@@ -64,52 +64,60 @@ EOT
 ###############
 
 # Used by the permissions "constants" below.
-sub _suexec { Bugzilla->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,201 +125,180 @@ 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 },
-        'jobqueue.pl'     => { perms => OWNER_EXECUTE },
-        'migrate.pl'      => { perms => OWNER_EXECUTE },
-        'Makefile.PL'     => { perms => OWNER_WRITE },
-        'gen-cpanfile.pl' => { perms => OWNER_EXECUTE },
-        'clean-bug-user-last-visit.pl' => { perms => WS_EXECUTE },
-
-        'app.psgi'      => { perms => CGI_READ },
-        'Bugzilla.pm'   => { perms => CGI_READ },
-        "$localconfig*" => { perms => CGI_READ },
-        'META.*'        => { perms => CGI_READ },
-        'MYMETA.*'      => { 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 },
-        'contrib/replyrc'      => { 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 },
-        'xt/config/generate_test_data.pl' => { perms => OWNER_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 => <<EOT
+  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},
+    'jobqueue.pl'                  => {perms => OWNER_EXECUTE},
+    'migrate.pl'                   => {perms => OWNER_EXECUTE},
+    'Makefile.PL'                  => {perms => OWNER_WRITE},
+    'gen-cpanfile.pl'              => {perms => OWNER_EXECUTE},
+    'clean-bug-user-last-visit.pl' => {perms => WS_EXECUTE},
+
+    'app.psgi'      => {perms => CGI_READ},
+    'Bugzilla.pm'   => {perms => CGI_READ},
+    "$localconfig*" => {perms => CGI_READ},
+    'META.*'        => {perms => CGI_READ},
+    'MYMETA.*'      => {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},
+    'contrib/replyrc'                 => {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},
+    'xt/config/generate_test_data.pl' => {perms => OWNER_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 => <<EOT
 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
 <html>
 <head>
@@ -322,35 +309,30 @@ sub FILESYSTEM {
 </body>
 </html>
 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 => <<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 => <<EOT
 # Allow access to .png and .gif files.
 <FilesMatch (\\.gif|\\.png)\$>
   <IfModule mod_version.c>
@@ -379,9 +361,11 @@ EOT
   Deny from all
 </IfModule>
 EOT
-        },
+    },
 
-        "$webdotdir/.htaccess" => { perms => WS_SERVE, contents => <<EOT
+    "$webdotdir/.htaccess" => {
+      perms    => WS_SERVE,
+      contents => <<EOT
 # If you run a local webdot server, you will need to allow it access to
 # generated .dot files. Uncomment this section and replace the IP address with
 # the IP address of your webdot server.
@@ -430,9 +414,11 @@ EOT
   Deny from all
 </IfModule>
 EOT
-        },
+    },
 
-        "$assetsdir/.htaccess" => { perms => WS_SERVE, contents => <<EOT
+    "$assetsdir/.htaccess" => {
+      perms    => WS_SERVE,
+      contents => <<EOT
 # Allow access to .css files
 <FilesMatch \\.(css|js)\$>
   <IfModule mod_version.c>
@@ -461,97 +447,102 @@ EOT
   Deny from all
 </IfModule>
 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") =~ /<FilesMatch \\\.css\$>/) {
-            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") =~ /<FilesMatch \\\.css\$>/) {
+      unlink("$assetsdir/.htaccess");
     }
-    elsif (-e 'index.html') {
-        my $templatedir = bz_locations()->{'templatedir'};
-        print <<EOT;
+  }
+
+  _create_files(%files);
+  if ($params->{index_html}) {
+    _create_files(%{$fs->{index_html}});
+  }
+  elsif (-e 'index.html') {
+    my $templatedir = bz_locations()->{'templatedir'};
+    print <<EOT;
 
 *** It appears that you still have an old index.html hanging around.
     Either the contents of this file should be moved into a template and 
@@ -559,445 +550,477 @@ sub update_filesystem {
     the file.
 
 EOT
-    }
-
-    # Delete old files that no longer need to exist
-
-    # 2001-04-29 jake@bugzilla.org - Remove oldemailtech
-    #   http://bugzilla.mozilla.org/show_bugs.cgi?id=71552
-    if (-d 'shadow') {
-        print "Removing shadow directory...\n";
-        rmtree("shadow");
-    }
-
-    if (-e "$datadir/versioncache") {
-        print "Removing versioncache...\n";
-        unlink "$datadir/versioncache";
-    }
-
-    if (-e "$datadir/duplicates.rdf") {
-        print "Removing duplicates.rdf...\n";
-        unlink "$datadir/duplicates.rdf";
-        unlink "$datadir/duplicates-old.rdf";
-    }
-
-    if (-e "$datadir/duplicates") {
-        print "Removing duplicates directory...\n";
-        rmtree("$datadir/duplicates");
-    }
-
-    _remove_empty_css_files();
-    _convert_single_file_skins();
-    _remove_dynamic_assets();
+  }
+
+  # Delete old files that no longer need to exist
+
+  # 2001-04-29 jake@bugzilla.org - Remove oldemailtech
+  #   http://bugzilla.mozilla.org/show_bugs.cgi?id=71552
+  if (-d 'shadow') {
+    print "Removing shadow directory...\n";
+    rmtree("shadow");
+  }
+
+  if (-e "$datadir/versioncache") {
+    print "Removing versioncache...\n";
+    unlink "$datadir/versioncache";
+  }
+
+  if (-e "$datadir/duplicates.rdf") {
+    print "Removing duplicates.rdf...\n";
+    unlink "$datadir/duplicates.rdf";
+    unlink "$datadir/duplicates-old.rdf";
+  }
+
+  if (-e "$datadir/duplicates") {
+    print "Removing duplicates directory...\n";
+    rmtree("$datadir/duplicates");
+  }
+
+  _remove_empty_css_files();
+  _convert_single_file_skins();
+  _remove_dynamic_assets();
 }
 
 sub _remove_empty_css_files {
-    my $skinsdir = bz_locations()->{'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 = <<EOT;
+  my ($file)         = @_;
+  my $basename       = basename($file);
+  my $empty_contents = <<EOT;
 /*
  * Custom rules for $basename.
  * The rules you put here override rules in that stylesheet.
  */
 EOT
-    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: $!";
-        }
-    };
+  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 (<IN>) {
-            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 (<IN>) {
+      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;
index d591ece15051aa0fd6fc52400a3deca8d133364b..811bfada6411ff242b3047000ba00e12571e8d6a 100644 (file)
@@ -34,195 +34,136 @@ use Term::ANSIColor;
 use parent qw(Exporter);
 
 our @EXPORT_OK = qw(
-    read_localconfig
-    update_localconfig
+  read_localconfig
+  update_localconfig
 );
 
 sub _sensible_group {
-    return '' if ON_WINDOWS;
-    my @groups     = qw( apache www-data _www );
-    my $sensible_group = first { return getgrnam($_) } @groups;
+  return '' if ON_WINDOWS;
+  my @groups = qw( apache www-data _www );
+  my $sensible_group = first { return getgrnam($_) } @groups;
 
-    return $sensible_group // getgrgid($EGID) // '';
+  return $sensible_group // getgrgid($EGID) // '';
 }
 
 use constant LOCALCONFIG_VARS => (
-    {
-        name    => 'create_htaccess',
-        default => 1,
-    },
-    {
-        name    => 'webservergroup',
-        default => _sensible_group(),
-    },
-    {
-        name    => 'use_suexec',
-        default => 0,
-    },
-    {
-        name => 'db_from_env',
-        default => 0,
-    },
-    {
-        name    => 'db_driver',
-        default => 'sqlite',
-    },
-    {
-        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    => 'font_file',
-        default => '',
-    },
-    {
-        name    => 'webdotbase',
-        default => '',
-    },
-    {
-        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    => 'apache_size_limit',
-        default => 250000,
-    },
+  {name => 'create_htaccess',          default => 1,},
+  {name => 'webservergroup',           default => _sensible_group(),},
+  {name => 'use_suexec',               default => 0,},
+  {name => 'db_from_env',              default => 0,},
+  {name => 'db_driver',                default => 'sqlite',},
+  {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 => 'font_file',  default => '',},
+  {name => 'webdotbase', default => '',},
+  {
+    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 => 'apache_size_limit', default => 250000,},
 );
 
 
 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);
     }
-    require Bugzilla::Error;
-    if ($localconfig{db_from_env}) {
-        if (Bugzilla->has_feature('db_from_env')) {
-            require URI::db;
-            unless ($ENV{DATABASE_URL}) {
-                Bugzilla::Error::ThrowUserError('missing_database_url');
-            }
-            my $uri = URI::db->new($ENV{DATABASE_URL});
-            $localconfig{db_driver} = $uri->canonical_engine;
-            $localconfig{db_name}   = $uri->dbname;
-            $localconfig{db_host}   = $uri->host;
-            $localconfig{db_user}   = $uri->user;
-            $localconfig{db_pass}   = $uri->password;
-            $localconfig{db_port}   = $uri->port // 0;
-        }
-        else {
-            Bugzilla::Error::ThrowUserError('feature_disabled', { feature => 'db_from_env' });
-        }
+    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;
+      }
+    }
+  }
+  require Bugzilla::Error;
+  if ($localconfig{db_from_env}) {
+    if (Bugzilla->has_feature('db_from_env')) {
+      require URI::db;
+      unless ($ENV{DATABASE_URL}) {
+        Bugzilla::Error::ThrowUserError('missing_database_url');
+      }
+      my $uri = URI::db->new($ENV{DATABASE_URL});
+      $localconfig{db_driver} = $uri->canonical_engine;
+      $localconfig{db_name}   = $uri->dbname;
+      $localconfig{db_host}   = $uri->host;
+      $localconfig{db_user}   = $uri->user;
+      $localconfig{db_pass}   = $uri->password;
+      $localconfig{db_port}   = $uri->port // 0;
     }
+    else {
+      Bugzilla::Error::ThrowUserError('feature_disabled', {feature => 'db_from_env'});
+    }
+  }
 
-    return \%localconfig;
+  return \%localconfig;
 }
 
 
@@ -251,105 +192,107 @@ 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 $db_from_env = Bugzilla->localconfig->{db_from_env};
-
-    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};
-            }
-            elsif (exists Bugzilla->params->{$name}) {
-                $localconfig->{$name} = Bugzilla->params->{$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) unless $db_from_env && $name =~ /^db_(?!from_env)/;
-                }
-                $localconfig->{$name} = $var->{default};
-            }
+  my ($params) = @_;
+
+  my $output      = $params->{output} || 0;
+  my $answer      = Bugzilla->installation_answers;
+  my $localconfig = read_localconfig('include deprecated');
+  my $db_from_env = Bugzilla->localconfig->{db_from_env};
+
+  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};
+      }
+      elsif (exists Bugzilla->params->{$name}) {
+        $localconfig->{$name} = Bugzilla->params->{$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) unless $db_from_env && $name =~ /^db_(?!from_env)/;
         }
+        $localconfig->{$name} = $var->{default};
+      }
     }
-
-    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);
+  }
+
+  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 $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";
+    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 = $var->{desc};
+
+    next if $db_from_env && $name =~ /^db_(?!from_env)/;
+
+    if (!length $desc) {
+      $desc = install_string("localconfig_$name", {root => ROOT_USER});
+      chomp($desc);
+
+      # Make the description into a comment.
+      $desc =~ s/^/# /mg;
+      $desc .= "\n";
     }
-
-    # Re-write localconfig
-    open(my $fh, ">:utf8", $filename) or die "$filename: $!";
-    foreach my $var (LOCALCONFIG_VARS) {
-        my $name = $var->{name};
-        my $desc = $var->{desc};
-
-        next if $db_from_env && $name =~ /^db_(?!from_env)/;
-
-        if(!length $desc){
-            $desc = install_string("localconfig_$name", { root => ROOT_USER });
-            chomp($desc);
-            # Make the description into a comment.
-            $desc =~ s/^/# /mg;
-            $desc .= "\n";
-        }
-        print $fh $desc,
-                  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 };
+    print $fh $desc, 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;
index e02c427357b640772d6bf17321adda6becd7e192..0127df103691b7bd0cb03e6726069f479d258468 100644 (file)
@@ -19,7 +19,7 @@ use warnings;
 
 use Bugzilla::Constants;
 use Bugzilla::Install::Util qw(install_string bin_loc success
-                               extension_requirement_packages);
+  extension_requirement_packages);
 use List::Util qw(max);
 use Term::ANSIColor;
 use CPAN::Meta;
@@ -31,15 +31,15 @@ use parent qw(Exporter);
 use autodie;
 
 our @EXPORT = qw(
-    FEATURE_FILES
-
-    load_cpan_meta
-    check_cpan_requirements
-    check_cpan_feature
-    check_all_cpan_features
-    check_webdotbase
-    check_font_file
-    map_files_to_features
+  FEATURE_FILES
+
+  load_cpan_meta
+  check_cpan_requirements
+  check_cpan_feature
+  check_all_cpan_features
+  check_webdotbase
+  check_font_file
+  map_files_to_features
 );
 
 our $checking_for_indent = 0;
@@ -54,11 +54,11 @@ 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'
+  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
@@ -71,261 +71,282 @@ 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(
+use constant APACHE_PATH => [
+  qw(
     /usr/sbin
     /usr/local/sbin
     /usr/libexec
     /usr/local/libexec
-)];
+    )
+];
 
 # 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'],
-    csp           => ['Bugzilla/CGI/ContentSecurityPolicy.pm'],
-    psgi          => ['app.psgi'],
-    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'],
-    markdown      => ['Bugzilla/Markdown.pm', 't/100markdown.t'],
-    memcached     => ['Bugzilla/Memcache.pm'],
-    auth_delegation => ['auth.cgi'],
+  jsonrpc => ['Bugzilla/WebService/Server/JSONRPC.pm', 'jsonrpc.cgi'],
+  xmlrpc  => [
+    'Bugzilla/WebService/Server/XMLRPC.pm', 'xmlrpc.cgi',
+    'Bugzilla/WebService.pm',               'Bugzilla/WebService/*.pm'
+  ],
+  csp           => ['Bugzilla/CGI/ContentSecurityPolicy.pm'],
+  psgi          => ['app.psgi'],
+  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'],
+  markdown        => ['Bugzilla/Markdown.pm', 't/100markdown.t'],
+  memcached       => ['Bugzilla/Memcache.pm'],
+  auth_delegation => ['auth.cgi'],
 );
 
 sub load_cpan_meta {
-    my $dir = bz_locations()->{libpath};
-    my @meta_json = map { File::Spec->catfile($dir, $_) } qw( MYMETA.json META.json );
-    my ($file) = grep { -f $_ } @meta_json;
-
-    if ($file) {
-        open my $meta_fh, '<', $file or die "unable to open $file: $!";
-        my $str = do { local $/ = undef; scalar <$meta_fh> };
-        # detaint
-        $str =~ /^(.+)$/s; $str = $1;
-        close $meta_fh;
-
-        return CPAN::Meta->load_json_string($str);
-    }
-    else {
-        ThrowCodeError('cpan_meta_missing');
-    }
+  my $dir = bz_locations()->{libpath};
+  my @meta_json
+    = map { File::Spec->catfile($dir, $_) } qw( MYMETA.json META.json );
+  my ($file) = grep { -f $_ } @meta_json;
+
+  if ($file) {
+    open my $meta_fh, '<', $file or die "unable to open $file: $!";
+    my $str = do { local $/ = undef; scalar <$meta_fh> };
+
+    # detaint
+    $str =~ /^(.+)$/s;
+    $str = $1;
+    close $meta_fh;
+
+    return CPAN::Meta->load_json_string($str);
+  }
+  else {
+    ThrowCodeError('cpan_meta_missing');
+  }
 }
 
 sub check_all_cpan_features {
-    my ($meta, $dirs, $output) = @_;
-    my %report;
-
-    local $checking_for_indent = 2;
-
-    print "\nOptional features:\n" if $output;
-    my @features = sort { $a->identifier cmp $b->identifier } $meta->features;
-    foreach my $feature (@features) {
-        next if $feature->identifier eq 'features';
-        printf "Feature '%s': %s\n", $feature->identifier, $feature->description if $output;
-        my $result = check_cpan_feature($feature, $dirs, $output);
-        print "\n" if $output;
-
-        $report{$feature->identifier} = {
-            description => $feature->description,
-            result => $result,
-        };
-    }
-
-    print install_string('all_optional_features_require'), "\n" if $output;
-    my $features = check_cpan_feature($meta->feature('features'), $dirs, $output);
+  my ($meta, $dirs, $output) = @_;
+  my %report;
+
+  local $checking_for_indent = 2;
+
+  print "\nOptional features:\n" if $output;
+  my @features = sort { $a->identifier cmp $b->identifier } $meta->features;
+  foreach my $feature (@features) {
+    next if $feature->identifier eq 'features';
+    printf "Feature '%s': %s\n", $feature->identifier, $feature->description
+      if $output;
+    my $result = check_cpan_feature($feature, $dirs, $output);
     print "\n" if $output;
 
-    $report{features} = {
-        description => $meta->feature('features')->description,
-        result => $features,
-    };
+    $report{$feature->identifier}
+      = {description => $feature->description, result => $result,};
+  }
+
+  print install_string('all_optional_features_require'), "\n" if $output;
+  my $features = check_cpan_feature($meta->feature('features'), $dirs, $output);
+  print "\n" if $output;
 
-    return \%report;
+  $report{features} = {
+    description => $meta->feature('features')->description,
+    result      => $features,
+  };
+
+  return \%report;
 }
 
 sub check_cpan_feature {
-    my ($feature, $dirs, $output) = @_;
+  my ($feature, $dirs, $output) = @_;
 
-    return _check_prereqs($feature->prereqs, $dirs, $output);
+  return _check_prereqs($feature->prereqs, $dirs, $output);
 }
 
 sub check_cpan_requirements {
-    my ($meta, $dirs, $output) = @_;
+  my ($meta, $dirs, $output) = @_;
 
-    my $result = _check_prereqs($meta->effective_prereqs, $dirs, $output);
-    print colored(install_string('installation_failed'), COLOR_ERROR), "\n" if !$result->{ok} && $output;
-    return $result;
+  my $result = _check_prereqs($meta->effective_prereqs, $dirs, $output);
+  print colored(install_string('installation_failed'), COLOR_ERROR), "\n"
+    if !$result->{ok} && $output;
+  return $result;
 }
 
 sub _check_prereqs {
-    my ($prereqs, $dirs, $output) = @_;
-    $dirs //= \@INC;
-    my $reqs = $prereqs->merged_requirements(['configure', 'runtime'], ['requires']);
-    my @found;
-    my @missing;
-
-    foreach my $module (sort $reqs->required_modules) {
-        my $ok = _check_module($reqs, $module, $dirs, $output);
-        if ($ok) {
-            push @found, $module;
-        }
-        else {
-            push @missing, $module;
-        }
+  my ($prereqs, $dirs, $output) = @_;
+  $dirs //= \@INC;
+  my $reqs
+    = $prereqs->merged_requirements(['configure', 'runtime'], ['requires']);
+  my @found;
+  my @missing;
+
+  foreach my $module (sort $reqs->required_modules) {
+    my $ok = _check_module($reqs, $module, $dirs, $output);
+    if ($ok) {
+      push @found, $module;
+    }
+    else {
+      push @missing, $module;
     }
+  }
 
-    return { ok => (@missing == 0), found => \@found, missing => \@missing };
+  return {ok => (@missing == 0), found => \@found, missing => \@missing};
 }
 
 sub _check_module {
-    my ($reqs, $module, $dirs, $output) = @_;
-    my $required_version = $reqs->requirements_for_module($module);
-
-    if ($module eq 'perl') {
-        my $ok = $reqs->accepts_module($module, $]);
-        _checking_for({package => "perl", found => $], wanted => $required_version, ok => $ok}) if $output;
-        return $ok;
-    } else {
-        my $metadata = Module::Metadata->new_from_module($module, inc => $dirs);
-        my $version = eval { $metadata->version };
-        my $ok = $metadata && $version && $reqs->accepts_module($module, $version || 0);
-        _checking_for({package => $module, $version ? ( found => $version ) : (), wanted => $required_version, ok => $ok}) if $output;
-
-        return $ok;
-    }
+  my ($reqs, $module, $dirs, $output) = @_;
+  my $required_version = $reqs->requirements_for_module($module);
+
+  if ($module eq 'perl') {
+    my $ok = $reqs->accepts_module($module, $]);
+    _checking_for(
+      {package => "perl", found => $], wanted => $required_version, ok => $ok})
+      if $output;
+    return $ok;
+  }
+  else {
+    my $metadata = Module::Metadata->new_from_module($module, inc => $dirs);
+    my $version = eval { $metadata->version };
+    my $ok = $metadata && $version && $reqs->accepts_module($module, $version || 0);
+    _checking_for({
+      package => $module,
+      $version ? (found => $version) : (),
+      wanted => $required_version,
+      ok     => $ok
+    })
+      if $output;
+
+    return $ok;
+  }
 }
 
 
 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_webdotbase {
-    my ($output) = @_;
+  my ($output) = @_;
 
-    my $webdotbase = Bugzilla->localconfig->{'webdotbase'};
-    return 1 if $webdotbase =~ /^https?:/;
+  my $webdotbase = Bugzilla->localconfig->{'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'};
-    # 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;
+  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;
+  }
 
-    return $return;
+  return $return;
 }
 
 sub check_font_file {
-    my ($output) = @_;
+  my ($output) = @_;
 
-    my $font_file = Bugzilla->localconfig->{'font_file'};
+  my $font_file = Bugzilla->localconfig->{'font_file'};
 
-    my $readable;
-    $readable = 1 if -r $font_file;
+  my $readable;
+  $readable = 1 if -r $font_file;
 
-    my $ttf;
-    $ttf = 1 if $font_file =~ /\.(ttf|otf)$/;
+  my $ttf;
+  $ttf = 1 if $font_file =~ /\.(ttf|otf)$/;
 
-    if ($output) {
-        _checking_for({ package => 'Font file', ok => $readable && $ttf});
-    }
+  if ($output) {
+    _checking_for({package => 'Font file', ok => $readable && $ttf});
+  }
 
-    if (!$readable) {
-        print install_string('bad_font_file', { file => $font_file }), "\n";
-    }
-    elsif (!$ttf) {
-        print install_string('bad_font_file_name', { file => $font_file }), "\n";
-    }
+  if (!$readable) {
+    print install_string('bad_font_file', {file => $font_file}), "\n";
+  }
+  elsif (!$ttf) {
+    print install_string('bad_font_file_name', {file => $font_file}), "\n";
+  }
 
-    return $readable && $ttf;
+  return $readable && $ttf;
 }
 
 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 (!$ok) {
-        $ok_string = install_string('module_not_found');
+    elsif ($found) {
+      $found_string = install_string('module_found', {ver => $found});
     }
-
-    my $black_string = $blacklisted ? install_string('blacklisted') : '';
-    my $want_string  = $wanted ? "$wanted" : install_string('any');
-
-    my $str = sprintf "%s %20s %-11s $ok_string $black_string\n",
-      ( ' ' x $checking_for_indent ) . install_string('checking_for'),
-      $package, "($want_string)";
-    print $ok ? $str : colored($str, COLOR_ERROR);
+    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 ? "$wanted" : install_string('any');
+
+  my $str = sprintf "%s %20s %-11s $ok_string $black_string\n",
+    (' ' x $checking_for_indent) . install_string('checking_for'), $package,
+    "($want_string)";
+  print $ok ? $str : colored($str, COLOR_ERROR);
 }
 
 # 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;
-            }
-        }
+  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;
+  }
+  return \%files;
 }
 
 1;
index 80820dcc680b58be6decb3f6e718e5ac58119df2..91555f829616cc7c7079e4b2274dd0f338fa5735 100644 (file)
@@ -29,198 +29,208 @@ use if ON_WINDOWS, 'Win32::API';
 
 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
-    i_am_persistent
-    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
+  i_am_persistent
+  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];
-    $os_name = Win32::GetOSName() if ON_WINDOWS;
-
-    # $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];
+  $os_name = Win32::GetOSName() if ON_WINDOWS;
+
+  # $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 i_am_persistent {
-    return ($ENV{MOD_PERL} || $ENV{BZ_PLACK}) ? 1 : 0;
+  return ($ENV{MOD_PERL} || $ENV{BZ_PLACK}) ? 1 : 0;
 }
 
 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 ($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);
+      }
     }
-
-    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);
+    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
@@ -228,272 +238,281 @@ 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 feature_description {
-    my ($feature_name) = @_;
-    eval {
-        my $meta = _cache()->{cpan_meta} //= Bugzilla::Install::Requirements::load_cpan_meta();
+  my ($feature_name) = @_;
+  eval {
+    my $meta = _cache()->{cpan_meta}
+      //= Bugzilla::Install::Requirements::load_cpan_meta();
 
-        return $meta->feature($feature_name)->description
-    } or warn $@;
+    return $meta->feature($feature_name)->description;
+  } or warn $@;
 }
 
 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) {
 
-    $cache->{supported_languages} = \@languages;
-    return \@languages;
+    # 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;
 }
 
 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 function 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 function 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);
     }
-    return \@include_path;
+
+    # Whereas everything else just gets a whole INCLUDE_PATH.
+    else {
+      push(@include_path, @lang_dirs);
+    }
+  }
+  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;
 }
 
 ######################
@@ -502,157 +521,166 @@ 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 <language-range>;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) {
-        # Although setlocale() works on Windows, it doesn't always return
-        # the current *console's* encoding.
-        $encoding = Win32::GetConsoleOutputCP();
-    }
-    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) {
+
+    # Although setlocale() works on Windows, it doesn't always return
+    # the current *console's* encoding.
+    $encoding = Win32::GetConsoleOutputCP();
+  }
+  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).
-    #
-    if (ON_WINDOWS) {
-        # 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).
+  #
+  if (ON_WINDOWS) {
+
+    # 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
@@ -664,20 +692,20 @@ use constant _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__
index 96ccb719e0f01751cc797b1040455ce14db0e5d7..415bf41cdfe7a70ae481045c5e1771d81ed21862 100644 (file)
@@ -15,8 +15,8 @@ use Bugzilla::BugMail;
 BEGIN { eval "use parent qw(Bugzilla::Job::Mailer)"; }
 
 sub process_job {
-    my ($class, $arg) = @_;
-    Bugzilla::BugMail::dequeue($arg->{vars});
+  my ($class, $arg) = @_;
+  Bugzilla::BugMail::dequeue($arg->{vars});
 }
 
 1;
index f4b9baaa32a4d8c91d01b6924a81c8cc534a8a3f..0ed5255307fbb2c089a275da3162917c852e0dad 100644 (file)
@@ -17,40 +17,42 @@ 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) = @_;
-    eval { $class->process_job($job->arg) };
-    if (my $error = $@) {
-        if ($error eq EMAIL_LIMIT_EXCEPTION) {
-            $job->declined();
-        }
-        else {
-            $job->failed($error);
-        }
-        undef $@;
+  my ($class, $job) = @_;
+  eval { $class->process_job($job->arg) };
+  if (my $error = $@) {
+    if ($error eq EMAIL_LIMIT_EXCEPTION) {
+      $job->declined();
     }
     else {
-        $job->completed;
+      $job->failed($error);
     }
+    undef $@;
+  }
+  else {
+    $job->completed;
+  }
 }
 
 sub process_job {
-    my ($class, $arg) = @_;
-    MessageToMTA($arg->{msg}, 1);
+  my ($class, $arg) = @_;
+  MessageToMTA($arg->{msg}, 1);
 }
 
 1;
index 66aba68d7db929985cdce256e5fad0d19ea050e3..200b9e4466da3fc8729af7340d9f841438668133 100644 (file)
@@ -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;
index 113193cd8514c37671f63eb892ba68f96914a29b..a64d0800692de9f1923a32d79d9658c6ecd32194 100644 (file)
@@ -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,190 +37,191 @@ 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},
-        'j=s@'      => \$self->{gd_args}{job_name},
-    );
+  my $self = shift;
+  return (
+    'pidfile=s' => \$self->{gd_args}{pidfile},
+    'n=s'       => \$self->{gd_args}{progname},
+    'j=s@'      => \$self->{gd_args}{job_name},
+  );
 }
 
 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 <<END;
+    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 <<END;
 #!/bin/sh
 BUGZILLA="$directory"
 # This user must have write access to Bugzilla's data/ directory.
 USER=$owner
 END
-            close($config_fh);
-        }
-        else {
-            print "Please edit $dest_file to configure the daemon.\n";
-        }
+      close($config_fh);
+    }
+    else {
+      print "Please edit $dest_file to configure the daemon.\n";
+    }
     }
 }
 
 sub gd_can_uninstall {
-    my $self = shift;
-
-    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";
-        }
-    }
+  my $self = shift;
 
-    return $self->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 @job_name = @{ $self->{gd_args}{job_name} // [] };
-    my $jq = Bugzilla->job_queue();
-    $jq->set_verbose($self->{debug});
-    $jq->set_pidfile($self->{gd_pidfile});
-    while (my ($key, $module) = each %{ Bugzilla::JobQueue->job_map() }) {
-        next if @job_name and ! grep { $_ eq $key } @job_name;
-        eval "use $module";
-        $jq->can_do($module);
-    }
-    $jq->$fn;
+  my ($self, $fn) = @_;
+
+  my @job_name = @{$self->{gd_args}{job_name} // []};
+  my $jq = Bugzilla->job_queue();
+  $jq->set_verbose($self->{debug});
+  $jq->set_pidfile($self->{gd_pidfile});
+  while (my ($key, $module) = each %{Bugzilla::JobQueue->job_map()}) {
+    next if @job_name and !grep { $_ eq $key } @job_name;
+    eval "use $module";
+    $jq->can_do($module);
+  }
+  $jq->$fn;
 }
 
 1;
index d2e1f91eb98050902f3b38da7313a334ae4146cc..ff8c45e18ebd415651d2e5f8a618b2d813a24a6f 100644 (file)
@@ -23,76 +23,78 @@ use Bugzilla::Util;
 use constant IS_CONFIG => 1;
 
 use constant DB_COLUMNS => qw(
-   keyworddefs.id
-   keyworddefs.name
-   keyworddefs.description
-   keyworddefs.is_active
+  keyworddefs.id
+  keyworddefs.name
+  keyworddefs.description
+  keyworddefs.is_active
 );
 
 use constant DB_TABLE => 'keyworddefs';
 
 use constant VALIDATORS => {
-    name        => \&_check_name,
-    description => \&_check_description,
-    is_active   => \&_check_is_active,
+  name        => \&_check_name,
+  description => \&_check_description,
+  is_active   => \&_check_is_active,
 };
 
 use constant UPDATE_COLUMNS => qw(
-    name
-    description
-    is_active
+  name
+  description
+  is_active
 );
 
 ###############################
 ####      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]); }
-sub set_is_active   { $_[0]->set('is_active', $_[1]); }
+sub set_is_active   { $_[0]->set('is_active',   $_[1]); }
 
 ###############################
 ####      Subroutines    ######
 ###############################
 
 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;
 }
 
 ###############################
@@ -100,33 +102,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;
 }
 
 sub _check_is_active { return $_[1] ? 1 : 0 }
index 3d8dccbcac860ea9dd9719e987d7879b874fc256..33b51e1c24d5538d2117256ed5bcfb4d2798952f 100644 (file)
@@ -13,91 +13,92 @@ use 5.14.0;
 use parent qw(Email::MIME);
 
 sub new {
-    my ($class, $msg) = @_;
-
-    # 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) = @_;
+
+  # 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) {
-            $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 (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) {
+      $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 (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;
-
-    # 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');
-    # Exchange does not respect the Auto-Submitted, but uses this.
-    $self->header_set('X-Auto-Response-Suppress', 'All');
-
-    # MIME-Version must be set otherwise some mailsystems ignore the charset
-    $self->header_set('MIME-Version', '1.0') if !$self->header('MIME-Version');
-
-    # Encode the headers correctly in quoted-printable
-    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');
-        $self->encoding_set('quoted-printable');
-    }
-
-    # Ensure we always return the encoded string
-    my $value = $self->SUPER::as_string();
-    if (utf8::is_utf8($value)) {
-        utf8::encode($value);
-    }
-
-    return $value;
+  my $self = shift;
+
+  # 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');
+
+  # Exchange does not respect the Auto-Submitted, but uses this.
+  $self->header_set('X-Auto-Response-Suppress', 'All');
+
+  # MIME-Version must be set otherwise some mailsystems ignore the charset
+  $self->header_set('MIME-Version', '1.0') if !$self->header('MIME-Version');
+
+  # Encode the headers correctly in quoted-printable
+  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');
+    $self->encoding_set('quoted-printable');
+  }
+
+  # Ensure we always return the encoded string
+  my $value = $self->SUPER::as_string();
+  if (utf8::is_utf8($value)) {
+    utf8::encode($value);
+  }
+
+  return $value;
 }
 
 1;
index 28b61b84276a140147ca1746557caa177ca56f0f..8bf94b2648199fa0b68f7ff6f53f0a645eec17db 100644 (file)
@@ -28,245 +28,253 @@ 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);
-
-    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      => 'UTF-8',
-                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      => 'UTF-8',
-                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');
-    }
-    $email->parts_set(\@parts);
-    return $email;
+  my ($vars, $templates) = @_;
+  my ($lang, $email_format, $msg_text, $msg_html, $msg_header);
+
+  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      => 'UTF-8',
+      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      => 'UTF-8',
+        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');
+  }
+  $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;
-    }
-
-    # Ensure that we are not sending emails too quickly to recipients.
-    if (Bugzilla->params->{use_mailer_queue}
-        && (EMAIL_LIMIT_PER_MINUTE || EMAIL_LIMIT_PER_HOUR))
-    {
-        $dbh->do(
-            "DELETE FROM email_rates WHERE message_ts < "
-            . $dbh->sql_date_math('LOCALTIMESTAMP(0)', '-', '1', 'HOUR'));
-
-        my $recipient = $email->header('To');
-
-        if (EMAIL_LIMIT_PER_MINUTE) {
-            my $minute_rate = $dbh->selectrow_array(
-                "SELECT COUNT(*)
+  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;
+  }
+
+  # Ensure that we are not sending emails too quickly to recipients.
+  if (Bugzilla->params->{use_mailer_queue}
+    && (EMAIL_LIMIT_PER_MINUTE || EMAIL_LIMIT_PER_HOUR))
+  {
+    $dbh->do("DELETE FROM email_rates WHERE message_ts < "
+        . $dbh->sql_date_math('LOCALTIMESTAMP(0)', '-', '1', 'HOUR'));
+
+    my $recipient = $email->header('To');
+
+    if (EMAIL_LIMIT_PER_MINUTE) {
+      my $minute_rate = $dbh->selectrow_array(
+        "SELECT COUNT(*)
                    FROM email_rates
                   WHERE recipient = ?  AND message_ts >= "
-                        . $dbh->sql_date_math('LOCALTIMESTAMP(0)', '-', '1', 'MINUTE'),
-                undef,
-                $recipient);
-            if ($minute_rate >= EMAIL_LIMIT_PER_MINUTE) {
-                die EMAIL_LIMIT_EXCEPTION;
-            }
-        }
-        if (EMAIL_LIMIT_PER_HOUR) {
-            my $hour_rate = $dbh->selectrow_array(
-                "SELECT COUNT(*)
+          . $dbh->sql_date_math('LOCALTIMESTAMP(0)', '-', '1', 'MINUTE'), undef,
+        $recipient
+      );
+      if ($minute_rate >= EMAIL_LIMIT_PER_MINUTE) {
+        die EMAIL_LIMIT_EXCEPTION;
+      }
+    }
+    if (EMAIL_LIMIT_PER_HOUR) {
+      my $hour_rate = $dbh->selectrow_array(
+        "SELECT COUNT(*)
                    FROM email_rates
                   WHERE recipient = ?  AND message_ts >= "
-                        . $dbh->sql_date_math('LOCALTIMESTAMP(0)', '-', '1', 'HOUR'),
-                undef,
-                $recipient);
-            if ($hour_rate >= EMAIL_LIMIT_PER_HOUR) {
-                die EMAIL_LIMIT_EXCEPTION;
-            }
-        }
-    }
-
-    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();
-        }
+          . $dbh->sql_date_math('LOCALTIMESTAMP(0)', '-', '1', 'HOUR'), undef,
+        $recipient
+      );
+      if ($hour_rate >= EMAIL_LIMIT_PER_HOUR) {
+        die EMAIL_LIMIT_EXCEPTION;
+      }
     }
-    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'} });
-    }
-
-    Bugzilla::Hook::process('mailer_before_send', { email => $email });
+  }
 
-    return if $email->header('to') eq '';
+  my $from = $email->header('From');
 
-    if ($method eq "Test") {
-        my $filename = bz_locations()->{'datadir'} . '/mailer.testfile';
-        open TESTFILE, '>>:encoding(UTF-8)', $filename;
-        # From - <date> is required to be a valid mbox file.
-        print TESTFILE "\n\nFrom - " . $email->header('Date') . "\n" . $email->as_string;
-        close TESTFILE;
+  my $hostname;
+  my $transport;
+  if ($method eq "Sendmail") {
+    if (ON_WINDOWS) {
+      $transport
+        = Bugzilla::Sender::Transport::Sendmail->new({sendmail => SENDMAIL_EXE});
     }
     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 });
-        }
+      $transport = Bugzilla::Sender::Transport::Sendmail->new();
     }
-
-    # insert into email_rates
-    if (Bugzilla->params->{use_mailer_queue}
-        && (EMAIL_LIMIT_PER_MINUTE || EMAIL_LIMIT_PER_HOUR))
-    {
-        $dbh->do(
-            "INSERT INTO email_rates(recipient, message_ts) VALUES (?, LOCALTIMESTAMP(0))",
-            undef,
-            $email->header('To')
-        );
+  }
+  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'}
+      });
+  }
+
+  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, '>>:encoding(UTF-8)', $filename;
+
+    # From - <date> 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});
     }
+  }
+
+  # insert into email_rates
+  if (Bugzilla->params->{use_mailer_queue}
+    && (EMAIL_LIMIT_PER_MINUTE || EMAIL_LIMIT_PER_HOUR))
+  {
+    $dbh->do(
+      "INSERT INTO email_rates(recipient, message_ts) VALUES (?, LOCALTIMESTAMP(0))",
+      undef, $email->header('To')
+    );
+  }
 }
 
 # 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: <bug-$bug_id-$user_id$sitespec>";
-    }
-    else {
-        my $rand_bits = generate_random_password(10);
-        $threadingmarker = "Message-ID: <bug-$bug_id-$user_id-$rand_bits$sitespec>" .
-                           "\nIn-Reply-To: <bug-$bug_id-$user_id$sitespec>" .
-                           "\nReferences: <bug-$bug_id-$user_id$sitespec>";
-    }
-
-    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: <bug-$bug_id-$user_id$sitespec>";
+  }
+  else {
+    my $rand_bits = generate_random_password(10);
+    $threadingmarker
+      = "Message-ID: <bug-$bug_id-$user_id-$rand_bits$sitespec>"
+      . "\nIn-Reply-To: <bug-$bug_id-$user_id$sitespec>"
+      . "\nReferences: <bug-$bug_id-$user_id$sitespec>";
+  }
+
+  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;
index b7d7b439370260fd2621d86d6dee2faa4509e17e..a0c8c671b28d32e2c675d82ec0f0297308dd3bea 100644 (file)
@@ -19,7 +19,7 @@ use Digest::MD5 qw(md5_hex);
 use parent qw(Text::MultiMarkdown);
 
 # use private code points
-use constant FENCED_BLOCK => "\N{U+F111}";
+use constant FENCED_BLOCK          => "\N{U+F111}";
 use constant INDENTED_FENCED_BLOCK => "\N{U+F222}";
 
 # Regex to match balanced [brackets]. See Friedl's
@@ -34,6 +34,7 @@ $g_nested_brackets = qr{
        \]
     )*
 }x;
+
 # Doesn't allow for whitespace, because we're using it to match URLs:
 $g_nested_parens = qr{
     (?>                                 # Atomic matching
@@ -48,70 +49,74 @@ $g_nested_parens = qr{
 our %g_escape_table;
 
 foreach my $char (split //, '\\`*_{}[]()>#+-.!~') {
-    $g_escape_table{$char} = md5_hex($char);
+  $g_escape_table{$char} = md5_hex($char);
 }
 $g_escape_table{'&lt;'} = md5_hex('&lt;');
 
 sub new {
-    my $invocant = shift;
-    my $class = ref $invocant || $invocant;
-    my $obj = $class->SUPER::new(tab_width => MARKDOWN_TAB_WIDTH,
-                              # Bugzilla uses HTML not XHTML
-                              empty_element_suffix => '>');
-    $obj->{tab_width} = MARKDOWN_TAB_WIDTH;
-    $obj->{empty_element_suffix} = '>';
-    return $obj;
+  my $invocant = shift;
+  my $class    = ref $invocant || $invocant;
+  my $obj      = $class->SUPER::new(
+    tab_width => MARKDOWN_TAB_WIDTH,
+
+    # Bugzilla uses HTML not XHTML
+    empty_element_suffix => '>'
+  );
+  $obj->{tab_width}            = MARKDOWN_TAB_WIDTH;
+  $obj->{empty_element_suffix} = '>';
+  return $obj;
 }
 
 sub markdown {
-    my ($self, $text, $bug, $comment) = @_;
-    my $user = Bugzilla->user;
-
-    if ($user->settings->{use_markdown}->{is_enabled}
-        && $user->setting('use_markdown') eq 'on')
-    {
-        $text = $self->_removeFencedCodeBlocks($text);
-        $text = Bugzilla::Template::quoteUrls($text, $bug, $comment, $user, 1);
-        return $self->SUPER::markdown($text);
-    }
-
-    return Bugzilla::Template::quoteUrls($text, $bug, $comment, $user);
+  my ($self, $text, $bug, $comment) = @_;
+  my $user = Bugzilla->user;
+
+  if ( $user->settings->{use_markdown}->{is_enabled}
+    && $user->setting('use_markdown') eq 'on')
+  {
+    $text = $self->_removeFencedCodeBlocks($text);
+    $text = Bugzilla::Template::quoteUrls($text, $bug, $comment, $user, 1);
+    return $self->SUPER::markdown($text);
+  }
+
+  return Bugzilla::Template::quoteUrls($text, $bug, $comment, $user);
 }
 
 sub _code_blocks {
-    my ($self) = @_;
-    $self->{code_blocks} = $self->{params}->{code_blocks} ||= [];
-    return $self->{code_blocks};
+  my ($self) = @_;
+  $self->{code_blocks} = $self->{params}->{code_blocks} ||= [];
+  return $self->{code_blocks};
 }
 
 sub _indented_code_blocks {
-    my ($self) = @_;
-    $self->{indented_code_blocks} = $self->{params}->{indented_code_blocks} ||= [];
-    return $self->{indented_code_blocks};
+  my ($self) = @_;
+  $self->{indented_code_blocks} = $self->{params}->{indented_code_blocks} ||= [];
+  return $self->{indented_code_blocks};
 }
 
 sub _RunSpanGamut {
-    # These are all the transformations that occur *within* block-level
-    # tags like paragraphs, headers, and list items.
 
-    my ($self, $text) = @_;
+  # These are all the transformations that occur *within* block-level
+  # tags like paragraphs, headers, and list items.
+
+  my ($self, $text) = @_;
 
-    $text = $self->_DoCodeSpans($text);
-    $text = $self->_EscapeSpecialCharsWithinTagAttributes($text);
-    $text = $self->_EscapeSpecialChars($text);
+  $text = $self->_DoCodeSpans($text);
+  $text = $self->_EscapeSpecialCharsWithinTagAttributes($text);
+  $text = $self->_EscapeSpecialChars($text);
 
-    $text = $self->_DoAnchors($text);
+  $text = $self->_DoAnchors($text);
 
-    # Strikethroughs is Bugzilla's extension
-    $text = $self->_DoStrikethroughs($text);
+  # Strikethroughs is Bugzilla's extension
+  $text = $self->_DoStrikethroughs($text);
 
-    $text = $self->_DoAutoLinks($text);
-    $text = $self->_EncodeAmpsAndAngles($text);
-    $text = $self->_DoItalicsAndBold($text);
+  $text = $self->_DoAutoLinks($text);
+  $text = $self->_EncodeAmpsAndAngles($text);
+  $text = $self->_DoItalicsAndBold($text);
 
-    $text =~ s/\n/<br$self->{empty_element_suffix}\n/g;
+  $text =~ s/\n/<br$self->{empty_element_suffix}\n/g;
 
-    return $text;
+  return $text;
 }
 
 # We first replace all fenced code blocks with just their
@@ -122,8 +127,8 @@ sub _RunSpanGamut {
 # structures. The contents of the body will be processed after
 # processing markdown structures.
 sub _removeFencedCodeBlocks {
-    my ($self, $text) = @_;
-    $text =~ s{
+  my ($self, $text) = @_;
+  $text =~ s{
         ^ `{3,} [\s\t]* \n
         (                # $1 = the entire code block
           (?: .* \n+)+?
@@ -134,7 +139,7 @@ sub _removeFencedCodeBlocks {
             "${\FENCED_BLOCK}\n";
         }egmx;
 
-    $text =~ s{
+  $text =~ s{
         (?:\n\n|\A)
         (                # $1 = the code block -- one or more lines, starting with a space/tab
           (?:
@@ -147,21 +152,22 @@ sub _removeFencedCodeBlocks {
             push @{$self->_indented_code_blocks}, $1;
             "\n${\INDENTED_FENCED_BLOCK}\n";
         }egmx;
-    return $text;
+  return $text;
 }
 
 # Override to check for HTML-escaped <>" chars.
 sub _StripLinkDefinitions {
-    my ($self, $text) = @_;
+  my ($self, $text) = @_;
 
-    #
-    # Strips link definitions from text, stores the URLs and titles in
-    # hash references.
-    #
-    my $less_than_tab = $self->{tab_width} - 1;
+  #
+  # Strips link definitions from text, stores the URLs and titles in
+  # hash references.
+  #
+  my $less_than_tab = $self->{tab_width} - 1;
 
-    # Link defs are in the form: ^[id]: url "optional title"
-    while ($text =~ s{
+  # Link defs are in the form: ^[id]: url "optional title"
+  while (
+    $text =~ s{
             ^[ ]{0,$less_than_tab}\[(.+)\]: # id = \$1
               [ \t]*
               \n?               # maybe *one* newline
@@ -178,26 +184,29 @@ sub _StripLinkDefinitions {
                 [ \t]*
             )?  # title is optional
             (?:\n+|\Z)
-        }{}omx) {
-        $self->{_urls}{lc $1} = $self->_EncodeAmpsAndAngles( $2 );    # Link IDs are case-insensitive
-        if ($3) {
-            $self->{_titles}{lc $1} = $3;
-            $self->{_titles}{lc $1} =~ s/"/&quot;/g;
-        }
-
+        }{}omx
+    )
+  {
+    $self->{_urls}{lc $1} = $self->_EncodeAmpsAndAngles($2); # Link IDs are case-insensitive
+    if ($3) {
+      $self->{_titles}{lc $1} = $3;
+      $self->{_titles}{lc $1} =~ s/"/&quot;/g;
     }
 
-    return $text;
+  }
+
+  return $text;
 }
 
 # We need to look for HTML-escaped '<' and '>' (i.e. &lt; and &gt;).
 # We also remove Email linkification from the original implementation
 # as it is already done in Bugzilla's quoteUrls().
 sub _DoAutoLinks {
-    my ($self, $text) = @_;
+  my ($self, $text) = @_;
 
-    $text =~ s{(?:<|&lt;)((?:https?|ftp):[^'">\s]+?)(?:>|&gt;)}{<a href="$1">$1</a>}gi;
-    return $text;
+  $text
+    =~ s{(?:<|&lt;)((?:https?|ftp):[^'">\s]+?)(?:>|&gt;)}{<a href="$1">$1</a>}gi;
+  return $text;
 }
 
 # The main reasons for overriding this method are
@@ -207,17 +216,17 @@ sub _DoAnchors {
 #
 # Turn Markdown link shortcuts into <a> tags.
 #
-    my ($self, $text) = @_;
+  my ($self, $text) = @_;
 
-    # We revert linkifications of non-email links and only
-    # those links whose URL and title are the same because
-    # this way we can be sure that link is generated by quoteUrls()
-    $text =~ s@<a \s+ href="(?! mailto ) (.+?)">\1</a>@$1@xmg;
+  # We revert linkifications of non-email links and only
+  # those links whose URL and title are the same because
+  # this way we can be sure that link is generated by quoteUrls()
+  $text =~ s@<a \s+ href="(?! mailto ) (.+?)">\1</a>@$1@xmg;
 
-    #
-    # First, handle reference-style links: [link text] [id]
-    #
-    $text =~ s{
+  #
+  # First, handle reference-style links: [link text] [id]
+  #
+  $text =~ s{
         (                   # wrap whole match in $1
           \[
             ($g_nested_brackets)    # link text = $2
@@ -244,10 +253,10 @@ sub _DoAnchors {
         $self->_GenerateAnchor($whole_match, $link_text, $link_id);
     }xsge;
 
-    #
-    # Next, inline-style links: [link text](url "optional title")
-    #
-    $text =~ s{
+  #
+  # Next, inline-style links: [link text](url "optional title")
+  #
+  $text =~ s{
         (               # wrap whole match in $1
           \[
             ($g_nested_brackets)    # link text = $2
@@ -283,12 +292,12 @@ sub _DoAnchors {
         $self->_GenerateAnchor($whole_match, $link_text, undef, $url, $title);
     }xsge;
 
-    #
-    # Handle reference-style shortcuts: [link text]
-    # These must come last in case you've also got [link test][1]
-    # or [link test](/foo)
-    #
-    $text =~ s{
+  #
+  # Handle reference-style shortcuts: [link text]
+  # These must come last in case you've also got [link test][1]
+  # or [link test](/foo)
+  #
+  $text =~ s{
         (                    # wrap whole match in $1
           \[
             ([^\[\]]+)        # link text = $2; can't contain '[' or ']'
@@ -303,10 +312,10 @@ sub _DoAnchors {
         $self->_GenerateAnchor($whole_match, $link_text, $link_id);
     }xsge;
 
-    # Last, handle "naked" references
-    # Caveat, does not handle ;http://amazon.com
-    my $safe_url_regexp = Bugzilla::Template::SAFE_URL_REGEXP();
-    $text =~ s{
+  # Last, handle "naked" references
+  # Caveat, does not handle ;http://amazon.com
+  my $safe_url_regexp = Bugzilla::Template::SAFE_URL_REGEXP();
+  $text =~ s{
         (
           (^|(?<![;^"'<>]))  # negative lookbehind, including ';' in '&lt;'
           (                  # wrap url in $3
@@ -319,7 +328,7 @@ sub _DoAnchors {
         $self->_GenerateAnchor($whole_match, $url, undef, $url, undef);
     }xsge;
 
-    return $text;
+  return $text;
 }
 
 # The purpose of overriding this function is to add support
@@ -330,69 +339,72 @@ sub _DoAnchors {
 # and we do not want a part of those variables to look emphasized/bold.
 # Instead, we render them as the way they originally are.
 sub _DoItalicsAndBold {
-    my ($self, $text) = @_;
+  my ($self, $text) = @_;
 
-    # Handle at beginning of lines:
-    $text =~ s{ (^__ (?=\S) (.+?[*_]*) (?<=\S) __ (?!\S)) }
+  # Handle at beginning of lines:
+  $text =~ s{ (^__ (?=\S) (.+?[*_]*) (?<=\S) __ (?!\S)) }
               {
                   my $result = _has_multiple_underscores($2) ? $1 : "<strong>$2</strong>";
                   $result;
               }gsxe;
 
-    $text =~ s{ ^\*\* (?=\S) (.+?[*_]*) (?<=\S) \*\* }{<strong>$1</strong>}gsx;
+  $text =~ s{ ^\*\* (?=\S) (.+?[*_]*) (?<=\S) \*\* }{<strong>$1</strong>}gsx;
 
-    $text =~ s{ (^_ (?=\S) (.+?) (?<=\S) _ (?!\S)) }
+  $text =~ s{ (^_ (?=\S) (.+?) (?<=\S) _ (?!\S)) }
               {
                   my $result = _has_multiple_underscores($2) ? $1 : "<em>$2</em>";
                   $result;
               }gsxe;
 
-    $text =~ s{ ^\* (?=\S) (.+?) (?<=\S) \* }{<em>$1</em>}gsx;
+  $text =~ s{ ^\* (?=\S) (.+?) (?<=\S) \* }{<em>$1</em>}gsx;
 
-    # <strong> must go first:
-    $text =~ s{ ( (?<=\s) __ (?=\S) (.+?[*_]*) (?<=\S) __ (?!\S) ) }
+  # <strong> must go first:
+  $text =~ s{ ( (?<=\s) __ (?=\S) (.+?[*_]*) (?<=\S) __ (?!\S) ) }
               {
                   my $result = _has_multiple_underscores($2) ? $1 : "<strong>$2</strong>";
                   $result;
               }gsxe;
 
 
-    $text =~ s{ (?<=\s) \*\* (?=\S) (.+?[*_]*) (?<=\S) \*\* }{<strong>$1</strong>}gsx;
+  $text
+    =~ s{ (?<=\s) \*\* (?=\S) (.+?[*_]*) (?<=\S) \*\* }{<strong>$1</strong>}gsx;
 
-    $text =~ s{ ( (?<=\s) _ (?=\S) (.+?) (?<=\S) _ (?!\S) ) }
+  $text =~ s{ ( (?<=\s) _ (?=\S) (.+?) (?<=\S) _ (?!\S) ) }
               {
                   my $result = _has_multiple_underscores($2) ? $1 : "<em>$2</em>";
                   $result;
               }gsxe;
 
-    $text =~ s{ (?<=\s) \* (?=\S) (.+?) (?<=\S) \* }{<em>$1</em>}gsx;
+  $text =~ s{ (?<=\s) \* (?=\S) (.+?) (?<=\S) \* }{<em>$1</em>}gsx;
 
-    # And now, a second pass to catch nested strong and emphasis special cases
-    $text =~ s{ ( (?<=\s) __ (?=\S) (.+?[*_]*) (?<=\S) __ (\S*) ) }
+  # And now, a second pass to catch nested strong and emphasis special cases
+  $text =~ s{ ( (?<=\s) __ (?=\S) (.+?[*_]*) (?<=\S) __ (\S*) ) }
               {
                   my $result = _has_multiple_underscores($3) ? $1 : "<strong>$2</strong>$3";
                   $result;
               }gsxe;
 
-    $text =~ s{ (?<=\s) \*\* (?=\S) (.+?[*_]*) (?<=\S) \*\* }{<strong>$1</strong>}gsx;
-    $text =~ s{ ( (?<=\s) _ (?=\S) (.+?) (?<=\S) _ (\S*) ) }
+  $text
+    =~ s{ (?<=\s) \*\* (?=\S) (.+?[*_]*) (?<=\S) \*\* }{<strong>$1</strong>}gsx;
+  $text =~ s{ ( (?<=\s) _ (?=\S) (.+?) (?<=\S) _ (\S*) ) }
               {
                   my $result = _has_multiple_underscores($3) ? $1 : "<em>$2</em>$3";
                   $result;
               }gsxe;
 
-    $text =~ s{ (?<=\s) \* (?=\S) (.+?) (?<=\S) \* }{<em>$1</em>}gsx;
+  $text =~ s{ (?<=\s) \* (?=\S) (.+?) (?<=\S) \* }{<em>$1</em>}gsx;
 
-    return $text;
+  return $text;
 }
 
 sub _DoStrikethroughs {
-    my ($self, $text) = @_;
+  my ($self, $text) = @_;
 
-    $text =~ s{ ^ ~~ (?=\S) ([^~]+?) (?<=\S) ~~ (?!~) }{<del>$1</del>}gsx;
-    $text =~ s{ (?<=_|[^~\w]) ~~ (?=\S) ([^~]+?) (?<=\S) ~~ (?!~) }{<del>$1</del>}gsx;
+  $text =~ s{ ^ ~~ (?=\S) ([^~]+?) (?<=\S) ~~ (?!~) }{<del>$1</del>}gsx;
+  $text
+    =~ s{ (?<=_|[^~\w]) ~~ (?=\S) ([^~]+?) (?<=\S) ~~ (?!~) }{<del>$1</del>}gsx;
 
-    return $text;
+  return $text;
 }
 
 # The original _DoCodeSpans() uses the 's' modifier in its regex
@@ -400,9 +412,9 @@ sub _DoStrikethroughs {
 # We copy the code from the original implementation and remove the
 # 's' modifier from it.
 sub _DoCodeSpans {
-    my ($self, $text) = @_;
+  my ($self, $text) = @_;
 
-    $text =~ s@
+  $text =~ s@
             (?<!\\)     # Character before opening ` can't be a backslash
             (`+)        # $1 = Opening run of `
             (.+?)       # $2 = The code block
@@ -417,21 +429,21 @@ sub _DoCodeSpans {
             "<code>$c</code>";
         @egx;
 
-    return $text;
+  return $text;
 }
 
 # Override to delay after DoBlockQuotes
 sub _DoCodeBlocks {
-    my ($self, $text) = @_;
+  my ($self, $text) = @_;
 
-    return $text;
+  return $text;
 }
 
 # add GFM Fenced Code Blocks
 sub _DoDelayCodeBlocks {
-    my ($self, $text) = @_;
+  my ($self, $text) = @_;
 
-    $text =~ s{
+  $text =~ s{
         ^ (${\FENCED_BLOCK}|${\INDENTED_FENCED_BLOCK})
         }{
             my $aref = ($1 eq FENCED_BLOCK) ? $self->_code_blocks : $self->_indented_code_blocks;
@@ -446,13 +458,13 @@ sub _DoDelayCodeBlocks {
             $result;
         }egmx;
 
-    return $text;
+  return $text;
 }
 
 sub _DoBlockQuotes {
-    my ($self, $text) = @_;
+  my ($self, $text) = @_;
 
-    $text =~ s{
+  $text =~ s{
           (                             # Wrap whole match in $1
             (?:
               ^[ \t]*&gt;[ \t]?         # '>' at the start of a line
@@ -476,61 +488,63 @@ sub _DoBlockQuotes {
             "<blockquote class=\"markdown\">\n$bq\n</blockquote>\n\n";
         }egmx;
 
-    $text = $self->_DoDelayCodeBlocks($text);
-    return $text;
+  $text = $self->_DoDelayCodeBlocks($text);
+  return $text;
 }
 
 sub _DoLists {
-    my ($self, $text) = @_;
+  my ($self, $text) = @_;
 
-    $text = $self->SUPER::_DoLists($text);
+  $text = $self->SUPER::_DoLists($text);
 
-    # strip trailing newlines created by DoLists
-    $text =~ s/\n</</g;
+  # strip trailing newlines created by DoLists
+  $text =~ s/\n</</g;
 
-    return $text;
+  return $text;
 }
 
 sub _EncodeCode {
-    my ($self, $text) = @_;
-
-    # We need to unescape the escaped HTML characters in code blocks.
-    # These are the reverse of the escapings done in Bugzilla::Util::html_quote()
-    $text =~ s/&lt;/</g;
-    $text =~ s/&gt;/>/g;
-    $text =~ s/&quot;/"/g;
-    $text =~ s/&#64;/@/g;
-    # '&amp;' substitution must be the last one, otherwise a literal like '&gt;'
-    # will turn to '>' because '&' is already changed to '&amp;' in Bugzilla::Util::html_quote().
-    # In other words, html_quote() will change '&gt;' to '&amp;gt;' and then we will
-    # change '&amp;gt' -> '&gt;' -> '>' if we write this substitution as the first one.
-    $text =~ s/&amp;/&/g;
-    $text =~ s{<a \s+ href="(?:mailto:)? (.+?)"> \1 </a>}{$1}xmgi;
-    $text = $self->SUPER::_EncodeCode($text);
-    $text =~ s/~/$g_escape_table{'~'}/go;
-    # Encode '&lt;' to prevent URLs from getting linkified in code spans
-    $text =~ s/&lt;/$g_escape_table{'&lt;'}/go;
-
-    return $text;
+  my ($self, $text) = @_;
+
+  # We need to unescape the escaped HTML characters in code blocks.
+  # These are the reverse of the escapings done in Bugzilla::Util::html_quote()
+  $text =~ s/&lt;/</g;
+  $text =~ s/&gt;/>/g;
+  $text =~ s/&quot;/"/g;
+  $text =~ s/&#64;/@/g;
+
+# '&amp;' substitution must be the last one, otherwise a literal like '&gt;'
+# will turn to '>' because '&' is already changed to '&amp;' in Bugzilla::Util::html_quote().
+# In other words, html_quote() will change '&gt;' to '&amp;gt;' and then we will
+# change '&amp;gt' -> '&gt;' -> '>' if we write this substitution as the first one.
+  $text =~ s/&amp;/&/g;
+  $text =~ s{<a \s+ href="(?:mailto:)? (.+?)"> \1 </a>}{$1}xmgi;
+  $text = $self->SUPER::_EncodeCode($text);
+  $text =~ s/~/$g_escape_table{'~'}/go;
+
+  # Encode '&lt;' to prevent URLs from getting linkified in code spans
+  $text =~ s/&lt;/$g_escape_table{'&lt;'}/go;
+
+  return $text;
 }
 
 sub _EncodeBackslashEscapes {
-    my ($self, $text) = @_;
+  my ($self, $text) = @_;
 
-    $text = $self->SUPER::_EncodeBackslashEscapes($text);
-    $text =~ s/\\~/$g_escape_table{'~'}/go;
+  $text = $self->SUPER::_EncodeBackslashEscapes($text);
+  $text =~ s/\\~/$g_escape_table{'~'}/go;
 
-    return $text;
+  return $text;
 }
 
 sub _UnescapeSpecialChars {
-    my ($self, $text) = @_;
+  my ($self, $text) = @_;
 
-    $text = $self->SUPER::_UnescapeSpecialChars($text);
-    $text =~ s/$g_escape_table{'~'}/~/go;
-    $text =~ s/$g_escape_table{'&lt;'}/&lt;/go;
+  $text = $self->SUPER::_UnescapeSpecialChars($text);
+  $text =~ s/$g_escape_table{'~'}/~/go;
+  $text =~ s/$g_escape_table{'&lt;'}/&lt;/go;
 
-    return $text;
+  return $text;
 }
 
 # Check if the passed string is of the form multiple_underscores_in_a_word.
@@ -538,11 +552,11 @@ sub _UnescapeSpecialChars {
 # any white-space. Then, if the string is composed of non-space chunks which
 # are bound together with underscores, the string has the desired form.
 sub _has_multiple_underscores {
-    my $string = shift;
-    return 0 unless $string;
-    return 0 if $string =~ /\s/;
-    return 1 if $string =~ /_/;
-    return 0;
+  my $string = shift;
+  return 0 unless $string;
+  return 0 if $string =~ /\s/;
+  return 1 if $string =~ /_/;
+  return 0;
 }
 
 1;
index ed32fa27b07f6d3955bce1a2b9581f791f6ad987..f71ff2572b98d8f2ca07beb9c1070e30f0fc3188 100644 (file)
@@ -20,238 +20,234 @@ 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::Fast;
-        $self->{namespace} = Bugzilla->params->{memcached_namespace} || '';
-        $self->{memcached} =
-            Cache::Memcached::Fast->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::Fast;
+    $self->{namespace} = Bugzilla->params->{memcached_namespace} || '';
+    $self->{memcached} = Cache::Memcached::Fast->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);
-    trick_taint($key) if defined $key;
-    return length($self->{namespace} . $key) > MAX_KEY_LENGTH
-        ? undef
-        : $key;
+  my ($self, $key) = @_;
+  $key = $self->_global_prefix . '.' . uri_escape_utf8($key);
+  trick_taint($key) if defined $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) = @_;
+  my ($self, $key) = @_;
 
-    $key = $self->_encode_key($key)
-        or return;
-    return $self->{memcached}->get($key);
+  $key = $self->_encode_key($key) or return;
+  return $self->{memcached}->get($key);
 }
 
 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;
index df30e34cb8060103ad8975b14259cb1209f43f1a..cf558c90c4f44a22b739274590c69e48bbb7c251 100644 (file)
@@ -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;
 
@@ -30,6 +30,7 @@ use DateTime;
 use Fcntl qw(SEEK_SET);
 use File::Basename;
 use List::Util qw(first);
+
 # Bug 1270550 - Tie::Hash::NamedCapture must be loaded before Safe.
 use Tie::Hash::NamedCapture;
 use Safe;
@@ -39,10 +40,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
@@ -66,11 +67,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.
@@ -110,22 +111,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".
@@ -135,7 +136,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);
@@ -145,43 +146,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();
+  }
 }
 
 ################
@@ -189,36 +193,34 @@ 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) {
-      require $module;
-      my $canonical_name = _canonical_name($module);
-      return "Bugzilla::Migrate::$canonical_name"->new;     
-    }
-    else {
-      my $migrate_module = {};
-      Bugzilla::Hook::process('migrate_modules', $migrate_module);
-      @migration_modules = glob($migrate_module->{path}."/*");
-      ($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 "$migrate_module->{prefix}::$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) {
+    require $module;
+    my $canonical_name = _canonical_name($module);
+    return "Bugzilla::Migrate::$canonical_name"->new;
+  }
+  else {
+    my $migrate_module = {};
+    Bugzilla::Hook::process('migrate_modules', $migrate_module);
+    @migration_modules = glob($migrate_module->{path} . "/*");
+    ($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 "$migrate_module->{prefix}::$canonical_name"->new;
+  }
 }
 
 #############
@@ -226,67 +228,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};
 }
 
 ###########
@@ -294,49 +296,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;
 }
 
 ###################
@@ -344,160 +346,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 $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);
-    }
-    
-    if (grep($_ eq $field, USER_FIELDS)) {
-        if (defined $self->map_value('user', $value)) {
-            return $self->map_value('user', $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 (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);
     }
-    
-    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};
 }
 
 #################
@@ -505,387 +514,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);
 }
 
 ######################
@@ -893,11 +917,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;
index f16faf78cf1a7324236a118826e51edc2bfda449..34e6cad2d40b0fd1a4159dda916c0e2ba26a0e52 100644 (file)
@@ -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    => <<END,
+  {
+    name    => 'gnats_path',
+    default => '/var/lib/gnats',
+    desc    => <<END,
 # The path to the directory that contains the GNATS database.
 END
-    },
-    {
-        name    => '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 = <<END;
+  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 = <<END;
 From: nobody
 MIME-Version: 1.0
 Content-Type: multipart/mixed; boundary="$boundary"
@@ -607,96 +626,103 @@ Content-Transfer-Encoding: 7bit
 $unformatted
 --$boundary--
 END
-    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 $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;
index 2fd68dfbafe0e98c81cbe6c082de1ebf9b7938d1..37b6dab36e8ddc7cd08fc4e75711e6100da8db52 100644 (file)
@@ -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,85 @@ 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'} ||= Bugzilla::Product->new({ id => $self->product_id, cache => 1 });
-    return $self->{'product'};
+  require Bugzilla::Product;
+  $self->{'product'}
+    ||= Bugzilla::Product->new({id => $self->product_id, cache => 1});
+  return $self->{'product'};
 }
 
 1;
index 6e66267f803ca034051a468958937debe3ee837c..3f4b90eb82d6796c52b5f2e564363ecebe93b4a1 100644 (file)
@@ -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;
index 592653afd7f891374b9985ca8b9ed1a48431c993..6b5a78c3a27243fe2cfa3d4e0c84007464cb090e 100644 (file)
@@ -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,257 +72,282 @@ 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_series() if $create_series;
+  # Create groups and series for the new product, if requested.
+  $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};
 
 }
 
@@ -331,91 +356,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});
     }
-    return $name;
+    else {
+      ThrowUserError('product_name_diff_in_case',
+        {'product' => $name, 'existing_product' => $product->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');
 
-    $version = trim($version);
-    $version || ThrowUserError('product_must_have_version');
-    # We will check the version length when Bugzilla::Version->create will do it.
-    return $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;
 }
 
 #####################################
@@ -430,372 +458,405 @@ use constant is_default => 0;
 ###############################
 
 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'};
 }
 
 ###############################
@@ -803,29 +864,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;
index 3c7d3d3077b12efdda9ae4711110a136c5a89391..63cb770c23976856f018e4a93e5a72e2f384e9f7 100644 (file)
@@ -27,13 +27,13 @@ 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 #
 #################
 
-# 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 => <<END;
 INT SystemFunction036(
@@ -47,40 +47,42 @@ END
 #################
 
 sub rand (;$) {
-    my ($limit) = @_;
-    my $int = irand();
-    return _to_float($int, $limit);
+  my ($limit) = @_;
+  my $int = irand();
+  return _to_float($int, $limit);
 }
 
 sub irand (;$) {
-    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;
+  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;
 }
 
 ##########################
@@ -88,78 +90,77 @@ 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 || ($major == 5 and $minor == 0)) {
-        die 'Bugzilla does not support versions of Windows before Windows XP';
-    }
-
-    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 || ($major == 5 and $minor == 0)) {
+    die 'Bugzilla does not support versions of Windows before Windows XP';
+  }
+
+  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;
 }
 
 1;
index 9820bbc72f93dcba85fb17c202d7c40b22bff41f..0f22e48c7e56b24047b1c0e5acd0930512e27afd 100644 (file)
@@ -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;
index b806ff8da59e4db84cd1191651c177680e251984..59a6fb195d6ce65af17415fa28863d46538b0d01 100644 (file)
@@ -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,326 +135,290 @@ 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,
-    isactive       => \&_isactive,
-    isnotactive    => \&_isactive,
+  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,
+  isactive       => \&_isactive,
+  isnotactive    => \&_isactive,
 };
 
 # 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',
-    isactive       => 'isnotactive',
-    isnotactive    => 'isactive',
-    # 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',
+  isactive       => 'isnotactive',
+  isnotactive    => 'isactive',
+
+  # 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
-    isactive
-    isnotactive
+  isempty
+  isnotempty
+  isactive
+  isnotactive
 );
 
 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,
-    dupe_of          => {
-        %{ &MULTI_SELECT_OVERRIDE },
-        changedby     => \&_dupe_of_changedby,
-        changedbefore => \&_dupe_of_changedbefore_after,
-        changedafter  => \&_dupe_of_changedbefore_after,
-        changedfrom   => \&_invalid_combination,
-        changedto     => \&_invalid_combination,
-    },
-    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,
+  dupe_of      => {
+    %{&MULTI_SELECT_OVERRIDE},
+    changedby     => \&_dupe_of_changedby,
+    changedbefore => \&_dupe_of_changedbefore_after,
+    changedafter  => \&_dupe_of_changedbefore_after,
+    changedfrom   => \&_invalid_combination,
+    changedto     => \&_invalid_combination,
+  },
+  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,
-        'attachments.submitter' => \&_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;
-        }
-    }
-    return $map;
-};
+  my $map = {
+
+    # Pronoun Fields (Ones that can accept %user%, etc.)
+    assigned_to             => \&_contact_pronoun,
+    'attachments.submitter' => \&_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;
+    }
+  }
+  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
@@ -462,109 +426,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:
 #
@@ -574,7 +510,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.
@@ -585,150 +521,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;
-
-    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',
-    );
+  my $invocant = shift;
+  my $user     = blessed($invocant) ? $invocant->_user : Bugzilla->user;
+  my $dbh      = Bugzilla->dbh;
+  my $cache    = Bugzilla->request_cache;
 
-    # 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
 );
 
 ###############
@@ -737,27 +672,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;
 }
 
 
@@ -766,148 +701,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 = <<END;
+  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 = <<END;
 SELECT $select
   FROM $from
  WHERE $where
 $group_by$order_by$limit
 END
-    $self->{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;
 }
 
 ######################
@@ -916,59 +859,61 @@ 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};
 }
 
 # Select custom fields have an active field, so we need to know these fields
 # too. The type is important as we handle them differently.
 sub _select_fields {
-    my ($self) = @_;
-    $self->{select_fields} ||= Bugzilla->fields({
-        by_name => 1,
-        type    => [FIELD_TYPE_MULTI_SELECT, FIELD_TYPE_SINGLE_SELECT]});
-    return $self->{select_fields};
+  my ($self) = @_;
+  $self->{select_fields}
+    ||= Bugzilla->fields({
+    by_name => 1, type => [FIELD_TYPE_MULTI_SELECT, FIELD_TYPE_SINGLE_SELECT]
+    });
+  return $self->{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} }
 
 ##############################
@@ -977,81 +922,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;
 }
 
 ################################
@@ -1060,85 +1008,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;
 }
 
 #############################
@@ -1146,32 +1092,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 '';
 }
 
 ############################
@@ -1179,176 +1123,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;
 }
 
 #############################
@@ -1361,54 +1305,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 .= <<END;
+  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 .= <<END;
 
         OR (bugs.reporter_accessible = 1 AND bugs.reporter = $userid)
         OR (bugs.cclist_accessible = 1 AND security_cc.who IS NOT NULL)
         OR bugs.assigned_to = $userid
 END
-        if (Bugzilla->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;
 }
 
 ################################
@@ -1418,40 +1368,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};
 }
 
 ##############################################
@@ -1460,243 +1410,253 @@ 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;
     }
-    return @{$clause->children} ? $clause : undef;
+    else {
+      $pass_value = join(',', @values);
+    }
+    $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+$/a } keys %$params;
-    
-    my $clause = new Bugzilla::Search::Clause();
-    foreach my $param (@email_params) {
-        $param =~ /(\d+)$/a;
-        my $id = $1;
-        my $email = trim($params->{"email$id"});
-        next if !$email;
-        my $type = $params->{"emailtype$id"} || 'equals';
-        $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+$/a } keys %$params;
+
+  my $clause = new Bugzilla::Search::Clause();
+  foreach my $param (@email_params) {
+    $param =~ /(\d+)$/a;
+    my $id    = $1;
+    my $email = trim($params->{"email$id"});
+    next if !$email;
+    my $type = $params->{"emailtype$id"} || 'equals';
+    $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);
+  }
 
-    return @{$clause->children} ? $clause : undef;
+  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;
 }
 
 ######################################
@@ -1704,238 +1664,246 @@ 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) = @_;
+
+  # 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;
 }
 
 sub _boolean_charts {
-    my ($self) = @_;
-
-    use re '/a';
-
-    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);
-    }
+  my ($self) = @_;
 
-    return @{$clause->children} ? $clause : undef;
-}
+  use re '/a';
 
-sub _custom_search {
-    my ($self) = @_;
-    my $params = $self->_params;
+  my $params     = $self->_params;
+  my @param_list = keys %$params;
 
-    my @field_ids = $self->_field_ids;
-    return unless scalar @field_ids;
+  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 $joiner = $params->{j_top} || '';
-    my $current_clause = $joiner eq 'AND_G'
-        ? new Bugzilla::Search::ClauseGroup()
-        : new Bugzilla::Search::Clause($joiner);
+  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;
-}
-
-sub _field_ids {
-    my ($self) = @_;
-    my $params = $self->_params;
-    my @param_list = keys %$params;
+    $clause->add($and_clause);
+  }
 
-    my @field_params = grep { /^f\d+$/a } @param_list;
-    my @field_ids = map { /(\d+)/a; $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);
+  }
+
+  # 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+$/a} @param_list;
+  my @field_ids = map { /(\d+)/a; $1 } @field_params;
+  @field_ids = sort { $a <=> $b } @field_ids;
+  return @field_ids;
+}
 
-    $condition->translated(\%search_args);
+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;
-    $string =~ s/([\x{10000}-\x{10FFFF}])/"\x{FDD0}[" . uc(sprintf('U+%04x', ord($1))) . "]\x{FDD1}"/eg;
-    return $string;
+  my $string = shift;
+  $string
+    =~ s/([\x{10000}-\x{10FFFF}])/"\x{FDD0}[" . uc(sprintf('U+%04x', ord($1))) . "]\x{FDD1}"/eg;
+  return $string;
 }
 
 ##################################
@@ -1945,121 +1913,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};
 }
 
 ###########################
@@ -2071,47 +2044,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
@@ -2119,68 +2094,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;
 }
 
 #####################################
@@ -2188,111 +2164,124 @@ 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);
-    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 (lc($str) eq 'now') {
-        my ($sec, $min, $hour, $mday, $month, $year, $wday) = localtime(time());
-        return sprintf("%4d-%02d-%02d %02d:%02d:%02d", $year+1900, $month+1, $mday, $hour, $min, $sec);
-    }
-
-    if ($str =~ /^(-|\+)?(\d+)([hdwmy])(s?)$/ai) {   # 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);
+  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 (lc($str) eq 'now') {
+    my ($sec, $min, $hour, $mday, $month, $year, $wday) = localtime(time());
+    return sprintf(
+      "%4d-%02d-%02d %02d:%02d:%02d",
+      $year + 1900,
+      $month + 1, $mday, $hour, $min, $sec
+    );
+  }
+
+  if ($str =~ /^(-|\+)?(\d+)([hdwmy])(s?)$/ai) {    # 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);
 }
 
 ######################################
@@ -2300,104 +2289,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);
 }
 
 #####################################################################
@@ -2405,592 +2399,605 @@ 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 ne 'notactive' && $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 ne 'notactive' && $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";
     }
+  }
 }
 
 # Changes to duplicates are stored in the longdesc table
 sub _dupe_of_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.type = " . CMT_DUPE_OF;
-
-    # 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 AND $table.type = " . CMT_DUPE_OF;
+
+  # 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 _dupe_of_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 AND $table.type = " . CMT_DUPE_OF ],
-    };
-    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 AND $table.type = " . CMT_DUPE_OF],
+  };
+  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";
+  }
 }
 
 
 # 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;
-    }
-    if ($operator =~ /^is(not)?active$/) {
-        # Comments can't be (in)active
-        $self->_invalid_combination($args);
-    }
-    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;
+  }
+  if ($operator =~ /^is(not)?active$/) {
+
+    # Comments can't be (in)active
+    $self->_invalid_combination($args);
+  }
+  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 ne 'notactive' && $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 ne 'notactive' && $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
@@ -3007,223 +3014,239 @@ 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 'dupe_of') {
-        $args->{_select_field} = 'dupe';
-        $args->{full_field} = 'dupe_of';
-        return "duplicates";
-    }
-    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 'dupe_of') {
+    $args->{_select_field} = 'dupe';
+    $args->{full_field}    = 'dupe_of';
+    return "duplicates";
+  }
+  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 'dupe_of') {
-        push @$joins, {
-            table => 'duplicates',
-            as    => "duplicates_$chart_id",
-            from  => 'bug_id',
-            to    => 'dupe',
-        };
-        return "duplicates_$chart_id.dupe 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 'dupe_of') {
+    push @$joins,
+      {
+      table => 'duplicates',
+      as    => "duplicates_$chart_id",
+      from  => 'bug_id',
+      to    => 'dupe',
+      };
+    return "duplicates_$chart_id.dupe 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";
+  }
 }
 
 ###############################
@@ -3231,304 +3254,309 @@ 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);
-    # 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) . ')' : '';
+  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) . ')' : '';
 }
 
 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',
-        };
+  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";
-    }
+    $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 "''";
 }
 
 sub _isactive {
-    my ($self, $args) = @_;
-    my ($field, $chart_id, $operator, $joins, $bugs_table) =
-        @$args{qw(field chart_id operator joins bugs_table)};
-
-    my $value = $operator eq 'isnotactive' ? 0 : 1;
-
-    my @simple_fields = qw(bug_severity bug_status op_sys priority rep_platform
-        resolution version
-     );
-    my @user_fields = qw(attachments.submitter assigned_to assigned_to_realname
-        cc commenter reporter reporter_realname requestees.login_name
-        setters.login_name qa_contact qa_contact_realname
-    );
-
-    if ($field eq 'product' or $field eq 'component') {
-        $args->{term} = "${field}s.isactive = $value";
-    }
-    elsif (grep {$_ eq $field} @simple_fields) {
-        push @$joins, {
-            table => ($field eq 'version' ? 'versions' : $field),
-            as    => "${field}_$chart_id",
-            from  => $field,
-            to    => 'value',
-        };
-        $args->{term} = "${field}_$chart_id.isactive = $value";
-    }
-    elsif ($field eq 'flagtypes.name') {
-        $args->{term} = "flagtypes_$chart_id.is_active = $value";
-    }
-    elsif ($field eq 'bug_group') {
-        $args->{term} = "groups.isactive = $value";
-    }
-    elsif ($field eq 'keywords') {
-        $args->{term} = "keyworddefs.is_active = $value";
-    }
-    elsif ($field eq 'target_milestone') {
-        push @$joins, {
-            table => 'milestones',
-            as    => "milestones_$chart_id",
-            from  => 'target_milestone',
-            to    => 'value',
-        };
-        $args->{term} = qq{
+  my ($self, $args) = @_;
+  my ($field, $chart_id, $operator, $joins, $bugs_table)
+    = @$args{qw(field chart_id operator joins bugs_table)};
+
+  my $value = $operator eq 'isnotactive' ? 0 : 1;
+
+  my @simple_fields = qw(bug_severity bug_status op_sys priority rep_platform
+    resolution version
+  );
+  my @user_fields = qw(attachments.submitter assigned_to assigned_to_realname
+    cc commenter reporter reporter_realname requestees.login_name
+    setters.login_name qa_contact qa_contact_realname
+  );
+
+  if ($field eq 'product' or $field eq 'component') {
+    $args->{term} = "${field}s.isactive = $value";
+  }
+  elsif (grep { $_ eq $field } @simple_fields) {
+    push @$joins,
+      {
+      table => ($field eq 'version' ? 'versions' : $field),
+      as    => "${field}_$chart_id",
+      from  => $field,
+      to    => 'value',
+      };
+    $args->{term} = "${field}_$chart_id.isactive = $value";
+  }
+  elsif ($field eq 'flagtypes.name') {
+    $args->{term} = "flagtypes_$chart_id.is_active = $value";
+  }
+  elsif ($field eq 'bug_group') {
+    $args->{term} = "groups.isactive = $value";
+  }
+  elsif ($field eq 'keywords') {
+    $args->{term} = "keyworddefs.is_active = $value";
+  }
+  elsif ($field eq 'target_milestone') {
+    push @$joins,
+      {
+      table => 'milestones',
+      as    => "milestones_$chart_id",
+      from  => 'target_milestone',
+      to    => 'value',
+      };
+    $args->{term} = qq{
             (milestones_$chart_id.product_id = bugs.product_id AND
              milestones_$chart_id.isactive = $value)
         };
-    }
-    elsif (grep {$_ eq $field} @user_fields) {
-        my $as = "name_${field}_$chart_id";
-        # For fields with periods in their name.
-        $as =~ s/\./_/;
-        $args->{full_field} = "$as.disabled_text";
-        my $op = $value ? '=' : '!=';
-        $args->{term} = "$as.disabledtext $op ''";
-    }
-
-    elsif (my $field_obj = $self->_select_fields->{$field}) {
-        my $field_type = $field_obj->type;
-        $args->{term} = $field_type == FIELD_TYPE_MULTI_SELECT
-            ? 'value'
-            : "bugs.$field";
-        $args->{term} .= " IN (SELECT value FROM $field WHERE $field.isactive = $value)";
-    }
-    else {
-        # This field does not have an active value
-        $self->_invalid_combination($args);
-    }
+  }
+  elsif (grep { $_ eq $field } @user_fields) {
+    my $as = "name_${field}_$chart_id";
+
+    # For fields with periods in their name.
+    $as =~ s/\./_/;
+    $args->{full_field} = "$as.disabled_text";
+    my $op = $value ? '=' : '!=';
+    $args->{term} = "$as.disabledtext $op ''";
+  }
+
+  elsif (my $field_obj = $self->_select_fields->{$field}) {
+    my $field_type = $field_obj->type;
+    $args->{term}
+      = $field_type == FIELD_TYPE_MULTI_SELECT ? 'value' : "bugs.$field";
+    $args->{term}
+      .= " IN (SELECT value FROM $field WHERE $field.isactive = $value)";
+  }
+  else {
+    # This field does not have an active value
+    $self->_invalid_combination($args);
+  }
 }
 
 ######################
@@ -3536,46 +3564,47 @@ sub _isactive {
 ######################
 
 # 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;
index 87fb91692e1aa485eb5f7aea4dec7bef526185ab..1f61cd68006904405d545cc6fa6e0fe54968943b 100644 (file)
@@ -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;
index bae0bba55c6d6000ff66e0a29dffcee2e8666001..83d6381da99af0d973ac6e26479df26a251f9d0a 100644 (file)
@@ -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;
index a963b1b8924e30b96940f646dd9f417bff58277c..d91191c54ac1bc619ff22fa7c3d8062115f23084 100644 (file)
@@ -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;
index 2497480620af72b3d2a0040db91fce78a9a4b1fa..a69139d138d496c987ed9eb0b70bcb3f02454e59 100644 (file)
@@ -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->multi_param() or 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->multi_param() or 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&amp;"
-                             . $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&amp;" . $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+))?$/ai) {
+    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+))?$/ai) {
-        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;
index 18d08b2d04fce9a80a59c0d04b22277c11fcd5ce..67859f563885518c6dcc3e6502564d8e76e6d118 100644 (file)
@@ -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]) : '' }
index 27a2e38cad15a286154e6604ce65d59074eace38..4dc0ca873c558d4e03c3fe5508d202338ed83fe0 100644 (file)
@@ -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,181 @@ 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_format')
-        || !IsValidQueryType(scalar $cgi->param('query_format')))
-    {
-        $cgi->param('query_format', '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_format')
+    || !IsValidQueryType(scalar $cgi->param('query_format')))
+  {
+    $cgi->param('query_format', '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;
 
index 585ef9cd568362f92158c9d7f11ff6956406fa62..cdab336b7c52ea3e58df4e33693d9f209281f74e 100644 (file)
@@ -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;
index 2e6b3cfc3c661c48411d6dd7844b7bc140d67da7..6ca2aa59ebc0696d7c94c2c32c9c78afd900a4e3 100644 (file)
@@ -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;
index fbc731ceae04fc92aee25ca774c1345494c83932..8b7e61c9f565de4246c5370e81b6c445ef7cbcda 100644 (file)
@@ -11,17 +11,17 @@ use 5.14.0;
 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;
index 10f8b4eeded9577a914c6f7d755a70f4a5a823c6..28d4ad5272eae0f375cc530b033ae6a223ca1b24 100644 (file)
@@ -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 i_am_persistent);
+use Bugzilla::Install::Util qw(install_string template_include_path
+  include_languages i_am_persistent);
 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,195 +145,211 @@ sub get_format {
 # If you want to modify this routine, read the comments carefully
 
 sub quoteUrls {
-    my ($text, $bug, $comment, $user, $for_markdown) = @_;
-    return $text unless $text;
-    $user ||= Bugzilla->user;
-    $for_markdown ||= 0;
-
-    # 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.
-
-    # If the comment is already wrapped, we should ignore newlines when
-    # looking for matching regexps. Else we should take them into account.
-    # And confirm comment is an object, it could be a hash from WebService::Bug::render_comment
-    my $s = ($comment && ref($comment) eq 'Bugzilla::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, $for_markdown) = @_;
+  return $text unless $text;
+  $user ||= Bugzilla->user;
+  $for_markdown ||= 0;
+
+  # 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.
+
+# If the comment is already wrapped, we should ignore newlines when
+# looking for matching regexps. Else we should take them into account.
+# And confirm comment is an object, it could be a hash from WebService::Bug::render_comment
+  my $s
+    = (  $comment
+      && ref($comment) eq 'Bugzilla::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++] = "<a href=\"$tmp\">$tmp</a>") &&
                ("\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);
 
-    unless ($for_markdown) {
-        # Color quoted text
-        $text =~ s~^(&gt;.+)$~<span class="quote">$1</span >~mg;
-        $text =~ s~</span >\n<span class="quote">~\n~g;
-    }
-    # mailto:
-    # Use |<nothing> so that $1 is defined regardless
-    # &#64; is the encoded '@' character.
-    $text =~ s~\b(mailto:|)?([\w\.\-\+\=]+&\#64;[\w\-]+(?:\.[\w\-]+)+)\b
+  unless ($for_markdown) {
+
+    # Color quoted text
+    $text =~ s~^(&gt;.+)$~<span class="quote">$1</span >~mg;
+    $text =~ s~</span >\n<span class="quote">~\n~g;
+  }
+
+  # mailto:
+  # Use |<nothing> so that $1 is defined regardless
+  # &#64; is the encoded '@' character.
+  $text =~ s~\b(mailto:|)?([\w\.\-\+\=]+&\#64;[\w\-]+(?:\.[\w\-]+)+)\b
               ~<a href=\"mailto:$2\">$1$2</a>~igx;
 
-    # attachment links
-    $text =~ s~\b(attachment$s*\#?$s*(\d+)(?:$s+\[details\])?)
+  # attachment links
+  $text =~ s~\b(attachment$s*\#?$s*(\d+)(?:$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*(\d+)/ai;
-    my $comment_word = template_var('terms')->{comment};
-    my $comment_re = qr/(?:\Q$comment_word\E|comment)$s*\#?$s*(\d+)/ai;
-    $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*(\d+)/ai;
+  my $comment_word = template_var('terms')->{comment};
+  my $comment_re   = qr/(?:\Q$comment_word\E|comment)$s*\#?$s*(\d+)/ai;
+  $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 }) :
                               "<a href=\"$current_bugurl#c$4\">$1</a>")
               ~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*
                      \d+(?:$s*,$s*\#?$s*\d+)+/ix;
 
-    $text =~ s{($bugs_re)}{
+  $text =~ s{($bugs_re)}{
         my $match = $1;
         $match =~ s/((?:#$s*)?(\d+))/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*
                          \d+(?:$s*,$s*\#?$s*\d+)+/ix;
 
-    $text =~ s{($comments_re)}{
+  $text =~ s{($comments_re)}{
         my $match = $1;
         $match =~ s|((?:#$s*)?(\d+))|<a href="$current_bugurl#c$2">$1</a>|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\ )
                (\d+)
                (?=\ \*\*\*\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));
+  my ($attachid, $link_text, $user) = @_;
+  $user ||= Bugzilla->user;
 
-        $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 = '&amp;action=diff';
-        }
+  my $attachment = new Bugzilla::Attachment({id => $attachid, cache => 1});
 
-        # Whitespace matters here because these links are in <pre> tags.
-        return qq|<span class="$className">|
-               . qq|<a href="${linkval}${patchlink}" name="attach_${attachid}" title="$title">$link_text</a>|
-               . qq| <a href="${linkval}&amp;action=edit" title="$title">[details]</a>|
-               . qq|</span>|;
+  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 = '&amp;action=diff';
+    }
+
+    # Whitespace matters here because these links are in <pre> tags.
+    return
+        qq|<span class="$className">|
+      . qq|<a href="${linkval}${patchlink}" name="attach_${attachid}" title="$title">$link_text</a>|
+      . qq| <a href="${linkval}&amp;action=edit" title="$title">[details]</a>|
+      . qq|</span>|;
+  }
+  else {
+    return qq{$link_text};
+  }
 }
 
 # Creates a link to a bug, including its title.
@@ -346,55 +360,61 @@ 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);
-    $linkified =~ s/\n//g;        # strip newlines to prevent markdown conflicts
-    $linkified =~ s/\|/&#124;/g;  # escape '|', it confuses markdown tables
-    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);
+  $linkified =~ s/\n//g;          # strip newlines to prevent markdown conflicts
+  $linkified =~ s/\|/&#124;/g;    # escape '|', it confuses markdown tables
+  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;
 }
 
 #####################
@@ -406,17 +426,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:
@@ -429,185 +450,188 @@ 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;
 
-    # build concatenated
-    if (CONCATENATE_ASSETS) {
-        $by_type{concatenated_standard_skin} = _concatenate_css($by_type{standard},
-                                                        $by_type{skin});
-        $by_type{concatenated_custom} = _concatenate_css($by_type{custom});
+  my %by_type = (standard => [], skin => [], custom => []);
+  foreach my $set (@css_sets) {
+    foreach my $key (keys %$set) {
+      push(@{$by_type{$key}}, $set->{$key});
     }
+  }
+
+  # build concatenated
+  if (CONCATENATE_ASSETS) {
+    $by_type{concatenated_standard_skin}
+      = _concatenate_css($by_type{standard}, $by_type{skin});
+    $by_type{concatenated_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 concatenated 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 concatenated 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;
 }
 
 ###############################################################################
@@ -626,73 +650,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.
@@ -702,15 +728,16 @@ $Template::Stash::SCALAR_OPS->{ truncate } =
 our $is_processing = 0;
 
 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});
-    local $is_processing = 1;
-    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});
+  local $is_processing = 1;
+  my $retval = $self->SUPER::process(@_);
+  shift @$current_langs;
+  return $retval;
 }
 
 # Construct the Template object
@@ -719,616 +746,645 @@ 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 => i_am_persistent() ? 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 => 'UTF-8',
-
-        # 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 => {
-
-            # 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) = @_;
-                $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 &#013;
-            # See bugs 4928, 22983 and 32000 for more details
-            html_linebreak => sub {
-                my ($var) = @_;
-                $var = html_quote($var);
-                $var =~ s/\r\n/\&#013;/g;
-                $var =~ s/\n\r/\&#013;/g;
-                $var =~ s/\r/\&#013;/g;
-                $var =~ s/\n/\&#013;/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
-                         ],
-
-            markdown => [ sub {
-                              my ($context, $bug, $comment, $user) = @_;
-                              return sub {
-                                  my $text = shift;
-                                  return unless $text;
-
-                                  if (Bugzilla->feature('markdown')
-                                      && ((ref($comment) eq 'HASH' && $comment->{is_markdown})
-                                         || (ref($comment) eq 'Bugzilla::Comment' && $comment->is_markdown)))
-                                  {
-                                      return Bugzilla->markdown->markdown($text, $bug, $comment);
-                                  }
-                                  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 $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 => i_am_persistent() ? 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 => 'UTF-8',
+
+    # 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 => {
+
+      # 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) = @_;
+        $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 &#013;
+      # See bugs 4928, 22983 and 32000 for more details
+      html_linebreak => sub {
+        my ($var) = @_;
+        $var = html_quote($var);
+        $var =~ s/\r\n/\&#013;/g;
+        $var =~ s/\n\r/\&#013;/g;
+        $var =~ s/\r/\&#013;/g;
+        $var =~ s/\n/\&#013;/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
+      ],
+
+      markdown => [
+        sub {
+          my ($context, $bug, $comment, $user) = @_;
+          return sub {
+            my $text = shift;
+            return unless $text;
+
+            if (
+              Bugzilla->feature('markdown')
+              && ( (ref($comment) eq 'HASH' && $comment->{is_markdown})
+                || (ref($comment) eq 'Bugzilla::Comment' && $comment->is_markdown))
+              )
             {
-                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/\&#64;/@/g;
-                $var =~ s/\&lt;/</g;
-                $var =~ s/\&gt;/>/g;
-                $var =~ s/\&quot;/\"/g;
-                $var =~ s/\&amp;/\&/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/\&nbsp;/ /g;
-
-                return $var;
-            },
-
-            # Wrap a displayed comment to the appropriate length
-            wrap_comment => [
-                sub {
-                    my ($context, $cols) = @_;
-                    return sub { wrap_comment($_[0], $cols) }
-                }, 1],
-
-            # Wrap cited text
-            wrap_cite => [
-                sub {
-                    my ($context, $cols) = @_;
-                    return sub { wrap_cite($_[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]; } ,
+              return Bugzilla->markdown->markdown($text, $bug, $comment);
+            }
+            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;
 
-        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; },
-
-            # TT directives are evaluated in list context, conflicting
-            # with CGI checks about using $cgi->param() in list context.
-            'cgi_param' => sub { return scalar Bugzilla->cgi->param($_[0]) },
-
-            # 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()];
-            },
-
-            # All the active keywords
-            'active_keywords' => sub {
-                return [map { $_->name } grep { $_->is_active } 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,
-
-            'feature_description' => \&Bugzilla::Install::Util::feature_description,
-
-            'report_columns' => \&Bugzilla::Search::REPORT_COLUMNS,
-
-            # These don't work as normal constants.
-            DB_MODULE        => \&Bugzilla::Constants::DB_MODULE,
-            REQUIRED_MODULES => sub {
-                my %required_modules;
-                my $cache = Bugzilla->request_cache;
-                my $meta = $cache->{cpan_meta} ||= Bugzilla::Install::Requirements::load_cpan_meta();
-                my $reqs = $meta->effective_prereqs->merged_requirements(['configure', 'runtime'], ['requires']);
-                foreach my $module (sort $reqs->required_modules) {
-                    next if $module eq 'perl';
-                    $required_modules{$module} = { version => $reqs->requirements_for_module($module) };
-                }
-                return \%required_modules;
-            },
-            OPTIONAL_MODULES => sub {
-                my %optional_modules;
-                my $cache = Bugzilla->request_cache;
-                my $meta = $cache->{cpan_meta} ||= Bugzilla::Install::Requirements::load_cpan_meta();
-                foreach my $feature ($meta->features) {
-                    my $reqs = $feature->prereqs->merged_requirements(['configure', 'runtime'], ['requires']);
-                    foreach my $module ($reqs->required_modules) {
-                        my $version = $reqs->requirements_for_module($module);
-                        $optional_modules{$module} ||= {};
-                        $optional_modules{$module}{version} = $version;
-                        $optional_modules{$module}{features} ||= [];
-                        push(@{$optional_modules{$module}{features}}, $feature->description);
-                    }
-                }
-                return \%optional_modules;
-            },
-            '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->{use_email_as_login}
-                     || ($params->{user_verify_class} =~ /LDAP/ && $params->{LDAPmailattribute})
-                     || ($params->{user_verify_class} =~ /RADIUS/ && $params->{RADIUS_email_suffix}))
-                  ? 1 : 0;
-            },
+            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/\&#64;/@/g;
+        $var =~ s/\&lt;/</g;
+        $var =~ s/\&gt;/>/g;
+        $var =~ s/\&quot;/\"/g;
+        $var =~ s/\&amp;/\&/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/\&nbsp;/ /g;
+
+        return $var;
+      },
+
+      # 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} ];
+        1
+      ],
+
+      # Wrap cited text
+      wrap_cite => [
+        sub {
+          my ($context, $cols) = @_;
+          return sub { wrap_cite($_[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]; },
+    },
 
-    local $Template::Config::CONTEXT = 'Bugzilla::Template::Context';
+    PLUGIN_BASE => 'Bugzilla::Template::Plugin',
 
-    Bugzilla::Hook::process('template_before_create', { config => $config });
-    my $template = $class->new($config) 
-        || die("Template creation failed: " . $class->error());
+    CONSTANTS => _load_constants(),
 
-    # Pass on our current language to any template hooks or inner templates
-    # called by this Template object.
-    $template->context->{bz_language} = $opts{language} || '';
+    # Default variables for all templates
+    VARIABLES => {
 
-    return $template;
+      # 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; },
+
+      # TT directives are evaluated in list context, conflicting
+      # with CGI checks about using $cgi->param() in list context.
+      'cgi_param' => sub { return scalar Bugzilla->cgi->param($_[0]) },
+
+      # 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()];
+      },
+
+      # All the active keywords
+      'active_keywords' => sub {
+        return [map { $_->name } grep { $_->is_active } 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,
+
+      'feature_description' => \&Bugzilla::Install::Util::feature_description,
+
+      'report_columns' => \&Bugzilla::Search::REPORT_COLUMNS,
+
+      # These don't work as normal constants.
+      DB_MODULE        => \&Bugzilla::Constants::DB_MODULE,
+      REQUIRED_MODULES => sub {
+        my %required_modules;
+        my $cache = Bugzilla->request_cache;
+        my $meta  = $cache->{cpan_meta}
+          ||= Bugzilla::Install::Requirements::load_cpan_meta();
+        my $reqs
+          = $meta->effective_prereqs->merged_requirements(['configure', 'runtime'],
+          ['requires']);
+        foreach my $module (sort $reqs->required_modules) {
+          next if $module eq 'perl';
+          $required_modules{$module}
+            = {version => $reqs->requirements_for_module($module)};
+        }
+        return \%required_modules;
+      },
+      OPTIONAL_MODULES => sub {
+        my %optional_modules;
+        my $cache = Bugzilla->request_cache;
+        my $meta  = $cache->{cpan_meta}
+          ||= Bugzilla::Install::Requirements::load_cpan_meta();
+        foreach my $feature ($meta->features) {
+          my $reqs = $feature->prereqs->merged_requirements(['configure', 'runtime'],
+            ['requires']);
+          foreach my $module ($reqs->required_modules) {
+            my $version = $reqs->requirements_for_module($module);
+            $optional_modules{$module} ||= {};
+            $optional_modules{$module}{version} = $version;
+            $optional_modules{$module}{features} ||= [];
+            push(@{$optional_modules{$module}{features}}, $feature->description);
+          }
+        }
+        return \%optional_modules;
+      },
+      '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->{use_email_as_login}
+            || ($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());
+
+  # 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'};
+  # 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);
+
+    # 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;
index 3893504b3a6cd8386c818792980a05ce3c79e8f1..b9d84ab859b66c311248346f9f38267b00b20e63 100644 (file)
@@ -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;
 
index 4481e33d207857cb47cb57408d4a2df53ce9a1fa..6e6100f6a64b40774fd4793ad6e2f79d3f1542de 100644 (file)
@@ -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;
index faba06b727c337cbfd4336a74d75f92e86c1bb43..18da2983fb8dd717a3307e9b6f2a1d498a71d636 100644 (file)
@@ -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;
index 4f522bda9529d5fa64989086c0acf9c5140e7282..2d69cea4fb917d48a56f935a30a66c1610c64783 100644 (file)
@@ -25,9 +25,9 @@ use Digest::SHA qw(hmac_sha256_base64);
 use parent qw(Exporter);
 
 @Bugzilla::Token::EXPORT = qw(issue_api_token issue_session_token
-                              issue_auth_delegation_token check_auth_delegation_token
-                              check_token_data delete_token
-                              issue_hash_token check_hash_token);
+  issue_auth_delegation_token check_auth_delegation_token
+  check_token_data delete_token
+  issue_hash_token check_hash_token);
 
 # 128 bits password:
 # 128 * log10(2) / log10(62) = 21.49, round up to 22.
@@ -42,436 +42,466 @@ 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;
-    # Allow certain UI components to work if impersonating another user.
-    my $user = Bugzilla->sudoer || 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;
+
+  # Allow certain UI components to work if impersonating another user.
+  my $user = Bugzilla->sudoer || 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', '');
 }
 
 sub issue_auth_delegation_token {
-    my ($uri) = @_;
-    my $dbh  = Bugzilla->dbh;
-    my $user = Bugzilla->user;
-    my $checksum = hmac_sha256_base64($user->id, $uri, Bugzilla->localconfig->{'site_wide_secret'});
+  my ($uri)    = @_;
+  my $dbh      = Bugzilla->dbh;
+  my $user     = Bugzilla->user;
+  my $checksum = hmac_sha256_base64($user->id, $uri,
+    Bugzilla->localconfig->{'site_wide_secret'});
 
-    return _create_token($user->id, 'auth_delegation', $checksum);
+  return _create_token($user->id, 'auth_delegation', $checksum);
 }
 
 sub check_auth_delegation_token {
-    my ($token, $uri) = @_;
-    my $dbh  = Bugzilla->dbh;
-    my $user = Bugzilla->user;
+  my ($token, $uri) = @_;
+  my $dbh  = Bugzilla->dbh;
+  my $user = Bugzilla->user;
 
-    my ($eventdata) = $dbh->selectrow_array("
+  my ($eventdata) = $dbh->selectrow_array("
         SELECT eventdata FROM tokens
          WHERE token = ? AND tokentype = 'auth_delegation'
-               AND (" . $dbh->sql_date_math('issuedate', '+', (MAX_TOKEN_AGE * 24 - 12), 'HOUR') . ") > NOW()",
-        undef, $token);
-
-    if ($eventdata) {
-        my $checksum = hmac_sha256_base64($user->id, $uri, Bugzilla->localconfig->{'site_wide_secret'});
-        if ($eventdata eq $checksum) {
-            delete_token($token);
-            return 1;
-        }
+               AND ("
+      . $dbh->sql_date_math('issuedate', '+', (MAX_TOKEN_AGE * 24 - 12), 'HOUR')
+      . ") > NOW()", undef, $token);
+
+  if ($eventdata) {
+    my $checksum = hmac_sha256_base64($user->id, $uri,
+      Bugzilla->localconfig->{'site_wide_secret'});
+    if ($eventdata eq $checksum) {
+      delete_token($token);
+      return 1;
     }
+  }
 
-    return 0;
+  return 0;
 }
 
 # 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, $email) = @_;
-    my $dbh = Bugzilla->dbh;
-    my $template = Bugzilla->template;
-    my $vars = {};
-
-    # Is there already a pending request for this email? 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 $regexp = "^$email:";
-    my $pending_requests = $dbh->selectrow_array(
-        'SELECT COUNT(*)
+  my ($login, $email) = @_;
+  my $dbh      = Bugzilla->dbh;
+  my $template = Bugzilla->template;
+  my $vars     = {};
+
+# Is there already a pending request for this email? 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 $regexp           = "^$email:";
+  my $pending_requests = $dbh->selectrow_array(
+    'SELECT COUNT(*)
            FROM tokens
           WHERE tokentype = ?
                 AND ' . $dbh->sql_regexp('eventdata', $dbh->quote($regexp)) . '
                 AND issuedate > '
-                    . $dbh->sql_date_math('NOW()', '-', ACCOUNT_CHANGE_INTERVAL, 'MINUTE'),
-        undef, 'account');
+      . $dbh->sql_date_math('NOW()', '-', ACCOUNT_CHANGE_INTERVAL, 'MINUTE'),
+    undef, 'account'
+  );
 
-    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', "$email:$login");
+  my ($token, $token_ts) = _create_token(undef, 'account', "$email:$login");
 
-    $vars->{'login'} = $login;
-    $vars->{'email'} = $email;
-    $vars->{'expiration_ts'} = ctime($token_ts + MAX_TOKEN_AGE * 86400);
-    $vars->{'token'} = $token;
+  $vars->{'login'}         = $login;
+  $vars->{'email'}         = $email;
+  $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->email . ":$new_email");
-    my $newtoken = _create_token($user->id, 'emailnew', $user->email . ":$new_email");
+  my ($token, $token_ts)
+    = _create_token($user->id, 'emailold', $user->email . ":$new_email");
+  my $newtoken
+    = _create_token($user->id, 'emailnew', $user->email . ":$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;
-    $vars->{'expiration_ts'} = ctime($token_ts + MAX_TOKEN_AGE * 86400);
+  $vars->{'newemailaddress'} = $new_email;
+  $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;
+
+  my $message = "";
+  $template->process("account/password/forgotten-password.txt.tmpl",
+    $vars, \$message)
+    || ThrowTemplateError($template->error());
 
-    MessageToMTA($message);
+  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.
-    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.
+  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(TOKEN_LENGTH);
-        $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(TOKEN_LENGTH);
+    $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);
+                                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");
+  # 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);
+  # 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);
 
-    if ($userid) {
-        $vars->{'emailaddress'} = $user->email;
-        $vars->{'login'} = $user->login;
-    }
-    else {
-        # Be careful! Some logins may contain ":" in them.
-        my ($email, $login) = split(':', $eventdata, 2);
-        $vars->{'emailaddress'} = $email;
-        $vars->{'login'} = $login;
-    }
-
-    $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);
+  if ($userid) {
+    $vars->{'emailaddress'} = $user->email;
+    $vars->{'login'}        = $user->login;
+  }
+  else {
+    # Be careful! Some logins may contain ":" in them.
+    my ($email, $login) = split(':', $eventdata, 2);
+    $vars->{'emailaddress'} = $email;
+    $vars->{'login'}        = $login;
+  }
+
+  $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;
 }
 
 ################################################################################
@@ -481,34 +511,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;
index 8babf9864c635f8ebda19f4263962afd4fc94709..516de43e8b7aaa96f3a032a5c54a46563da17f9a 100644 (file)
@@ -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);
 
 }
 
index b932a23c8e4a0fd726a2c8a3013c427e15a9377f..ead64cc7d211c19b0de436eacc08db9ff5f244a9 100644 (file)
@@ -33,9 +33,9 @@ use URI::QueryParam;
 
 use parent qw(Bugzilla::Object Exporter);
 @Bugzilla::User::EXPORT = qw(is_available_email email_to_id
-    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,17 +46,17 @@ 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,
-    'login_name'     => '',
-    'email'          => '',
-    'realname'       => '',
-    'showmybugslink' => 0,
-    'disabledtext'   => '',
-    'disable_mail'   => 0,
-    'is_enabled'     => 1,
+  'userid'         => 0,
+  'login_name'     => '',
+  'email'          => '',
+  'realname'       => '',
+  'showmybugslink' => 0,
+  'disabledtext'   => '',
+  'disable_mail'   => 0,
+  'is_enabled'     => 1,
 };
 
 use constant DB_TABLE => 'profiles';
@@ -66,19 +66,20 @@ 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.email',
-        '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.email',
+    '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';
@@ -86,35 +87,33 @@ 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,
-    email         => \&check_email,
-    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,
+  email         => \&check_email,
+  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
-        email
-        realname
-        extern_id
-        is_enabled
-    );
-    push(@cols, 'cryptpassword') if exists $self->{cryptpassword};
-    return @cols;
-};
-
-use constant VALIDATOR_DEPENDENCIES => {
-    is_enabled => ['disabledtext'],
-    login_name => ['email'],
-};
+  my $self = shift;
+  my @cols = qw(
+    disable_mail
+    disabledtext
+    login_name
+    email
+    realname
+    extern_id
+    is_enabled
+  );
+  push(@cols, 'cryptpassword') if exists $self->{cryptpassword};
+  return @cols;
+}
+
+use constant VALIDATOR_DEPENDENCIES =>
+  {is_enabled => ['disabledtext'], login_name => ['email'],};
 
 use constant EXTRA_REQUIRED_FIELDS => qw(is_enabled);
 
@@ -123,130 +122,128 @@ 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->{email}) {
-        # 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->{email}) {
+
+    # 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->{email}
-                || exists $changes->{disabledtext}
-                || exists $changes->{cryptpassword}));
+    # And rederive regex groups
+    $self->derive_regexp_groups();
+  }
 
-    # XXX Can update profiles_activity here as soon as it understands
-    #     field names like login_name.
+  # Logout the user if necessary.
+  Bugzilla->logout_user($self)
+    if (
+    !$options->{keep_session}
+    && ( exists $changes->{login_name}
+      || exists $changes->{email}
+      || exists $changes->{disabledtext}
+      || exists $changes->{cryptpassword})
+    );
 
-    return $changes;
+  # XXX Can update profiles_activity here as soon as it understands
+  #     field names like login_name.
+
+  return $changes;
 }
 
 ################################################################################
@@ -258,92 +255,96 @@ 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;
 }
 
 sub check_login_name {
-    my ($invocant, $login, undef, $data) = @_;
-
-    # No control characters
-    $login = clean_text($login);
-    $login || ThrowUserError('user_login_required');
-    # No whitespace
-    $login !~ /\s/ || ThrowUserError('login_illegal_character');
-
-    # No @ sign unless login is email (VALIDATOR_DEPENDENCIES means
-    # this will be set already)
-    if ($login =~ /@/) {
-        my $email = ref($invocant) ? $invocant->email : $data->{email};
-        # We should really use fc() instead of lc(), but this requires Perl 5.16.
-        ThrowUserError('login_at_sign_disallowed') unless lc($login) eq lc($email);
-    }
-    # We set the max length to 127 to ensure logins aren't truncated when
-    # inserted into the tokens.eventdata field.
-    length($login) <= 127 or ThrowUserError('login_too_long');
+  my ($invocant, $login, undef, $data) = @_;
 
-    trick_taint($login);
+  # No control characters
+  $login = clean_text($login);
+  $login || ThrowUserError('user_login_required');
 
-    # Check the login name if it's a new user, or if we're changing the login name.
-    if (!ref($invocant) || lc($invocant->login) ne lc($login)) {
-        if (login_to_id($login)) {
-            ThrowUserError('account_exists', { login => $login });
-        }
+  # No whitespace
+  $login !~ /\s/ || ThrowUserError('login_illegal_character');
+
+  # No @ sign unless login is email (VALIDATOR_DEPENDENCIES means
+  # this will be set already)
+  if ($login =~ /@/) {
+    my $email = ref($invocant) ? $invocant->email : $data->{email};
+
+    # We should really use fc() instead of lc(), but this requires Perl 5.16.
+    ThrowUserError('login_at_sign_disallowed') unless lc($login) eq lc($email);
+  }
+
+  # We set the max length to 127 to ensure logins aren't truncated when
+  # inserted into the tokens.eventdata field.
+  length($login) <= 127 or ThrowUserError('login_too_long');
+
+  trick_taint($login);
+
+  # Check the login name if it's a new user, or if we're changing the login name.
+  if (!ref($invocant) || lc($invocant->login) ne lc($login)) {
+    if (login_to_id($login)) {
+      ThrowUserError('account_exists', {login => $login});
     }
+  }
 
-    return $login;
+  return $login;
 }
 
 sub check_email {
-    my ($invocant, $email) = @_;
-    $email = clean_text($email);
-    $email || ThrowUserError('email_required');
+  my ($invocant, $email) = @_;
+  $email = clean_text($email);
+  $email || ThrowUserError('email_required');
 
-    check_email_syntax($email);
+  check_email_syntax($email);
 
-    # Check the email if it's a new user, or if we're changing the email.
-    my $old_email = ref($invocant) ? $invocant->email : undef;
-    if (!defined($old_email) || lc($old_email) ne lc($email)) {
-        is_available_email($email, $old_email)
-            || ThrowUserError('account_exists', { email => $email });
-    }
+  # Check the email if it's a new user, or if we're changing the email.
+  my $old_email = ref($invocant) ? $invocant->email : undef;
+  if (!defined($old_email) || lc($old_email) ne lc($email)) {
+    is_available_email($email, $old_email)
+      || ThrowUserError('account_exists', {email => $email});
+  }
 
-    return $email;
+  return $email;
 }
 
 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;
 }
 
 ################################################################################
@@ -352,156 +353,157 @@ 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_email {
-    my ($self, $email) = @_;
-    $self->set('email', $email);
-    $self->set_login($email) if Bugzilla->params->{'use_email_as_login'};
+  my ($self, $email) = @_;
+  $self->set('email', $email);
+  $self->set_login($email) if Bugzilla->params->{'use_email_as_login'};
 }
 
 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});
+  }
 }
 
 ################################################################################
@@ -509,37 +511,41 @@ 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]->{email}; }
-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]->{email}; }
+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
@@ -548,144 +554,152 @@ sub authorizer {
 # See also get_userlist(), which constructs pseudo-Bugzilla::Users, including
 # the 'identity' value.
 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}) {
-        # This has the correct result even if the login does not contain an @.
-        $self->{nick} = (split(/@/, $self->login, 2))[0];
-    }
+  if (!defined $self->{nick}) {
 
-    return $self->{nick};
+    # This has the correct result even if the login does not contain an @.
+    $self->{nick} = (split(/@/, $self->login, 2))[0];
+  }
+
+  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;
 }
 
 sub use_markdown {
-    my ($self, $comment) = @_;
-    return Bugzilla->feature('markdown')
-           && $self->settings->{use_markdown}->{is_enabled}
-           && $self->settings->{use_markdown}->{value} eq 'on'
-           && (!defined $comment || $comment->is_markdown);
+  my ($self, $comment) = @_;
+  return
+       Bugzilla->feature('markdown')
+    && $self->settings->{use_markdown}->{is_enabled}
+    && $self->settings->{use_markdown}->{value} eq 'on'
+    && (!defined $comment || $comment->is_markdown);
 }
 
 ##########################
@@ -693,309 +707,316 @@ sub use_markdown {
 ##########################
 
 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;
+    }
+    else {
+      $self->{timezone} = DateTime::TimeZone->new(name => $tz);
     }
-    return $self->{timezone};
+  }
+  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;
 
-        Bugzilla->memcached->set_config({
-            key  => $user_groups_key,
-            data => $groups,
-        });
+      # 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];
 
-    $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
@@ -1003,296 +1024,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) = @_;
 
-        Bugzilla->params->{'or_groups'}
-            ? $self->_visible_bugs_check_or(\@check_ids)
-            : $self->_visible_bugs_check_and(\@check_ids);
+  # 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'});
     }
 
-    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
@@ -1302,1063 +1338,1132 @@ 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;
 
-    die "can_enter_product reached an unreachable location.";
+  # 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});
+  }
+
+  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 = components.product_id)
                 AND products.id IN (SELECT DISTINCT versions.product_id
                                       FROM versions
-                                     WHERE versions.isactive = 1 AND products.id = versions.product_id)');
-    }
+                                     WHERE versions.isactive = 1 AND products.id = versions.product_id)'
+    );
+  }
 
-    $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;
+
+    $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};
+  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 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, and match
-        # login names only.
-        trick_taint($str);
+  # Generates a list of users whose login name 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 $user_id = $dbh->selectrow_array('SELECT userid FROM profiles
-                                             WHERE ' . $dbh->sql_istrcmp('login_name', '?'),
-                                             undef, $str);
+  my @users = ();
+  return \@users if $str =~ /^\s*$/;
 
-        push(@users, new Bugzilla::User($user_id)) if $user_id;
+  # 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, and match
+            # login names only.
+    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+)$/a) {
-                    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+)$/a) {
+          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',
         }
+      );
+    }
 
-        # 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] || '';
+    # 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));
+
+        $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;
+  }
+
+  # 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 $template = Bugzilla->template;
-    my $cgi = Bugzilla->cgi;
-    my $vars = {};
+  my $template = Bugzilla->template;
+  my $cgi      = Bugzilla->cgi;
+  my $vars     = {};
 
-    $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;
+  $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;
 
-    print $cgi->header();
+  print $cgi->header();
 
-    $template->process("global/confirm-user-match.html.tmpl", $vars)
-      || ThrowTemplateError($template->error());
-    exit;
+  $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,
-        $minor_update) = @_;
-
-    # 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,
+    $minor_update)
+    = @_;
 
-        # 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;
-    }
-    elsif (defined($$comments[0])) {
-        $events{+EVT_COMMENT} = 1;
+    else {
+      # Catch-all for any change not caught by a more specific event
+      $events{+EVT_OTHER} = 1;
     }
-    
-    # Dependent changed bugmails must have an event to ensure the bugmail is
-    # emailed.
-    if ($dep_mail) {
-        $events{+EVT_DEPEND_BLOCK} = 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;
     }
 
-    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);
+    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) {
 
-    if ($wants_mail && $minor_update) {
-        $wants_mail &= $self->wants_mail([EVT_MINOR_UPDATE], $relationship);
+    # Notify about new bugs.
+    $events{+EVT_BUG_CREATED} = 1;
+
+    # 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);
+  }
+
+  if ($wants_mail && $minor_update) {
+    $wants_mail &= $self->wants_mail([EVT_MINOR_UPDATE], $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) = @_;
 
-    # Skip DB query if relationship is explicit
-    return 1 if $relationship == REL_GLOBAL_WATCHER;
+  # 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'};
 
-    my $wants_mail = grep { $self->mail_settings->{$relationship}{$_} } @$events;
-    return $wants_mail ? 1 : 0;
+  # 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;
+  }
+
+  # 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;
 }
 
 ###########################
@@ -2366,56 +2471,57 @@ 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};
 }
 
 sub check_current_password {
-    my $self = shift;
-    my $password = shift || ThrowUserError("current_password_required");
+  my $self = shift;
+  my $password = shift || ThrowUserError("current_password_required");
 
-    my $cryptpwd
-        = $self->cryptpassword || ThrowCodeError("unable_to_retrieve_password");
+  my $cryptpwd
+    = $self->cryptpassword || ThrowCodeError("unable_to_retrieve_password");
 
-    if (bz_crypt($password, $cryptpwd) ne $cryptpwd) {
-        ThrowUserError("current_password_incorrect");
-    }
+  if (bz_crypt($password, $cryptpwd) ne $cryptpwd) {
+    ThrowUserError("current_password_incorrect");
+  }
 
 }
 
@@ -2424,161 +2530,179 @@ sub check_current_password {
 ###############
 
 sub is_available_email {
-    my ($email, $old_email) = @_;
+  my ($email, $old_email) = @_;
 
-    return 0 if email_to_id($email);
+  return 0 if email_to_id($email);
 
-    my $dbh = Bugzilla->dbh;
-    # $email is safe because it is only used in SELECT placeholders.
-    trick_taint($email);
-    # Reject if the new email 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
+  my $dbh = Bugzilla->dbh;
+
+  # $email is safe because it is only used in SELECT placeholders.
+  trick_taint($email);
+
+  # Reject if the new email 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, ($email, $email));
-
-    if ($eventdata) {
-        # Allow thru owner of token
-        if ($old_email
-            && (($tokentype eq 'emailnew' && $eventdata eq "$old_email:$email")
-                || ($tokentype eq 'emailold' && $eventdata eq "$email:$old_email")))
-        {
-            return 1;
-        }
-        return 0;
+                AND SUBSTRING(eventdata, ("
+      . $dbh->sql_position(q{':'}, 'eventdata')
+      . "+ 1), LENGTH(eventdata)) = ?)",
+    undef, ($email, $email)
+  );
+
+  if ($eventdata) {
+
+    # Allow thru owner of token
+    if (
+      $old_email
+      && ( ($tokentype eq 'emailnew' && $eventdata eq "$old_email:$email")
+        || ($tokentype eq 'emailold' && $eventdata eq "$email:$old_email"))
+      )
+    {
+      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, $email) = @_;
-    my $class = ref($self) || $self;
-    my $dbh = Bugzilla->dbh;
+  my ($self, $login, $email) = @_;
+  my $class = ref($self) || $self;
+  my $dbh = Bugzilla->dbh;
 
-    $dbh->bz_start_transaction;
+  $dbh->bz_start_transaction;
 
-    $email = $class->check_email($email);
-    $login = $class->check_login_name($login, undef, { email => $email });
-    my $creation_regexp = Bugzilla->params->{'createemailregexp'};
+  $email = $class->check_email($email);
+  $login = $class->check_login_name($login, undef, {email => $email});
+  my $creation_regexp = Bugzilla->params->{'createemailregexp'};
 
-    if ($email !~ /$creation_regexp/i) {
-        ThrowUserError('account_creation_restricted');
-    }
+  if ($email !~ /$creation_regexp/i) {
+    ThrowUserError('account_creation_restricted');
+  }
 
-    # Allow extensions to do extra checks.
-    Bugzilla::Hook::process('user_check_account_creation',
-                            { login => $login, email => $email });
+  # Allow extensions to do extra checks.
+  Bugzilla::Hook::process('user_check_account_creation',
+    {login => $login, email => $email});
 
-    # Create and send a token for this new account.
-    require Bugzilla::Token;
-    Bugzilla::Token::issue_new_user_account_token($login, $email);
+  # Create and send a token for this new account.
+  require Bugzilla::Token;
+  Bugzilla::Token::issue_new_user_account_token($login, $email);
 
-    $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.
-    my $user_id;
-    if (defined $cache->{$login}) {
-        $user_id = $cache->{$login};
-    }
-    else {
-        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.
+  my $user_id;
+  if (defined $cache->{$login}) {
+    $user_id = $cache->{$login};
+  }
+  else {
+    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 email_to_id {
-    my ($email, $throw_error) = @_;
-    my $dbh = Bugzilla->dbh;
-    trick_taint($email);
-    my $user_id = $dbh->selectrow_array("SELECT userid FROM profiles WHERE " .
-                                        $dbh->sql_istrcmp('email', '?'),
-                                        undef, $email);
-    if ($user_id) {
-        return $user_id;
-    }
-    elsif ($throw_error) {
-        ThrowUserError('invalid_email', { email => $email });
-    }
-    else {
-        return 0;
-    }
+  my ($email, $throw_error) = @_;
+  my $dbh = Bugzilla->dbh;
+  trick_taint($email);
+  my $user_id
+    = $dbh->selectrow_array(
+    "SELECT userid FROM profiles WHERE " . $dbh->sql_istrcmp('email', '?'),
+    undef, $email);
+  if ($user_id) {
+    return $user_id;
+  }
+  elsif ($throw_error) {
+    ThrowUserError('invalid_email', {email => $email});
+  }
+  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;
 }
 
 
index 41a091193e2392db9f495b5a382115e21633b31e..2ce8a256de1148e891439081e0a8cf3f8d8275e1 100644 (file)
@@ -20,52 +20,54 @@ use Bugzilla::Util qw(generate_random_password trim);
 # Overridden 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__
index d0bf1f15b9d25801dac346305d7a39d07d31d870..f55515bae6b9b3c901dc17d5c081fff4a312954c 100644 (file)
@@ -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__
index ba144aa309c13b749dcae64a51046ef156d52a09..d413910c29b98ca7c0c86ada05a6ee05bb8365e3 100644 (file)
@@ -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;
index 81ce6fd2c3aa162e5073ee34af03dd99525d8ec7..0eb97d389adf938831bd758cec5506c6b3308c3e 100644 (file)
@@ -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;
index 2810395e8499f1bb735db2bf880e3fffe7345970..508f448ed145993601212a28d24ebf56fdb58fd5 100644 (file)
@@ -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;
index 22e2639a30bf4af5b55a978c6a1a7761f09dc533..c677003deeb6b4b501027c8575555bdba6c10a2f 100644 (file)
@@ -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;
index fcd4aff918de539f0e75b00d9aaecba4768aa1cc..0e33ede4e521e3406575707c2c6f88fdf39e0803 100644 (file)
@@ -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 wrap_cite
-                             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 wrap_cite
+  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,658 +43,704 @@ 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] =~ /^(\d+)$/a;
-    $_[0] = $match ? int($1) : undef;
-    return (defined($_[0]));
+  my $match = $_[0] =~ /^(\d+)$/a;
+  $_[0] = $match ? int($1) : undef;
+  return (defined($_[0]));
 }
 
 sub detaint_signed {
-    my $match = $_[0] =~ /^([-+]?\d+)$/a;
-    # The "int()" call removes any leading plus sign.
-    $_[0] = $match ? int($1) : undef;
-    return (defined($_[0]));
+  my $match = $_[0] =~ /^([-+]?\d+)$/a;
+
+  # 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/&/&amp;/g;
-    $var =~ s/</&lt;/g;
-    $var =~ s/>/&gt;/g;
-    $var =~ s/"/&quot;/g;
-    # Obscure '@'.
-    $var =~ s/\@/\&#64;/g;
-
-    # Remove control characters.
-    $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/&/&amp;/g;
+  $var =~ s/</&lt;/g;
+  $var =~ s/>/&gt;/g;
+  $var =~ s/"/&quot;/g;
+
+  # Obscure '@'.
+  $var =~ s/\@/\&#64;/g;
+
+  # Remove control characters.
+  $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;
-    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;
+  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#<#&lt;#g;
-        $text =~ s#>#&gt;#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#<#&lt;#g;
+    $text =~ s#>#&gt;#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) = (@_);
-    # The below regex works only on bytes
-    utf8::encode($toencode) if utf8::is_utf8($toencode);
-    $toencode =~ s/([^a-zA-Z0-9_\-.])/uc sprintf("%%%02x",ord($1))/eg;
-    return $toencode;
+  my ($toencode) = (@_);
+
+  # The below regex works only on bytes
+  utf8::encode($toencode) if utf8::is_utf8($toencode);
+  $toencode =~ s/([^a-zA-Z0-9_\-.])/uc sprintf("%%%02x",ord($1))/eg;
+  return $toencode;
 }
 
 sub css_class_quote {
-    my ($toencode) = (@_);
-    $toencode =~ s#[ /]#_#g;
-    $toencode =~ s/([^a-zA-Z0-9_\-.])/uc sprintf("&#x%x;",ord($1))/eg;
-    return $toencode;
+  my ($toencode) = (@_);
+  $toencode =~ s#[ /]#_#g;
+  $toencode =~ s/([^a-zA-Z0-9_\-.])/uc sprintf("&#x%x;",ord($1))/eg;
+  return $toencode;
 }
 
 sub xml_quote {
-    my ($var) = (@_);
-    $var =~ s/\&/\&amp;/g;
-    $var =~ s/</\&lt;/g;
-    $var =~ s/>/\&gt;/g;
-    $var =~ s/\"/\&quot;/g;
-    $var =~ s/\'/\&apos;/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/\&/\&amp;/g;
+  $var =~ s/</\&lt;/g;
+  $var =~ s/>/\&gt;/g;
+  $var =~ s/\"/\&quot;/g;
+  $var =~ s/\'/\&apos;/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->{urlbase} =~ /^https/i;
-    return if !Bugzilla->params->{'ssl_redirect'};
-    return if !Bugzilla->params->{'sslbase'};
-
-    # If we're already running under SSL, never redirect.
-    if (Bugzilla->params->{'inbound_proxies'}
-        && uc($ENV{HTTP_X_FORWARDED_PROTO} || '') eq 'HTTPS') {
-        return;
-    }
-    return if uc($ENV{HTTPS} || '') eq 'ON';
-
-    # If called from Bugzilla::CGI->new itself, use the newly created
-    # CGI object, to avoid deep recursions.
-    my $cgi = shift || Bugzilla->cgi;
-    $cgi->redirect_to_https();
+  return if !i_am_cgi();
+  return if Bugzilla->params->{urlbase} =~ /^https/i;
+  return if !Bugzilla->params->{'ssl_redirect'};
+  return if !Bugzilla->params->{'sslbase'};
+
+  # If we're already running under SSL, never redirect.
+  if (Bugzilla->params->{'inbound_proxies'}
+    && uc($ENV{HTTP_X_FORWARDED_PROTO} || '') eq 'HTTPS')
+  {
+    return;
+  }
+  return if uc($ENV{HTTPS} || '') eq 'ON';
+
+  # If called from Bugzilla::CGI->new itself, use the newly created
+  # CGI object, to avoid deep recursions.
+  my $cgi = shift || Bugzilla->cgi;
+  $cgi->redirect_to_https();
 }
 
 sub correct_urlbase {
-    my $urlbase = Bugzilla->params->{'urlbase'};
-    my $sslbase = Bugzilla->params->{'sslbase'};
-
-    if (!$sslbase) {
-        return $urlbase;
-    }
-    elsif (Bugzilla->params->{'ssl_redirect'}) {
-        return $sslbase;
-    }
-    # Return what the user currently uses.
-    elsif (Bugzilla->params->{'inbound_proxies'}) {
-        return (uc($ENV{HTTP_X_FORWARDED_PROTO} || '') eq 'HTTPS') ? $sslbase : $urlbase;
-    }
-    else {
-        return (uc($ENV{HTTPS} || '') eq 'ON') ? $sslbase : $urlbase;
-    }
+  my $urlbase = Bugzilla->params->{'urlbase'};
+  my $sslbase = Bugzilla->params->{'sslbase'};
+
+  if (!$sslbase) {
+    return $urlbase;
+  }
+  elsif (Bugzilla->params->{'ssl_redirect'}) {
+    return $sslbase;
+  }
+
+  # Return what the user currently uses.
+  elsif (Bugzilla->params->{'inbound_proxies'}) {
+    return (uc($ENV{HTTP_X_FORWARDED_PROTO} || '') eq 'HTTPS')
+      ? $sslbase
+      : $urlbase;
+  }
+  else {
+    return (uc($ENV{HTTPS} || '') eq 'ON') ? $sslbase : $urlbase;
+  }
 }
 
 # Returns the real remote address of the client,
 sub remote_ip {
-    my $remote_ip       = $ENV{'REMOTE_ADDR'} || '127.0.0.1';
-    my @proxies         = split(/[\s,]+/, Bugzilla->params->{inbound_proxies});
-    my @x_forwarded_for = split(/[\s,]+/, $ENV{HTTP_X_FORWARDED_FOR} // '');
-
-    return $remote_ip unless @x_forwarded_for;
-    return $x_forwarded_for[0] if (@proxies && $proxies[0] eq '*');
-    return $remote_ip if none { $_ eq $remote_ip } @proxies;
-
-    foreach my $ip (reverse @x_forwarded_for) {
-        if (none { $_ eq $ip } @proxies) {
-            # Keep the original IP address if the remote IP is invalid.
-            return validate_ip($ip) || $remote_ip;
-        }
+  my $remote_ip = $ENV{'REMOTE_ADDR'} || '127.0.0.1';
+  my @proxies         = split(/[\s,]+/, Bugzilla->params->{inbound_proxies});
+  my @x_forwarded_for = split(/[\s,]+/, $ENV{HTTP_X_FORWARDED_FOR} // '');
+
+  return $remote_ip unless @x_forwarded_for;
+  return $x_forwarded_for[0] if (@proxies && $proxies[0] eq '*');
+  return $remote_ip if none { $_ eq $remote_ip } @proxies;
+
+  foreach my $ip (reverse @x_forwarded_for) {
+    if (none { $_ eq $ip } @proxies) {
+
+      # Keep the original IP address if the remote IP is invalid.
+      return validate_ip($ip) || $remote_ip;
     }
-    return $remote_ip;
+  }
+  return $remote_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 wrap_cite {
-    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;
-
-    foreach my $line (split(/\r\n|\r|\n/, $comment)) {
-      if ($line =~ /^(>+ *)/) {
-        $wrappedcomment .= wrap('', $1, $line) . "\n";
-      } else {
-        $wrappedcomment .= $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;
+
+  foreach my $line (split(/\r\n|\r|\n/, $comment)) {
+    if ($line =~ /^(>+ *)/) {
+      $wrappedcomment .= wrap('', $1, $line) . "\n";
     }
-    chomp($wrappedcomment); # remove extra newline at the end
-    return $wrappedcomment;
+    else {
+      $wrappedcomment .= $line . "\n";
+    }
+  }
+  chomp($wrappedcomment);    # remove 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.
 
-    # All other fields get a space unless the first character of the second
-    # string is a comma or space
+  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;
+      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);
+      # Default date format. See DateTime for other formats available.
+      $format = "%Y-%m-%d %R %Z";
     }
+  }
 
-    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);
+  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.
-    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.
+  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
@@ -704,188 +750,197 @@ 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 ($email) = @_;
-    my $match = Bugzilla->params->{'emailregexp'};
-    # 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 ($email =~ /$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 ($email) = @_;
+  my $match = Bugzilla->params->{'emailregexp'};
+
+  # 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 ( $email =~ /$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 ($email) = @_;
+  my ($email) = @_;
 
-    unless (validate_email_syntax(@_)) {
-        ThrowUserError('illegal_email_address', { email => $email });
-    }
+  unless (validate_email_syntax(@_)) {
+    ThrowUserError('illegal_email_address', {email => $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/a;
-        $date2 =~ s/(\d+)-0*(\d+?)-0*(\d+?)/$1-$2-$3/a;
-    }
-    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/a;
+    $date2 =~ s/(\d+)-0*(\d+?)-0*(\d+?)/$1-$2-$3/a;
+  }
+  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 {
-    # Turn off UTF8 encoding.
-    binmode STDOUT, ':bytes';
+
+  # Turn off UTF8 encoding.
+  binmode STDOUT, ':bytes';
 }
 
 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;
index d135c4a7aff52e4263e327c4b2726ea954cbfb44..143475c49412586e3f07458d0a5e28b40ca00f7c 100644 (file)
@@ -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,48 @@ 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'} ||= Bugzilla::Product->new({ id => $self->product_id, cache => 1 });
-    return $self->{'product'};
+  require Bugzilla::Product;
+  $self->{'product'}
+    ||= Bugzilla::Product->new({id => $self->product_id, cache => 1});
+  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 +209,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;
index 743d54c879ec72b9d98359dd4a66100962082c7d..16bc86e61504c4654c9af258b7e54a360dba6c45 100644 (file)
@@ -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;
index 82676b0b62e61d15b0564c58b0db52840f7ce58e..f128d3949b8285077eab4985a30e9147e345629b 100644 (file)
@@ -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,1170 +99,1197 @@ 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) {
-            next unless $value->is_active;
-            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) {
+      next unless $value->is_active;
+      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;
-
-    my $tmpl
-      = $params->{markdown}
-      ? '[% text FILTER markdown(bug, { is_markdown => 1 }) %]'
-      : '[% text FILTER wrap_cite FILTER markdown(bug) %]';
-    my $html;
-    my $template = Bugzilla->template;
-    $template->process(
-        \$tmpl,
-        { bug => $bug, text => $params->{text}},
-        \$html
-    );
+  Bugzilla->switch_to_shadow_db();
+  my $bug = $params->{id} ? Bugzilla::Bug->check($params->{id}) : undef;
 
-    return { html => $html };
+  my $tmpl
+    = $params->{markdown}
+    ? '[% text FILTER markdown(bug, { is_markdown => 1 }) %]'
+    : '[% text FILTER wrap_cite FILTER markdown(bug) %]';
+  my $html;
+  my $template = Bugzilla->template;
+  $template->process(\$tmpl, {bug => $bug, text => $params->{text}}, \$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('login', $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),
-        is_markdown   => $self->type('boolean', $comment->is_markdown),
-        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('login',    $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),
+    is_markdown   => $self->type('boolean',  $comment->is_markdown),
+    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;
+  my ($self, $params) = @_;
+  my $user = Bugzilla->user;
+  my $dbh  = Bugzilla->dbh;
 
-    Bugzilla->switch_to_shadow_db();
+  Bugzilla->switch_to_shadow_db();
 
-    my $match_params = dclone($params);
-    delete $match_params->{include_fields};
-    delete $match_params->{exclude_fields};
+  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};
-    }
+  # 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;
+  }
 
-    $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+)$/a, 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'}) {
-            $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+)$/a, 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';
     }
-
-    # Allow the use of order shortcuts similar to web UI
-    if ($match_params->{order}) {
-        # Convert the value of the "order" form field into a list of columns
-        # by which to sort the results.
-        my %order_types = (
-            "Bug Number"   => [ "bug_id" ],
-            "Importance"   => [ "priority", "bug_severity" ],
-            "Assignee"     => [ "assigned_to", "bug_status", "priority", "bug_id" ],
-            "Last Changed" => [ "changeddate", "bug_status", "priority",
-                                "assigned_to", "bug_id" ],
-        );
-        if ($order_types{$match_params->{order}}) {
-            $options{order} = $order_types{$match_params->{order}};
-        }
-        else {
-            $options{order} = [ split(/\s*,\s*/, $match_params->{order}) ];
-        }
+  }
+  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');
+  }
+
+  # Allow the use of order shortcuts similar to web UI
+  if ($match_params->{order}) {
+
+    # Convert the value of the "order" form field into a list of columns
+    # by which to sort the results.
+    my %order_types = (
+      "Bug Number" => ["bug_id"],
+      "Importance" => ["priority", "bug_severity"],
+      "Assignee"   => ["assigned_to", "bug_status", "priority", "bug_id"],
+      "Last Changed" =>
+        ["changeddate", "bug_status", "priority", "assigned_to", "bug_id"],
+    );
+    if ($order_types{$match_params->{order}}) {
+      $options{order} = $order_types{$match_params->{order}};
+    }
+    else {
+      $options{order} = [split(/\s*,\s*/, $match_params->{order})];
     }
+  }
 
-    $options{params} = $match_params;
+  $options{params} = $match_params;
 
-    my $search = new Bugzilla::Search(%options);
-    my ($data) = $search->data;
+  my $search = new Bugzilla::Search(%options);
+  my ($data) = $search->data;
 
-    if (!scalar @$data) {
-        return { bugs => [] };
-    }
+  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;
+# 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 };
+  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 $minor_update = delete $params->{minor_update} ? 1 : 0;
-    my %values = %$params;
-    $values{other_bugs} = \@bugs;
+  my $minor_update = delete $params->{minor_update} ? 1 : 0;
+  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->flag_types, $bug->flags);
-            $bug->set_flags($old_flags, $new_flags);
-        }
+    elsif (ref $values{alias} eq 'ARRAY') {
+      $values{alias} = {set => $values{alias}};
     }
+  }
 
-    my %all_changes;
-    my %minor_updates;
-    $dbh->bz_start_transaction();
-    foreach my $bug (@bugs) {
-        $minor_updates{$bug->id} = $bug->has_unsent_changes ? 0 : $minor_update;
-        $all_changes{$bug->id} = $bug->update();
+  my $flags = delete $values{flags};
+
+  foreach my $bug (@bugs) {
+    $bug->set_all(\%values);
+    if ($flags) {
+      my ($old_flags, $new_flags)
+        = extract_flags($flags, $bug->flag_types, $bug->flags);
+      $bug->set_flags($old_flags, $new_flags);
     }
-    $dbh->bz_commit_transaction();
+  }
+
+  my %all_changes;
+  my %minor_updates;
+  $dbh->bz_start_transaction();
+  foreach my $bug (@bugs) {
+    $minor_updates{$bug->id} = $bug->has_unsent_changes ? 0 : $minor_update;
+    $all_changes{$bug->id} = $bug->update();
+  }
+  $dbh->bz_commit_transaction();
+
+  foreach my $bug (@bugs) {
+    $bug->send_changes($all_changes{$bug->id}, undef, $minor_updates{$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          => {},
+    );
 
-    foreach my $bug (@bugs) {
-        $bug->send_changes($all_changes{$bug->id}, undef, $minor_updates{$bug->id});
+    # 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])
+      };
     }
 
-    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';
+    push(@result, \%hash);
+  }
 
-    my @result;
-    foreach my $bug (@bugs) {
-        my %hash = (
-            id               => $self->type('int', $bug->id),
-            last_change_time => $self->type('dateTime', $bug->delta_ts),
-            changes          => {},
-        );
+  return {bugs => \@result};
+}
 
-        # 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]) 
-            };
-        }
+sub create {
+  my ($self, $params) = @_;
+  my $dbh = Bugzilla->dbh;
 
-        push(@result, \%hash);
-    }
+  Bugzilla->login(LOGIN_REQUIRED);
 
-    return { bugs => \@result };
-}
+  $params = Bugzilla::Bug::map_fields($params);
 
-sub create {
-    my ($self, $params) = @_;
-    my $dbh = Bugzilla->dbh;
+  my $flags = delete $params->{flags};
 
-    Bugzilla->login(LOGIN_REQUIRED);
+  # Set bug flags
+  if ($flags) {
+    my $product = Bugzilla::Product->check($params->{product});
+    my $component
+      = Bugzilla::Component->check({
+      product => $product, name => $params->{component}
+      });
+    my $flag_types
+      = Bugzilla::FlagType::match({
+      product_id => $product->id, component_id => $component->id, is_active => 1,
+      });
 
-    $params = Bugzilla::Bug::map_fields($params);
+    my (undef, $new_flags) = extract_flags($flags, $flag_types);
 
-    my $flags = delete $params->{flags};
-    # Set bug flags
-    if ($flags) {
-        my $product = Bugzilla::Product->check($params->{product});
-        my $component = Bugzilla::Component->check({
-            product => $product,
-            name => $params->{component}
-        });
-        my $flag_types = Bugzilla::FlagType::match({
-            product_id => $product->id,
-            component_id => $component->id,
-            is_active => 1,
-        });
-
-        my(undef, $new_flags) = extract_flags($flags, $flag_types);
-
-        $params->{flags} = $new_flags;
-    }
+    $params->{flags} = $new_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);
 
-    $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}});
+  }
 
-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 $minor_update = delete $params->{minor_update} ? 1 : 0;
-
-    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,
-                $attachment->flag_types,
-                $attachment->flags);
-
-            $attachment->set_flags($old_flags, $new_flags);
-        }
+  my @result;
+  foreach my $val (@$values) {
+    push(@result, $self->type('string', $val));
+  }
 
-        $attachment->update($timestamp);
-        my $comment = $params->{comment} || '';
+  return {values => \@result};
+}
 
-        my $is_markdown = 0;
-        if (ref $params->{comment} eq 'HASH') {
-            $is_markdown = $params->{comment}->{is_markdown};
-            $comment     = $params->{comment}->{body};
-        }
+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 $minor_update = delete $params->{minor_update} ? 1 : 0;
+
+  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},
+    });
 
-        ThrowUserError('markdown_disabled')
-            if $is_markdown && !Bugzilla->user->use_markdown();
+    if ($flags) {
+      my ($old_flags, $new_flags)
+        = extract_flags($flags, $attachment->flag_types, $attachment->flags);
 
-        $attachment->bug->add_comment($comment,
-            { is_markdown => $is_markdown,
-              isprivate   => $attachment->isprivate,
-              type        => CMT_ATTACHMENT_CREATED,
-              extra_data  => $attachment->id });
-        push(@created, $attachment);
-    }
-    my %minor_updates;
-    foreach my $attachment (@created) {
-        my $bug = $attachment->bug;
-        $minor_updates{$bug->id} = $bug->has_unsent_changes ? 0 : $minor_update;
-        $bug->update($timestamp);
+      $attachment->set_flags($old_flags, $new_flags);
     }
-    $dbh->bz_commit_transaction();
 
-    foreach my $bug (@bugs) {
-        $bug->send_changes(undef, undef, $minor_updates{$bug->id});
-    }
+    $attachment->update($timestamp);
+    my $comment = $params->{comment} || '';
 
-    my @created_ids = map { $_->id } @created;
+    my $is_markdown = 0;
+    if (ref $params->{comment} eq 'HASH') {
+      $is_markdown = $params->{comment}->{is_markdown};
+      $comment     = $params->{comment}->{body};
+    }
 
-    return { ids => \@created_ids };
+    ThrowUserError('markdown_disabled')
+      if $is_markdown && !Bugzilla->user->use_markdown();
+
+    $attachment->bug->add_comment(
+      $comment,
+      {
+        is_markdown => $is_markdown,
+        isprivate   => $attachment->isprivate,
+        type        => CMT_ATTACHMENT_CREATED,
+        extra_data  => $attachment->id
+      }
+    );
+    push(@created, $attachment);
+  }
+  my %minor_updates;
+  foreach my $attachment (@created) {
+    my $bug = $attachment->bug;
+    $minor_updates{$bug->id} = $bug->has_unsent_changes ? 0 : $minor_update;
+    $bug->update($timestamp);
+  }
+  $dbh->bz_commit_transaction();
+
+  foreach my $bug (@bugs) {
+    $bug->send_changes(undef, undef, $minor_updates{$bug->id});
+  }
+
+  my @created_ids = map { $_->id } @created;
+
+  return {ids => \@created_ids};
 }
 
 sub update_attachment {
-    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' });
-
-    my $req_minor_update = delete $params->{minor_update} ? 1 : 0;
-    # Some fields cannot be sent to set_all
-    foreach my $key (qw(login password token)) {
-        delete $params->{$key};
+  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'});
+
+  my $req_minor_update = delete $params->{minor_update} ? 1 : 0;
+
+  # 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};
+  my $is_markdown = 0;
+
+  if (ref $comment eq 'HASH') {
+    $is_markdown = $comment->{is_markdown};
+    $comment     = $comment->{body};
+  }
+
+  ThrowUserError('markdown_disabled') if $is_markdown && !$user->use_markdown();
+
+  # Update the values
+  foreach my $attachment (@attachments) {
+    my ($update_flags, $new_flags)
+      = $flags
+      ? extract_flags($flags, $attachment->flag_types, $attachment->flags)
+      : ([], []);
+    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 targeted 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});
+    }
+  }
 
-    $params = translate($params, ATTACHMENT_MAPPED_SETTERS);
+  $dbh->bz_start_transaction();
 
-    # 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;
+  # Do the actual update and get information to return to user
+  my @result;
+  foreach my $attachment (@attachments) {
+    my $changes = $attachment->update();
 
-        push @attachments, $attachment;
-        $bugs{$bug->id} = $bug;
+    if ($comment = trim($comment)) {
+      $attachment->bug->add_comment(
+        $comment,
+        {
+          is_markdown => $is_markdown,
+          isprivate   => $attachment->isprivate,
+          type        => CMT_ATTACHMENT_UPDATED,
+          extra_data  => $attachment->id
+        }
+      );
     }
 
-    my $flags = delete $params->{flags};
-    my $comment = delete $params->{comment};
-    my $is_markdown = 0;
+    $changes = translate($changes, ATTACHMENT_MAPPED_RETURNS);
 
-    if (ref $comment eq 'HASH') {
-        $is_markdown = $comment->{is_markdown};
-        $comment     = $comment->{body};
-    }
+    my %hash = (
+      id               => $self->type('int',      $attachment->id),
+      last_change_time => $self->type('dateTime', $attachment->modification_time),
+      changes          => {},
+    );
 
-    ThrowUserError('markdown_disabled')
-        if $is_markdown && !$user->use_markdown();
-
-    # Update the values
-    foreach my $attachment (@attachments) {
-        my ($update_flags, $new_flags) = $flags
-            ? extract_flags($flags, $attachment->flag_types, $attachment->flags)
-            : ([], []);
-        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 targeted 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 });
-        }
+    foreach my $field (keys %$changes) {
+      my $change = $changes->{$field};
+
+      # 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();
+    push(@result, \%hash);
+  }
 
-    # 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,
-                { is_markdown => $is_markdown,
-                  isprivate   => $attachment->isprivate,
-                  type        => CMT_ATTACHMENT_UPDATED,
-                  extra_data  => $attachment->id });
-        }
+  $dbh->bz_commit_transaction();
 
-        $changes = translate($changes, ATTACHMENT_MAPPED_RETURNS);
+  # Email users about the change
+  foreach my $bug (values %bugs) {
+    my $minor_update = $bug->has_unsent_changes ? 0 : $req_minor_update;
+    $bug->update();
+    $bug->send_changes(undef, undef, $minor_update);
+  }
 
-        my %hash = (
-            id               => $self->type('int', $attachment->id),
-            last_change_time => $self->type('dateTime', $attachment->modification_time),
-            changes          => {},
-        );
+  # Return the information to the user
+  return {attachments => \@result};
+}
 
-        foreach my $field (keys %$changes) {
-            my $change = $changes->{$field};
+sub add_comment {
+  my ($self, $params) = @_;
 
-            # 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] // '')
-            };
-        }
+  # The user must login in order add a comment
+  my $user = Bugzilla->login(LOGIN_REQUIRED);
 
-        push(@result, \%hash);
-    }
+  # Check parameters
+  defined $params->{id} || ThrowCodeError('param_required', {param => 'id'});
+  my $comment = $params->{comment};
+  (defined $comment && trim($comment) ne '')
+    || ThrowCodeError('param_required', {param => 'comment'});
 
-    $dbh->bz_commit_transaction();
+  my $bug = Bugzilla::Bug->check_for_edit($params->{id});
+  my $minor_update = delete $params->{minor_update} ? 1 : 0;
+  $minor_update = $bug->has_unsent_changes ? 0 : $minor_update;
 
-    # Email users about the change
-    foreach my $bug (values %bugs) {
-        my $minor_update = $bug->has_unsent_changes ? 0 : $req_minor_update;
-        $bug->update();
-        $bug->send_changes(undef, undef, $minor_update);
-    }
+  # Backwards-compatibility for versions before 3.6
+  if (defined $params->{private}) {
+    $params->{is_private} = delete $params->{private};
+  }
 
-    # Return the information to the user
-    return { attachments => \@result };
-}
+  ThrowUserError('markdown_disabled')
+    if $params->{is_markdown} && !$user->use_markdown();
 
-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});
-    my $minor_update = delete $params->{minor_update} ? 1 : 0;
-    $minor_update = $bug->has_unsent_changes ? 0 : $minor_update;
-
-    # 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},
+      is_markdown => $params->{is_markdown},
+      work_time   => $params->{work_time}
     }
+  );
+  $bug->update();
 
-    ThrowUserError('markdown_disabled')
-        if $params->{is_markdown} && !$user->use_markdown();
-
-    # Append comment
-    $bug->add_comment($comment, { isprivate   => $params->{is_private},
-                                  is_markdown => $params->{is_markdown},
-                                  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 },
-            { minor_update => $minor_update });
+  # Send mail.
+  Bugzilla::BugMail::Send(
+    $bug->bug_id,
+    {changer      => $user},
+    {minor_update => $minor_update}
+  );
 
-    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 $req_minor_update = delete $params->{minor_update} ? 1 : 0;
-
-    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 $req_minor_update = delete $params->{minor_update} ? 1 : 0;
+
+  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 $minor_update = $bug->has_unsent_changes ? 0 : $req_minor_update;
-        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 },
-                { minor_update => $minor_update });
+    if ($add) {
+      $bug->add_see_also($_) foreach @$add;
+    }
+  }
+
+  my %changes;
+  foreach my $bug (@bugs) {
+    my $minor_update = $bug->has_unsent_changes ? 0 : $req_minor_update;
+    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},
+      {minor_update => $minor_update}
+    );
+  }
 
-    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;
-
-    if (!(defined $params->{ids}
-          or defined $params->{attachment_ids}))
-    {
-        ThrowCodeError('param_required',
-                       { function => 'Bug.attachments', 
-                         params   => ['ids', 'attachment_ids'] });
-    }
+  Bugzilla->switch_to_shadow_db() unless Bugzilla->user->id;
 
-    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);
-        }
+  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 %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 =~ /^\d+$/a;
-
-    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 =~ /^\d+$/a;
+
+  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];
 }
 
 ##############################
@@ -1278,234 +1302,240 @@ 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('login', $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('login', $_) } @{ $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('login', $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('login', $qa_login);
-        if ($bug->qa_contact) {
-            $item{'qa_contact_detail'} = $self->_user_to_hash($bug->qa_contact, $params, undef, 'qa_contact');
-        }
+  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, 'see_also') {
-        my @see_also = map { $self->type('string', $_->name) }
-                       @{ $bug->see_also };
-        $item{'see_also'} = \@see_also;
+  };
+
+  # 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('login', $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('login', $_) } @{$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('login', $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('login', $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, 'flags') {
-        $item{'flags'} = [ map { $self->_flag_to_hash($_) } @{$bug->flags} ];
+  }
+  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;
+  }
+  if (filter_wants $params, 'duplicates', 'extra') {
+    $item{'duplicates'} = [map { $self->type('int', $_->id) } @{$bug->duplicates}];
+  }
+
+  # 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, 'tags', 'extra') {
-        $item{'tags'} = $bug->tags;
+    elsif ($field->type == FIELD_TYPE_DATETIME || $field->type == FIELD_TYPE_DATE) {
+      $item{$name} = $self->type('dateTime', $bug->$name);
     }
-    if (filter_wants $params, 'duplicates', 'extra') {
-        $item{'duplicates'} = [ map { $self->type('int', $_->id) } @{ $bug->duplicates } ];
+    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('login', $user->login),
-    }, $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('login',  $user->login),
+    },
+    $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 it if
-    # the filter wants it.
-    if (filter_wants $filters, 'creator', $types, $prefix) {
-        $item->{'creator'} = $self->type('login', $attach->attacher->login);
-    }
-
-    if (filter_wants $filters, 'data', $types, $prefix) {
-        $item->{'data'} = $self->type('base64', $attach->data);
-    }
+  my ($self, $attach, $filters, $types, $prefix) = @_;
 
-    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;
+  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 it if
+  # the filter wants it.
+  if (filter_wants $filters, 'creator', $types, $prefix) {
+    $item->{'creator'} = $self->type('login', $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('login', $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('login', $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;
index b82faa33a17020a0a262d1387b9269b21b781f19..8e0042a84dc81ad41eda5971b1eece644802e1ba 100644 (file)
@@ -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->bug_id, $last_visit_ts, $params
-            ));
-    }
-    $dbh->bz_commit_transaction();
+    push(@results,
+      $self->_bug_user_last_visit_to_hash($bug->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($_, $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($_, $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;
index c0df4754f75c1f4f4496491d46227168c1ad466b..a9595d867c9087fe563fada16da5eda1bee53bf7 100644 (file)
@@ -20,152 +20,149 @@ 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
-    defaultopsys
-    defaultplatform
-    defaultpriority
-    defaultseverity
-    duplicate_or_move_bug_status
-    emailregexpdesc
-    letsubmitterchoosemilestone
-    letsubmitterchoosepriority
-    mailfrom
-    maintainer
-    maxattachmentsize
-    maxlocalattachment
-    password_complexity
-    rememberlogin
-    requirelogin
-    resolution_forbidden_with_open_blockers
-    search_allow_no_criteria
-    urlbase
-    use_email_as_login
-    use_see_also
-    useclassification
-    usemenuforusers
-    useqacontact
-    usestatuswhiteboard
-    usetargetmilestone
+  allowemailchange
+  attachment_base
+  commentonchange_resolution
+  commentonduplicate
+  defaultopsys
+  defaultplatform
+  defaultpriority
+  defaultseverity
+  duplicate_or_move_bug_status
+  emailregexpdesc
+  letsubmitterchoosemilestone
+  letsubmitterchoosepriority
+  mailfrom
+  maintainer
+  maxattachmentsize
+  maxlocalattachment
+  password_complexity
+  rememberlogin
+  requirelogin
+  resolution_forbidden_with_open_blockers
+  search_allow_no_criteria
+  urlbase
+  use_email_as_login
+  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),
-    };
+  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),
+  };
 }
 
 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;
index e9044406199483d8d81a4585b742fc25c596c611..008b4974e2368cc155c6b7040e6fa0cccb321953 100644 (file)
@@ -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;
index 1410a12f633d2d699bef12fee2899b0c3776dee6..92def92063847238c3843e7234589fdffa250ebd 100644 (file)
@@ -20,212 +20,209 @@ use Bugzilla::WebService::Constants;
 use Bugzilla::WebService::Util qw(translate params_to_objects validate);
 
 use constant PUBLIC_METHODS => qw(
-    create
-    delete
-    update
+  create
+  delete
+  update
 );
 
 use constant CREATE_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',
 };
 
-use constant MAPPED_FIELDS => {
-    is_open => 'is_active',
-};
+use constant MAPPED_FIELDS => {is_open => 'is_active',};
 
 use constant MAPPED_RETURNS => {
-    initialowner     => 'default_assignee',
-    initialqacontact => 'default_qa_contact',
-    cc_list          => 'default_cc',
-    isactive         => 'isopen',
+  initialowner     => 'default_assignee',
+  initialqacontact => 'default_qa_contact',
+  cc_list          => 'default_cc',
+  isactive         => 'isopen',
 };
 
 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, CREATE_MAPPED_FIELDS);
-    $values->{product} = $product;
+  # Translate the fields
+  my $values = translate($params, CREATE_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)};
 }
 
 sub _component_params_to_objects {
-    # We can't use Util's _param_to_objects since name is a hash
-    my $params = shift;
-    my $user   = Bugzilla->user;
 
-    my @components = ();
+  # We can't use Util's _param_to_objects since name is a hash
+  my $params = shift;
+  my $user   = Bugzilla->user;
 
-    if (defined $params->{ids}) {
-        push @components, @{ Bugzilla::Component->new_from_list($params->{ids}) };
-    }
+  my @components = ();
+
+  if (defined $params->{ids}) {
+    push @components, @{Bugzilla::Component->new_from_list($params->{ids})};
+  }
+
+  if (defined $params->{names}) {
 
-    if (defined $params->{names}) {
-        # To get the component objects for product/component combination
-        # first obtain the product object from the passed product name
-        foreach my $name_hash (@{$params->{names}}) {
-            my $product = $user->check_can_admin_product($name_hash->{product});
-            push @components, @{ Bugzilla::Component->match({
-                product_id => $product->id,
-                name       => $name_hash->{component}
-            })};
-        }
+    # To get the component objects for product/component combination
+    # first obtain the product object from the passed product name
+    foreach my $name_hash (@{$params->{names}}) {
+      my $product = $user->check_can_admin_product($name_hash->{product});
+      push @components,
+        @{Bugzilla::Component->match({
+          product_id => $product->id, name => $name_hash->{component}
+        })};
     }
+  }
 
-    my %seen_component_ids = ();
+  my %seen_component_ids = ();
 
-    my @accessible_components;
-    foreach my $component (@components) {
-        # Skip if we already included this component
-        next if $seen_component_ids{$component->id}++;
+  my @accessible_components;
+  foreach my $component (@components) {
 
-        # Can the user see and admin this product?
-        my $product = $component->product;
-        $user->check_can_admin_product($product->name);
+    # Skip if we already included this component
+    next if $seen_component_ids{$component->id}++;
 
-        push @accessible_components, $component;
-    }
+    # Can the user see and admin this product?
+    my $product = $component->product;
+    $user->check_can_admin_product($product->name);
+
+    push @accessible_components, $component;
+  }
 
-    return \@accessible_components;
+  return \@accessible_components;
 }
 
 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 => "components" });
-
-    defined($params->{names}) || defined($params->{ids})
-        || ThrowCodeError('params_required',
-               { function => 'Component.update', params => ['ids', 'names'] });
-
-    my $component_objects = _component_params_to_objects($params);
-
-    # If the user tries to change component name for several
-    # components of the same product then throw an error
-    if ($params->{name}) {
-        my %unique_product_comps;
-        foreach my $comp (@$component_objects) {
-            if($unique_product_comps{$comp->product_id}) {
-                ThrowUserError("multiple_components_update_not_allowed");
-            }
-            else {
-                $unique_product_comps{$comp->product_id} = 1;
-            }
-        }
+  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 => "components"});
+
+  defined($params->{names})
+    || defined($params->{ids})
+    || ThrowCodeError('params_required',
+    {function => 'Component.update', params => ['ids', 'names']});
+
+  my $component_objects = _component_params_to_objects($params);
+
+  # If the user tries to change component name for several
+  # components of the same product then throw an error
+  if ($params->{name}) {
+    my %unique_product_comps;
+    foreach my $comp (@$component_objects) {
+      if ($unique_product_comps{$comp->product_id}) {
+        ThrowUserError("multiple_components_update_not_allowed");
+      }
+      else {
+        $unique_product_comps{$comp->product_id} = 1;
+      }
     }
-
-    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 $component (@$component_objects) {
-        $component->set_all($values);
+  }
+
+  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 $component (@$component_objects) {
+    $component->set_all($values);
+  }
+
+  my %changes;
+  foreach my $component (@$component_objects) {
+    my $returned_changes = $component->update();
+    $changes{$component->id} = translate($returned_changes, MAPPED_RETURNS);
+  }
+  $dbh->bz_commit_transaction();
+
+  my @result;
+  foreach my $component (@$component_objects) {
+    my %hash = (id => $component->id, changes => {},);
+
+    foreach my $field (keys %{$changes{$component->id}}) {
+      my $change = $changes{$component->id}->{$field};
+
+      if ( $field eq 'default_assignee'
+        || $field eq 'default_qa_contact'
+        || $field eq 'default_cc')
+      {
+        # We need to convert user ids to login names
+        my @old_user_ids = split(/[,\s]+/, $change->[0]);
+        my @new_user_ids = split(/[,\s]+/, $change->[1]);
+
+        my @old_users
+          = map { $_->login } @{Bugzilla::User->new_from_list(\@old_user_ids)};
+        my @new_users
+          = map { $_->login } @{Bugzilla::User->new_from_list(\@new_user_ids)};
+
+        $hash{changes}{$field} = {
+          removed => $self->type('string', join(', ', @old_users)),
+          added   => $self->type('string', join(', ', @new_users)),
+        };
+      }
+      else {
+        $hash{changes}{$field} = {
+          removed => $self->type('string', $change->[0]),
+          added   => $self->type('string', $change->[1])
+        };
+      }
     }
 
-    my %changes;
-    foreach my $component (@$component_objects) {
-        my $returned_changes = $component->update();
-        $changes{$component->id} = translate($returned_changes, MAPPED_RETURNS);
-    }
-    $dbh->bz_commit_transaction();
-
-    my @result;
-    foreach my $component (@$component_objects) {
-        my %hash = (
-            id      => $component->id,
-            changes => {},
-        );
-
-        foreach my $field (keys %{ $changes{$component->id} }) {
-            my $change = $changes{$component->id}->{$field};
-
-            if ($field eq 'default_assignee'
-                || $field eq 'default_qa_contact'
-                || $field eq 'default_cc'
-            ) {
-                # We need to convert user ids to login names
-                my @old_user_ids = split(/[,\s]+/, $change->[0]);
-                my @new_user_ids = split(/[,\s]+/, $change->[1]);
-
-                my @old_users = map { $_->login }
-                    @{Bugzilla::User->new_from_list(\@old_user_ids)};
-                my @new_users = map { $_->login }
-                    @{Bugzilla::User->new_from_list(\@new_user_ids)};
-
-                $hash{changes}{$field} = {
-                    removed => $self->type('string', join(', ', @old_users)),
-                    added   => $self->type('string', join(', ', @new_users)),
-                };
-            }
-            else {
-                $hash{changes}{$field} = {
-                    removed => $self->type('string', $change->[0]),
-                    added   => $self->type('string', $change->[1])
-                };
-            }
-        }
-
-        push(@result, \%hash);
-    }
+    push(@result, \%hash);
+  }
 
-    return { components => \@result };
+  return {components => \@result};
 }
 
 sub delete {
-    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 => "components" });
-
-    defined($params->{names}) || defined($params->{ids})
-        || ThrowCodeError('params_required',
-               { function => 'Component.delete', params => ['ids', 'names'] });
-
-    my $component_objects = _component_params_to_objects($params);
-
-    $dbh->bz_start_transaction();
-    my %changes;
-    foreach my $component (@$component_objects) {
-        my $returned_changes = $component->remove_from_db();
-    }
-    $dbh->bz_commit_transaction();
-
-    my @result;
-    foreach my $component (@$component_objects) {
-        push @result, { id => $component->id };
-    }
-
-    return { components => \@result };
+  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 => "components"});
+
+  defined($params->{names})
+    || defined($params->{ids})
+    || ThrowCodeError('params_required',
+    {function => 'Component.delete', params => ['ids', 'names']});
+
+  my $component_objects = _component_params_to_objects($params);
+
+  $dbh->bz_start_transaction();
+  my %changes;
+  foreach my $component (@$component_objects) {
+    my $returned_changes = $component->remove_from_db();
+  }
+  $dbh->bz_commit_transaction();
+
+  my @result;
+  foreach my $component (@$component_objects) {
+    push @result, {id => $component->id};
+  }
+
+  return {components => \@result};
 }
 
 1;
index 33316699646432a777e632d3dd6fdbef538712f9..f18f9f0ecbdc4d8eab53d48d175b37a5bcfa68a5 100644 (file)
@@ -14,27 +14,27 @@ 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
 
-    API_AUTH_HEADERS
+  API_AUTH_HEADERS
 );
 
 # This maps the error names in global/*-error.html.tmpl to numbers.
@@ -56,182 +56,205 @@ 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,
-    markdown_disabled => 140,
-    # 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,
-    product_admin_denied                 => 706,
-
-    # 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,
-    component_blank_name                   => 1210,
-    component_blank_description            => 1211,
-    multiple_components_update_not_allowed => 1212,
-    component_need_initialowner            => 1213,
-
-    # BugUserLastVisited errors
-    user_not_involved => 1300,
-
-    # 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,
+  markdown_disabled         => 140,
+
+  # 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,
+  product_admin_denied                 => 706,
+
+  # 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,
+  component_blank_name                   => 1210,
+  component_blank_description            => 1211,
+  multiple_components_update_not_allowed => 1212,
+  component_need_initialowner            => 1213,
+
+  # BugUserLastVisited errors
+  user_not_involved => 1300,
+
+  # 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
@@ -252,82 +275,83 @@ 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;
+}
 
 # Custom HTTP headers that can be used for API authentication rather than
 # passing as URL parameters. This is useful if you do not want sensitive
 # information to show up in webserver log files.
 use constant API_AUTH_HEADERS => {
-    X_BUGZILLA_LOGIN    => 'Bugzilla_login',
-    X_BUGZILLA_PASSWORD => 'Bugzilla_password',
-    X_BUGZILLA_API_KEY  => 'Bugzilla_api_key',
-    X_BUGZILLA_TOKEN    => 'Bugzilla_token',
+  X_BUGZILLA_LOGIN    => 'Bugzilla_login',
+  X_BUGZILLA_PASSWORD => 'Bugzilla_password',
+  X_BUGZILLA_API_KEY  => 'Bugzilla_api_key',
+  X_BUGZILLA_TOKEN    => 'Bugzilla_token',
 };
 
 1;
index 4c626914fae91f2bc924ff705e55f90c812998d8..0b0aff439359f85338eaaf783ec0932d578e3136 100644 (file)
@@ -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;
index a9539f83d76dbfb75f367de1398c60e752f2cc70..04aa6e9a010aaa5f9cdf6351a1ad7d2106ece668 100644 (file)
@@ -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('login', $_->login),
-            email             => $self->type('email', $_->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('login',   $_->login),
+    email             => $self->type('email',   $_->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;
index 66bd32808edaafa80a365cb40ea67a97d85b6522..e7eb0bf8f52a7d9dd0bcfff4e008b21e8c70b9da 100644 (file)
@@ -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('login', $component->default_assignee->login),
-        default_qa_contact =>
-            $self->type('login', $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('login', $component->default_assignee->login),
+    default_qa_contact => $self->type(
+      'login',
+      $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;
index da92b4d7fc26f708589dc0caa88b125b224baa47..190c3d56954d9fcadcdaba14938383d4a9824560 100644 (file)
@@ -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;
index b488c8de5fc28f7adde593723fda5357c8b15386..2aebc4cea37944f1a4467d205bcd4812a379fafc 100644 (file)
@@ -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);
-    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);
+  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,75 +197,82 @@ 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 'login') {
-        $retval = Bugzilla->params->{'use_email_as_login'} ? email_filter($retval) : "$retval";
-    }
-    elsif ($type eq 'email') {
-        $retval = Bugzilla->user->in_group('editusers') ? "$retval" : '';
-    }
-
-    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 'login') {
+    $retval
+      = Bugzilla->params->{'use_email_as_login'}
+      ? email_filter($retval)
+      : "$retval";
+  }
+  elsif ($type eq 'email') {
+    $retval = Bugzilla->user->in_group('editusers') ? "$retval" : '';
+  }
+
+  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);
 }
 
 ######################################
@@ -270,171 +281,171 @@ 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(@_);
 
-    # Reset in_eval to so we get normal exceptions from here
-    Bugzilla->request_cache->{in_eval} = 0;
+  # Reset in_eval to so we get normal exceptions from here
+  Bugzilla->request_cache->{in_eval} = 0;
 
-    # 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, $self->cgi);
-
-    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];
-    }
-
-    # Let Bugzilla::Error know we are inside an eval() after this point
-    Bugzilla->request_cache->{in_eval} = 1;
-
-    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, $self->cgi);
+
+  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];
+  }
+
+  # Let Bugzilla::Error know we are inside an eval() after this point
+  Bugzilla->request_cache->{in_eval} = 1;
+
+  return $params;
 }
 
 ##########################
@@ -443,22 +454,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;
index c6461abd68d158c70bb8d289c94a5d3821974489..00dadbadf0f16dba315f8f65d34fa1a0ebc6af7d 100644 (file)
@@ -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,102 +27,105 @@ 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 'login') {
-            $type = 'string';
-            $value = email_filter($value) if Bugzilla->params->{'use_email_as_login'};
-        }
-        elsif ($type eq 'email') {
-            $type = 'string';
-            $value = '' unless Bugzilla->user->in_group('editusers');
-        }
-        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 'login') {
+      $type = 'string';
+      $value = email_filter($value) if Bugzilla->params->{'use_email_as_login'};
+    }
+    elsif ($type eq 'email') {
+      $type = 'string';
+      $value = '' unless Bugzilla->user->in_group('editusers');
+    }
+    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};
-    if (!$class) {
-        ThrowCodeError('unknown_method', { method => $method eq 'methodName' ? '' : '.' . $method });
-    }
-    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};
+  if (!$class) {
+    ThrowCodeError('unknown_method',
+      {method => $method eq 'methodName' ? '' : '.' . $method});
+  }
+  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;
@@ -145,100 +149,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 <int>,
-    # <double>,or <dateTime.iso8601>,  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 <int>,
+  # <double>,or <dateTime.iso8601>,  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;
@@ -254,16 +269,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;
@@ -277,43 +292,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,
@@ -321,58 +339,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;
index 98323d4e18941d01fe56f0a5d5686750434dd28f..1175767b0bf749bd30f6c37653dc291a9aec64e3 100644 (file)
@@ -18,78 +18,72 @@ 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
-    whoami
+  create
+  get
+  login
+  logout
+  offer_account_by_email
+  update
+  valid_login
+  whoami
 );
 
-use constant MAPPED_FIELDS => {
-    full_name => 'name',
-    login_denied_text => 'disabledtext',
-};
+use constant MAPPED_FIELDS =>
+  {full_name => 'name', login_denied_text => 'disabledtext',};
 
-use constant MAPPED_RETURNS => {
-    realname => 'full_name',
-    disabledtext => 'login_denied_text',
-};
+use constant MAPPED_RETURNS =>
+  {realname => 'full_name', disabledtext => 'login_denied_text',};
 
 ##############
 # User Login #
 ##############
 
 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);
+  }
 
-    # Login name and password params are required
-    foreach my $param ("login", "password") {
-        (defined $params->{$param} || defined $params->{'Bugzilla_' . $param})
-            || ThrowCodeError('param_required', { param => $param });
-    }
+  # Login name 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);
 }
 
 #################
@@ -97,176 +91,182 @@ sub valid_login {
 #################
 
 sub offer_account_by_email {
-    my $self = shift;
-    my ($params) = @_;
-    my $email = trim($params->{email})
-        || ThrowCodeError('param_required', { param => 'email' });
-
-    my $login = Bugzilla->params->{use_email_as_login} ? $email : trim($params->{login});
-    $login or ThrowCodeError('param_required', { param => 'login' });
-
-    Bugzilla->user->check_account_creation_enabled;
-    Bugzilla->user->check_and_send_account_creation_confirmation($login, $email);
-    return undef;
+  my $self     = shift;
+  my ($params) = @_;
+  my $email    = trim($params->{email})
+    || ThrowCodeError('param_required', {param => 'email'});
+
+  my $login
+    = Bugzilla->params->{use_email_as_login} ? $email : trim($params->{login});
+  $login or ThrowCodeError('param_required', {param => 'login'});
+
+  Bugzilla->user->check_account_creation_enabled;
+  Bugzilla->user->check_and_send_account_creation_confirmation($login, $email);
+  return undef;
 }
 
 sub create {
-    my $self = shift;
-    my ($params) = @_;
+  my $self = shift;
+  my ($params) = @_;
 
-    Bugzilla->user->in_group('editusers') 
-        || ThrowUserError("auth_failure", { group  => "editusers",
-                                            action => "add",
-                                            object => "users"});
+  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 $email = trim($params->{email})
+    || ThrowCodeError('param_required', {param => 'email'});
 
-    my $login = Bugzilla->params->{use_email_as_login} ? $email : trim($params->{login});
-    $login or ThrowCodeError('param_required', { param => 'login' });
+  my $login
+    = Bugzilla->params->{use_email_as_login} ? $email : trim($params->{login});
+  $login or ThrowCodeError('param_required', {param => 'login'});
 
-    my $realname = trim($params->{full_name});
-    my $password = trim($params->{password}) || '*';
+  my $realname = trim($params->{full_name});
+  my $password = trim($params->{password}) || '*';
 
-    my $user = Bugzilla::User->create({
-        login_name    => $login,
-        email         => $email,
-        realname      => $realname,
-        cryptpassword => $password
-    });
+  my $user = Bugzilla::User->create({
+    login_name    => $login,
+    email         => $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('login', $_->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('login', $user->login),
-            can_login => $self->type('boolean', $user->is_enabled ? 1 : 0),
-        };
-
-        if (Bugzilla->user->in_group('editusers')) {
-            $user_info->{email}             = $self->type('email', $user->email),
-            $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('login',  $_->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('login',   $user->login),
+      can_login => $self->type('boolean', $user->is_enabled ? 1 : 0),
+      };
+
+    if (Bugzilla->user->in_group('editusers')) {
+      $user_info->{email}           = $self->type('email',   $user->email),
+        $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};
 }
 
 ###############
@@ -274,168 +274,170 @@ sub get {
 ###############
 
 sub update {
-    my ($self, $params) = @_;
-
-    my $dbh = Bugzilla->dbh;
+  my ($self, $params) = @_;
 
-    my $user = Bugzilla->login(LOGIN_REQUIRED);
+  my $dbh = Bugzilla->dbh;
 
-    # Reject access if there is no sense in continuing.
-    $user->in_group('editusers')
-        || $user->can_bless()
-        || ThrowUserError("auth_failure", {group  => "editusers",
-                                           action => "edit",
-                                           object => "users"});
+  my $user = Bugzilla->login(LOGIN_REQUIRED);
 
-    defined($params->{names}) || defined($params->{ids})
-        || ThrowCodeError('params_required', 
-               { function => 'User.update', params => ['ids', 'names'] });
+  # Reject access if there is no sense in continuing.
+  $user->in_group('editusers')
+    || $user->can_bless()
+    || ThrowUserError("auth_failure",
+    {group => "editusers", action => "edit", object => "users"});
 
-    my $user_objects = params_to_objects($params, 'Bugzilla::User');
+  defined($params->{names})
+    || defined($params->{ids})
+    || ThrowCodeError('params_required',
+    {function => 'User.update', params => ['ids', 'names']});
 
-    my $values = translate($params, MAPPED_FIELDS);
+  my $user_objects = params_to_objects($params, 'Bugzilla::User');
 
-    # We delete names and ids to keep only new values to set.
-    delete $values->{names};
-    delete $values->{ids};
+  my $values = translate($params, MAPPED_FIELDS);
 
-    $dbh->bz_start_transaction();
+  # We delete names and ids to keep only new values to set.
+  delete $values->{names};
+  delete $values->{ids};
 
-    $values = { groups => $values->{groups} } unless $user->in_group('editusers');
-    foreach my $user (@$user_objects){
-        $user->set_all($values);
-    }
+  $dbh->bz_start_transaction();
 
-    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]) 
-            };
-        }
+  $values = {groups => $values->{groups}} unless $user->in_group('editusers');
+  foreach my $user (@$user_objects) {
+    $user->set_all($values);
+  }
 
-        push(@result, \%hash);
-    }
+  my %changes;
+  foreach my $user (@$user_objects) {
+    my $returned_changes = $user->update();
+    $changes{$user->id} = translate($returned_changes, MAPPED_RETURNS);
+  }
+  $dbh->bz_commit_transaction();
 
-    return { users => \@result };
-}
+  my @result;
+  foreach my $user (@$user_objects) {
+    my %hash = (id => $user->id, changes => {},);
 
-sub _filter_users_by_group {
-    my ($self, $users, $params) = @_;
-    my ($group_ids, $group_names) = @$params{qw(group_ids groups)};
+    foreach my $field (keys %{$changes{$user->id}}) {
+      my $change = $changes{$user->id}->{$field};
 
-    # 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;
 }
 
 sub whoami {
-    my ($self, $params) = @_;
-    my $user = Bugzilla->login(LOGIN_REQUIRED);
-    return filter $params, {
-        id        => $self->type('int', $user->id),
-        real_name => $self->type('string', $user->name),
-        name      => $self->type('email', $user->login),
+  my ($self, $params) = @_;
+  my $user = Bugzilla->login(LOGIN_REQUIRED);
+  return filter $params,
+    {
+    id        => $self->type('int',    $user->id),
+    real_name => $self->type('string', $user->name),
+    name      => $self->type('email',  $user->login),
     };
 }
 
index d4da2f5751ee3f8ec45440b78a9dc01861bcc623..dacf2b12543f31d38b50d0940c8ea7e9aba53fa7 100644 (file)
@@ -27,278 +27,287 @@ 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, $flag_types, $current_flags) = @_;
-    my (@new_flags, @old_flags);
+  my ($flags, $flag_types, $current_flags) = @_;
+  my (@new_flags, @old_flags);
 
-    $current_flags //= [];
+  $current_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, $cgi) = @_;
-
-    # Allow user to pass in authentication details in X-Headers
-    # This allows callers to keep credentials out of GET request query-strings
-    if ($cgi) {
-        foreach my $field (keys %{ API_AUTH_HEADERS() }) {
-            next if exists $params->{API_AUTH_HEADERS->{$field}} || ($cgi->http($field) // '') eq '';
-            $params->{API_AUTH_HEADERS->{$field}} = uri_unescape($cgi->http($field));
-        }
+  my ($params, $cgi) = @_;
+
+  # Allow user to pass in authentication details in X-Headers
+  # This allows callers to keep credentials out of GET request query-strings
+  if ($cgi) {
+    foreach my $field (keys %{API_AUTH_HEADERS()}) {
+      next
+        if exists $params->{API_AUTH_HEADERS->{$field}}
+        || ($cgi->http($field) // '') eq '';
+      $params->{API_AUTH_HEADERS->{$field}} = uri_unescape($cgi->http($field));
     }
-
-    # 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 });
+  }
+
+  # 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__
index 6f322fe96cf37d962b7ed95c499ee28b419cd285..6170df75145c523836d230c8fc4f7b29faee5812 100644 (file)
@@ -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;
index a691fd2ff6379cc19a8b9ada398e41b26c47a16a..a4c2f36e8ff058ca2635ae2c9bcf63f07524b193 100644 (file)
@@ -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;
index 8417affb16d4ebec999be22b71ff0638ccf132a6..b55fd8b10a8872fe79714254f4626be6a9747207 100644 (file)
@@ -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;
index f84d863b0ddf8662df909e967bf6c087f0e05d79..de1f30529addcd2828a62592ea0677cc3559b5d5 100755 (executable)
--- 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());
index 59c07c84dd353395e0f7bc70b3579badae4a44d9..cf5c6a6432a56d67bb5e9b3191f8b35c86bcdbaf 100755 (executable)
@@ -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;
@@ -33,9 +33,9 @@ use Cwd qw(realpath);
 # 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
@@ -50,57 +50,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});
 }
 
@@ -122,72 +113,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;
@@ -196,125 +188,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;
 }
 
 ################################################################################
@@ -323,286 +329,311 @@ 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 (my $content_type = $cgi->param('content_type')) {
-        $contenttype = $attachment->_check_content_type($content_type);
-    }
+  # Bug 111522: allow overriding content-type manually in the posted form
+  # params.
+  if (my $content_type = $cgi->param('content_type')) {
+    $contenttype = $attachment->_check_content_type($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
-    # Follow RFC 6266 section 4.1 (which itself points to RFC 5987 section 3.2)
-    $filename = uri_escape_utf8($filename);
-
-    my $disposition = (Bugzilla->params->{'allow_attachment_display'}
-                       && $attachment->is_viewable) ? '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);
-            }
-        }
-    }
-    my $sendfile_header = {};
-    my $sendfile_param = Bugzilla->params->{'xsendfile_header'};
-    if ($attachment->is_on_filesystem && $sendfile_param ne 'off') {
-        # attachment is on filesystem and Admin turned on feature.
-        # This means we can let webserver handle the request and stream the file
-        # for us. This is called the X-Sendfile feature. see bug 1073264.
-        my $attachment_path = realpath($attachment->_get_local_filename());
-        $sendfile_header->{$sendfile_param} = $attachment_path;
-    }
-    # IE8 and older do not support RFC 6266. So for these old browsers
-    # we still pass the old 'filename' attribute. Modern browsers will
-    # automatically pick the new 'filename*' attribute.
-    print $cgi->header(-type=> $contenttype,
-                       -content_disposition=> "$disposition; filename=\"$filename\"; filename*=UTF-8''$filename",
-                       -content_length => $attachment->datasize,
-                       %$sendfile_header);
-    if ($attachment->is_on_filesystem && $sendfile_param ne 'off') {
-        # in case of X-Sendfile, we do not print the data.
-        # that is handled directly by the webserver.
-        return;
+  # 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
+      # Follow RFC 6266 section 4.1 (which itself points to RFC 5987 section 3.2)
+  $filename = uri_escape_utf8($filename);
+
+  my $disposition
+    = (Bugzilla->params->{'allow_attachment_display'} && $attachment->is_viewable)
+    ? '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);
+      }
     }
-    disable_utf8();
-    print $attachment->data;
+  }
+  my $sendfile_header = {};
+  my $sendfile_param  = Bugzilla->params->{'xsendfile_header'};
+  if ($attachment->is_on_filesystem && $sendfile_param ne 'off') {
+
+    # attachment is on filesystem and Admin turned on feature.
+    # This means we can let webserver handle the request and stream the file
+    # for us. This is called the X-Sendfile feature. see bug 1073264.
+    my $attachment_path = realpath($attachment->_get_local_filename());
+    $sendfile_header->{$sendfile_param} = $attachment_path;
+  }
+
+  # IE8 and older do not support RFC 6266. So for these old browsers
+  # we still pass the old 'filename' attribute. Modern browsers will
+  # automatically pick the new 'filename*' attribute.
+  print $cgi->header(
+    -type => $contenttype,
+    -content_disposition =>
+      "$disposition; filename=\"$filename\"; filename*=UTF-8''$filename",
+    -content_length => $attachment->datasize,
+    %$sendfile_header
+  );
+  if ($attachment->is_on_filesystem && $sendfile_param ne 'off') {
+
+    # in case of X-Sendfile, we do not print the data.
+    # that is handled directly by the webserver.
+    return;
+  }
+  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;
-
-    # Retrieve and validate parameters
-    my $bug = Bugzilla::Bug->check(scalar $cgi->param('bugid'));
-    my $bugid = $bug->id;
-    my ($timestamp) = $dbh->selectrow_array("SELECT NOW()");
+  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(scalar $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->multi_param('obsolete');
+    @obsolete_attachments
+      = Bugzilla::Attachment->validate_obsolete($bug, \@obsolete);
+  }
 
-    # Detect if the user already used the same form to submit an attachment
-    my $token = trim(scalar $cgi->param('token'));
-    check_token_data($token, 'create_attachment', 'index.cgi');
+  my $minor_update = $cgi->param('minor_update') ? 1 : 0;
+  $minor_update = $bug->has_unsent_changes ? 0 : $minor_update;
+
+  # 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);
+  }
 
-    # Check attachments the user tries to mark as obsolete.
-    my @obsolete_attachments;
-    if ($cgi->param('obsolete')) {
-        my @obsolete = $cgi->multi_param('obsolete');
-        @obsolete_attachments = Bugzilla::Attachment->validate_obsolete($bug, \@obsolete);
+  my ($flags, $new_flags)
+    = Bugzilla::Flag->extract_flags_from_cgi($vars, SKIP_REQUESTEE_ON_ERROR,
+    {bug => $bug, attachment => $attachment});
+  $attachment->set_flags($flags, $new_flags);
+
+  # Insert a comment about the new attachment into the database.
+  my $comment = $cgi->param('comment');
+  $comment = '' unless defined $comment;
+  my $is_markdown = ($user->use_markdown && $cgi->param('use_markdown')) ? 1 : 0;
+  $bug->add_comment(
+    $comment,
+    {
+      isprivate   => $attachment->isprivate,
+      type        => CMT_ATTACHMENT_CREATED,
+      extra_data  => $attachment->id,
+      is_markdown => $is_markdown
     }
+  );
 
-    my $minor_update = $cgi->param('minor_update') ? 1 : 0;
-    $minor_update = $bug->has_unsent_changes ? 0 : $minor_update;
-
-    # 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');
+  # 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)) {
 
-    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);
+    # 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};
 
-    foreach my $obsolete_attachment (@obsolete_attachments) {
-        $obsolete_attachment->set_is_obsolete(1);
-        $obsolete_attachment->update($timestamp);
+    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(
-                                  $vars, SKIP_REQUESTEE_ON_ERROR,
-                                  { bug => $bug, attachment => $attachment });
-    $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;
-    my $is_markdown = ($user->use_markdown
-                       && $cgi->param('use_markdown')) ? 1 : 0;
-    $bug->add_comment($comment, { isprivate => $attachment->isprivate,
-                                  type => CMT_ATTACHMENT_CREATED,
-                                  extra_data => $attachment->id,
-                                  is_markdown => $is_markdown});
-
-    # 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 };
-    my $params = { 'minor_update' => $minor_update };
-    $vars->{'sent_bugmail'} = Bugzilla::BugMail::Send($bugid, $recipients, $params);
+  my $recipients = {'changer' => $user, 'owner' => $owner};
+  my $params = {'minor_update' => $minor_update};
+  $vars->{'sent_bugmail'} = Bugzilla::BugMail::Send($bugid, $recipients, $params);
 
-    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.
@@ -610,231 +641,244 @@ 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;
+
+  print $cgi->header();
 
-    # Generate and return the UI (HTML page) from the appropriate template.
-    $template->process("attachment/edit.html.tmpl", $vars)
-        || ThrowTemplateError($template->error());
+  # 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]));
 
-    # 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]);
-
-    my $minor_update = $cgi->param('minor_update') ? 1 : 0;
-    $minor_update = $bug->has_unsent_changes ? 0 : $minor_update;
-
-    # 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 });
+        $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;
+      }
     }
+  }
+
+  # 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]);
+
+  my $minor_update = $cgi->param('minor_update') ? 1 : 0;
+  $minor_update = $bug->has_unsent_changes ? 0 : $minor_update;
+
+  # 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');
 
-    $bug->add_cc($user) if $cgi->param('addselfcc');
+  my ($flags, $new_flags)
+    = Bugzilla::Flag->extract_flags_from_cgi($vars, undef,
+    {bug => $bug, attachment => $attachment});
 
-    my ($flags, $new_flags) =
-      Bugzilla::Flag->extract_flags_from_cgi($vars, undef, { bug => $bug, attachment => $attachment });
+  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 },
-            {'minor_update' => $minor_update });
+  # 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},
+    {'minor_update' => $minor_update}
+  );
 
-    # 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(scalar $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');
 
-        # Paste the reason provided by the admin into a comment.
-        $bug->add_comment($msg);
+  # We don't want to let a malicious URL accidentally delete an attachment.
+  my $token = trim(scalar $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');
+    }
 
-        $attachment->remove_from_db();
+    my $bug = new Bugzilla::Bug($attachment->bug_id);
 
-        # Now delete the token.
-        delete_token($token);
+    # The token is valid. Delete the content of the attachment.
+    my $msg;
+    $vars->{'attachment'} = $attachment;
+    $vars->{'reason'} = clean_text($cgi->param('reason') || '');
 
-        # Insert the comment.
-        $bug->update();
+    $template->process("attachment/delete_reason.txt.tmpl", $vars, \$msg)
+      || ThrowTemplateError($template->error());
 
-        # Required to display the bug the deleted attachment belongs to.
-        $vars->{'bugs'} = [$bug];
-        $vars->{'header_done'} = 1;
+    # Paste the reason provided by the admin into a comment.
+    $bug->add_comment($msg);
 
-        $vars->{'sent_bugmail'} =
-            Bugzilla::BugMail::Send($bug->id, { 'changer' => $user });
+    $attachment->remove_from_db();
 
-        $template->process("attachment/updated.html.tmpl", $vars)
-          || ThrowTemplateError($template->error());
-    }
-    else {
-        # Create a token.
-        $token = issue_session_token('delete_attachment' . $attachment->id);
+    # Now delete the token.
+    delete_token($token);
 
-        $vars->{'a'} = $attachment;
-        $vars->{'token'} = $token;
+    # Insert the comment.
+    $bug->update();
 
-        $template->process("attachment/confirm-delete.html.tmpl", $vars)
-          || ThrowTemplateError($template->error());
-    }
+    # Required to display the bug the deleted attachment belongs to.
+    $vars->{'bugs'}        = [$bug];
+    $vars->{'header_done'} = 1;
+
+    $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());
+  }
 }
index 3f459095358ca0749f58d22095a8727e6f2c801b..6ef3e2b28203338741f0e9b589be54fe498e7988 100755 (executable)
--- a/auth.cgi
+++ b/auth.cgi
@@ -28,108 +28,115 @@ use JSON qw(decode_json encode_json);
 
 Bugzilla->login(LOGIN_REQUIRED);
 
-ThrowUserError('auth_delegation_disabled') unless Bugzilla->params->{auth_delegation};
+ThrowUserError('auth_delegation_disabled')
+  unless Bugzilla->params->{auth_delegation};
 
-my $cgi         = Bugzilla->cgi;
-my $template    = Bugzilla->template;
-my $user        = Bugzilla->user;
-my $callback    = $cgi->param('callback') or ThrowUserError("auth_delegation_missing_callback");
-my $description = $cgi->param('description') or ThrowUserError("auth_delegation_missing_description");
+my $cgi      = Bugzilla->cgi;
+my $template = Bugzilla->template;
+my $user     = Bugzilla->user;
+my $callback = $cgi->param('callback')
+  or ThrowUserError("auth_delegation_missing_callback");
+my $description = $cgi->param('description')
+  or ThrowUserError("auth_delegation_missing_description");
 
 trick_taint($callback);
 trick_taint($description);
 
-my $callback_uri  = URI->new($callback);
+my $callback_uri = URI->new($callback);
 $callback_uri->scheme =~ /^https?$/
-  or ThrowUserError('auth_delegation_illegal_protocol', { protocol => $callback_uri->scheme });
+  or ThrowUserError('auth_delegation_illegal_protocol',
+  {protocol => $callback_uri->scheme});
 my $callback_base = $callback_uri->clone;
 $callback_base->query(undef);
 
 my $skip_confirmation = 0;
-my %args = ( skip_confirmation => \$skip_confirmation,
-             callback          => $callback_uri,
-             description       => $description,
-             callback_base     => $callback_base );
+my %args              = (
+  skip_confirmation => \$skip_confirmation,
+  callback          => $callback_uri,
+  description       => $description,
+  callback_base     => $callback_base
+);
 
 Bugzilla::Hook::process('auth_delegation_confirm', \%args);
 
 my $confirmed = lc($cgi->request_method) eq 'post' && $cgi->param('confirm');
 
 if ($confirmed || $skip_confirmation) {
-    my $token = $cgi->param('token');
-    unless ($skip_confirmation) {
-        ThrowUserError("auth_delegation_missing_token") unless $token;
-        trick_taint($token);
-
-        unless (check_auth_delegation_token($token, $callback)) {
-            ThrowUserError('auth_delegation_invalid_token',
-                           { token => $token, callback => $callback });
-        }
+  my $token = $cgi->param('token');
+  unless ($skip_confirmation) {
+    ThrowUserError("auth_delegation_missing_token") unless $token;
+    trick_taint($token);
+
+    unless (check_auth_delegation_token($token, $callback)) {
+      ThrowUserError('auth_delegation_invalid_token',
+        {token => $token, callback => $callback});
     }
-    my $app_id = sha256_hex($callback_uri, $description);
-    my $keys = Bugzilla::User::APIKey->match({
-        user_id => $user->id,
-        app_id  => $app_id,
-        revoked => 0,
+  }
+  my $app_id = sha256_hex($callback_uri, $description);
+  my $keys
+    = Bugzilla::User::APIKey->match({
+    user_id => $user->id, app_id => $app_id, revoked => 0,
     });
 
-    my $api_key;
-    if (@$keys) {
-        $api_key = $keys->[0];
-    }
-    else {
-        $api_key = Bugzilla::User::APIKey->create({
-            user_id     => $user->id,
-            description => $description,
-            app_id      => $app_id,
-        });
-        my $template = Bugzilla->template_inner($user->setting('lang'));
-        my $vars = { user => $user, new_key => $api_key };
-        my $message;
-        $template->process('email/new-api-key.txt.tmpl', $vars, \$message)
-          or ThrowTemplateError($template->error());
-
-        MessageToMTA($message);
-    }
+  my $api_key;
+  if (@$keys) {
+    $api_key = $keys->[0];
+  }
+  else {
+    $api_key
+      = Bugzilla::User::APIKey->create({
+      user_id => $user->id, description => $description, app_id => $app_id,
+      });
+    my $template = Bugzilla->template_inner($user->setting('lang'));
+    my $vars = {user => $user, new_key => $api_key};
+    my $message;
+    $template->process('email/new-api-key.txt.tmpl', $vars, \$message)
+      or ThrowTemplateError($template->error());
 
-    my $ua = LWP::UserAgent->new();
-    $ua->timeout(2);
-    $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);
+    MessageToMTA($message);
+  }
+
+  my $ua = LWP::UserAgent->new();
+  $ua->timeout(2);
+  $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 $content = encode_json(
+    {client_api_key => $api_key->api_key, client_api_login => $user->login});
+  my $resp = $ua->post(
+    $callback_uri,
+    'Content-Type' => 'application/json',
+    Content        => $content
+  );
+  if ($resp->code == 200) {
+    $callback_uri->query_param(client_api_login => $user->login);
+    eval {
+      my $data = decode_json($resp->content);
+      $callback_uri->query_param(callback_result => $data->{result});
+    };
+    if ($@) {
+      ThrowUserError('auth_delegation_json_error', {json_text => $resp->content});
     }
     else {
-        $ua->env_proxy;
-    }
-    my $content = encode_json({ client_api_key => $api_key->api_key,
-                                client_api_login => $user->login });
-    my $resp = $ua->post($callback_uri,
-                         'Content-Type' => 'application/json',
-                         Content => $content);
-    if ($resp->code == 200) {
-        $callback_uri->query_param(client_api_login => $user->login);
-        eval {
-            my $data = decode_json($resp->content);
-            $callback_uri->query_param(callback_result => $data->{result});
-        };
-        if ($@) {
-            ThrowUserError('auth_delegation_json_error', { json_text => $resp->content });
-        }
-        else {
-            print $cgi->redirect($callback_uri);
-        }
-    }
-    else {
-        ThrowUserError('auth_delegation_post_error', { code => $resp->code });
+      print $cgi->redirect($callback_uri);
     }
+  }
+  else {
+    ThrowUserError('auth_delegation_post_error', {code => $resp->code});
+  }
 }
 else {
-    $args{token} = issue_auth_delegation_token($callback);
+  $args{token} = issue_auth_delegation_token($callback);
 
-    print $cgi->header();
-    $template->process("account/auth/delegation.html.tmpl", \%args)
-      or ThrowTemplateError($template->error());
+  print $cgi->header();
+  $template->process("account/auth/delegation.html.tmpl", \%args)
+    or ThrowTemplateError($template->error());
 }
index d0463735306d2f0c2e62200aba09e0eeb20b2215..1b92210e6708c22416b4f78dba2389c7cf7a3823 100755 (executable)
@@ -29,10 +29,10 @@ use Bugzilla::Hook;
 
 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'
@@ -43,26 +43,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
 # 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");
 }
 
 ################################################################################
@@ -74,19 +76,21 @@ 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");
 }
 
 my $order = $cgi->param('order') || "";
@@ -97,25 +101,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,
@@ -125,10 +130,10 @@ my $fulltext = 0;
 if ($cgi->param('content')) { $fulltext = 1 }
 my @charts = map { /^field(\d-\d-\d)$/ ? $1 : () } $cgi->multi_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;
+  }
 }
 
 ################################################################################
@@ -136,34 +141,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
@@ -185,109 +191,111 @@ sub LookupNamedQuery {
 # 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);
 }
 
 ################################################################################
@@ -301,8 +309,8 @@ 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";
+  $cmdtype   = "dorem";
+  $remaction = "run";
 }
 
 # Now we're going to be running, so ensure that the params object is set up,
@@ -314,41 +322,42 @@ $params ||= new Bugzilla::CGI($cgi);
 
 # 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 ($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
@@ -357,99 +366,110 @@ 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;
 }
 
 # 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 $params->param('format'),
-                                   scalar $params->param('ctype'));
+my $format = $template->get_format(
+  "list/list",
+  scalar $params->param('format'),
+  scalar $params->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
@@ -459,14 +479,13 @@ my $format = $template->get_format("list/list", scalar $params->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');
 
 
 # Generate a reasonable filename for the user agent to suggest to the user
@@ -476,7 +495,7 @@ my $serverpush =
 # 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');
 }
 
 ################################################################################
@@ -493,32 +512,34 @@ my $columns = Bugzilla::Search::COLUMNS;
 # 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
@@ -533,14 +554,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);
 }
 
 ################################################################################
@@ -552,13 +573,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
@@ -566,58 +588,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
 # 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);
+  }
 }
 
 ################################################################################
@@ -629,66 +644,70 @@ 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.
-    my $descending = $params->param('descending') ? " DESC" : "";
-    my %order_types = (
-        "Bug Number"   => [ "bug_id$descending" ],
-        "Importance"   => [ "priority$descending", "bug_severity" ],
-        "Assignee"     => [ "assigned_to$descending", "bug_status", "priority", "bug_id" ],
-        "Last Changed" => [ "changeddate$descending", "bug_status", "priority",
-                            "assigned_to", "bug_id" ],
-    );
-    if ($order_types{$order}) {
-        @order_columns = @{ $order_types{$order} };
-    }
-    else {
-        @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.
+  my $descending = $params->param('descending') ? " DESC" : "";
+  my %order_types = (
+    "Bug Number" => ["bug_id$descending"],
+    "Importance" => ["priority$descending", "bug_severity"],
+    "Assignee"   => ["assigned_to$descending", "bug_status", "priority", "bug_id"],
+    "Last Changed" =>
+      ["changeddate$descending", "bug_status", "priority", "assigned_to", "bug_id"],
+  );
+  if ($order_types{$order}) {
+    @order_columns = @{$order_types{$order}};
+  }
+  else {
+    @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
@@ -702,22 +721,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
@@ -734,24 +753,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});
     }
+  }
 }
 
 ################################################################################
@@ -763,72 +783,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'
@@ -836,38 +858,40 @@ 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;
 }
 
 ################################################################################
@@ -876,45 +900,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')
-    && Bugzilla->params->{'use_email_as_login'})
+if ( scalar(@bugowners) > 1
+  && $user->in_group('editbugs')
+  && Bugzilla->params->{'use_email_as_login'})
 {
-    $vars->{'bugowners'} = join(",", @bugowners);
+  $vars->{'bugowners'} = join(",", @bugowners);
 }
 
 # Whether or not to split the column titles across two rows to make
@@ -922,7 +946,7 @@ if (scalar(@bugowners) > 1
 $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());
@@ -932,18 +956,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->multi_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
 # 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
@@ -951,50 +977,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->multi_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
@@ -1003,67 +1029,71 @@ 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
@@ -1084,40 +1114,40 @@ 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
 ################################################################################
 
-Bugzilla::Hook::process("buglist_format", {'vars' => $vars,
-                                           'format' => $format,
-                                           'params' => $params});
+Bugzilla::Hook::process("buglist_format",
+  {'vars' => $vars, 'format' => $format, 'params' => $params});
 
 # Generate and return the UI (HTML page) from the appropriate template.
 $template->process($format->{'template'}, $vars)
index b3a52245cb1836097febf97bd092ac19bf340e2c..5fa6a2dfb71f3519a1fe8f8bdea4b439cab77b91 100755 (executable)
--- 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->multi_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->multi_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 = '&amp;' . 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 = '&amp;' . 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->multi_param("name"));
+  my @series_ids = grep(/^\d+$/, $cgi->multi_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+)$/a ? $1 : () } $cgi->multi_param();
+  my @ids = map { /^select(\d+)$/a ? $1 : () } $cgi->multi_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());
 }
index 7ce9673ceda69d2c35fdd5cabf0ca5f7c4bd5424..a63e53dce5bdcc3b8aa40180a0da7ea96b123b9a 100755 (executable)
@@ -25,14 +25,15 @@ our @BUGZILLA_INC = grep { !/checksetup_lib/ } @INC;
 
 use Getopt::Long qw(:config bundling);
 use Pod::Usage;
+
 # Bug 1270550 - Tie::Hash::NamedCapture must be loaded before Safe.
 use Tie::Hash::NamedCapture;
 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
@@ -46,16 +47,18 @@ Bugzilla::Install::Util::no_checksetup_from_cgi() if $ENV{'SERVER_SOFTWARE'};
 init_console();
 
 my %switch;
-GetOptions(\%switch, 'help|h|?',
-                     'no-templates|t', 'verbose|v|no-silent',
-                     'cpanm:s', 'check-modules',
-                     'make-admin=s', 'reset-password=s', 'version|V',
-                     'no-permissions|p');
+GetOptions(
+  \%switch,         'help|h|?',
+  'no-templates|t', 'verbose|v|no-silent',
+  'cpanm:s',        'check-modules',
+  'make-admin=s',   'reset-password=s',
+  'version|V',      'no-permissions|p'
+);
 
 # 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'};
@@ -63,32 +66,37 @@ print(install_string('header', get_version_and_os()) . "\n") unless $silent;
 exit 0 if $switch{'version'};
 
 if (defined $switch{cpanm}) {
-    my $default = join(' ', qw(
-        all notest -oracle -mysql -pg -mod_perl -old_charts -new_charts
-        -graphical_reports -detect_charset -auth_radius -auth_ldap
-    ));
-    my @features = split(/\s+/, $switch{cpanm} || $default);
-    my @cpanm_args = ('-l', 'local', '--installdeps');
-    while (my $feature = shift @features) {
-        if ($feature eq 'all') {
-            push @cpanm_args, '--with-all-features';
-        }
-        elsif ($feature eq 'default') {
-            unshift @features, split(/\s+/, $default);
-        }
-        elsif ($feature eq 'notest' || $feature eq 'skip-satisfied' || $feature eq 'quiet') {
-            push @cpanm_args, "--$feature";
-        }
-        elsif ($feature =~ /^-(.+)$/) {
-            push @cpanm_args, "--without-feature=$1";
-        }
-        else {
-            push @cpanm_args, "--with-feature=$feature";
-        }
+  my $default = join(
+    ' ', qw(
+      all notest -oracle -mysql -pg -mod_perl -old_charts -new_charts
+      -graphical_reports -detect_charset -auth_radius -auth_ldap
+      )
+  );
+  my @features = split(/\s+/, $switch{cpanm} || $default);
+  my @cpanm_args = ('-l', 'local', '--installdeps');
+  while (my $feature = shift @features) {
+    if ($feature eq 'all') {
+      push @cpanm_args, '--with-all-features';
+    }
+    elsif ($feature eq 'default') {
+      unshift @features, split(/\s+/, $default);
+    }
+    elsif ($feature eq 'notest'
+      || $feature eq 'skip-satisfied'
+      || $feature eq 'quiet')
+    {
+      push @cpanm_args, "--$feature";
+    }
+    elsif ($feature =~ /^-(.+)$/) {
+      push @cpanm_args, "--without-feature=$1";
     }
-    print "cpanm @cpanm_args \".\"\n" if !$silent;
-    my $rv = system('cpanm', @cpanm_args, '.');
-    exit 1 if $rv != 0;
+    else {
+      push @cpanm_args, "--with-feature=$feature";
+    }
+  }
+  print "cpanm @cpanm_args \".\"\n" if !$silent;
+  my $rv = system('cpanm', @cpanm_args, '.');
+  exit 1 if $rv != 0;
 }
 
 $ENV{PERL_MM_USE_DEFAULT} = 1;
@@ -97,10 +105,10 @@ system($^X, "Makefile.PL");
 
 my $meta = load_cpan_meta();
 if (keys %{$meta->{optional_features}} < 1) {
-    warn "Your version of ExtUtils::MakeMaker is probably too old\n";
-    warn "Falling back to static (and wrong) META.json\n";
-    unlink('MYMETA.json');
-    $meta = load_cpan_meta();
+  warn "Your version of ExtUtils::MakeMaker is probably too old\n";
+  warn "Falling back to static (and wrong) META.json\n";
+  unlink('MYMETA.json');
+  $meta = load_cpan_meta();
 }
 my $requirements = check_cpan_requirements($meta, \@BUGZILLA_INC, !$silent);
 
@@ -132,7 +140,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;
@@ -146,8 +154,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;
 
 ###########################################################################
@@ -163,8 +171,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 <select> fields.
 $dbh->bz_populate_enum_tables();
 
@@ -172,7 +182,7 @@ $dbh->bz_populate_enum_tables();
 # Check --DATA-- directory
 ###########################################################################
 
-update_filesystem({ index_html => $lc_hash->{'index_html'} });
+update_filesystem({index_html => $lc_hash->{'index_html'}});
 create_htaccess() if $lc_hash->{'create_htaccess'};
 
 # Remove parameters from the params file that no longer exist in Bugzilla,
@@ -184,7 +194,7 @@ my %old_params = update_params();
 ###########################################################################
 
 Bugzilla::Template::precompile_templates(!$silent)
-    unless $switch{'no-templates'};
+  unless $switch{'no-templates'};
 
 ###########################################################################
 # Set proper rights (--CHMOD--)
@@ -249,7 +259,7 @@ Bugzilla::Install::make_admin($switch{'make-admin'}) if $switch{'make-admin'};
 Bugzilla::Install::create_admin();
 
 Bugzilla::Install::reset_password($switch{'reset-password'})
-    if $switch{'reset-password'};
+  if $switch{'reset-password'};
 
 ###########################################################################
 # Create default Product
@@ -257,7 +267,7 @@ Bugzilla::Install::reset_password($switch{'reset-password'})
 
 Bugzilla::Install::create_default_product();
 
-Bugzilla::Hook::process('install_before_final_checks', { silent => $silent });
+Bugzilla::Hook::process('install_before_final_checks', {silent => $silent});
 
 ###########################################################################
 # Final checks
@@ -267,13 +277,12 @@ Bugzilla::Hook::process('install_before_final_checks', { silent => $silent });
 Bugzilla->memcached->clear_all();
 
 # Check if the default parameter for urlbase is still set, and if so, give
-# notification that they should go and visit editparams.cgi 
+# notification that they should go and visit editparams.cgi
 if (Bugzilla->params->{'urlbase'} eq '') {
-    print "\n" . get_text('install_urlbase_default') . "\n"
-        unless $silent;
+  print "\n" . get_text('install_urlbase_default') . "\n" unless $silent;
 }
 if (!$silent) {
-    success(get_text('install_success'));
+  success(get_text('install_success'));
 }
 
 __END__
index 4a6661dcee1c5b632ea6ea35f82c1e1e80ba7b72..85bc93e91e33daf8d32870c78cfbd4eee6943562 100755 (executable)
@@ -31,8 +31,6 @@ Bugzilla->usage_mode(USAGE_MODE_CMDLINE);
 
 my $dbh = Bugzilla->dbh;
 my $sql = 'DELETE FROM bug_user_last_visit WHERE last_visit_ts < '
-  . $dbh->sql_date_math('NOW()',
-                        '-',
-                        Bugzilla->params->{last_visit_keep_days},
-                        'DAY');
+  . $dbh->sql_date_math('NOW()', '-', Bugzilla->params->{last_visit_keep_days},
+  'DAY');
 $dbh->do($sql);
index f2da452de140ca1c652e869ca42209720cefc80e..84bff074fe7046a191ad3490ad1a2c51d0926e08 100755 (executable)
@@ -24,147 +24,155 @@ use Storable qw(dclone);
 
 # Maps parameters that control columns to the names of columns.
 use constant COLUMN_PARAMS => {
-    'useclassification'   => ['classification'],
-    'usetargetmilestone'  => ['target_milestone'],
-    'useqacontact'        => ['qa_contact', 'qa_contact_realname'],
-    'usestatuswhiteboard' => ['status_whiteboard'],
-    'timetrackinggroup'   => ['deadline'],
+  'useclassification'   => ['classification'],
+  'usetargetmilestone'  => ['target_milestone'],
+  'useqacontact'        => ['qa_contact', 'qa_contact_realname'],
+  'usestatuswhiteboard' => ['status_whiteboard'],
+  'timetrackinggroup'   => ['deadline'],
 };
 
 # We only show these columns if an object of this type exists in the
 # database.
-use constant COLUMN_CLASSES => {
-    'Bugzilla::Flag'    => 'flagtypes.name',
-    'Bugzilla::Keyword' => 'keywords',
-};
+use constant COLUMN_CLASSES =>
+  {'Bugzilla::Flag' => 'flagtypes.name', 'Bugzilla::Keyword' => 'keywords',};
 
 my $user = Bugzilla->login();
 
-my $cgi = Bugzilla->cgi;
+my $cgi      = Bugzilla->cgi;
 my $template = Bugzilla->template;
-my $vars = {};
+my $vars     = {};
 
 my $columns = dclone(Bugzilla::Search::COLUMNS);
 
 # You can't manually select "relevance" as a column you want to see.
 delete $columns->{'relevance'};
 
-foreach my $param (keys %{ COLUMN_PARAMS() }) {
-    next if Bugzilla->params->{$param};
-    foreach my $column (@{ COLUMN_PARAMS->{$param} }) {
-        delete $columns->{$column};
-    }
+foreach my $param (keys %{COLUMN_PARAMS()}) {
+  next if Bugzilla->params->{$param};
+  foreach my $column (@{COLUMN_PARAMS->{$param}}) {
+    delete $columns->{$column};
+  }
 }
 
-foreach my $class (keys %{ COLUMN_CLASSES() }) {
-    eval("use $class; 1;") || die $@;
-    my $column = COLUMN_CLASSES->{$class};
-    delete $columns->{$column} if !$class->any_exist;
+foreach my $class (keys %{COLUMN_CLASSES()}) {
+  eval("use $class; 1;") || die $@;
+  my $column = COLUMN_CLASSES->{$class};
+  delete $columns->{$column} if !$class->any_exist;
 }
 
 if (!$user->is_timetracker) {
-    foreach my $column (TIMETRACKING_FIELDS) {
-        delete $columns->{$column};
-    }
+  foreach my $column (TIMETRACKING_FIELDS) {
+    delete $columns->{$column};
+  }
 }
 
 $vars->{'columns'} = $columns;
 
 my @collist;
 if (my $rememberedquery = $cgi->param('rememberedquery')) {
-    my $search;
-    if (my $saved_search = $cgi->param('saved_search')) {
-        $search = new Bugzilla::Search::Saved($saved_search);
-    }
-
-    my $token = $cgi->param('token');
-    if ($search) {
-        check_hash_token($token, [$search->id, $search->name]);
-    }
-    else {
-        check_hash_token($token, ['default-list']);
-    }
-
-    my $splitheader = 0;
-    if (defined $cgi->param('resetit')) {
-        @collist = DEFAULT_COLUMN_LIST;
-    } else {
-        if (defined $cgi->param("selected_columns")) {
-            @collist = grep { exists $columns->{$_} } 
-                            $cgi->multi_param("selected_columns");
-        }
-        if (defined $cgi->param('splitheader')) {
-            $splitheader = $cgi->param('splitheader')? 1: 0;
-        }
-    }
-    my $list = join(" ", @collist);
-
-    if ($list) {
-        # Only set the cookie if this is not a saved search.
-        # Saved searches have their own column list
-        if (!$cgi->param('save_columns_for_search')) {
-            $cgi->send_cookie(-name => 'COLUMNLIST',
-                              -value => $list,
-                              -expires => 'Fri, 01-Jan-2038 00:00:00 GMT');
-        }
-    }
-    else {
-        $cgi->remove_cookie('COLUMNLIST');
-    }
-    if ($splitheader) {
-        $cgi->send_cookie(-name => 'SPLITHEADER',
-                          -value => $splitheader,
-                          -expires => 'Fri, 01-Jan-2038 00:00:00 GMT');
+  my $search;
+  if (my $saved_search = $cgi->param('saved_search')) {
+    $search = new Bugzilla::Search::Saved($saved_search);
+  }
+
+  my $token = $cgi->param('token');
+  if ($search) {
+    check_hash_token($token, [$search->id, $search->name]);
+  }
+  else {
+    check_hash_token($token, ['default-list']);
+  }
+
+  my $splitheader = 0;
+  if (defined $cgi->param('resetit')) {
+    @collist = DEFAULT_COLUMN_LIST;
+  }
+  else {
+    if (defined $cgi->param("selected_columns")) {
+      @collist = grep { exists $columns->{$_} } $cgi->multi_param("selected_columns");
     }
-    else {
-        $cgi->remove_cookie('SPLITHEADER');
+    if (defined $cgi->param('splitheader')) {
+      $splitheader = $cgi->param('splitheader') ? 1 : 0;
     }
-
-    $vars->{'message'} = "change_columns";
-
-    if ($cgi->param('save_columns_for_search')
-        && defined $search && $search->user->id == $user->id)
-    {
-        my $params = new Bugzilla::CGI($search->url);
-        $params->param('columnlist', join(",", @collist));
-        $search->set_url($params->query_string());
-        $search->update();
+  }
+  my $list = join(" ", @collist);
+
+  if ($list) {
+
+    # Only set the cookie if this is not a saved search.
+    # Saved searches have their own column list
+    if (!$cgi->param('save_columns_for_search')) {
+      $cgi->send_cookie(
+        -name    => 'COLUMNLIST',
+        -value   => $list,
+        -expires => 'Fri, 01-Jan-2038 00:00:00 GMT'
+      );
     }
-
-    utf8::decode($rememberedquery);
-    my $params = new Bugzilla::CGI($rememberedquery);
+  }
+  else {
+    $cgi->remove_cookie('COLUMNLIST');
+  }
+  if ($splitheader) {
+    $cgi->send_cookie(
+      -name    => 'SPLITHEADER',
+      -value   => $splitheader,
+      -expires => 'Fri, 01-Jan-2038 00:00:00 GMT'
+    );
+  }
+  else {
+    $cgi->remove_cookie('SPLITHEADER');
+  }
+
+  $vars->{'message'} = "change_columns";
+
+  if ( $cgi->param('save_columns_for_search')
+    && defined $search
+    && $search->user->id == $user->id)
+  {
+    my $params = new Bugzilla::CGI($search->url);
     $params->param('columnlist', join(",", @collist));
-    $vars->{'redirect_url'} = "buglist.cgi?".$params->query_string();
-
-
-    # If we're running on Microsoft IIS, $cgi->redirect discards
-    # the Set-Cookie lines. In mod_perl, $cgi->redirect with cookies
-    # causes the page to be rendered as text/plain.
-    # Workaround is to use the old-fashioned  redirection mechanism. 
-    # See bug 214466 and bug 376044 for details.
-    if ($ENV{'MOD_PERL'} 
-        || $ENV{'SERVER_SOFTWARE'} =~ /Microsoft-IIS/
-        || $ENV{'SERVER_SOFTWARE'} =~ /Sun ONE Web/)
-    {
-      print $cgi->header(-type => "text/html",
-                         -refresh => "0; URL=$vars->{'redirect_url'}");
-    }
-    else {
-      print $cgi->redirect($vars->{'redirect_url'});
-      exit;
-    }
-    
-    $template->process("global/message.html.tmpl", $vars)
-      || ThrowTemplateError($template->error());
+    $search->set_url($params->query_string());
+    $search->update();
+  }
+
+  utf8::decode($rememberedquery);
+  my $params = new Bugzilla::CGI($rememberedquery);
+  $params->param('columnlist', join(",", @collist));
+  $vars->{'redirect_url'} = "buglist.cgi?" . $params->query_string();
+
+
+  # If we're running on Microsoft IIS, $cgi->redirect discards
+  # the Set-Cookie lines. In mod_perl, $cgi->redirect with cookies
+  # causes the page to be rendered as text/plain.
+  # Workaround is to use the old-fashioned  redirection mechanism.
+  # See bug 214466 and bug 376044 for details.
+  if ( $ENV{'MOD_PERL'}
+    || $ENV{'SERVER_SOFTWARE'} =~ /Microsoft-IIS/
+    || $ENV{'SERVER_SOFTWARE'} =~ /Sun ONE Web/)
+  {
+    print $cgi->header(
+      -type    => "text/html",
+      -refresh => "0; URL=$vars->{'redirect_url'}"
+    );
+  }
+  else {
+    print $cgi->redirect($vars->{'redirect_url'});
     exit;
+  }
+
+  $template->process("global/message.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
+  exit;
 }
 
 if (defined $cgi->param('columnlist')) {
-    @collist = split(/[ ,]+/, $cgi->param('columnlist'));
-} elsif (defined $cgi->cookie('COLUMNLIST')) {
-    @collist = split(/ /, $cgi->cookie('COLUMNLIST'));
-} else {
-    @collist = DEFAULT_COLUMN_LIST;
+  @collist = split(/[ ,]+/, $cgi->param('columnlist'));
+}
+elsif (defined $cgi->cookie('COLUMNLIST')) {
+  @collist = split(/ /, $cgi->cookie('COLUMNLIST'));
+}
+else {
+  @collist = DEFAULT_COLUMN_LIST;
 }
 
 $vars->{'collist'} = \@collist;
@@ -174,12 +182,12 @@ $vars->{'buffer'} = $cgi->query_string();
 
 my $search;
 if (defined $cgi->param('query_based_on')) {
-    my $searches = $user->queries;
-    my ($search) = grep($_->name eq $cgi->param('query_based_on'), @$searches);
+  my $searches = $user->queries;
+  my ($search) = grep($_->name eq $cgi->param('query_based_on'), @$searches);
 
-    if ($search) {
-        $vars->{'saved_search'} = $search;
-    }
+  if ($search) {
+    $vars->{'saved_search'} = $search;
+  }
 }
 
 # Generate and return the UI (HTML page) from the appropriate template.
index 528df752945ddbb164f8e3daec6365302877cbb0..59aa72a4f70c74d2da4a84a8cf6e672d8458b294 100755 (executable)
@@ -37,7 +37,7 @@ pod2usage({-verbose => 1, -exitval => 1}) if $switch{'help'};
 # in the regenerate mode).
 $| = 1;
 
-my $datadir = bz_locations()->{'datadir'};
+my $datadir   = bz_locations()->{'datadir'};
 my $graphsdir = bz_locations()->{'graphsdir'};
 
 # We use a dummy product instance with ID 0, representing all products
@@ -47,10 +47,11 @@ bless($product_all, 'Bugzilla::Product');
 # Tidy up after graphing module
 my $cwd = Cwd::getcwd();
 if (chdir($graphsdir)) {
-    unlink <./*.gif>;
-    unlink <./*.png>;
-    # chdir("..") doesn't work if graphs is a symlink, see bug 429378
-    chdir($cwd);
+  unlink <./*.gif>;
+  unlink <./*.png>;
+
+  # chdir("..") doesn't work if graphs is a symlink, see bug 429378
+  chdir($cwd);
 }
 
 my $dbh = Bugzilla->switch_to_shadow_db();
@@ -60,9 +61,9 @@ my $dbh = Bugzilla->switch_to_shadow_db();
 # may have existed in the past, or have been renamed. We want them all.
 my $fields = {};
 foreach my $field ('bug_status', 'resolution') {
-    my $values = get_legal_field_values($field);
-    my $old_values = $dbh->selectcol_arrayref(
-                             "SELECT bugs_activity.added
+  my $values     = get_legal_field_values($field);
+  my $old_values = $dbh->selectcol_arrayref(
+    "SELECT bugs_activity.added
                                 FROM bugs_activity
                           INNER JOIN fielddefs
                                   ON fielddefs.id = bugs_activity.fieldid
@@ -80,15 +81,16 @@ foreach my $field ('bug_status', 'resolution') {
                            LEFT JOIN $field
                                   ON $field.value = bugs_activity.removed
                                WHERE fielddefs.name = ?
-                                 AND $field.id IS NULL",
-                               undef, ($field, $field));
+                                 AND $field.id IS NULL", undef, ($field, $field)
+  );
 
-    push(@$values, @$old_values);
-    $fields->{$field} = $values;
+  push(@$values, @$old_values);
+  $fields->{$field} = $values;
 }
 
-my @statuses = @{$fields->{'bug_status'}};
+my @statuses    = @{$fields->{'bug_status'}};
 my @resolutions = @{$fields->{'resolution'}};
+
 # Exclude "" from the resolution list.
 @resolutions = grep {$_} @resolutions;
 
@@ -97,30 +99,34 @@ my @resolutions = @{$fields->{'resolution'}};
 # at once and stuff it into some data structures.
 my (%bug_status, %bug_resolution, %removed);
 if ($switch{'regenerate'}) {
-    %bug_resolution = @{ $dbh->selectcol_arrayref(
-        'SELECT bug_id, resolution FROM bugs', {Columns=>[1,2]}) };
-    %bug_status = @{ $dbh->selectcol_arrayref(
-        'SELECT bug_id, bug_status FROM bugs', {Columns=>[1,2]}) };
-
-    my $removed_sth = $dbh->prepare(
+  %bug_resolution = @{
+    $dbh->selectcol_arrayref('SELECT bug_id, resolution FROM bugs',
+      {Columns => [1, 2]})
+  };
+  %bug_status = @{
+    $dbh->selectcol_arrayref('SELECT bug_id, bug_status FROM bugs',
+      {Columns => [1, 2]})
+  };
+
+  my $removed_sth = $dbh->prepare(
         q{SELECT bugs_activity.bug_id, bugs_activity.removed,}
-        . $dbh->sql_to_days('bugs_activity.bug_when')
-       . q{ FROM bugs_activity
+      . $dbh->sql_to_days('bugs_activity.bug_when')
+      . q{ FROM bugs_activity
            WHERE bugs_activity.fieldid = ?
-        ORDER BY bugs_activity.bug_when});
-
-    %removed = (bug_status => {}, resolution => {});
-    foreach my $field (qw(bug_status resolution)) {
-        my $field_id = Bugzilla::Field->check($field)->id;
-        my $rows = $dbh->selectall_arrayref($removed_sth, undef, $field_id);
-        my $hash = $removed{$field};
-        foreach my $row (@$rows) {
-            my ($bug_id, $removed, $when) = @$row;
-            $hash->{$bug_id} ||= [];
-            push(@{ $hash->{$bug_id} }, { when    => int($when),
-                                          removed => $removed });
-        }
+        ORDER BY bugs_activity.bug_when}
+  );
+
+  %removed = (bug_status => {}, resolution => {});
+  foreach my $field (qw(bug_status resolution)) {
+    my $field_id = Bugzilla::Field->check($field)->id;
+    my $rows     = $dbh->selectall_arrayref($removed_sth, undef, $field_id);
+    my $hash     = $removed{$field};
+    foreach my $row (@$rows) {
+      my ($bug_id, $removed, $when) = @$row;
+      $hash->{$bug_id} ||= [];
+      push(@{$hash->{$bug_id}}, {when => int($when), removed => $removed});
     }
+  }
 }
 
 my $tstart = time;
@@ -130,82 +136,85 @@ unshift(@myproducts, $product_all);
 
 my $dir = "$datadir/mining";
 if (!-d $dir) {
-    mkdir $dir or die "mkdir $dir failed: $!";
-    fix_dir_permissions($dir);
+  mkdir $dir or die "mkdir $dir failed: $!";
+  fix_dir_permissions($dir);
 }
 
 foreach (@myproducts) {
-    if ($switch{'regenerate'}) {
-        regenerate_stats($dir, $_, \%bug_resolution, \%bug_status, \%removed);
-    } else {
-        &collect_stats($dir, $_);
-    }
+  if ($switch{'regenerate'}) {
+    regenerate_stats($dir, $_, \%bug_resolution, \%bug_status, \%removed);
+  }
+  else {
+    &collect_stats($dir, $_);
+  }
 }
+
 # Fix permissions for all files in mining/.
 fix_dir_permissions($dir);
 
 my $tend = time;
+
 # Uncomment the following line for performance testing.
 #say "Total time taken " . delta_time($tstart, $tend);
 
 CollectSeriesData();
 
 sub collect_stats {
-    my $dir = shift;
-    my $product = shift;
-    my $when = localtime (time);
-    my $dbh = Bugzilla->dbh;
-
-    my $file = join '/', $dir, $product->id;
-    my $exists = -f $file;
-
-    # if the file exists, get the old status and resolution list for that product.
-    my @data;
-    @data = get_old_data($file) if $exists;
-
-    # If @data is not empty, then we have to recreate the data file.
-    if (scalar(@data)) {
-        open(DATA, '>', $file)
-          || ThrowCodeError('chart_file_open_fail', {'filename' => $file});
-    }
-    else {
-        open(DATA, '>>', $file)
-          || ThrowCodeError('chart_file_open_fail', {'filename' => $file});
-    }
+  my $dir     = shift;
+  my $product = shift;
+  my $when    = localtime(time);
+  my $dbh     = Bugzilla->dbh;
 
-    binmode DATA, ':utf8';
+  my $file = join '/', $dir, $product->id;
+  my $exists = -f $file;
 
-    # Now collect current data.
-    my @row = (today());
-    my $status_sql = q{SELECT COUNT(*) FROM bugs WHERE bug_status = ?};
-    my $reso_sql   = q{SELECT COUNT(*) FROM bugs WHERE resolution = ?};
-
-    if ($product->id) {
-        $status_sql .= q{ AND product_id = ?};
-        $reso_sql   .= q{ AND product_id = ?};
-    }
+  # if the file exists, get the old status and resolution list for that product.
+  my @data;
+  @data = get_old_data($file) if $exists;
 
-    my $sth_status = $dbh->prepare($status_sql);
-    my $sth_reso   = $dbh->prepare($reso_sql);
-
-    my @values ;
-    foreach my $status (@statuses) {
-        @values = ($status);
-        push (@values, $product->id) if ($product->id);
-        my $count = $dbh->selectrow_array($sth_status, undef, @values);
-        push(@row, $count);
-    }
-    foreach my $resolution (@resolutions) {
-        @values = ($resolution);
-        push (@values, $product->id) if ($product->id);
-        my $count = $dbh->selectrow_array($sth_reso, undef, @values);
-        push(@row, $count);
-    }
-
-    if (!$exists || scalar(@data)) {
-        my $fields = join('|', ('DATE', @statuses, @resolutions));
-        my $product_name = $product->name;
-        print DATA <<FIN;
+  # If @data is not empty, then we have to recreate the data file.
+  if (scalar(@data)) {
+    open(DATA, '>', $file)
+      || ThrowCodeError('chart_file_open_fail', {'filename' => $file});
+  }
+  else {
+    open(DATA, '>>', $file)
+      || ThrowCodeError('chart_file_open_fail', {'filename' => $file});
+  }
+
+  binmode DATA, ':utf8';
+
+  # Now collect current data.
+  my @row        = (today());
+  my $status_sql = q{SELECT COUNT(*) FROM bugs WHERE bug_status = ?};
+  my $reso_sql   = q{SELECT COUNT(*) FROM bugs WHERE resolution = ?};
+
+  if ($product->id) {
+    $status_sql .= q{ AND product_id = ?};
+    $reso_sql   .= q{ AND product_id = ?};
+  }
+
+  my $sth_status = $dbh->prepare($status_sql);
+  my $sth_reso   = $dbh->prepare($reso_sql);
+
+  my @values;
+  foreach my $status (@statuses) {
+    @values = ($status);
+    push(@values, $product->id) if ($product->id);
+    my $count = $dbh->selectrow_array($sth_status, undef, @values);
+    push(@row, $count);
+  }
+  foreach my $resolution (@resolutions) {
+    @values = ($resolution);
+    push(@values, $product->id) if ($product->id);
+    my $count = $dbh->selectrow_array($sth_reso, undef, @values);
+    push(@row, $count);
+  }
+
+  if (!$exists || scalar(@data)) {
+    my $fields = join('|', ('DATE', @statuses, @resolutions));
+    my $product_name = $product->name;
+    print DATA <<FIN;
 # Bugzilla Daily Bug Stats
 #
 # Do not edit me! This file is generated.
@@ -214,101 +223,107 @@ sub collect_stats {
 # Product: $product_name
 # Created: $when
 FIN
-    }
-
-    # Add existing data, if needed. Note that no count is not treated
-    # the same way as a count with 0 bug.
-    foreach my $data (@data) {
-        print DATA join('|', map {defined $data->{$_} ? $data->{$_} : ''}
-                                 ('DATE', @statuses, @resolutions)) . "\n";
-    }
-    print DATA (join '|', @row) . "\n";
-    close DATA;
+  }
+
+  # Add existing data, if needed. Note that no count is not treated
+  # the same way as a count with 0 bug.
+  foreach my $data (@data) {
+    print DATA join('|',
+      map { defined $data->{$_} ? $data->{$_} : '' }
+        ('DATE', @statuses, @resolutions))
+      . "\n";
+  }
+  print DATA (join '|', @row) . "\n";
+  close DATA;
 }
 
 sub get_old_data {
-    my $file = shift;
-
-    open(DATA, '<', $file)
-      || ThrowCodeError('chart_file_open_fail', {'filename' => $file});
-
-    binmode DATA, ':utf8';
-
-    my @data;
-    my @columns;
-    my $recreate = 0;
-    while (<DATA>) {
-        chomp;
-        next unless $_;
-        if (/^# fields?:\s*(.+)\s*$/) {
-            @columns = split(/\|/, $1);
-            # Compare this list with @statuses and @resolutions.
-            # If they are identical, then we can safely append new data
-            # to the end of the file; else we have to recreate it.
-            $recreate = 1;
-            my @new_cols = ($columns[0], @statuses, @resolutions);
-            if (scalar(@columns) == scalar(@new_cols)) {
-                my $identical = 1;
-                for (0 .. $#columns) {
-                    $identical = 0 if ($columns[$_] ne $new_cols[$_]);
-                }
-                last if $identical;
-            }
+  my $file = shift;
+
+  open(DATA, '<', $file)
+    || ThrowCodeError('chart_file_open_fail', {'filename' => $file});
+
+  binmode DATA, ':utf8';
+
+  my @data;
+  my @columns;
+  my $recreate = 0;
+  while (<DATA>) {
+    chomp;
+    next unless $_;
+    if (/^# fields?:\s*(.+)\s*$/) {
+      @columns = split(/\|/, $1);
+
+      # Compare this list with @statuses and @resolutions.
+      # If they are identical, then we can safely append new data
+      # to the end of the file; else we have to recreate it.
+      $recreate = 1;
+      my @new_cols = ($columns[0], @statuses, @resolutions);
+      if (scalar(@columns) == scalar(@new_cols)) {
+        my $identical = 1;
+        for (0 .. $#columns) {
+          $identical = 0 if ($columns[$_] ne $new_cols[$_]);
         }
-        next unless $recreate;
-        next if (/^#/); # Ignore comments.
-        # If we have to recreate the file, we have to load all existing
-        # data first.
-        my @line = split /\|/;
-        my %data;
-        foreach my $column (@columns) {
-            $data{$column} = shift @line;
-        }
-        push(@data, \%data);
+        last if $identical;
+      }
+    }
+    next unless $recreate;
+    next if (/^#/);  # Ignore comments.
+                     # If we have to recreate the file, we have to load all existing
+                     # data first.
+    my @line = split /\|/;
+    my %data;
+    foreach my $column (@columns) {
+      $data{$column} = shift @line;
     }
-    close(DATA);
-    return @data;
+    push(@data, \%data);
+  }
+  close(DATA);
+  return @data;
 }
 
 # This regenerates all statistics from the database.
 sub regenerate_stats {
-    my ($dir, $product, $bug_resolution, $bug_status, $removed) = @_;
-
-    my $dbh = Bugzilla->dbh;
-    my $when = localtime(time());
-    my $tstart = time();
-
-    my $file = join '/', $dir, $product->id;
-
-    my $and_product = "";
-
-    my @values = ();
-    if ($product->id) {
-        $and_product = q{ AND product_id = ?};
-        push (@values, $product->id);
-    }
-
-    # Determine the start date from the date the first bug in the
-    # database was created, and the end date from the current day.
-    # If there were no bugs in the search, return early.
-    my $query = q{SELECT } .
-                $dbh->sql_to_days('creation_ts') . q{ AS start_day, } .
-                $dbh->sql_to_days('current_date') . q{ AS end_day, } . 
-                $dbh->sql_to_days("'1970-01-01'") . 
-                 qq{ FROM bugs
-                   WHERE } . $dbh->sql_to_days('creation_ts') . 
-                         qq{ IS NOT NULL $and_product 
+  my ($dir, $product, $bug_resolution, $bug_status, $removed) = @_;
+
+  my $dbh    = Bugzilla->dbh;
+  my $when   = localtime(time());
+  my $tstart = time();
+
+  my $file = join '/', $dir, $product->id;
+
+  my $and_product = "";
+
+  my @values = ();
+  if ($product->id) {
+    $and_product = q{ AND product_id = ?};
+    push(@values, $product->id);
+  }
+
+  # Determine the start date from the date the first bug in the
+  # database was created, and the end date from the current day.
+  # If there were no bugs in the search, return early.
+  my $query
+    = q{SELECT }
+    . $dbh->sql_to_days('creation_ts')
+    . q{ AS start_day, }
+    . $dbh->sql_to_days('current_date')
+    . q{ AS end_day, }
+    . $dbh->sql_to_days("'1970-01-01'")
+    . qq{ FROM bugs
+                   WHERE }
+    . $dbh->sql_to_days('creation_ts') . qq{ IS NOT NULL $and_product 
                 ORDER BY start_day } . $dbh->sql_limit(1);
-    my ($start, $end, $base) = $dbh->selectrow_array($query, undef, @values);
+  my ($start, $end, $base) = $dbh->selectrow_array($query, undef, @values);
 
-    if (!defined $start) {
-        return;
-    }
+  if (!defined $start) {
+    return;
+  }
 
-    if (open DATA, ">", $file) {
-        my $fields = join('|', ('DATE', @statuses, @resolutions));
-        my $product_name = $product->name;
-        print DATA <<FIN;
+  if (open DATA, ">", $file) {
+    my $fields = join('|', ('DATE', @statuses, @resolutions));
+    my $product_name = $product->name;
+    print DATA <<FIN;
 # Bugzilla Daily Bug Stats
 #
 # Do not edit me! This file is generated.
@@ -317,66 +332,69 @@ sub regenerate_stats {
 # Product: $product_name
 # Created: $when
 FIN
-        # For each day, generate a line of statistics.
-        my $total_days = $end - $start;
-        my @bugs;
-        for (my $day = $start + 1; $day <= $end; $day++) {
-            # Some output feedback
-            my $percent_done = ($day - $start - 1) * 100 / $total_days;
-            printf "\rRegenerating %s \[\%.1f\%\%]", $product_name,
-                                                     $percent_done;
-
-            # Get a list of bugs that were created the previous day, and
-            # add those bugs to the list of bugs for this product.
-            $query = qq{SELECT bug_id 
+
+    # For each day, generate a line of statistics.
+    my $total_days = $end - $start;
+    my @bugs;
+    for (my $day = $start + 1; $day <= $end; $day++) {
+
+      # Some output feedback
+      my $percent_done = ($day - $start - 1) * 100 / $total_days;
+      printf "\rRegenerating %s \[\%.1f\%\%]", $product_name, $percent_done;
+
+      # Get a list of bugs that were created the previous day, and
+      # add those bugs to the list of bugs for this product.
+      $query = qq{SELECT bug_id 
                           FROM bugs
-                         WHERE bugs.creation_ts < } . 
-                         $dbh->sql_from_days($day - 1) . 
-                         q{ AND bugs.creation_ts >= } . 
-                         $dbh->sql_from_days($day - 2) . 
-                        $and_product . q{ ORDER BY bug_id};
-
-            my $bug_ids = $dbh->selectcol_arrayref($query, undef, @values);
-            push(@bugs, @$bug_ids);
-
-            my %bugcount;
-            foreach (@statuses) { $bugcount{$_} = 0; }
-            foreach (@resolutions) { $bugcount{$_} = 0; }
-            # Get information on bug states and resolutions.
-            for my $bug (@bugs) {
-                my $status = _get_value(
-                    $removed->{'bug_status'}->{$bug},
-                    $bug_status,  $day, $bug);
-
-                if (defined $bugcount{$status}) {
-                    $bugcount{$status}++;
-                }
-
-                my $resolution = _get_value(
-                    $removed->{'resolution'}->{$bug},
-                    $bug_resolution, $day, $bug);
-
-                if (defined $bugcount{$resolution}) {
-                    $bugcount{$resolution}++;
-                }
-            }
-
-            # Generate a line of output containing the date and counts
-            # of bugs in each state.
-            my $date = sqlday($day, $base);
-            print DATA "$date";
-            foreach (@statuses) { print DATA "|$bugcount{$_}"; }
-            foreach (@resolutions) { print DATA "|$bugcount{$_}"; }
-            print DATA "\n";
+                         WHERE bugs.creation_ts < }
+        . $dbh->sql_from_days($day - 1)
+        . q{ AND bugs.creation_ts >= }
+        . $dbh->sql_from_days($day - 2)
+        . $and_product
+        . q{ ORDER BY bug_id};
+
+      my $bug_ids = $dbh->selectcol_arrayref($query, undef, @values);
+      push(@bugs, @$bug_ids);
+
+      my %bugcount;
+      foreach (@statuses)    { $bugcount{$_} = 0; }
+      foreach (@resolutions) { $bugcount{$_} = 0; }
+
+      # Get information on bug states and resolutions.
+      for my $bug (@bugs) {
+        my $status
+          = _get_value($removed->{'bug_status'}->{$bug}, $bug_status, $day, $bug);
+
+        if (defined $bugcount{$status}) {
+          $bugcount{$status}++;
         }
 
-        # Finish up output feedback for this product.
-        my $tend = time;
-        say "\rRegenerating " . $product_name . ' [100.0%] - ' .
-            delta_time($tstart, $tend);
+        my $resolution
+          = _get_value($removed->{'resolution'}->{$bug}, $bug_resolution, $day, $bug);
 
-        close DATA;
+        if (defined $bugcount{$resolution}) {
+          $bugcount{$resolution}++;
+        }
+      }
+
+      # Generate a line of output containing the date and counts
+      # of bugs in each state.
+      my $date = sqlday($day, $base);
+      print DATA "$date";
+      foreach (@statuses)    { print DATA "|$bugcount{$_}"; }
+      foreach (@resolutions) { print DATA "|$bugcount{$_}"; }
+      print DATA "\n";
     }
+
+    # Finish up output feedback for this product.
+    my $tend = time;
+    say "\rRegenerating "
+      . $product_name
+      . ' [100.0%] - '
+      . delta_time($tstart, $tend);
+
+    close DATA;
+  }
 }
 
 # A helper for --regenerate.
@@ -384,105 +402,117 @@ FIN
 # at the beginning of the day.  If there were no status/resolution
 # changes on or after that day, the status was the same as it
 # is today (the "current" value).  Otherwise, the status was equal to the
-# first "previous value" entry in the bugs_activity table for that 
+# first "previous value" entry in the bugs_activity table for that
 # bug made on or after that day.
 sub _get_value {
-    my ($removed, $current, $day, $bug) = @_;
+  my ($removed, $current, $day, $bug) = @_;
 
-    # Get the first change that's on or after this day.
-    my $item = first { $_->{when} >= $day } @{ $removed || [] };
+  # Get the first change that's on or after this day.
+  my $item = first { $_->{when} >= $day } @{$removed || []};
 
-    # If there's no change on or after this day, then we just return the
-    # current value.
-    return $item ? $item->{removed} : $current->{$bug};
+  # If there's no change on or after this day, then we just return the
+  # current value.
+  return $item ? $item->{removed} : $current->{$bug};
 }
 
 sub today {
-    my ($dom, $mon, $year) = (localtime(time))[3, 4, 5];
-    return sprintf "%04d%02d%02d", 1900 + $year, ++$mon, $dom;
+  my ($dom, $mon, $year) = (localtime(time))[3, 4, 5];
+  return sprintf "%04d%02d%02d", 1900 + $year, ++$mon, $dom;
 }
 
 sub today_dash {
-    my ($dom, $mon, $year) = (localtime(time))[3, 4, 5];
-    return sprintf "%04d-%02d-%02d", 1900 + $year, ++$mon, $dom;
+  my ($dom, $mon, $year) = (localtime(time))[3, 4, 5];
+  return sprintf "%04d-%02d-%02d", 1900 + $year, ++$mon, $dom;
 }
 
 sub sqlday {
-    my ($day, $base) = @_;
-    $day = ($day - $base) * 86400;
-    my ($dom, $mon, $year) = (gmtime($day))[3, 4, 5];
-    return sprintf "%04d%02d%02d", 1900 + $year, ++$mon, $dom;
+  my ($day, $base) = @_;
+  $day = ($day - $base) * 86400;
+  my ($dom, $mon, $year) = (gmtime($day))[3, 4, 5];
+  return sprintf "%04d%02d%02d", 1900 + $year, ++$mon, $dom;
 }
 
 sub delta_time {
-    my $tstart = shift;
-    my $tend = shift;
-    my $delta = $tend - $tstart;
-    my $hours = int($delta/3600);
-    my $minutes = int($delta/60) - ($hours * 60);
-    my $seconds = $delta - ($minutes * 60) - ($hours * 3600);
-    return sprintf("%02d:%02d:%02d" , $hours, $minutes, $seconds);
+  my $tstart  = shift;
+  my $tend    = shift;
+  my $delta   = $tend - $tstart;
+  my $hours   = int($delta / 3600);
+  my $minutes = int($delta / 60) - ($hours * 60);
+  my $seconds = $delta - ($minutes * 60) - ($hours * 3600);
+  return sprintf("%02d:%02d:%02d", $hours, $minutes, $seconds);
 }
 
 sub CollectSeriesData {
-    # We need some way of randomising the distribution of series, such that
-    # all of the series which are to be run every 7 days don't run on the same
-    # day. This is because this might put the server under severe load if a
-    # particular frequency, such as once a week, is very common. We achieve
-    # this by only running queries when:
-    # (days_since_epoch + series_id) % frequency = 0. So they'll run every
-    # <frequency> days, but the start date depends on the series_id.
-    my $days_since_epoch = int(time() / (60 * 60 * 24));
-    my $today = today_dash();
-
-    # We save a copy of the main $dbh and then switch to the shadow and get
-    # that one too. Remember, these may be the same.
-    my $dbh = Bugzilla->switch_to_main_db();
-    my $shadow_dbh = Bugzilla->switch_to_shadow_db();
-    
-    my $serieses = $dbh->selectall_hashref("SELECT series_id, query, creator " .
-                      "FROM series " .
-                      "WHERE frequency != 0 AND " . 
-                      "MOD(($days_since_epoch + series_id), frequency) = 0",
-                      "series_id");
-
-    # We prepare the insertion into the data table, for efficiency.
-    my $sth = $dbh->prepare("INSERT INTO series_data " .
-                            "(series_id, series_date, series_value) " .
-                            "VALUES (?, " . $dbh->quote($today) . ", ?)");
-
-    # We delete from the table beforehand, to avoid SQL errors if people run
-    # collectstats.pl twice on the same day.
-    my $deletesth = $dbh->prepare("DELETE FROM series_data 
-                                   WHERE series_id = ? AND series_date = " .
-                                   $dbh->quote($today));
-                                     
-    foreach my $series_id (keys %$serieses) {
-        # We set up the user for Search.pm's permission checking - each series
-        # runs with the permissions of its creator.
-        my $user = new Bugzilla::User($serieses->{$series_id}->{'creator'});
-        my $cgi = new Bugzilla::CGI($serieses->{$series_id}->{'query'});
-        my $data;
-
-        # Do not die if Search->new() detects invalid data, such as an obsolete
-        # login name or a renamed product or component, etc.
-        eval {
-            my $search = new Bugzilla::Search('params' => scalar $cgi->Vars,
-                                              'fields' => ["bug_id"],
-                                              'allow_unlimited' => 1,
-                                              'user'   => $user);
-            $data = $search->data;
-        };
-
-        if (!$@) {
-            # We need to count the returned rows. Without subselects, we can't
-            # do this directly in the SQL for all queries. So we do it by hand.
-            my $count = scalar(@$data) || 0;
-
-            $deletesth->execute($series_id);
-            $sth->execute($series_id, $count);
-        }
+
+  # We need some way of randomising the distribution of series, such that
+  # all of the series which are to be run every 7 days don't run on the same
+  # day. This is because this might put the server under severe load if a
+  # particular frequency, such as once a week, is very common. We achieve
+  # this by only running queries when:
+  # (days_since_epoch + series_id) % frequency = 0. So they'll run every
+  # <frequency> days, but the start date depends on the series_id.
+  my $days_since_epoch = int(time() / (60 * 60 * 24));
+  my $today = today_dash();
+
+  # We save a copy of the main $dbh and then switch to the shadow and get
+  # that one too. Remember, these may be the same.
+  my $dbh        = Bugzilla->switch_to_main_db();
+  my $shadow_dbh = Bugzilla->switch_to_shadow_db();
+
+  my $serieses = $dbh->selectall_hashref(
+    "SELECT series_id, query, creator "
+      . "FROM series "
+      . "WHERE frequency != 0 AND "
+      . "MOD(($days_since_epoch + series_id), frequency) = 0",
+    "series_id"
+  );
+
+  # We prepare the insertion into the data table, for efficiency.
+  my $sth
+    = $dbh->prepare("INSERT INTO series_data "
+      . "(series_id, series_date, series_value) "
+      . "VALUES (?, "
+      . $dbh->quote($today)
+      . ", ?)");
+
+  # We delete from the table beforehand, to avoid SQL errors if people run
+  # collectstats.pl twice on the same day.
+  my $deletesth = $dbh->prepare(
+    "DELETE FROM series_data 
+                                   WHERE series_id = ? AND series_date = "
+      . $dbh->quote($today)
+  );
+
+  foreach my $series_id (keys %$serieses) {
+
+    # We set up the user for Search.pm's permission checking - each series
+    # runs with the permissions of its creator.
+    my $user = new Bugzilla::User($serieses->{$series_id}->{'creator'});
+    my $cgi  = new Bugzilla::CGI($serieses->{$series_id}->{'query'});
+    my $data;
+
+    # Do not die if Search->new() detects invalid data, such as an obsolete
+    # login name or a renamed product or component, etc.
+    eval {
+      my $search = new Bugzilla::Search(
+        'params'          => scalar $cgi->Vars,
+        'fields'          => ["bug_id"],
+        'allow_unlimited' => 1,
+        'user'            => $user
+      );
+      $data = $search->data;
+    };
+
+    if (!$@) {
+
+      # We need to count the returned rows. Without subselects, we can't
+      # do this directly in the SQL for all queries. So we do it by hand.
+      my $count = scalar(@$data) || 0;
+
+      $deletesth->execute($series_id);
+      $sth->execute($series_id, $count);
     }
+  }
 }
 
 __END__
index 32c55acf35cafd67564975cf813162d596cb89dd..13ef30bdbd834feb08eca3ea58a223b15b7dd2b0 100755 (executable)
@@ -31,60 +31,64 @@ Bugzilla->switch_to_shadow_db;
 # If the 'requirelogin' parameter is on and the user is not
 # authenticated, return empty fields.
 if (Bugzilla->params->{'requirelogin'} && !$user->id) {
-    display_data();
-    exit;
+  display_data();
+  exit;
 }
 
 # Pass a bunch of Bugzilla configuration to the templates.
 my $vars = {};
-$vars->{'priority'}  = get_legal_field_values('priority');
-$vars->{'severity'}  = get_legal_field_values('bug_severity');
-$vars->{'platform'}  = get_legal_field_values('rep_platform');
-$vars->{'op_sys'}    = get_legal_field_values('op_sys');
-$vars->{'keywords'}  = [Bugzilla::Keyword->get_all];
+$vars->{'priority'}   = get_legal_field_values('priority');
+$vars->{'severity'}   = get_legal_field_values('bug_severity');
+$vars->{'platform'}   = get_legal_field_values('rep_platform');
+$vars->{'op_sys'}     = get_legal_field_values('op_sys');
+$vars->{'keywords'}   = [Bugzilla::Keyword->get_all];
 $vars->{'resolution'} = get_legal_field_values('resolution');
-$vars->{'status'}    = get_legal_field_values('bug_status');
-$vars->{'custom_fields'} =
-    [ grep {$_->is_select} Bugzilla->active_custom_fields ];
+$vars->{'status'}     = get_legal_field_values('bug_status');
+$vars->{'custom_fields'}
+  = [grep { $_->is_select } Bugzilla->active_custom_fields];
 
 # Include a list of product objects.
 if ($cgi->param('product')) {
-    my @products = $cgi->multi_param('product');
-    foreach my $product_name (@products) {
-        # We don't use check() because config.cgi outputs mostly
-        # in XML and JS and we don't want to display an HTML error
-        # instead of that.
-        my $product = new Bugzilla::Product({ name => $product_name });
-        if ($product && $user->can_see_product($product->name)) {
-            push (@{$vars->{'products'}}, $product);
-        }
+  my @products = $cgi->multi_param('product');
+  foreach my $product_name (@products) {
+
+    # We don't use check() because config.cgi outputs mostly
+    # in XML and JS and we don't want to display an HTML error
+    # instead of that.
+    my $product = new Bugzilla::Product({name => $product_name});
+    if ($product && $user->can_see_product($product->name)) {
+      push(@{$vars->{'products'}}, $product);
     }
-} else {
-    $vars->{'products'} = $user->get_selectable_products;
+  }
+}
+else {
+  $vars->{'products'} = $user->get_selectable_products;
 }
 
 # We set the 2nd argument to 1 to also preload flag types.
 Bugzilla::Product::preload($vars->{'products'}, 1);
 
 if (Bugzilla->params->{'useclassification'}) {
-    my $class = {};
-    # Get all classifications with at least one selectable product.
-    foreach my $product (@{$vars->{'products'}}) {
-        $class->{$product->classification_id} ||= $product->classification;
-    }
-    my @classifications = sort {$a->sortkey <=> $b->sortkey
-        || lc($a->name) cmp lc($b->name)} (values %$class);
-    $vars->{'class_names'} = $class;
-    $vars->{'classifications'} = \@classifications;
+  my $class = {};
+
+  # Get all classifications with at least one selectable product.
+  foreach my $product (@{$vars->{'products'}}) {
+    $class->{$product->classification_id} ||= $product->classification;
+  }
+  my @classifications
+    = sort { $a->sortkey <=> $b->sortkey || lc($a->name) cmp lc($b->name) }
+    (values %$class);
+  $vars->{'class_names'}     = $class;
+  $vars->{'classifications'} = \@classifications;
 }
 
 # Allow consumers to specify whether or not they want flag data.
 if (defined $cgi->param('flags')) {
-    $vars->{'show_flags'} = $cgi->param('flags');
+  $vars->{'show_flags'} = $cgi->param('flags');
 }
 else {
-    # We default to sending flag data.
-    $vars->{'show_flags'} = 1;
+  # We default to sending flag data.
+  $vars->{'show_flags'} = 1;
 }
 
 # Create separate lists of open versus resolved statuses.  This should really
@@ -92,31 +96,33 @@ else {
 my @open_status;
 my @closed_status;
 foreach my $status (@{$vars->{'status'}}) {
-    is_open_state($status) ? push(@open_status, $status) 
-                           : push(@closed_status, $status);
+  is_open_state($status)
+    ? push(@open_status,   $status)
+    : push(@closed_status, $status);
 }
-$vars->{'open_status'} = \@open_status;
+$vars->{'open_status'}   = \@open_status;
 $vars->{'closed_status'} = \@closed_status;
 
 # Generate a list of fields that can be queried.
 my @fields = @{Bugzilla::Field->match({obsolete => 0})};
+
 # Exclude fields the user cannot query.
 if (!$user->is_timetracker) {
-    foreach my $tt_field (TIMETRACKING_FIELDS) {
-        @fields = grep { $_->name ne $tt_field } @fields;
-    }
+  foreach my $tt_field (TIMETRACKING_FIELDS) {
+    @fields = grep { $_->name ne $tt_field } @fields;
+  }
 }
 
 my %FIELD_PARAMS = (
-    classification    => 'useclassification',
-    target_milestone  => 'usetargetmilestone',
-    qa_contact        => 'useqacontact',
-    status_whiteboard => 'usestatuswhiteboard',
-    see_also          => 'use_see_also',
+  classification    => 'useclassification',
+  target_milestone  => 'usetargetmilestone',
+  qa_contact        => 'useqacontact',
+  status_whiteboard => 'usestatuswhiteboard',
+  see_also          => 'use_see_also',
 );
 foreach my $field (@fields) {
-    my $param = $FIELD_PARAMS{$field->name};
-    $field->{is_active} =  Bugzilla->params->{$param} if $param;
+  my $param = $FIELD_PARAMS{$field->name};
+  $field->{is_active} = Bugzilla->params->{$param} if $param;
 }
 $vars->{'field'} = \@fields;
 
@@ -124,36 +130,37 @@ display_data($vars);
 
 
 sub display_data {
-    my $vars = shift;
-
-    my $cgi      = Bugzilla->cgi;
-    my $template = Bugzilla->template;
-
-    # Determine how the user would like to receive the output; 
-    # default is JavaScript.
-    my $format = $template->get_format("config", scalar($cgi->param('format')),
-                                       scalar($cgi->param('ctype')) || "js");
-
-    # Generate the configuration data.
-    my $output;
-    $template->process($format->{'template'}, $vars, \$output)
-      || ThrowTemplateError($template->error());
-
-    # Remove leading whitespaces, to save some bandwidth.
-    $output =~ s/^\s+(?=<)//gm if $format->{'ctype'} =~ /rdf/;
-
-    # Wide characters cause md5_base64() to die.
-    my $digest_data = $output;
-    utf8::encode($digest_data) if utf8::is_utf8($digest_data);
-    my $digest = md5_base64($digest_data);
-
-    if ($cgi->check_etag($digest)) {
-        print $cgi->header(-ETag   => $digest,
-                           -status => '304 Not Modified');
-        exit;
-    }
+  my $vars = shift;
+
+  my $cgi      = Bugzilla->cgi;
+  my $template = Bugzilla->template;
+
+  # Determine how the user would like to receive the output;
+  # default is JavaScript.
+  my $format = $template->get_format(
+    "config",
+    scalar($cgi->param('format')),
+    scalar($cgi->param('ctype')) || "js"
+  );
+
+  # Generate the configuration data.
+  my $output;
+  $template->process($format->{'template'}, $vars, \$output)
+    || ThrowTemplateError($template->error());
+
+  # Remove leading whitespaces, to save some bandwidth.
+  $output =~ s/^\s+(?=<)//gm if $format->{'ctype'} =~ /rdf/;
+
+  # Wide characters cause md5_base64() to die.
+  my $digest_data = $output;
+  utf8::encode($digest_data) if utf8::is_utf8($digest_data);
+  my $digest = md5_base64($digest_data);
+
+  if ($cgi->check_etag($digest)) {
+    print $cgi->header(-ETag => $digest, -status => '304 Not Modified');
+    exit;
+  }
 
-    print $cgi->header (-ETag => $digest,
-                        -type => $format->{'ctype'});
-    print $output;
+  print $cgi->header(-ETag => $digest, -type => $format->{'ctype'});
+  print $output;
 }
index bfc2fe385e321dc3be84285f49ec48e1e1c2cae9..6806b36ebc9807b853ba11b18aba802e976ac55c 100644 (file)
@@ -36,7 +36,7 @@ use warnings;
 #######################################################################
 
 use constant BZ_ROOT_DIR => '/usr/share/bugzilla/lib';
-use constant BZ_LIB_DIR => BZ_ROOT_DIR . '/lib';
+use constant BZ_LIB_DIR  => BZ_ROOT_DIR . '/lib';
 
 #######################################################################
 #   DO NOT EDIT THE CODE BELOW, UNLESS YOU KNOW WHAT YOU ARE DOING!!  #
index dda9a6f93611be9d79dd680130ee6dada126b2b3..c6fd77eeb8ac9af5a51dcefc8a99c603b831a623 100755 (executable)
@@ -50,21 +50,22 @@ my $work_time;
 my $fetch_extension_info = 0;
 my $debug;
 
-GetOptions('help|h|?'       => \$help,
-           'uri=s'          => \$Bugzilla_uri,
-           'login:s'        => \$Bugzilla_login,
-           'password=s'     => \$Bugzilla_password,
-           'restrictlogin!' => \$Bugzilla_restrict,
-           'bug_id:s'       => \$bug_id,
-           'product_name:s' => \$product_name,
-           'create:s'       => \$create_file_name,
-           'field:s'        => \$legal_field_values,
-           'comment:s'      => \$add_comment,
-           'private:i'      => \$private,
-           'worktime:f'     => \$work_time,
-           'extension_info' => \$fetch_extension_info,
-           'debug'          => \$debug
-          ) or pod2usage({'-verbose' => 0, '-exitval' => 1});
+GetOptions(
+  'help|h|?'       => \$help,
+  'uri=s'          => \$Bugzilla_uri,
+  'login:s'        => \$Bugzilla_login,
+  'password=s'     => \$Bugzilla_password,
+  'restrictlogin!' => \$Bugzilla_restrict,
+  'bug_id:s'       => \$bug_id,
+  'product_name:s' => \$product_name,
+  'create:s'       => \$create_file_name,
+  'field:s'        => \$legal_field_values,
+  'comment:s'      => \$add_comment,
+  'private:i'      => \$private,
+  'worktime:f'     => \$work_time,
+  'extension_info' => \$fetch_extension_info,
+  'debug'          => \$debug
+) or pod2usage({'-verbose' => 0, '-exitval' => 1});
 
 =head1 OPTIONS
 
@@ -171,7 +172,7 @@ Enable tracing at the debug level of XMLRPC requests and responses if requested.
 =cut
 
 if ($debug) {
-   $proxy->import(+trace => 'debug');
+  $proxy->import(+trace => 'debug');
 }
 
 =head2 Checking Bugzilla's version
@@ -184,7 +185,8 @@ minimum required version your application needs.
 
 $soapresult = $proxy->call('Bugzilla.version');
 _die_on_fault($soapresult);
-print 'Connecting to a Bugzilla of version ' . $soapresult->result()->{version} . ".\n";
+print 'Connecting to a Bugzilla of version '
+  . $soapresult->result()->{version} . ".\n";
 
 =head2 Checking Bugzilla's timezone
 
@@ -217,22 +219,27 @@ parameter).
 =cut
 
 if (defined($Bugzilla_login)) {
-    if ($Bugzilla_login ne '') {
-        # Log in.
-        $soapresult = $proxy->call('User.login',
-                                   { login => $Bugzilla_login,
-                                     password => $Bugzilla_password,
-                                     restrict_login => $Bugzilla_restrict } );
-        $Bugzilla_token = $soapresult->result->{token};
-        _die_on_fault($soapresult);
-        print "Login successful.\n";
-    }
-    else {
-        # Log out.
-        $soapresult = $proxy->call('User.logout');
-        _die_on_fault($soapresult);
-        print "Logout successful.\n";
-    }
+  if ($Bugzilla_login ne '') {
+
+    # Log in.
+    $soapresult = $proxy->call(
+      'User.login',
+      {
+        login          => $Bugzilla_login,
+        password       => $Bugzilla_password,
+        restrict_login => $Bugzilla_restrict
+      }
+    );
+    $Bugzilla_token = $soapresult->result->{token};
+    _die_on_fault($soapresult);
+    print "Login successful.\n";
+  }
+  else {
+    # Log out.
+    $soapresult = $proxy->call('User.logout');
+    _die_on_fault($soapresult);
+    print "Logout successful.\n";
+  }
 }
 
 =head2 Getting Extension Information
@@ -242,16 +249,16 @@ Returns all the information any extensions have decided to provide to the webser
 =cut
 
 if ($fetch_extension_info) {
-    $soapresult = $proxy->call('Bugzilla.extensions', {token => $Bugzilla_token});
-    _die_on_fault($soapresult);
-    my $extensions = $soapresult->result()->{extensions};
-    foreach my $extensionname (keys(%$extensions)) {
-        print "Extension '$extensionname' information\n";
-        my $extension = $extensions->{$extensionname};
-        foreach my $data (keys(%$extension)) {
-            print '  ' . $data . ' => ' . $extension->{$data} . "\n";
-        }
+  $soapresult = $proxy->call('Bugzilla.extensions', {token => $Bugzilla_token});
+  _die_on_fault($soapresult);
+  my $extensions = $soapresult->result()->{extensions};
+  foreach my $extensionname (keys(%$extensions)) {
+    print "Extension '$extensionname' information\n";
+    my $extension = $extensions->{$extensionname};
+    foreach my $data (keys(%$extension)) {
+      print '  ' . $data . ' => ' . $extension->{$data} . "\n";
     }
+  }
 }
 
 =head2 Retrieving Bug Information
@@ -262,21 +269,22 @@ The call will return a C<Bugzilla::Bug> object.
 =cut
 
 if ($bug_id) {
-    $soapresult = $proxy->call('Bug.get', { ids => [$bug_id], token => $Bugzilla_token});
-    _die_on_fault($soapresult);
-    $result = $soapresult->result;
-    my $bug = $result->{bugs}->[0];
-    foreach my $field (keys(%$bug)) {
-        my $value = $bug->{$field};
-        if (ref($value) eq 'HASH') {
-            foreach (keys %$value) {
-                print "$_: " . $value->{$_} . "\n";
-            }
-        }
-        else {
-            print "$field: $value\n";
-        }
+  $soapresult
+    = $proxy->call('Bug.get', {ids => [$bug_id], token => $Bugzilla_token});
+  _die_on_fault($soapresult);
+  $result = $soapresult->result;
+  my $bug = $result->{bugs}->[0];
+  foreach my $field (keys(%$bug)) {
+    my $value = $bug->{$field};
+    if (ref($value) eq 'HASH') {
+      foreach (keys %$value) {
+        print "$_: " . $value->{$_} . "\n";
+      }
     }
+    else {
+      print "$field: $value\n";
+    }
+  }
 }
 
 =head2 Retrieving Product Information
@@ -287,27 +295,28 @@ The call will return a C<Bugzilla::Product> object.
 =cut
 
 if ($product_name) {
-    $soapresult = $proxy->call('Product.get', {'names' => [$product_name], token => $Bugzilla_token});
-    _die_on_fault($soapresult);
-    $result = $soapresult->result()->{'products'}->[0];
-
-    # Iterate all entries, the values may be scalars or array refs with hash refs.
-    foreach my $key (sort(keys %$result)) {
-      my $value = $result->{$key};
-
-      if (ref($value)) {
-        my $counter = 0;
-        foreach my $hash (@$value) {
-          while (my ($innerKey, $innerValue) = each %$hash) {
-            print "$key.$counter.$innerKey: $innerValue\n";
-          }
-          ++$counter;
+  $soapresult = $proxy->call('Product.get',
+    {'names' => [$product_name], token => $Bugzilla_token});
+  _die_on_fault($soapresult);
+  $result = $soapresult->result()->{'products'}->[0];
+
+  # Iterate all entries, the values may be scalars or array refs with hash refs.
+  foreach my $key (sort(keys %$result)) {
+    my $value = $result->{$key};
+
+    if (ref($value)) {
+      my $counter = 0;
+      foreach my $hash (@$value) {
+        while (my ($innerKey, $innerValue) = each %$hash) {
+          print "$key.$counter.$innerKey: $innerValue\n";
         }
-      }
-      else {
-        print "$key: $value\n"
+        ++$counter;
       }
     }
+    else {
+      print "$key: $value\n";
+    }
+  }
 }
 
 =head2 Creating A Bug
@@ -320,20 +329,20 @@ The call will return a hash with a bug id for the newly created bug.
 =cut
 
 if ($create_file_name) {
-    my $bug_fields = do "$create_file_name";
-    $bug_fields->{Bugzilla_token} = $Bugzilla_token;
-    $soapresult = $proxy->call('Bug.create', \%$bug_fields);
-    _die_on_fault($soapresult);
-    $result = $soapresult->result;
-
-    if (ref($result) eq 'HASH') {
-        foreach (keys(%$result)) {
-            print "$_: $$result{$_}\n";
-        }
-    }
-    else {
-        print "$result\n";
+  my $bug_fields = do "$create_file_name";
+  $bug_fields->{Bugzilla_token} = $Bugzilla_token;
+  $soapresult = $proxy->call('Bug.create', \%$bug_fields);
+  _die_on_fault($soapresult);
+  $result = $soapresult->result;
+
+  if (ref($result) eq 'HASH') {
+    foreach (keys(%$result)) {
+      print "$_: $$result{$_}\n";
     }
+  }
+  else {
+    print "$result\n";
+  }
 
 }
 
@@ -346,11 +355,12 @@ list of legal values for this field.
 =cut
 
 if ($legal_field_values) {
-    $soapresult = $proxy->call('Bug.legal_values', {field => $legal_field_values, token => $Bugzilla_token} );
-    _die_on_fault($soapresult);
-    $result = $soapresult->result;
+  $soapresult = $proxy->call('Bug.legal_values',
+    {field => $legal_field_values, token => $Bugzilla_token});
+  _die_on_fault($soapresult);
+  $result = $soapresult->result;
 
-    print join("\n", @{$result->{values}}) . "\n";
+  print join("\n", @{$result->{values}}) . "\n";
 }
 
 =head2 Adding a comment to a bug
@@ -362,15 +372,23 @@ or not.
 =cut
 
 if ($add_comment) {
-    if ($bug_id) {
-        $soapresult = $proxy->call('Bug.add_comment', {id => $bug_id,
-            comment => $add_comment, private => $private, work_time => $work_time, token => $Bugzilla_token});
-        _die_on_fault($soapresult);
-        print "Comment added.\n";
-    }
-    else {
-        print "A --bug_id must be supplied to add a comment.";
-    }
+  if ($bug_id) {
+    $soapresult = $proxy->call(
+      'Bug.add_comment',
+      {
+        id        => $bug_id,
+        comment   => $add_comment,
+        private   => $private,
+        work_time => $work_time,
+        token     => $Bugzilla_token
+      }
+    );
+    _die_on_fault($soapresult);
+    print "Comment added.\n";
+  }
+  else {
+    print "A --bug_id must be supplied to add a comment.";
+  }
 }
 
 =head1 NOTES
@@ -407,18 +425,19 @@ help to you.
 =cut
 
 sub _die_on_fault {
-    my $soapresult = shift;
-
-    if ($soapresult->fault) {
-        my ($package, $filename, $line) = caller;
-        die $soapresult->faultcode . ' ' . $soapresult->faultstring .
-            " in SOAP call near $filename line $line.\n";
-    }
+  my $soapresult = shift;
+
+  if ($soapresult->fault) {
+    my ($package, $filename, $line) = caller;
+    die $soapresult->faultcode . ' '
+      . $soapresult->faultstring
+      . " in SOAP call near $filename line $line.\n";
+  }
 }
 
 sub _syntaxhelp {
-    my $msg = shift;
+  my $msg = shift;
 
-    print "Error: $msg\n";
-    pod2usage({'-verbose' => 0, '-exitval' => 1});
+  print "Error: $msg\n";
+  pod2usage({'-verbose' => 0, '-exitval' => 1});
 }
index 887d6b60ef4e07051847f494c56252a78bb915b4..2b929401d636c5b95a83df4796e1098c27a157e6 100755 (executable)
@@ -22,51 +22,56 @@ use Bugzilla::Util;
 #####################################################################
 
 # Settings for the 'Source' DB that you are copying from.
-use constant SOURCE_DB_TYPE => 'Mysql';
-use constant SOURCE_DB_NAME => 'bugs';
-use constant SOURCE_DB_USER => 'bugs';
+use constant SOURCE_DB_TYPE     => 'Mysql';
+use constant SOURCE_DB_NAME     => 'bugs';
+use constant SOURCE_DB_USER     => 'bugs';
 use constant SOURCE_DB_PASSWORD => '';
-use constant SOURCE_DB_HOST => 'localhost';
+use constant SOURCE_DB_HOST     => 'localhost';
 
 # Settings for the 'Target' DB that you are copying to.
-use constant TARGET_DB_TYPE => 'Pg';
-use constant TARGET_DB_NAME => 'bugs';
-use constant TARGET_DB_USER => 'bugs';
+use constant TARGET_DB_TYPE     => 'Pg';
+use constant TARGET_DB_NAME     => 'bugs';
+use constant TARGET_DB_USER     => 'bugs';
 use constant TARGET_DB_PASSWORD => '';
-use constant TARGET_DB_HOST => 'localhost';
+use constant TARGET_DB_HOST     => 'localhost';
 
 #####################################################################
 # MAIN SCRIPT
 #####################################################################
 
-print "Connecting to the '" . SOURCE_DB_NAME . "' source database on " 
-      . SOURCE_DB_TYPE . "...\n";
+print "Connecting to the '"
+  . SOURCE_DB_NAME
+  . "' source database on "
+  . SOURCE_DB_TYPE . "...\n";
 my $source_db = Bugzilla::DB::_connect({
-    db_driver => SOURCE_DB_TYPE, 
-    db_host   => SOURCE_DB_HOST, 
-    db_name   => SOURCE_DB_NAME, 
-    db_user   => SOURCE_DB_USER, 
-    db_pass   => SOURCE_DB_PASSWORD,
+  db_driver => SOURCE_DB_TYPE,
+  db_host   => SOURCE_DB_HOST,
+  db_name   => SOURCE_DB_NAME,
+  db_user   => SOURCE_DB_USER,
+  db_pass   => SOURCE_DB_PASSWORD,
 });
+
 # Don't read entire tables into memory.
 if (SOURCE_DB_TYPE eq 'Mysql') {
-    $source_db->{'mysql_use_result'} = 1;
+  $source_db->{'mysql_use_result'} = 1;
 
-    # MySQL cannot have two queries running at the same time. Ensure the schema
-    # is loaded from the database so bz_column_info will not execute a query
-    $source_db->_bz_real_schema;
+  # MySQL cannot have two queries running at the same time. Ensure the schema
+  # is loaded from the database so bz_column_info will not execute a query
+  $source_db->_bz_real_schema;
 }
 
-print "Connecting to the '" . TARGET_DB_NAME . "' target database on "
-      . TARGET_DB_TYPE . "...\n";
+print "Connecting to the '"
+  . TARGET_DB_NAME
+  . "' target database on "
+  . TARGET_DB_TYPE . "...\n";
 my $target_db = Bugzilla::DB::_connect({
-    db_driver => TARGET_DB_TYPE,
-    db_host   => TARGET_DB_HOST, 
-    db_name   => TARGET_DB_NAME, 
-    db_user   => TARGET_DB_USER,
-    db_pass   => TARGET_DB_PASSWORD,
+  db_driver => TARGET_DB_TYPE,
+  db_host   => TARGET_DB_HOST,
+  db_name   => TARGET_DB_NAME,
+  db_user   => TARGET_DB_USER,
+  db_pass   => TARGET_DB_PASSWORD,
 });
-my $ident_char = $target_db->get_info( 29 ); # SQL_IDENTIFIER_QUOTE_CHAR
+my $ident_char = $target_db->get_info(29);    # SQL_IDENTIFIER_QUOTE_CHAR
 
 # We use the table list from the target DB, because if somebody
 # has customized their source DB, we still want the script to work,
@@ -79,136 +84,145 @@ my @table_list = grep { $_ ne 'bz_schema' } $target_db->bz_table_list_real();
 # Instead of figuring out some fancy algorithm to insert data in the right
 # order and not break FK integrity, we just drop them all.
 $target_db->bz_drop_foreign_keys();
+
 # We start a transaction on the target DB, which helps when we're doing
 # so many inserts.
 $target_db->bz_start_transaction();
 foreach my $table (@table_list) {
-    my @serial_cols;
-    print "Reading data from the source '$table' table on " 
-          . SOURCE_DB_TYPE . "...\n";
-    my @table_columns = $target_db->bz_table_columns_real($table);
-    # The column names could be quoted using the quote identifier char
-    # Remove these chars as different databases use different quote chars
-    @table_columns = map { s/^\Q$ident_char\E?(.*?)\Q$ident_char\E?$/$1/; $_ }
-                         @table_columns;
-
-    my ($total) = $source_db->selectrow_array("SELECT COUNT(*) FROM $table");
-    my $select_query = "SELECT " . join(',', @table_columns) . " FROM $table";
-    my $select_sth = $source_db->prepare($select_query);
-    $select_sth->execute();
-
-    my $insert_query = "INSERT INTO $table ( " . join(',', @table_columns) 
-                       . " ) VALUES (";
-    $insert_query .= '?,' foreach (@table_columns);
-    # Remove the last comma.
-    chop($insert_query);
-    $insert_query .= ")";
-    my $insert_sth = $target_db->prepare($insert_query);
-
-    print "Clearing out the target '$table' table on " 
-          . TARGET_DB_TYPE . "...\n";
-    $target_db->do("DELETE FROM $table");
-
-    # Oracle doesn't like us manually inserting into tables that have
-    # auto-increment PKs set, because of the way we made auto-increment
-    # fields work.
-    if ($target_db->isa('Bugzilla::DB::Oracle')) {
-        foreach my $column (@table_columns) {
-            my $col_info = $source_db->bz_column_info($table, $column);
-            if ($col_info && $col_info->{TYPE} =~ /SERIAL/i) {
-                print "Dropping the sequence + trigger on $table.$column...\n";
-                $target_db->do("DROP TRIGGER ${table}_${column}_TR");
-                $target_db->do("DROP SEQUENCE ${table}_${column}_SEQ");
-            }
-        }
+  my @serial_cols;
+  print "Reading data from the source '$table' table on "
+    . SOURCE_DB_TYPE . "...\n";
+  my @table_columns = $target_db->bz_table_columns_real($table);
+
+  # The column names could be quoted using the quote identifier char
+  # Remove these chars as different databases use different quote chars
+  @table_columns
+    = map { s/^\Q$ident_char\E?(.*?)\Q$ident_char\E?$/$1/; $_ } @table_columns;
+
+  my ($total) = $source_db->selectrow_array("SELECT COUNT(*) FROM $table");
+  my $select_query = "SELECT " . join(',', @table_columns) . " FROM $table";
+  my $select_sth = $source_db->prepare($select_query);
+  $select_sth->execute();
+
+  my $insert_query
+    = "INSERT INTO $table ( " . join(',', @table_columns) . " ) VALUES (";
+  $insert_query .= '?,' foreach (@table_columns);
+
+  # Remove the last comma.
+  chop($insert_query);
+  $insert_query .= ")";
+  my $insert_sth = $target_db->prepare($insert_query);
+
+  print "Clearing out the target '$table' table on " . TARGET_DB_TYPE . "...\n";
+  $target_db->do("DELETE FROM $table");
+
+  # Oracle doesn't like us manually inserting into tables that have
+  # auto-increment PKs set, because of the way we made auto-increment
+  # fields work.
+  if ($target_db->isa('Bugzilla::DB::Oracle')) {
+    foreach my $column (@table_columns) {
+      my $col_info = $source_db->bz_column_info($table, $column);
+      if ($col_info && $col_info->{TYPE} =~ /SERIAL/i) {
+        print "Dropping the sequence + trigger on $table.$column...\n";
+        $target_db->do("DROP TRIGGER ${table}_${column}_TR");
+        $target_db->do("DROP SEQUENCE ${table}_${column}_SEQ");
+      }
     }
-    
-    print "Writing data to the target '$table' table on " 
-          . TARGET_DB_TYPE . "...\n";
-    my $count = 0;
-    while (my $row = $select_sth->fetchrow_arrayref) {
-        # Each column needs to be bound separately, because
-        # many columns need to be dealt with specially.
-        my $colnum = 0;
-        foreach my $column (@table_columns) {
-            # bind_param args start at 1, but arrays start at 0.
-            my $param_num = $colnum + 1;
-            my $already_bound;
-
-            # Certain types of columns need special handling.
-            my $col_info = $source_db->bz_column_info($table, $column);
-            if ($col_info && $col_info->{TYPE} eq 'LONGBLOB') {
-                $insert_sth->bind_param($param_num, 
-                    $row->[$colnum], $target_db->BLOB_TYPE);
-                $already_bound = 1;
-            }
-            elsif ($col_info && $col_info->{TYPE} =~ /decimal/) {
-                # In MySQL, decimal cols can be too long.
-                my $col_type = $col_info->{TYPE};
-                $col_type =~ /decimal\((\d+),(\d+)\)/;
-                my ($precision, $decimals) = ($1, $2);
-                # If it's longer than precision + decimal point
-                if ( length($row->[$colnum]) > ($precision + 1) ) {
-                    # Truncate it to the highest allowed value.
-                    my $orig_value = $row->[$colnum];
-                    $row->[$colnum] = '';
-                    my $non_decimal = $precision - $decimals;
-                    $row->[$colnum] .= '9' while ($non_decimal--);
-                    $row->[$colnum] .= '.';
-                    $row->[$colnum] .= '9' while ($decimals--);
-                    print "Truncated value $orig_value to " . $row->[$colnum] 
-                         . " for $table.$column.\n";
-                }
-            }
-            elsif ($col_info && $col_info->{TYPE} =~ /DATETIME/i) {
-                my $date = $row->[$colnum];
-                # MySQL can have strange invalid values for Datetimes.
-                $row->[$colnum] = '1901-01-01 00:00:00'
-                    if $date && $date eq '0000-00-00 00:00:00';
-            }
-
-            $insert_sth->bind_param($param_num, $row->[$colnum])
-                unless $already_bound;
-            $colnum++;
-        }
+  }
 
-        $insert_sth->execute();
-        $count++;
-        indicate_progress({ current => $count, total => $total, every => 100 });
-    }
+  print "Writing data to the target '$table' table on "
+    . TARGET_DB_TYPE . "...\n";
+  my $count = 0;
+  while (my $row = $select_sth->fetchrow_arrayref) {
 
-    # For some DBs, we have to do clever things with auto-increment fields.
+    # Each column needs to be bound separately, because
+    # many columns need to be dealt with specially.
+    my $colnum = 0;
     foreach my $column (@table_columns) {
-        next if $target_db->isa('Bugzilla::DB::Mysql');
-        my $col_info = $source_db->bz_column_info($table, $column);
-        if ($col_info && $col_info->{TYPE} =~ /SERIAL/i) {
-            my ($max_val) = $target_db->selectrow_array(
-                    "SELECT MAX($column) FROM $table");
-            # Set the sequence to the current max value + 1.
-            $max_val = 0 if !defined $max_val;
-            $max_val++;
-            print "\nSetting the next value for $table.$column to $max_val.";
-            if ($target_db->isa('Bugzilla::DB::Pg')) {
-                # PostgreSQL doesn't like it when you insert values into
-                # a serial field; it doesn't increment the counter 
-                # automatically.
-                $target_db->bz_set_next_serial_value($table, $column);
-            }
-            elsif ($target_db->isa('Bugzilla::DB::Oracle')) {
-                # Oracle increments the counter on every insert, and *always*
-                # sets the field, even if you gave it a value. So if there
-                # were already rows in the target DB (like the default rows
-                # created by checksetup), you'll get crazy values in your
-                # id columns. So we just dropped the sequences above and
-                # we re-create them here, starting with the right number.
-                my @sql = $target_db->_bz_real_schema->_get_create_seq_ddl(
-                    $table, $column, $max_val);
-                $target_db->do($_) foreach @sql;
-            }
+
+      # bind_param args start at 1, but arrays start at 0.
+      my $param_num = $colnum + 1;
+      my $already_bound;
+
+      # Certain types of columns need special handling.
+      my $col_info = $source_db->bz_column_info($table, $column);
+      if ($col_info && $col_info->{TYPE} eq 'LONGBLOB') {
+        $insert_sth->bind_param($param_num, $row->[$colnum], $target_db->BLOB_TYPE);
+        $already_bound = 1;
+      }
+      elsif ($col_info && $col_info->{TYPE} =~ /decimal/) {
+
+        # In MySQL, decimal cols can be too long.
+        my $col_type = $col_info->{TYPE};
+        $col_type =~ /decimal\((\d+),(\d+)\)/;
+        my ($precision, $decimals) = ($1, $2);
+
+        # If it's longer than precision + decimal point
+        if (length($row->[$colnum]) > ($precision + 1)) {
+
+          # Truncate it to the highest allowed value.
+          my $orig_value = $row->[$colnum];
+          $row->[$colnum] = '';
+          my $non_decimal = $precision - $decimals;
+          $row->[$colnum] .= '9' while ($non_decimal--);
+          $row->[$colnum] .= '.';
+          $row->[$colnum] .= '9' while ($decimals--);
+          print "Truncated value $orig_value to "
+            . $row->[$colnum]
+            . " for $table.$column.\n";
         }
+      }
+      elsif ($col_info && $col_info->{TYPE} =~ /DATETIME/i) {
+        my $date = $row->[$colnum];
+
+        # MySQL can have strange invalid values for Datetimes.
+        $row->[$colnum] = '1901-01-01 00:00:00'
+          if $date && $date eq '0000-00-00 00:00:00';
+      }
+
+      $insert_sth->bind_param($param_num, $row->[$colnum]) unless $already_bound;
+      $colnum++;
+    }
+
+    $insert_sth->execute();
+    $count++;
+    indicate_progress({current => $count, total => $total, every => 100});
+  }
+
+  # For some DBs, we have to do clever things with auto-increment fields.
+  foreach my $column (@table_columns) {
+    next if $target_db->isa('Bugzilla::DB::Mysql');
+    my $col_info = $source_db->bz_column_info($table, $column);
+    if ($col_info && $col_info->{TYPE} =~ /SERIAL/i) {
+      my ($max_val) = $target_db->selectrow_array("SELECT MAX($column) FROM $table");
+
+      # Set the sequence to the current max value + 1.
+      $max_val = 0 if !defined $max_val;
+      $max_val++;
+      print "\nSetting the next value for $table.$column to $max_val.";
+      if ($target_db->isa('Bugzilla::DB::Pg')) {
+
+        # PostgreSQL doesn't like it when you insert values into
+        # a serial field; it doesn't increment the counter
+        # automatically.
+        $target_db->bz_set_next_serial_value($table, $column);
+      }
+      elsif ($target_db->isa('Bugzilla::DB::Oracle')) {
+
+        # Oracle increments the counter on every insert, and *always*
+        # sets the field, even if you gave it a value. So if there
+        # were already rows in the target DB (like the default rows
+        # created by checksetup), you'll get crazy values in your
+        # id columns. So we just dropped the sequences above and
+        # we re-create them here, starting with the right number.
+        my @sql
+          = $target_db->_bz_real_schema->_get_create_seq_ddl($table, $column, $max_val);
+        $target_db->do($_) foreach @sql;
+      }
     }
+  }
 
-    print "\n\n";
+  print "\n\n";
 }
 
 print "Committing changes to the target database...\n";
index b2060bbdbf43b3ff36445f2af16c9f68b4cbeb34..9e8f520f9b7bc3446ec581239b1c12891b9de9d2 100755 (executable)
@@ -20,115 +20,120 @@ use Bugzilla::Bug;
 use Term::ReadLine;
 use Data::Dumper;
 $Data::Dumper::Sortkeys = 1;
-$Data::Dumper::Terse = 1;
-$Data::Dumper::Indent = 1;
-$Data::Dumper::Useqq = 1;
+$Data::Dumper::Terse    = 1;
+$Data::Dumper::Indent   = 1;
+$Data::Dumper::Useqq    = 1;
 $Data::Dumper::Maxdepth = 1;
-$Data::Dumper::Deparse = 0;
+$Data::Dumper::Deparse  = 0;
 
 my $sysname = get_text('term', {term => 'Bugzilla'});
 my $term = new Term::ReadLine "$sysname Console";
 read_history($term);
 END { write_history($term) }
 
-while ( defined (my $input = $term->readline("$sysname> ")) ) {
-    my @res = eval($input);
-    if ($@) {
-        warn $@;
-    }
-    else {
-        print Dumper(@res);
-    }
+while (defined(my $input = $term->readline("$sysname> "))) {
+  my @res = eval($input);
+  if ($@) {
+    warn $@;
+  }
+  else {
+    print Dumper(@res);
+  }
 }
 print STDERR "\n";
 exit 0;
 
 # d: full dump (normal behavior is limited to depth of 1)
 sub d {
-    local $Data::Dumper::Maxdepth = 0;
-    local $Data::Dumper::Deparse = 1;
-    print Dumper(@_);
-    return ();
+  local $Data::Dumper::Maxdepth = 0;
+  local $Data::Dumper::Deparse  = 1;
+  print Dumper(@_);
+  return ();
 }
 
 # p: print as a single string (normal behavior puts list items on separate lines)
 sub p {
-    no warnings; # suppress possible undefined var message 
-    print(@_, "\n");
-    return ();
+  no warnings;    # suppress possible undefined var message
+  print(@_, "\n");
+  return ();
 }
 
 sub filter {
-    my $name = shift;
-    my $filter = Bugzilla->template->{SERVICE}->{CONTEXT}->{CONFIG}->{FILTERS}->{$name};
-    if (scalar @_) {
-        return $filter->(@_);
-    }
-    else {
-        return $filter;
-    }
+  my $name = shift;
+  my $filter
+    = Bugzilla->template->{SERVICE}->{CONTEXT}->{CONFIG}->{FILTERS}->{$name};
+  if (scalar @_) {
+    return $filter->(@_);
+  }
+  else {
+    return $filter;
+  }
 }
 
-sub b { get_object('Bugzilla::Bug', @_) }
-sub u { get_object('Bugzilla::User', @_) }
+sub b { get_object('Bugzilla::Bug',   @_) }
+sub u { get_object('Bugzilla::User',  @_) }
 sub f { get_object('Bugzilla::Field', @_) }
 
 sub get_object {
-    my $class = shift;
-    $_ = shift;
-    my @results = ();
-    
-    if (ref $_ eq 'HASH' && keys %$_) {
-        @results = @{$class->match($_)};
-    }
-    elsif (m/^\d+$/) {
-        @results = ($class->new($_));
-    }
-    elsif (m/\w/i && grep {$_ eq 'name'} ($class->_get_db_columns)) {
-        @results = @{$class->match({name => $_})};
-    }
-    else {
-        @results = ();
-    }
-    
-    if (wantarray) {
-        return @results;
-    }
-    else {
-        return shift @results;
-    }
+  my $class = shift;
+  $_ = shift;
+  my @results = ();
+
+  if (ref $_ eq 'HASH' && keys %$_) {
+    @results = @{$class->match($_)};
+  }
+  elsif (m/^\d+$/) {
+    @results = ($class->new($_));
+  }
+  elsif (m/\w/i && grep { $_ eq 'name' } ($class->_get_db_columns)) {
+    @results = @{$class->match({name => $_})};
+  }
+  else {
+    @results = ();
+  }
+
+  if (wantarray) {
+    return @results;
+  }
+  else {
+    return shift @results;
+  }
 }
 
 sub read_history {
-    my ($term) = @_;
-    
-    if (open HIST, "<$ENV{HOME}/.bugzilla_console_history") {
-        foreach (<HIST>) {
-            chomp;
-            $term->addhistory($_);
-        }
-        close HIST;
+  my ($term) = @_;
+
+  if (open HIST, "<$ENV{HOME}/.bugzilla_console_history") {
+    foreach (<HIST>) {
+      chomp;
+      $term->addhistory($_);
     }
+    close HIST;
+  }
 }
 
 sub write_history {
-    my ($term) = @_;
-
-    if ($term->can('GetHistory') && open HIST, ">$ENV{HOME}/.bugzilla_console_history") {
-        my %seen_hist = ();
-        my @hist = ();
-        foreach my $line (reverse $term->GetHistory()) {
-            next unless $line =~ m/\S/;
-            next if $seen_hist{$line};
-            $seen_hist{$line} = 1;
-            push @hist, $line;
-            last if (scalar @hist > 500);
-        }
-        foreach (reverse @hist) {
-            print HIST $_, "\n";
-        }
-        close HIST;
+  my ($term) = @_;
+
+  if (
+    $term->can('GetHistory') && open HIST,
+    ">$ENV{HOME}/.bugzilla_console_history"
+    )
+  {
+    my %seen_hist = ();
+    my @hist      = ();
+    foreach my $line (reverse $term->GetHistory()) {
+      next unless $line =~ m/\S/;
+      next if $seen_hist{$line};
+      $seen_hist{$line} = 1;
+      push @hist, $line;
+      last if (scalar @hist > 500);
+    }
+    foreach (reverse @hist) {
+      print HIST $_, "\n";
     }
+    close HIST;
+  }
 }
 
 __END__
index edabb56922e6643bde55cf1b1f9796ca4b6341e0..2a00d42df0c3a38aac7bdaaa4b7fbef81f807e37 100755 (executable)
@@ -18,14 +18,14 @@ use Bugzilla::Search::Saved;
 use Bugzilla::Status;
 use Getopt::Long;
 
-my $confirmed   = new Bugzilla::Status({ name => 'CONFIRMED' });
-my $in_progress = new Bugzilla::Status({ name => 'IN_PROGRESS' });
+my $confirmed   = new Bugzilla::Status({name => 'CONFIRMED'});
+my $in_progress = new Bugzilla::Status({name => 'IN_PROGRESS'});
 
 if ($confirmed and $in_progress) {
-    print "You are already using the new workflow.\n";
-    exit 1;
+  print "You are already using the new workflow.\n";
+  exit 1;
 }
-my $enable_unconfirmed  = 0;
+my $enable_unconfirmed = 0;
 my $result = GetOptions("enable-unconfirmed" => \$enable_unconfirmed);
 
 print <<END;
@@ -44,9 +44,10 @@ Emails will not be sent for the change.
 
 END
 if ($enable_unconfirmed) {
-    print "UNCONFIRMED will be enabled in all products.\n";
-} else {
-    print <<END;
+  print "UNCONFIRMED will be enabled in all products.\n";
+}
+else {
+  print <<END;
 If you also want to enable the UNCONFIRMED status in every product,
 restart this script with the --enable-unconfirmed option.
 END
@@ -55,103 +56,116 @@ print "\nTo continue, press any key, or press Ctrl-C to stop this program...";
 getc;
 
 my $dbh = Bugzilla->dbh;
+
 # This is an array instead of a hash so that we can be sure that
 # the translation happens in the right order. In particular, we
 # want NEW to be renamed to CONFIRMED, instead of having REOPENED
 # be the one that gets renamed.
 my @translation = (
-    [NEW      => 'CONFIRMED'],
-    [ASSIGNED => 'IN_PROGRESS'],
-    [REOPENED => 'CONFIRMED'],
-    [CLOSED   => 'VERIFIED'],
+  [NEW      => 'CONFIRMED'],
+  [ASSIGNED => 'IN_PROGRESS'],
+  [REOPENED => 'CONFIRMED'],
+  [CLOSED   => 'VERIFIED'],
 );
 
 my $status_field = Bugzilla::Field->check('bug_status');
 $dbh->bz_start_transaction();
 foreach my $pair (@translation) {
-    my ($from, $to) = @$pair;
-    print "Converting $from to $to...\n";
-    # There is no FK on bugs.bug_status pointing to bug_status.value,
-    # so it's fine to update the bugs table first.
-    $dbh->do('UPDATE bugs SET bug_status = ? WHERE bug_status = ?',
-             undef, $to, $from);
-
-    if (Bugzilla->params->{'duplicate_or_move_bug_status'} eq $from) {
-        SetParam('duplicate_or_move_bug_status', $to);
-        write_params();
-    }
-
-    foreach my $what (qw(added removed)) {
-        $dbh->do("UPDATE bugs_activity SET $what = ?
-                   WHERE fieldid = ? AND $what = ?",
-                 undef, $to, $status_field->id, $from);
-    }
-
-    # Delete any transitions where it now appears that
-    # a bug moved from a status to itself.
-    $dbh->do('DELETE FROM bugs_activity WHERE fieldid = ? AND added = removed',
-             undef, $status_field->id);
-
-    # If the new status already exists, just delete the old one, but retain
-    # the workflow items from it.
-    my $new_status = new Bugzilla::Status({ name => $to });
-    my $old_status = new Bugzilla::Status({ name => $from });
-
-    if ($new_status && $old_status) {
-        my $to_id = $new_status->id;
-        my $from_id = $old_status->id;
-        # The subselect collects existing transitions from the target bug status.
-        # The main select collects existing transitions from the renamed bug status.
-        # The diff tells us which transitions are missing from the target bug status.
-        my $missing_transitions =
-          $dbh->selectcol_arrayref('SELECT sw1.new_status
+  my ($from, $to) = @$pair;
+  print "Converting $from to $to...\n";
+
+  # There is no FK on bugs.bug_status pointing to bug_status.value,
+  # so it's fine to update the bugs table first.
+  $dbh->do('UPDATE bugs SET bug_status = ? WHERE bug_status = ?',
+    undef, $to, $from);
+
+  if (Bugzilla->params->{'duplicate_or_move_bug_status'} eq $from) {
+    SetParam('duplicate_or_move_bug_status', $to);
+    write_params();
+  }
+
+  foreach my $what (qw(added removed)) {
+    $dbh->do(
+      "UPDATE bugs_activity SET $what = ?
+                   WHERE fieldid = ? AND $what = ?", undef, $to, $status_field->id,
+      $from
+    );
+  }
+
+  # Delete any transitions where it now appears that
+  # a bug moved from a status to itself.
+  $dbh->do('DELETE FROM bugs_activity WHERE fieldid = ? AND added = removed',
+    undef, $status_field->id);
+
+  # If the new status already exists, just delete the old one, but retain
+  # the workflow items from it.
+  my $new_status = new Bugzilla::Status({name => $to});
+  my $old_status = new Bugzilla::Status({name => $from});
+
+  if ($new_status && $old_status) {
+    my $to_id   = $new_status->id;
+    my $from_id = $old_status->id;
+
+    # The subselect collects existing transitions from the target bug status.
+    # The main select collects existing transitions from the renamed bug status.
+    # The diff tells us which transitions are missing from the target bug status.
+    my $missing_transitions = $dbh->selectcol_arrayref(
+      'SELECT sw1.new_status
                                       FROM status_workflow sw1
                                      WHERE sw1.old_status = ?
                                        AND sw1.new_status NOT IN (SELECT sw2.new_status
                                                                     FROM status_workflow sw2
                                                                    WHERE sw2.old_status = ?)',
-                                     undef, ($from_id, $to_id));
-
-        $dbh->do('UPDATE status_workflow SET old_status = ? WHERE old_status = ? AND '
-                 . $dbh->sql_in('new_status', $missing_transitions),
-                 undef, ($to_id, $from_id)) if @$missing_transitions;
-
-        # The subselect collects existing transitions to the target bug status.
-        # The main select collects existing transitions to the renamed bug status.
-        # The diff tells us which transitions are missing to the target bug status.
-        # We have to explicitly exclude NULL from the subselect, because NOT IN
-        # doesn't know what to do with it (neither true nor false) and no data is returned.
-        $missing_transitions =
-          $dbh->selectcol_arrayref('SELECT sw1.old_status
+      undef, ($from_id, $to_id)
+    );
+
+    $dbh->do(
+      'UPDATE status_workflow SET old_status = ? WHERE old_status = ? AND '
+        . $dbh->sql_in('new_status', $missing_transitions),
+      undef,
+      ($to_id, $from_id)
+    ) if @$missing_transitions;
+
+ # The subselect collects existing transitions to the target bug status.
+ # The main select collects existing transitions to the renamed bug status.
+ # The diff tells us which transitions are missing to the target bug status.
+ # We have to explicitly exclude NULL from the subselect, because NOT IN
+ # doesn't know what to do with it (neither true nor false) and no data is returned.
+    $missing_transitions = $dbh->selectcol_arrayref(
+      'SELECT sw1.old_status
                                       FROM status_workflow sw1
                                      WHERE sw1.new_status = ?
                                        AND sw1.old_status NOT IN (SELECT sw2.old_status
                                                                     FROM status_workflow sw2
                                                                    WHERE sw2.new_status = ?
                                                                      AND sw2.old_status IS NOT NULL)',
-                                     undef, ($from_id, $to_id));
-
-        $dbh->do('UPDATE status_workflow SET new_status = ? WHERE new_status = ? AND '
-                 . $dbh->sql_in('old_status', $missing_transitions),
-                 undef, ($to_id, $from_id)) if @$missing_transitions;
-
-        # Delete rows where old_status = new_status, and then the old status itself.
-        $dbh->do('DELETE FROM status_workflow WHERE old_status = new_status');
-        $dbh->do('DELETE FROM bug_status WHERE value = ?', undef, $from);
-    }
-    # Otherwise, rename the old status to the new one.
-    elsif ($old_status) {
-        $dbh->do('UPDATE bug_status SET value = ? WHERE value = ?',
-                 undef, $to, $from);
-    }
-
-    Bugzilla::Search::Saved->rename_field_value('bug_status', $from, $to);
-    Bugzilla::Series->Bugzilla::Search::Saved::rename_field_value('bug_status',
-        $from, $to);
+      undef, ($from_id, $to_id)
+    );
+
+    $dbh->do(
+      'UPDATE status_workflow SET new_status = ? WHERE new_status = ? AND '
+        . $dbh->sql_in('old_status', $missing_transitions),
+      undef,
+      ($to_id, $from_id)
+    ) if @$missing_transitions;
+
+    # Delete rows where old_status = new_status, and then the old status itself.
+    $dbh->do('DELETE FROM status_workflow WHERE old_status = new_status');
+    $dbh->do('DELETE FROM bug_status WHERE value = ?', undef, $from);
+  }
+
+  # Otherwise, rename the old status to the new one.
+  elsif ($old_status) {
+    $dbh->do('UPDATE bug_status SET value = ? WHERE value = ?', undef, $to, $from);
+  }
+
+  Bugzilla::Search::Saved->rename_field_value('bug_status', $from, $to);
+  Bugzilla::Series->Bugzilla::Search::Saved::rename_field_value('bug_status',
+    $from, $to);
 }
 if ($enable_unconfirmed) {
-    print "Enabling UNCONFIRMED in all products...\n";
-    $dbh->do('UPDATE products SET allows_unconfirmed = 1');
+  print "Enabling UNCONFIRMED in all products...\n";
+  $dbh->do('UPDATE products SET allows_unconfirmed = 1');
 }
 $dbh->bz_commit_transaction();
 Bugzilla->memcached->clear_all();
index f5e4977c33fe03219be34f31bdb21398f03e5b73..6fd9073fd1852cf83c98cff9fe4087b100ac5fda 100755 (executable)
@@ -21,8 +21,7 @@ use File::Copy qw(move);
 use File::Find;
 use File::Path qw(mkpath rmtree);
 
-my $from = $ARGV[0]
-  or die <<END;
+my $from = $ARGV[0] or die <<END;
 You must specify the name of the extension you are converting from,
 as the first argument.
 END
@@ -32,33 +31,33 @@ my $extdir = bz_locations()->{'extensionsdir'};
 
 my $from_dir = "$extdir/$from";
 if (!-d $from_dir) {
-    die "$from_dir does not exist.\n";
+  die "$from_dir does not exist.\n";
 }
 
 my $to_dir = "$extdir/$extension_name";
 if (-d $to_dir) {
-    die "$to_dir already exists, not converting.\n";
+  die "$to_dir already exists, not converting.\n";
 }
 
 if (ON_WINDOWS) {
-    # There's no easy way to recursively copy a directory on Windows.
-    print "WARNING: This will modify the contents of $from_dir.\n",
-          "Press Ctrl-C to stop or any other key to continue...\n";
-    getc;
-    move($from_dir, $to_dir) 
-        || die "rename of $from_dir to $to_dir failed: $!";
+
+  # There's no easy way to recursively copy a directory on Windows.
+  print "WARNING: This will modify the contents of $from_dir.\n",
+    "Press Ctrl-C to stop or any other key to continue...\n";
+  getc;
+  move($from_dir, $to_dir) || die "rename of $from_dir to $to_dir failed: $!";
 }
 else {
-    print "Copying $from_dir to $to_dir...\n";
-    system("cp", "-r", $from_dir, $to_dir);
+  print "Copying $from_dir to $to_dir...\n";
+  system("cp", "-r", $from_dir, $to_dir);
 }
 
-# Make sure we don't accidentally modify the $from_dir anywhere else 
+# Make sure we don't accidentally modify the $from_dir anywhere else
 # in this script.
 undef $from_dir;
 
 if (!-d $to_dir) {
-    die "$to_dir was not created.\n";
+  die "$to_dir was not created.\n";
 }
 
 my $version = get_version($to_dir);
@@ -96,7 +95,7 @@ END
 open(my $config_fh, '>', "$to_dir/Config.pm") || die "$to_dir/Config.pm: $!";
 print $config_fh $config_pm;
 close($config_fh);
-open(my $extension_fh, '>', "$to_dir/Extension.pm") 
+open(my $extension_fh, '>', "$to_dir/Extension.pm")
   || die "$to_dir/Extension.pm: $!";
 print $extension_fh $extension_pm;
 close($extension_fh);
@@ -109,176 +108,179 @@ unlink("$to_dir/info.pl");
 ###############
 
 sub rename_module_packages {
-    my ($dir, $name) = @_;
-    my $lib_dir = "$dir/lib";
-
-    # We don't want things like Bugzilla::Extension::Testopia::Testopia.
-    if (-d "$lib_dir/$name") {
-        print "Moving contents of $lib_dir/$name into $lib_dir...\n";
-        foreach my $file (glob("$lib_dir/$name/*")) {
-            my $dirname = dirname($file);
-            my $basename = basename($file);
-            rename($file, "$dirname/../$basename") || warn "$file: $!\n";
-        }
+  my ($dir, $name) = @_;
+  my $lib_dir = "$dir/lib";
+
+  # We don't want things like Bugzilla::Extension::Testopia::Testopia.
+  if (-d "$lib_dir/$name") {
+    print "Moving contents of $lib_dir/$name into $lib_dir...\n";
+    foreach my $file (glob("$lib_dir/$name/*")) {
+      my $dirname  = dirname($file);
+      my $basename = basename($file);
+      rename($file, "$dirname/../$basename") || warn "$file: $!\n";
     }
+  }
 
-    my @modules;
-    find({ wanted   => sub { $_ =~ /\.pm$/i and push(@modules, $_) }, 
-           no_chdir => 1 }, $lib_dir);
-    my %module_rename;
-    foreach my $file (@modules) {
-        open(my $fh, '<', $file) || die "$file: $!";
-        my $content = do { local $/ = undef; <$fh> };
-        close($fh);
-        if ($content =~ /^package (\S+);/m) {
-            my $package = $1;
-            my $new_name = $file;
-            $new_name =~ s/^$lib_dir\///;
-            $new_name =~ s/\.pm$//;
-            $new_name = join('::', File::Spec->splitdir($new_name));
-            $new_name = "Bugzilla::Extension::${name}::$new_name";
-            print "Renaming $package to $new_name...\n";
-            $content =~ s/^package \Q$package\E;/package \Q$new_name\E;/;
-            open(my $write_fh, '>', $file) || die "$file: $!";
-            print $write_fh $content;
-            close($write_fh);
-            $module_rename{$package} = $new_name;
-        }
+  my @modules;
+  find({wanted => sub { $_ =~ /\.pm$/i and push(@modules, $_) }, no_chdir => 1},
+    $lib_dir);
+  my %module_rename;
+  foreach my $file (@modules) {
+    open(my $fh, '<', $file) || die "$file: $!";
+    my $content = do { local $/ = undef; <$fh> };
+    close($fh);
+    if ($content =~ /^package (\S+);/m) {
+      my $package  = $1;
+      my $new_name = $file;
+      $new_name =~ s/^$lib_dir\///;
+      $new_name =~ s/\.pm$//;
+      $new_name = join('::', File::Spec->splitdir($new_name));
+      $new_name = "Bugzilla::Extension::${name}::$new_name";
+      print "Renaming $package to $new_name...\n";
+      $content =~ s/^package \Q$package\E;/package \Q$new_name\E;/;
+      open(my $write_fh, '>', $file) || die "$file: $!";
+      print $write_fh $content;
+      close($write_fh);
+      $module_rename{$package} = $new_name;
     }
+  }
 
-    print "Renaming module names inside of library and code files...\n";
-    my @code_files = glob("$dir/code/*.pl");
-    rename_modules_internally(\%module_rename, [@modules, @code_files]);
+  print "Renaming module names inside of library and code files...\n";
+  my @code_files = glob("$dir/code/*.pl");
+  rename_modules_internally(\%module_rename, [@modules, @code_files]);
 }
 
 sub rename_modules_internally {
-    my ($rename, $files) = @_;
-
-    # We can't use \b because :: matches \b.
-    my $break = qr/^|[^\w:]|$/;
-    foreach my $file (@$files) {
-        open(my $fh, '<', $file) || die "$file: $!";
-        my $content = do { local $/ = undef; <$fh> };
-        close($fh);
-        foreach my $old_name (keys %$rename) {
-            my $new_name = $rename->{$old_name};
-            $content =~ s/($break)\Q$old_name\E($break)/$1$new_name$2/gms;
-        }
-        open(my $write_fh, '>', $file) || die "$file: $!";
-        print $write_fh $content;
-        close($write_fh);
+  my ($rename, $files) = @_;
+
+  # We can't use \b because :: matches \b.
+  my $break = qr/^|[^\w:]|$/;
+  foreach my $file (@$files) {
+    open(my $fh, '<', $file) || die "$file: $!";
+    my $content = do { local $/ = undef; <$fh> };
+    close($fh);
+    foreach my $old_name (keys %$rename) {
+      my $new_name = $rename->{$old_name};
+      $content =~ s/($break)\Q$old_name\E($break)/$1$new_name$2/gms;
     }
+    open(my $write_fh, '>', $file) || die "$file: $!";
+    print $write_fh $content;
+    close($write_fh);
+  }
 }
 
 sub get_version {
-    my ($dir) = @_;
-    print "Getting version info from info.pl...\n";
-    my $info;
-    {
-        local @INC = ("$dir/lib", @INC);
-        $info = do "$dir/info.pl"; die $@ if $@;
-    }
-    return $info->{version};
+  my ($dir) = @_;
+  print "Getting version info from info.pl...\n";
+  my $info;
+  {
+    local @INC = ("$dir/lib", @INC);
+    $info = do "$dir/info.pl";
+    die $@ if $@;
+  }
+  return $info->{version};
 }
 
 sub get_install_requirements {
-    my ($dir) = @_;
-    my $file = "$dir/code/install-requirements.pl";
-    return '' if !-f $file;
-
-    print "Moving install-requirements.pl code into Config.pm...\n";
-    my ($modules, $code) = process_code_file($file);
-    $modules = join('', @$modules);
-    $code = join('', @$code);
-    if ($modules) {
-        return "$modules\n\n$code";
-    }
-    return $code;
+  my ($dir) = @_;
+  my $file = "$dir/code/install-requirements.pl";
+  return '' if !-f $file;
+
+  print "Moving install-requirements.pl code into Config.pm...\n";
+  my ($modules, $code) = process_code_file($file);
+  $modules = join('', @$modules);
+  $code    = join('', @$code);
+  if ($modules) {
+    return "$modules\n\n$code";
+  }
+  return $code;
 }
 
 sub process_code_file {
-    my ($file) = @_;
-    open(my $fh, '<', $file) || die "$file: $!";
-    my $stuff_started;
-    my (@modules, @code);
-    foreach my $line (<$fh>) {
-        $stuff_started = 1 if $line !~ /^#/;
-        next if !$stuff_started;
-        next if $line =~ /^use (warnings|strict|lib|Bugzilla)[^\w:]/;
-        if ($line =~ /^(?:use|require)\b/) {
-            push(@modules, $line);
-        }
-        else {
-            push(@code, $line);
-        }
+  my ($file) = @_;
+  open(my $fh, '<', $file) || die "$file: $!";
+  my $stuff_started;
+  my (@modules, @code);
+  foreach my $line (<$fh>) {
+    $stuff_started = 1 if $line !~ /^#/;
+    next if !$stuff_started;
+    next if $line =~ /^use (warnings|strict|lib|Bugzilla)[^\w:]/;
+    if ($line =~ /^(?:use|require)\b/) {
+      push(@modules, $line);
+    }
+    else {
+      push(@code, $line);
     }
-    close $fh;
-    return (\@modules, \@code);
+  }
+  close $fh;
+  return (\@modules, \@code);
 }
 
 sub code_files_to_subroutines {
-    my ($dir) = @_;
-
-    my @dir_files = glob("$dir/code/*.pl");
-    my (@all_modules, @subroutines);
-    foreach my $file (@dir_files) {
-        next if $file =~ /install-requirements/;
-        print "Moving $file code into Extension.pm...\n";
-        my ($modules, $code) = process_code_file($file);
-        my @code_lines = map { "    $_" } @$code;
-        my $code_string = join('', @code_lines);
-        $code_string =~ s/Bugzilla->hook_args/\$args/g;
-        $code_string =~ s/my\s+\$args\s+=\s+\$args;//gs;
-        chomp($code_string);
-        push(@all_modules, @$modules);
-        my $name = basename($file);
-        $name =~ s/-/_/;
-        $name =~ s/\.pl$//;
-
-        my $subroutine = <<END;
+  my ($dir) = @_;
+
+  my @dir_files = glob("$dir/code/*.pl");
+  my (@all_modules, @subroutines);
+  foreach my $file (@dir_files) {
+    next if $file =~ /install-requirements/;
+    print "Moving $file code into Extension.pm...\n";
+    my ($modules, $code) = process_code_file($file);
+    my @code_lines = map {"    $_"} @$code;
+    my $code_string = join('', @code_lines);
+    $code_string =~ s/Bugzilla->hook_args/\$args/g;
+    $code_string =~ s/my\s+\$args\s+=\s+\$args;//gs;
+    chomp($code_string);
+    push(@all_modules, @$modules);
+    my $name = basename($file);
+    $name =~ s/-/_/;
+    $name =~ s/\.pl$//;
+
+    my $subroutine = <<END;
 sub $name {
     my (\$self, \$args) = \@_;
 $code_string
 }
 END
-        push(@subroutines, $subroutine);
-    }
+    push(@subroutines, $subroutine);
+  }
 
-    my %seen_modules = map { trim($_) => 1 } @all_modules;
-    my $module_string = join("\n", sort keys %seen_modules);
-    my $subroutine_string = join("\n", @subroutines);
-    return ($module_string, $subroutine_string);
+  my %seen_modules = map { trim($_) => 1 } @all_modules;
+  my $module_string     = join("\n", sort keys %seen_modules);
+  my $subroutine_string = join("\n", @subroutines);
+  return ($module_string, $subroutine_string);
 }
 
 sub move_template_hooks {
-    my ($dir) = @_;
-    foreach my $lang (glob("$dir/template/*")) {
-        next if !_file_matters($lang);
-        my $hook_container = "$lang/default/hook";
-        mkpath($hook_container) || warn "$hook_container: $!";
-        # Hooks can be in all sorts of weird places, including
-        # template/default/hook.
-        foreach my $file (glob("$lang/*")) {
-            next if !_file_matters($file, 1);
-            my $dirname = basename($file);
-            print "Moving $file to $hook_container/$dirname...\n";
-            rename($file, "$hook_container/$dirname") || die "move failed: $!";
-        }
+  my ($dir) = @_;
+  foreach my $lang (glob("$dir/template/*")) {
+    next if !_file_matters($lang);
+    my $hook_container = "$lang/default/hook";
+    mkpath($hook_container) || warn "$hook_container: $!";
+
+    # Hooks can be in all sorts of weird places, including
+    # template/default/hook.
+    foreach my $file (glob("$lang/*")) {
+      next if !_file_matters($file, 1);
+      my $dirname = basename($file);
+      print "Moving $file to $hook_container/$dirname...\n";
+      rename($file, "$hook_container/$dirname") || die "move failed: $!";
     }
+  }
 }
 
 sub _file_matters {
-     my ($path, $tmpl) = @_;
-     my @ignore = qw(default custom CVS);
-     my $file = basename($path);
-     return 0 if grep(lc($_) eq lc($file), @ignore);
-      # Hidden files
-     return 0 if $file =~ /^\./;
-     if ($tmpl) {
-         return 1 if $file =~ /\.tmpl$/;
-     }
-     return 0 if !-d $path;
-     return 1;
+  my ($path, $tmpl) = @_;
+  my @ignore = qw(default custom CVS);
+  my $file   = basename($path);
+  return 0 if grep(lc($_) eq lc($file), @ignore);
+
+  # Hidden files
+  return 0 if $file =~ /^\./;
+  if ($tmpl) {
+    return 1 if $file =~ /\.tmpl$/;
+  }
+  return 0 if !-d $path;
+  return 1;
 }
 
 __END__
index 16d50bb91289e5004a8eea64a9e1f91a1c6e018b..fc58a303f9ada0b5ca9d70675620c36152855f79 100755 (executable)
@@ -44,7 +44,7 @@ use Pod::Usage;
 my $dbh = Bugzilla->dbh;
 
 # Display the help if called with --help or -?.
-my $help  = 0;
+my $help = 0;
 my $result = GetOptions("help|?" => \$help);
 pod2usage(0) if $help;
 
@@ -53,49 +53,54 @@ pod2usage(0) if $help;
 my $old = $ARGV[0] || die "You must specify an old user account.\n";
 my $old_id;
 if ($old =~ /^id:(\d+)$/) {
-    # As the old user account may be a deleted one, we don't
-    # check whether this user ID is valid or not.
-    # If it never existed, no damage will be done.
-    $old_id = $1;
+
+  # As the old user account may be a deleted one, we don't
+  # check whether this user ID is valid or not.
+  # If it never existed, no damage will be done.
+  $old_id = $1;
 }
 else {
-    trick_taint($old);
-    $old_id = $dbh->selectrow_array('SELECT userid FROM profiles
-                                      WHERE login_name = ?',
-                                      undef, $old);
+  trick_taint($old);
+  $old_id = $dbh->selectrow_array(
+    'SELECT userid FROM profiles
+                                      WHERE login_name = ?', undef, $old
+  );
 }
 if ($old_id) {
-    print "OK, old user account $old found; user ID: $old_id.\n";
+  print "OK, old user account $old found; user ID: $old_id.\n";
 }
 else {
-    die "The old user account $old does not exist.\n";
+  die "The old user account $old does not exist.\n";
 }
 
 my $new = $ARGV[1] || die "You must specify a new user account.\n";
 my $new_id;
 if ($new =~ /^id:(\d+)$/) {
-    $new_id = $1;
-    # Make sure this user ID exists.
-    $new_id = $dbh->selectrow_array('SELECT userid FROM profiles
-                                      WHERE userid = ?',
-                                      undef, $new_id);
+  $new_id = $1;
+
+  # Make sure this user ID exists.
+  $new_id = $dbh->selectrow_array(
+    'SELECT userid FROM profiles
+                                      WHERE userid = ?', undef, $new_id
+  );
 }
 else {
-    trick_taint($new);
-    $new_id = $dbh->selectrow_array('SELECT userid FROM profiles
-                                      WHERE login_name = ?',
-                                      undef, $new);
+  trick_taint($new);
+  $new_id = $dbh->selectrow_array(
+    'SELECT userid FROM profiles
+                                      WHERE login_name = ?', undef, $new
+  );
 }
 if ($new_id) {
-    print "OK, new user account $new found; user ID: $new_id.\n";
+  print "OK, new user account $new found; user ID: $new_id.\n";
 }
 else {
-    die "The new user account $new does not exist.\n";
+  die "The new user account $new does not exist.\n";
 }
 
 # Make sure the old and new accounts are different.
 if ($old_id == $new_id) {
-    die "\nBoth accounts are identical. There is nothing to migrate.\n";
+  die "\nBoth accounts are identical. There is nothing to migrate.\n";
 }
 
 
@@ -112,32 +117,34 @@ if ($old_id == $new_id) {
 # We set the tables that require custom stuff (multiple columns to check)
 # here, but the simple stuff is all handled below by bz_get_related_fks.
 my %changes = (
-    cc              => ['who bug_id'],
-    # Tables affecting global behavior / other users.
-    component_cc    => ['user_id component_id'],
-    watch           => ['watcher watched', 'watched watcher'],
-    # Tables affecting the user directly.
-    namedqueries    => ['userid name'],
-    namedqueries_link_in_footer => ['user_id namedquery_id'],
-    user_group_map  => ['user_id group_id isbless grant_type'],
-    email_setting   => ['user_id relationship event'],
-    profile_setting => ['user_id setting_name'],
-
-    # Only do it if mailto_type = 0, i.e is pointing to a user account!
-    # This requires to be done separately due to this condition.
-    whine_schedules => [], # ['mailto'],
+  cc => ['who bug_id'],
+
+  # Tables affecting global behavior / other users.
+  component_cc => ['user_id component_id'],
+  watch        => ['watcher watched', 'watched watcher'],
+
+  # Tables affecting the user directly.
+  namedqueries                => ['userid name'],
+  namedqueries_link_in_footer => ['user_id namedquery_id'],
+  user_group_map              => ['user_id group_id isbless grant_type'],
+  email_setting               => ['user_id relationship event'],
+  profile_setting             => ['user_id setting_name'],
+
+  # Only do it if mailto_type = 0, i.e is pointing to a user account!
+  # This requires to be done separately due to this condition.
+  whine_schedules => [],    # ['mailto'],
 );
 
 my $userid_fks = $dbh->bz_get_related_fks('profiles', 'userid');
 foreach my $item (@$userid_fks) {
-    my ($table, $column) = @$item;
-    $changes{$table} ||= [];
-    push(@{ $changes{$table} }, $column);
+  my ($table, $column) = @$item;
+  $changes{$table} ||= [];
+  push(@{$changes{$table}}, $column);
 }
 
 # Delete all old records for these tables; no migration.
 foreach my $table (qw(logincookies tokens profiles)) {
-    $changes{$table} = [];
+  $changes{$table} = [];
 }
 
 # Start the transaction
@@ -145,7 +152,7 @@ $dbh->bz_start_transaction();
 
 # Delete old records from logincookies and tokens tables.
 $dbh->do('DELETE FROM logincookies WHERE userid = ?', undef, $old_id);
-$dbh->do('DELETE FROM tokens WHERE userid = ?', undef, $old_id);
+$dbh->do('DELETE FROM tokens WHERE userid = ?',       undef, $old_id);
 
 # Special care needs to be done with bug_user_last_visit table as the
 # source user and destination user may have visited the same bug id at one time.
@@ -158,78 +165,87 @@ my $dupe_ids = $dbh->selectcol_arrayref("
                AND earlier.last_visit_ts < later.last_visit_ts
                AND earlier.bug_id = later.bug_id)
      WHERE (earlier.user_id = ? OR earlier.user_id = ?)
-           AND (later.user_id = ? OR later.user_id = ?)",
-    undef, $old_id, $new_id, $old_id, $new_id);
+           AND (later.user_id = ? OR later.user_id = ?)", undef, $old_id,
+  $new_id, $old_id, $new_id);
 
 if (@$dupe_ids) {
-    $dbh->do("DELETE FROM bug_user_last_visit WHERE " .
-             $dbh->sql_in('id', $dupe_ids));
+  $dbh->do(
+    "DELETE FROM bug_user_last_visit WHERE " . $dbh->sql_in('id', $dupe_ids));
 }
 
 # Migrate records from old user to new user.
 foreach my $table (keys %changes) {
-    foreach my $column_list (@{ $changes{$table} }) {
-        # Get all columns to consider. There is always at least
-        # one column given: the one to update.
-        my @columns = split(/[\s]+/, $column_list);
-        my $cols_to_check = join(' AND ', map {"$_ = ?"} @columns);
-        # The first column of the list is the one to update.
-        my $col_to_update = shift @columns;
-
-        # Will be used to migrate the old user account to the new one.
-        my $sth_update = $dbh->prepare("UPDATE $table
+  foreach my $column_list (@{$changes{$table}}) {
+
+    # Get all columns to consider. There is always at least
+    # one column given: the one to update.
+    my @columns = split(/[\s]+/, $column_list);
+    my $cols_to_check = join(' AND ', map {"$_ = ?"} @columns);
+
+    # The first column of the list is the one to update.
+    my $col_to_update = shift @columns;
+
+    # Will be used to migrate the old user account to the new one.
+    my $sth_update = $dbh->prepare(
+      "UPDATE $table
                                            SET $col_to_update = ?
-                                         WHERE $cols_to_check");
+                                         WHERE $cols_to_check"
+    );
 
-        # Do we have additional columns to take care of?
-        if (scalar(@columns)) {
-            my $cols_to_query = join(', ', @columns);
+    # Do we have additional columns to take care of?
+    if (scalar(@columns)) {
+      my $cols_to_query = join(', ', @columns);
 
-            # Get existing entries for the old user account.
-            my $old_entries = 
-                $dbh->selectall_arrayref("SELECT $cols_to_query
+      # Get existing entries for the old user account.
+      my $old_entries = $dbh->selectall_arrayref(
+        "SELECT $cols_to_query
                                             FROM $table
-                                           WHERE $col_to_update = ?",
-                                          undef, $old_id);
+                                           WHERE $col_to_update = ?", undef, $old_id
+      );
 
-            # Will be used to check whether the same entry exists
-            # for the new user account.
-            my $sth_select = $dbh->prepare("SELECT COUNT(*)
+      # Will be used to check whether the same entry exists
+      # for the new user account.
+      my $sth_select = $dbh->prepare(
+        "SELECT COUNT(*)
                                               FROM $table
-                                             WHERE $cols_to_check");
-
-            # Will be used to delete duplicated entries.
-            my $sth_delete = $dbh->prepare("DELETE FROM $table
-                                             WHERE $cols_to_check");
-
-            foreach my $entry (@$old_entries) {
-                my $exists = $dbh->selectrow_array($sth_select, undef,
-                                                   ($new_id, @$entry));
-
-                if ($exists) {
-                    $sth_delete->execute($old_id, @$entry);
-                }
-                else {
-                    $sth_update->execute($new_id, $old_id, @$entry);
-                }
-            }
+                                             WHERE $cols_to_check"
+      );
+
+      # Will be used to delete duplicated entries.
+      my $sth_delete = $dbh->prepare(
+        "DELETE FROM $table
+                                             WHERE $cols_to_check"
+      );
+
+      foreach my $entry (@$old_entries) {
+        my $exists = $dbh->selectrow_array($sth_select, undef, ($new_id, @$entry));
+
+        if ($exists) {
+          $sth_delete->execute($old_id, @$entry);
         }
-        # No check required. Update the column directly.
         else {
-            $sth_update->execute($new_id, $old_id);
+          $sth_update->execute($new_id, $old_id, @$entry);
         }
-        print "OK, records in the '$col_to_update' column of the '$table' table\n" .
-              "have been migrated to the new user account.\n";
+      }
     }
+
+    # No check required. Update the column directly.
+    else {
+      $sth_update->execute($new_id, $old_id);
+    }
+    print "OK, records in the '$col_to_update' column of the '$table' table\n"
+      . "have been migrated to the new user account.\n";
+  }
 }
 
 # Only update 'whine_schedules' if mailto_type = 0.
 # (i.e. is pointing to a user ID).
-$dbh->do('UPDATE whine_schedules SET mailto = ?
-           WHERE mailto = ? AND mailto_type = ?',
-          undef, ($new_id, $old_id, 0));
-print "OK, records in the 'mailto' column of the 'whine_schedules' table\n" .
-      "have been migrated to the new user account.\n";
+$dbh->do(
+  'UPDATE whine_schedules SET mailto = ?
+           WHERE mailto = ? AND mailto_type = ?', undef, ($new_id, $old_id, 0)
+);
+print "OK, records in the 'mailto' column of the 'whine_schedules' table\n"
+  . "have been migrated to the new user account.\n";
 
 # Delete the old record from the profiles table.
 $dbh->do('DELETE FROM profiles WHERE userid = ?', undef, $old_id);
index ff580a7efab7b42a17608fd623287316dcf96b6d..57a0f1206cec00525240d426e37329bc4e07b295 100755 (executable)
@@ -8,12 +8,12 @@
 
 # mysqld-watcher.pl - a script that watches the running instance of
 # mysqld and kills off any long-running SELECTs against the shadow_db
-# 
+#
 use 5.14.0;
 use strict;
 use warnings;
 
-# some configurables: 
+# some configurables:
 
 # length of time before a thread is eligible to be killed, in seconds
 #
@@ -40,65 +40,70 @@ my $long = {};
 # queries so we know which queries are taking too long to run, but complete
 # queries with line breaks get missed by this script, so we get abbreviated
 # queries as well to make sure we don't miss any.
-foreach my $command ("/opt/mysql/bin/mysqladmin --verbose processlist", 
-                     "/opt/mysql/bin/mysqladmin processlist")
+foreach my $command (
+  "/opt/mysql/bin/mysqladmin --verbose processlist",
+  "/opt/mysql/bin/mysqladmin processlist"
+  )
 {
-    close(STDIN);
-    open(STDIN, "$command |");
+  close(STDIN);
+  open(STDIN, "$command |");
 
-    # iterate through the running threads
-    #
-    while ( <STDIN> ) { 
-        my @F = split(/\|/);
+  # iterate through the running threads
+  #
+  while (<STDIN>) {
+    my @F = split(/\|/);
 
-        # if this line is not the correct number of fields, or if the thread-id
-        # field contains Id, skip this line.  both these cases indicate that this
-        # line contains pretty-printing gunk and not thread info.
-        #
-        next if ( $#F != 9 || $F[1] =~ /Id/); 
+    # if this line is not the correct number of fields, or if the thread-id
+    # field contains Id, skip this line.  both these cases indicate that this
+    # line contains pretty-printing gunk and not thread info.
+    #
+    next if ($#F != 9 || $F[1] =~ /Id/);
 
-        if ( $F[4] =~ /shadow_bugs/             # shadowbugs database in use
-             && $F[5] =~ /Query/                # this is actually a query
-             && $F[6] > $long_query_time        # this query has taken too long
-             && $F[8] =~ /(select|SELECT)/      # only kill a select
-             && !defined($long->{$F[1]}) )      # haven't seen this one already
-        {
-           $long->{$F[1]} = \@F;
-            system("/opt/mysql/bin/mysqladmin", "kill", $F[1]);
-        }
+    if (
+      $F[4] =~ /shadow_bugs/           # shadowbugs database in use
+      && $F[5] =~ /Query/              # this is actually a query
+      && $F[6] > $long_query_time      # this query has taken too long
+      && $F[8] =~ /(select|SELECT)/    # only kill a select
+      && !defined($long->{$F[1]})
+      )                                # haven't seen this one already
+    {
+      $long->{$F[1]} = \@F;
+      system("/opt/mysql/bin/mysqladmin", "kill", $F[1]);
     }
+  }
 }
 
 # send an email message
 #
-# should perhaps be moved to somewhere more global for use in bugzilla as a 
+# should perhaps be moved to somewhere more global for use in bugzilla as a
 # whole; should also do more error-checking
 #
 sub sendEmail($$$$) {
-    ($#_ == 3) || die("sendEmail: invalid number of arguments");
-    my ($from, $to, $subject, $body) = @_;
+  ($#_ == 3) || die("sendEmail: invalid number of arguments");
+  my ($from, $to, $subject, $body) = @_;
+
+  open(MTA, "|$mta_program");
+  print MTA "From: $from\n";
+  print MTA "To: $to\n";
+  print MTA "Subject: $subject\n";
+  print MTA "\n";
+  print MTA $body;
+  print MTA "\n";
+  close(MTA);
 
-    open(MTA, "|$mta_program");
-    print MTA "From: $from\n";
-    print MTA "To: $to\n";
-    print MTA "Subject: $subject\n";
-    print MTA "\n";
-    print MTA $body;
-    print MTA "\n";
-    close(MTA);
-   
 }
 
 # if we found anything, kill the database thread and send mail about it
 #
 if (scalar(keys(%$long))) {
-    my $message = "";
-    foreach my $process_id (keys(%$long)) {
-        my $qry = $long->{$process_id};
-        $message .= join(" ", @$qry) . "\n\n";
-    }
+  my $message = "";
+  foreach my $process_id (keys(%$long)) {
+    my $qry = $long->{$process_id};
+    $message .= join(" ", @$qry) . "\n\n";
+  }
 
-    # fire off an email telling the maintainer that we had to kill some threads
-    #
-    sendEmail($mail_from, $mail_to, "long running MySQL thread(s) killed", $message);
+  # fire off an email telling the maintainer that we had to kill some threads
+  #
+  sendEmail($mail_from, $mail_to, "long running MySQL thread(s) killed",
+    $message);
 }
index a2feb3dda3c97fa498c8d8d01e3bb6570a962de4..e4325c5be95ba5d89d01032fc90420654d6747a6 100755 (executable)
@@ -6,14 +6,14 @@ use File::Copy::Recursive qw(dircopy);
 ($ARGV[0] && $ARGV[0] =~ /\w\w(-\w\w)?/) || usage();
 
 sub process {
-    if ($_ eq 'en' && $File::Find::name !~ /\/data\//) {
-        dircopy($_, $ARGV[0]);
-    }
+  if ($_ eq 'en' && $File::Find::name !~ /\/data\//) {
+    dircopy($_, $ARGV[0]);
+  }
 }
 
 find(\&process, ".");
 
 sub usage {
-    print "Usage: new-locale.pl <lang code>\n";
-    print " e.g.: new-locale.pl fr\n";
+  print "Usage: new-locale.pl <lang code>\n";
+  print " e.g.: new-locale.pl fr\n";
 }
index 8894371b844ad5dccdedc8e791a23c26731d236b..656bacc3b8c6aac4127bd1dbf2e07cc237abc227 100755 (executable)
@@ -32,21 +32,26 @@ use constant MAX_STRING_LEN => 25;
 # For certain tables, we can't automatically determine their Primary Key.
 # So, we specify it here as a string.
 use constant SPECIAL_KEYS => {
-    # bugs_activity since 4.4 has a unique primary key added
-    bugs_activity     => 'bug_id,bug_when,fieldid',
-    profile_setting   => 'user_id,setting_name',
-    # profiles_activity since 4.4 has a unique primary key added
-    profiles_activity => 'userid,profiles_when,fieldid',
-    setting_value     => 'name,value',
-    # longdescs didn't used to have a PK, before 2.20.
-    longdescs         => 'bug_id,bug_when',
-    # The 2.16 versions table lacked a PK
-    versions          => 'product_id,value',
-    # These are all for earlier versions of Bugzilla. On a modern
-    # version of Bugzilla, this script will ignore these (thanks to
-    # code further down).
-    components        => 'program,value',
-    products          => 'product',
+
+  # bugs_activity since 4.4 has a unique primary key added
+  bugs_activity   => 'bug_id,bug_when,fieldid',
+  profile_setting => 'user_id,setting_name',
+
+  # profiles_activity since 4.4 has a unique primary key added
+  profiles_activity => 'userid,profiles_when,fieldid',
+  setting_value     => 'name,value',
+
+  # longdescs didn't used to have a PK, before 2.20.
+  longdescs => 'bug_id,bug_when',
+
+  # The 2.16 versions table lacked a PK
+  versions => 'product_id,value',
+
+  # These are all for earlier versions of Bugzilla. On a modern
+  # version of Bugzilla, this script will ignore these (thanks to
+  # code further down).
+  components => 'program,value',
+  products   => 'product',
 };
 
 ###############
@@ -55,18 +60,18 @@ use constant SPECIAL_KEYS => {
 
 # "truncate" is a file operation in perl, so we can't use that name.
 sub trunc {
-    my ($str) = @_;
-    my $truncated = substr($str, 0, MAX_STRING_LEN);
-    if (length($truncated) ne length($str)) {
-        $truncated .= '...';
-    }
-    return $truncated;
+  my ($str) = @_;
+  my $truncated = substr($str, 0, MAX_STRING_LEN);
+  if (length($truncated) ne length($str)) {
+    $truncated .= '...';
+  }
+  return $truncated;
 }
 
 sub is_valid_utf8 {
-    my ($str) = @_;
-    Encode::_utf8_on($str);
-    return is_utf8($str, 1);
+  my ($str) = @_;
+  Encode::_utf8_on($str);
+  return is_utf8($str, 1);
 }
 
 ###############
@@ -75,171 +80,173 @@ sub is_valid_utf8 {
 
 my %switch;
 GetOptions(\%switch, 'dry-run', 'guess', 'charset=s', 'show-failures',
-                     'overrides=s', 'help|h');
+  'overrides=s', 'help|h');
 
-pod2usage({ -verbose => 1 }) if $switch{'help'};
+pod2usage({-verbose => 1}) if $switch{'help'};
 
 # You have to specify at least one of these switches.
-pod2usage({ -verbose => 0 }) if (!$switch{'charset'} && !$switch{'guess'});
+pod2usage({-verbose => 0}) if (!$switch{'charset'} && !$switch{'guess'});
 
 if (exists $switch{'charset'}) {
-    $switch{'charset'} = resolve_alias($switch{'charset'})
-        || die "'$switch{charset}' is not a valid charset.";
+  $switch{'charset'} = resolve_alias($switch{'charset'})
+    || die "'$switch{charset}' is not a valid charset.";
 }
 
 if ($switch{'guess'}) {
-    if (!Bugzilla->has_feature('detect_charset')) {
-        my $root = ROOT_USER;
-        print STDERR <<EOT;
+  if (!Bugzilla->has_feature('detect_charset')) {
+    my $root = ROOT_USER;
+    print STDERR <<EOT;
 Using --guess requires that Encode::Detect be installed. To install
 Encode::Detect, run the following command:
 
    cpanm --installdeps --with-feature=detect_charset -l local .
 
 EOT
-        exit;
-    }
-    require Encode::Detect;
+    exit;
+  }
+  require Encode::Detect;
 }
 
 my %overrides;
 if (exists $switch{'overrides'}) {
-    my $file = new IO::File($switch{'overrides'}, 'r') 
-        || die "$switch{overrides}: $!";
-    my @lines = $file->getlines();
-    $file->close();
-    foreach my $line (@lines) {
-        chomp($line);
-        my ($digest, $encoding) = split(' ', $line);
-        $overrides{$digest} = $encoding;
-    }
+  my $file = new IO::File($switch{'overrides'}, 'r')
+    || die "$switch{overrides}: $!";
+  my @lines = $file->getlines();
+  $file->close();
+  foreach my $line (@lines) {
+    chomp($line);
+    my ($digest, $encoding) = split(' ', $line);
+    $overrides{$digest} = $encoding;
+  }
 }
 
 my $dbh = Bugzilla->dbh;
 
 if ($dbh->isa('Bugzilla::DB::Mysql')) {
-    # Get the actual current encoding of the DB.
-    my $collation_data = $dbh->selectrow_arrayref(
-        "SHOW VARIABLES LIKE 'character_set_database'");
-    my $db_charset = $collation_data->[1];
-    # Set our connection encoding to *that* encoding, so that MySQL
-    # correctly accepts our changes.
-    $dbh->do("SET NAMES $db_charset");
-    # Make the database give us raw bytes.
-    $dbh->do('SET character_set_results = NULL')
+
+  # Get the actual current encoding of the DB.
+  my $collation_data
+    = $dbh->selectrow_arrayref("SHOW VARIABLES LIKE 'character_set_database'");
+  my $db_charset = $collation_data->[1];
+
+  # Set our connection encoding to *that* encoding, so that MySQL
+  # correctly accepts our changes.
+  $dbh->do("SET NAMES $db_charset");
+
+  # Make the database give us raw bytes.
+  $dbh->do('SET character_set_results = NULL');
 }
 
 $dbh->begin_work;
 
 foreach my $table ($dbh->bz_table_list_real) {
-    my @columns = $dbh->bz_table_columns($table);
-
-    my $pk = SPECIAL_KEYS->{$table};
-    if ($pk) {
-        # Assure that we're on a version of Bugzilla where those keys
-        # actually exist.
-        foreach my $column (split ',', $pk) {
-            $pk = undef if !$dbh->bz_column_info($table, $column);
-        }
+  my @columns = $dbh->bz_table_columns($table);
+
+  my $pk = SPECIAL_KEYS->{$table};
+  if ($pk) {
+
+    # Assure that we're on a version of Bugzilla where those keys
+    # actually exist.
+    foreach my $column (split ',', $pk) {
+      $pk = undef if !$dbh->bz_column_info($table, $column);
     }
+  }
 
-    # Figure out the primary key.
+  # Figure out the primary key.
+  foreach my $column (@columns) {
+    my $def = $dbh->bz_column_info($table, $column);
+    $pk = $column if $def->{PRIMARYKEY};
+  }
+
+  # If there's no PK, it's defined by a UNIQUE index.
+  if (!$pk) {
     foreach my $column (@columns) {
-        my $def = $dbh->bz_column_info($table, $column);
-        $pk = $column if $def->{PRIMARYKEY};
-    }
-    # If there's no PK, it's defined by a UNIQUE index.
-    if (!$pk) {
-        foreach my $column (@columns) {
-            my $index = $dbh->bz_index_info($table, "${table}_${column}_idx");
-            if ($index && ref($index) eq 'HASH') {
-                $pk = join(',', @{$index->{FIELDS}}) 
-                    if $index->{TYPE} eq 'UNIQUE';
-            }
-        }
+      my $index = $dbh->bz_index_info($table, "${table}_${column}_idx");
+      if ($index && ref($index) eq 'HASH') {
+        $pk = join(',', @{$index->{FIELDS}}) if $index->{TYPE} eq 'UNIQUE';
+      }
     }
+  }
 
-    foreach my $column (@columns) {
-        my $def = $dbh->bz_column_info($table, $column);
-        # If this is a text column, it may need work.
-        if ($def->{TYPE} =~ /text|char/i) {
-            # If there's still no PK, we're upgrading from 2.14 or earlier.
-            # We can't reliably determine the PK (or at least, I don't want to
-            # maintain code to record what the PK was at all points in history).
-            # So instead we just use the field itself.
-            $pk = $column if !$pk;
-
-            print "Converting $table.$column...\n";
-            my $sth = $dbh->prepare("SELECT $column, $pk FROM $table 
+  foreach my $column (@columns) {
+    my $def = $dbh->bz_column_info($table, $column);
+
+    # If this is a text column, it may need work.
+    if ($def->{TYPE} =~ /text|char/i) {
+
+      # If there's still no PK, we're upgrading from 2.14 or earlier.
+      # We can't reliably determine the PK (or at least, I don't want to
+      # maintain code to record what the PK was at all points in history).
+      # So instead we just use the field itself.
+      $pk = $column if !$pk;
+
+      print "Converting $table.$column...\n";
+      my $sth = $dbh->prepare(
+        "SELECT $column, $pk FROM $table 
                                       WHERE $column IS NOT NULL
-                                            AND $column != ''");
-
-            my @pk_array = map {"$_ = ?"} split(',', $pk);
-            my $pk_where = join(' AND ', @pk_array);
-            my $update_sth = $dbh->prepare(
-                "UPDATE $table SET $column = ? WHERE $pk_where");
-
-            $sth->execute();
-
-            while (my @result = $sth->fetchrow_array) {
-                my $data = shift @result;
-                # Wide characters cause md5_base64() to die.
-                my $digest_data = utf8::is_utf8($data) 
-                                  ? Encode::encode_utf8($data) : $data;
-                my $digest = md5_base64($digest_data);
-
-                my @primary_keys = reverse split(',', $pk);
-                # We copy the array so that we can pop things from it without
-                # affecting the original.
-                my @pk_data = @result;
-                my $pk_line = join (', ',
-                    map { "$_ = " . pop @pk_data } @primary_keys);
-
-                my $encoding;
-                if ($switch{'guess'}) {
-                    $encoding = detect_encoding($data);
-
-                    # We only show failures if they don't appear to be
-                    # ASCII.
-                    if ($switch{'show-failures'} && !$encoding
-                        && !is_valid_utf8($data)) 
-                    {
-                        my $truncated = trunc($data);
-                        print "Row: [$pk_line]\n",
-                              "Failed to guess: Key: $digest",
-                              " DATA: $truncated\n";
-                    }
-
-                    # If we fail a guess, and the data is valid UTF-8,
-                    # just assume we failed because it's UTF-8.
-                    next if is_valid_utf8($data);
-                }
-
-                # If we couldn't detect the charset (or were instructed
-                # not to try), we fall back to --charset. If there's no 
-                # fallback, we just do nothing.
-                if (!$encoding && $switch{'charset'}) {
-                    $encoding = $switch{'charset'};
-                }
-
-                $encoding = $overrides{$digest} if $overrides{$digest};
-
-                # We only fix it if it's not ASCII or UTF-8 already.
-                if ($encoding && !grep($_ eq $encoding, IGNORE_ENCODINGS)) {
-                    my $decoded = encode('utf8', decode($encoding, $data));
-                    if ($switch{'dry-run'} && $data ne $decoded) {
-                        print "Row:  [$pk_line]\n",
-                              "From: [" . trunc($data) . "] Key: $digest\n",
-                              "To:   [" . trunc($decoded) . "]",
-                              " Encoding : $encoding\n";
-                    }
-                    else {
-                        $update_sth->execute($decoded, @result);
-                    }
-                }
-            } # while (my @result = $sth->fetchrow_array)
-        } # if ($column->{TYPE} =~ /text|char/i)
-    } # foreach my $column (@columns)
+                                            AND $column != ''"
+      );
+
+      my @pk_array = map {"$_ = ?"} split(',', $pk);
+      my $pk_where = join(' AND ', @pk_array);
+      my $update_sth = $dbh->prepare("UPDATE $table SET $column = ? WHERE $pk_where");
+
+      $sth->execute();
+
+      while (my @result = $sth->fetchrow_array) {
+        my $data = shift @result;
+
+        # Wide characters cause md5_base64() to die.
+        my $digest_data = utf8::is_utf8($data) ? Encode::encode_utf8($data) : $data;
+        my $digest = md5_base64($digest_data);
+
+        my @primary_keys = reverse split(',', $pk);
+
+        # We copy the array so that we can pop things from it without
+        # affecting the original.
+        my @pk_data = @result;
+        my $pk_line = join(', ', map { "$_ = " . pop @pk_data } @primary_keys);
+
+        my $encoding;
+        if ($switch{'guess'}) {
+          $encoding = detect_encoding($data);
+
+          # We only show failures if they don't appear to be
+          # ASCII.
+          if ($switch{'show-failures'} && !$encoding && !is_valid_utf8($data)) {
+            my $truncated = trunc($data);
+            print "Row: [$pk_line]\n", "Failed to guess: Key: $digest",
+              " DATA: $truncated\n";
+          }
+
+          # If we fail a guess, and the data is valid UTF-8,
+          # just assume we failed because it's UTF-8.
+          next if is_valid_utf8($data);
+        }
+
+        # If we couldn't detect the charset (or were instructed
+        # not to try), we fall back to --charset. If there's no
+        # fallback, we just do nothing.
+        if (!$encoding && $switch{'charset'}) {
+          $encoding = $switch{'charset'};
+        }
+
+        $encoding = $overrides{$digest} if $overrides{$digest};
+
+        # We only fix it if it's not ASCII or UTF-8 already.
+        if ($encoding && !grep($_ eq $encoding, IGNORE_ENCODINGS)) {
+          my $decoded = encode('utf8', decode($encoding, $data));
+          if ($switch{'dry-run'} && $data ne $decoded) {
+            print "Row:  [$pk_line]\n", "From: [" . trunc($data) . "] Key: $digest\n",
+              "To:   [" . trunc($decoded) . "]", " Encoding : $encoding\n";
+          }
+          else {
+            $update_sth->execute($decoded, @result);
+          }
+        }
+      }    # while (my @result = $sth->fetchrow_array)
+    }    # if ($column->{TYPE} =~ /text|char/i)
+  }    # foreach my $column (@columns)
 }
 
 $dbh->commit;
index 2ebf163e7466d6016d5c1230a52a5b9acd187ec4..e8031d7753d46ceaebf69a5e74a93c3626bdbd73 100755 (executable)
@@ -20,16 +20,16 @@ use Bugzilla::User;
 my $dbh = Bugzilla->dbh;
 
 sub usage {
-    say STDERR "Usage: $0 bug_id user_email";
-    exit;
+  say STDERR "Usage: $0 bug_id user_email";
+  exit;
 }
 
 if (($#ARGV < 1) || ($#ARGV > 2)) {
-    usage();
+  usage();
 }
 
 # Get the arguments.
-my $bugnum = $ARGV[0];
+my $bugnum  = $ARGV[0];
 my $changer = $ARGV[1];
 
 # Validate the bug number.
@@ -40,8 +40,8 @@ if (!($bugnum =~ /^(\d+)$/)) {
 
 detaint_natural($bugnum);
 
-my ($id) = $dbh->selectrow_array("SELECT bug_id FROM bugs WHERE bug_id = ?", 
-                                 undef, $bugnum);
+my ($id) = $dbh->selectrow_array("SELECT bug_id FROM bugs WHERE bug_id = ?",
+  undef, $bugnum);
 
 if (!$id) {
   say STDERR "Bug number $bugnum does not exist.";
@@ -51,25 +51,26 @@ if (!$id) {
 # Validate the changer address.
 my $match = Bugzilla->params->{'emailregexp'};
 if ($changer !~ /$match/) {
-    say STDERR "Changer \"$changer\" doesn't match email regular expression.";
-    usage();
+  say STDERR "Changer \"$changer\" doesn't match email regular expression.";
+  usage();
 }
-my $changer_user = new Bugzilla::User({ name => $changer });
+my $changer_user = new Bugzilla::User({name => $changer});
 unless ($changer_user) {
-    say STDERR "\"$changer\" is not a valid user.";
-    usage();
+  say STDERR "\"$changer\" is not a valid user.";
+  usage();
 }
 
 # Send the email.
-my $outputref = Bugzilla::BugMail::Send($bugnum, {'changer' => $changer_user });
+my $outputref = Bugzilla::BugMail::Send($bugnum, {'changer' => $changer_user});
 
 # Report the results.
 my $sent = scalar(@{$outputref->{sent}});
 
 if ($sent) {
-    say "email sent to $sent recipients:";
-} else {
-    say "No email sent.";
+  say "email sent to $sent recipients:";
+}
+else {
+  say "No email sent.";
 }
 
 foreach my $sent (@{$outputref->{sent}}) {
@@ -78,12 +79,12 @@ foreach my $sent (@{$outputref->{sent}}) {
 
 # This document is copyright (C) 2004 Perforce Software, Inc.  All rights
 # reserved.
-# 
+#
 # Redistribution and use of this document in any form, with or without
 # modification, is permitted provided that redistributions of this
 # document retain the above copyright notice, this condition and the
 # following disclaimer.
-# 
+#
 # THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
 # IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 # TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
index 16fd00aa1af3a690de0d2eb749bf1c5276a8db7d..3d11145ae7d7d27242849f3cc1d2ea042fd3f634 100755 (executable)
@@ -19,28 +19,28 @@ use Bugzilla::BugMail;
 my $dbh = Bugzilla->dbh;
 
 my $list = $dbh->selectcol_arrayref(
-        'SELECT bug_id FROM bugs
+  'SELECT bug_id FROM bugs
           WHERE (lastdiffed IS NULL OR lastdiffed < delta_ts)
             AND delta_ts < '
-                . $dbh->sql_date_math('NOW()', '-', 30, 'MINUTE') .
-     ' ORDER BY bug_id');
+    . $dbh->sql_date_math('NOW()', '-', 30, 'MINUTE') . ' ORDER BY bug_id'
+);
 
 if (scalar(@$list) > 0) {
-    say "OK, now attempting to send unsent mail";
-    say scalar(@$list) . " bugs found with possibly unsent mail.\n";
-    foreach my $bugid (@$list) {
-        my $start_time = time;
-        say "Sending mail for bug $bugid...";
-        my $outputref = Bugzilla::BugMail::Send($bugid);
-        if ($ARGV[0] && $ARGV[0] eq "--report") {
-          say "Mail sent to:";
-          say $_ foreach (sort @{$outputref->{sent}});
-        }
-        else {
-            my $sent = scalar @{$outputref->{sent}};
-            say "$sent mails sent.";
-            say "Took " . (time - $start_time) . " seconds.\n";
-        }
+  say "OK, now attempting to send unsent mail";
+  say scalar(@$list) . " bugs found with possibly unsent mail.\n";
+  foreach my $bugid (@$list) {
+    my $start_time = time;
+    say "Sending mail for bug $bugid...";
+    my $outputref = Bugzilla::BugMail::Send($bugid);
+    if ($ARGV[0] && $ARGV[0] eq "--report") {
+      say "Mail sent to:";
+      say $_ foreach (sort @{$outputref->{sent}});
     }
-    say "Unsent mail has been sent.";
+    else {
+      my $sent = scalar @{$outputref->{sent}};
+      say "$sent mails sent.";
+      say "Took " . (time - $start_time) . " seconds.\n";
+    }
+  }
+  say "Unsent mail has been sent.";
 }
index 52ff067e3ef300d8994dfa125fc9a7983b1873f9..7a610ac55aa4f52c544f87264954a7475344d25b 100755 (executable)
@@ -19,47 +19,49 @@ use Bugzilla::User;
 my $cgi = Bugzilla->cgi;
 my $dbh = Bugzilla->dbh;
 
-my $readonly = 0;
+my $readonly  = 0;
 my $nodisable = 0;
-my $noupdate = 0;
-my $nocreate = 0;
-my $quiet    = 0;
+my $noupdate  = 0;
+my $nocreate  = 0;
+my $quiet     = 0;
 
 ###
 # Do some preparations
 ###
-foreach my $arg (@ARGV)
-{
-   if($arg eq '-r') {
-      $readonly = 1;
-   }
-   elsif($arg eq '-d') {
-      $nodisable = 1;
-   }
-   elsif($arg eq '-u') {
-      $noupdate = 1;
-   }
-   elsif($arg eq '-c') {
-      $nocreate = 1;
-   }
-   elsif($arg eq '-q') {
-      $quiet = 1;
-   }
-   else {
-         print "LDAP Sync Script\n";
-         print "Syncronizes the users table from the LDAP server with the Bugzilla users.\n";
-         print "Takes mail-attribute from preferences and description from 'cn' or,\n";
-         print "if not available, from the uid-attribute.\n\n";
-         print "usage:\n syncLDAP.pl [options]\n\n";
-         print "options:\n";
-         print " -r Readonly, do not make changes to Bugzilla tables\n";
-         print " -d No disable, don't disable login by users who are not in LDAP\n";
-         print " -u No update, don't update users, which have different description in LDAP\n";
-         print " -c No create, don't create users, which are in LDAP but not in Bugzilla\n";
-         print " -q Quiet mode, give less output\n";
-         print "\n";
-         exit;
-   }
+foreach my $arg (@ARGV) {
+  if ($arg eq '-r') {
+    $readonly = 1;
+  }
+  elsif ($arg eq '-d') {
+    $nodisable = 1;
+  }
+  elsif ($arg eq '-u') {
+    $noupdate = 1;
+  }
+  elsif ($arg eq '-c') {
+    $nocreate = 1;
+  }
+  elsif ($arg eq '-q') {
+    $quiet = 1;
+  }
+  else {
+    print "LDAP Sync Script\n";
+    print
+      "Syncronizes the users table from the LDAP server with the Bugzilla users.\n";
+    print "Takes mail-attribute from preferences and description from 'cn' or,\n";
+    print "if not available, from the uid-attribute.\n\n";
+    print "usage:\n syncLDAP.pl [options]\n\n";
+    print "options:\n";
+    print " -r Readonly, do not make changes to Bugzilla tables\n";
+    print " -d No disable, don't disable login by users who are not in LDAP\n";
+    print
+      " -u No update, don't update users, which have different description in LDAP\n";
+    print
+      " -c No create, don't create users, which are in LDAP but not in Bugzilla\n";
+    print " -q Quiet mode, give less output\n";
+    print "\n";
+    exit;
+  }
 }
 
 my %ldap_users;
@@ -67,13 +69,18 @@ my %ldap_users;
 ###
 # Get current bugzilla users
 ###
-my %bugzilla_users = %{ $dbh->selectall_hashref(
-    'SELECT login_name AS new_login_name, realname, disabledtext ' .
-    'FROM profiles', 'new_login_name') };
+my %bugzilla_users = %{
+  $dbh->selectall_hashref(
+    'SELECT login_name AS new_login_name, realname, disabledtext '
+      . 'FROM profiles',
+    'new_login_name'
+  )
+};
 
 foreach my $login_name (keys %bugzilla_users) {
-    # remove whitespaces
-    $bugzilla_users{$login_name}{'realname'} =~ s/^\s+|\s+$//g;
+
+  # remove whitespaces
+  $bugzilla_users{$login_name}{'realname'} =~ s/^\s+|\s+$//g;
 }
 
 ###
@@ -81,72 +88,79 @@ foreach my $login_name (keys %bugzilla_users) {
 ###
 my $LDAPserver = Bugzilla->params->{"LDAPserver"};
 if ($LDAPserver eq "") {
-   print "No LDAP server defined in bugzilla preferences.\n";
-   exit;
+  print "No LDAP server defined in bugzilla preferences.\n";
+  exit;
 }
 
 my $LDAPconn;
-if($LDAPserver =~ /:\/\//) {
-    # if the "LDAPserver" parameter is in uri scheme
-    $LDAPconn = Net::LDAP->new($LDAPserver, version => 3);
-} else {
-    my $LDAPport = "389";  # default LDAP port
-    if($LDAPserver =~ /:/) {
-        ($LDAPserver, $LDAPport) = split(":",$LDAPserver);
-    }
-    $LDAPconn = Net::LDAP->new($LDAPserver, port => $LDAPport, version => 3);
+if ($LDAPserver =~ /:\/\//) {
+
+  # if the "LDAPserver" parameter is in uri scheme
+  $LDAPconn = Net::LDAP->new($LDAPserver, version => 3);
+}
+else {
+  my $LDAPport = "389";    # default LDAP port
+  if ($LDAPserver =~ /:/) {
+    ($LDAPserver, $LDAPport) = split(":", $LDAPserver);
+  }
+  $LDAPconn = Net::LDAP->new($LDAPserver, port => $LDAPport, version => 3);
 }
 
-if(!$LDAPconn) {
-   print "Connecting to LDAP server failed. Check LDAPserver setting.\n";
-   exit;
+if (!$LDAPconn) {
+  print "Connecting to LDAP server failed. Check LDAPserver setting.\n";
+  exit;
 }
 my $mesg;
 if (Bugzilla->params->{"LDAPbinddn"}) {
-    my ($LDAPbinddn,$LDAPbindpass) = split(":",Bugzilla->params->{"LDAPbinddn"});
-    $mesg = $LDAPconn->bind($LDAPbinddn, password => $LDAPbindpass);
+  my ($LDAPbinddn, $LDAPbindpass) = split(":", Bugzilla->params->{"LDAPbinddn"});
+  $mesg = $LDAPconn->bind($LDAPbinddn, password => $LDAPbindpass);
 }
 else {
-    $mesg = $LDAPconn->bind();
+  $mesg = $LDAPconn->bind();
 }
-if($mesg->code) {
-   print "Binding to LDAP server failed: " . $mesg->error . "\nCheck LDAPbinddn setting.\n";
-   exit;
+if ($mesg->code) {
+  print "Binding to LDAP server failed: "
+    . $mesg->error
+    . "\nCheck LDAPbinddn setting.\n";
+  exit;
 }
 
 # We've got our anonymous bind;  let's look up the users.
-$mesg = $LDAPconn->search( base   => Bugzilla->params->{"LDAPBaseDN"},
-                           scope  => "sub",
-                           filter => '(&(' . Bugzilla->params->{"LDAPuidattribute"} . "=*)" . Bugzilla->params->{"LDAPfilter"} . ')',
-                         );
-                         
-
-if(! $mesg->count) {
-   print "LDAP lookup failure. Check LDAPBaseDN setting.\n";
-   exit;
+$mesg = $LDAPconn->search(
+  base   => Bugzilla->params->{"LDAPBaseDN"},
+  scope  => "sub",
+  filter => '(&('
+    . Bugzilla->params->{"LDAPuidattribute"} . "=*)"
+    . Bugzilla->params->{"LDAPfilter"} . ')',
+);
+
+
+if (!$mesg->count) {
+  print "LDAP lookup failure. Check LDAPBaseDN setting.\n";
+  exit;
 }
-   
-my %val = %{ $mesg->as_struct };
-
-while( my ($key, $value) = each(%val) ) {
-
-   my @login_name = @{ $value->{Bugzilla->params->{"LDAPmailattribute"}} };
-   my @realname  = @{ $value->{"cn"} };
-
-   # no mail entered? go to next
-   if(! @login_name) { 
-      print "$key has no valid mail address\n";
-      next; 
-   }
-
-   # no cn entered? use uid instead
-   if(! @realname) { 
-       @realname = @{ $value->{Bugzilla->params->{"LDAPuidattribute"}} };
-   }
-  
-   my $login = shift @login_name;
-   my $real = shift @realname;
-   $ldap_users{$login} = { realname => $real };
+
+my %val = %{$mesg->as_struct};
+
+while (my ($key, $value) = each(%val)) {
+
+  my @login_name = @{$value->{Bugzilla->params->{"LDAPmailattribute"}}};
+  my @realname   = @{$value->{"cn"}};
+
+  # no mail entered? go to next
+  if (!@login_name) {
+    print "$key has no valid mail address\n";
+    next;
+  }
+
+  # no cn entered? use uid instead
+  if (!@realname) {
+    @realname = @{$value->{Bugzilla->params->{"LDAPuidattribute"}}};
+  }
+
+  my $login = shift @login_name;
+  my $real  = shift @realname;
+  $ldap_users{$login} = {realname => $real};
 }
 
 print "\n" unless $quiet;
@@ -159,120 +173,126 @@ my %update_users;
 my %create_users;
 
 print "Bugzilla-Users: \n" unless $quiet;
-while( my ($key, $value) = each(%bugzilla_users) ) {
-  print " " . $key . " '" . $value->{'realname'} . "' " . $value->{'disabledtext'} ."\n" unless $quiet==1;
-  if(!exists $ldap_users{$key}){
-     if($value->{'disabledtext'} eq '') {
-       $disable_users{$key} = $value;
-     }
+while (my ($key, $value) = each(%bugzilla_users)) {
+  print " "
+    . $key . " '"
+    . $value->{'realname'} . "' "
+    . $value->{'disabledtext'} . "\n"
+    unless $quiet == 1;
+  if (!exists $ldap_users{$key}) {
+    if ($value->{'disabledtext'} eq '') {
+      $disable_users{$key} = $value;
+    }
   }
 }
 
 print "\nLDAP-Users: \n" unless $quiet;
-while( my ($key, $value) = each(%ldap_users) ) {
-  print " " . $key . " '" . $value->{'realname'} . "'\n" unless $quiet==1;
-  if(!defined $bugzilla_users{$key}){
+while (my ($key, $value) = each(%ldap_users)) {
+  print " " . $key . " '" . $value->{'realname'} . "'\n" unless $quiet == 1;
+  if (!defined $bugzilla_users{$key}) {
     $create_users{$key} = $value;
   }
-  else { 
+  else {
     my $bugzilla_user_value = $bugzilla_users{$key};
-    if($bugzilla_user_value->{'realname'} ne $value->{'realname'}) {
+    if ($bugzilla_user_value->{'realname'} ne $value->{'realname'}) {
       $update_users{$key} = $value;
     }
   }
 }
 
 print "\nDetecting email changes: \n" unless $quiet;
-while( my ($create_key, $create_value) = each(%create_users) ) {
-  while( my ($disable_key, $disable_value) = each(%disable_users) ) {
-    if($create_value->{'realname'} eq $disable_value->{'realname'}) {
-       print " " . $disable_key . " => " . $create_key ."'\n" unless $quiet==1;
-       $update_users{$disable_key} = { realname => $create_value->{'realname'},
-                                       new_login_name => $create_key };
-       delete $create_users{$create_key};
-       delete $disable_users{$disable_key};
+while (my ($create_key, $create_value) = each(%create_users)) {
+  while (my ($disable_key, $disable_value) = each(%disable_users)) {
+    if ($create_value->{'realname'} eq $disable_value->{'realname'}) {
+      print " " . $disable_key . " => " . $create_key . "'\n" unless $quiet == 1;
+      $update_users{$disable_key}
+        = {realname => $create_value->{'realname'}, new_login_name => $create_key};
+      delete $create_users{$create_key};
+      delete $disable_users{$disable_key};
     }
   }
 }
 
-if($quiet == 0) {
-   print "\nUsers to disable login for: \n";
-   while( my ($key, $value) = each(%disable_users) ) {
-     print " " . $key . " '" . $value->{'realname'} . "'\n";
-   }
-   
-   print "\nUsers to update: \n";
-   while( my ($key, $value) = each(%update_users) ) {
-     print " " . $key . " '" . $value->{'realname'} . "' ";
-     if(defined $value->{'new_login_name'}) {
-       print "has changed email to " . $value->{'new_login_name'};
-     }
-     print "\n";
-   }
-   
-   print "\nUsers to create: \n";
-   while( my ($key, $value) = each(%create_users) ) {
-     print " " . $key . " '" . $value->{'realname'} . "'\n";
-   }
-   
-   print "\n\n";
+if ($quiet == 0) {
+  print "\nUsers to disable login for: \n";
+  while (my ($key, $value) = each(%disable_users)) {
+    print " " . $key . " '" . $value->{'realname'} . "'\n";
+  }
+
+  print "\nUsers to update: \n";
+  while (my ($key, $value) = each(%update_users)) {
+    print " " . $key . " '" . $value->{'realname'} . "' ";
+    if (defined $value->{'new_login_name'}) {
+      print "has changed email to " . $value->{'new_login_name'};
+    }
+    print "\n";
+  }
+
+  print "\nUsers to create: \n";
+  while (my ($key, $value) = each(%create_users)) {
+    print " " . $key . " '" . $value->{'realname'} . "'\n";
+  }
+
+  print "\n\n";
 }
 
 
 ###
 # now do the DB-Update
 ###
-if($readonly == 0) {
-   print "Performing DB update:\nPhase 1: disabling login for users not in LDAP... " unless $quiet;
+if ($readonly == 0) {
+  print
+    "Performing DB update:\nPhase 1: disabling login for users not in LDAP... "
+    unless $quiet;
 
-   my $sth_disable = $dbh->prepare(
-       'UPDATE profiles
+  my $sth_disable = $dbh->prepare(
+    'UPDATE profiles
            SET disabledtext = ?
-         WHERE ' . $dbh->sql_istrcmp('login_name', '?'));
+         WHERE ' . $dbh->sql_istrcmp('login_name', '?')
+  );
 
-   if($nodisable == 0) {
-      while( my ($key, $value) = each(%disable_users) ) {
-        $sth_disable->execute('auto-disabled by ldap sync', $key);
-      }
-      print "done!\n" unless $quiet;
-   }
-   else {
-      print "disabled!\n" unless $quiet;
-   }
-   
-   print "Phase 2: updating existing users... " unless $quiet;
-
-   if($noupdate == 0) {
-      while( my ($key, $value) = each(%update_users) ) {
-        my $user = Bugzilla::User->check($key);
-        if(defined $value->{'new_login_name'}) {
-          $user->set_login($value->{'new_login_name'});
-        } else {
-          $user->set_name($value->{'realname'});
-        }
-        $user->update();
+  if ($nodisable == 0) {
+    while (my ($key, $value) = each(%disable_users)) {
+      $sth_disable->execute('auto-disabled by ldap sync', $key);
+    }
+    print "done!\n" unless $quiet;
+  }
+  else {
+    print "disabled!\n" unless $quiet;
+  }
+
+  print "Phase 2: updating existing users... " unless $quiet;
+
+  if ($noupdate == 0) {
+    while (my ($key, $value) = each(%update_users)) {
+      my $user = Bugzilla::User->check($key);
+      if (defined $value->{'new_login_name'}) {
+        $user->set_login($value->{'new_login_name'});
       }
-      print "done!\n" unless $quiet;
-   }
-   else {
-      print "disabled!\n" unless $quiet;
-   }
-   
-   print "Phase 3: creating new users... " unless $quiet;
-   if($nocreate == 0) {
-      while( my ($key, $value) = each(%create_users) ) {
-        Bugzilla::User->create({
-            login_name => $key, 
-            realname   => $value->{'realname'},
-            cryptpassword   => '*'});
+      else {
+        $user->set_name($value->{'realname'});
       }
-      print "done!\n" unless $quiet;
-   }
-   else {
-      print "disabled!\n" unless $quiet;
-   }
+      $user->update();
+    }
+    print "done!\n" unless $quiet;
+  }
+  else {
+    print "disabled!\n" unless $quiet;
+  }
+
+  print "Phase 3: creating new users... " unless $quiet;
+  if ($nocreate == 0) {
+    while (my ($key, $value) = each(%create_users)) {
+      Bugzilla::User->create({
+        login_name => $key, realname => $value->{'realname'}, cryptpassword => '*'
+      });
+    }
+    print "done!\n" unless $quiet;
+  }
+  else {
+    print "disabled!\n" unless $quiet;
+  }
 }
-else
-{
-   print "No changes to DB because readonly mode\n" unless $quiet;
+else {
+  print "No changes to DB because readonly mode\n" unless $quiet;
 }
index 3b3dd7c1f717c915b7b0a5f8817cb57582084fdc..b2ee95f20bee193862a8e12b3b80241a6a0799c1 100755 (executable)
@@ -20,39 +20,42 @@ use Bugzilla::Token;
 # Just in case someone already has an account, let them get the correct footer
 # on an error message. The user is logged out just after the account is
 # actually created.
-my $user = Bugzilla->login(LOGIN_OPTIONAL);
-my $cgi = Bugzilla->cgi;
+my $user     = Bugzilla->login(LOGIN_OPTIONAL);
+my $cgi      = Bugzilla->cgi;
 my $template = Bugzilla->template;
-my $vars = { doc_section => 'using/creating-an-account.html' };
+my $vars     = {doc_section => 'using/creating-an-account.html'};
 
 print $cgi->header();
 
 my $email = $cgi->param('email');
-my $login = Bugzilla->params->{'use_email_as_login'} ? $email : $cgi->param('login');
+my $login
+  = Bugzilla->params->{'use_email_as_login'} ? $email : $cgi->param('login');
 
 my $request_new_password = $cgi->param('request_new_password');
 
 if ($request_new_password) {
-    $template->process('account/request-new-password.html.tmpl', $vars)
-      || ThrowTemplateError($template->error());
+  $template->process('account/request-new-password.html.tmpl', $vars)
+    || ThrowTemplateError($template->error());
 }
 elsif ($email && $login) {
-    $user->check_account_creation_enabled;
-    # Check the hash token to make sure this user actually submitted
-    # the create account form.
-    my $token = $cgi->param('token');
-    check_hash_token($token, ['create_account']);
-
-    $user->check_and_send_account_creation_confirmation($login, $email);
-    $vars->{'login'} = $login;
-    $vars->{'email'} = $email;
-
-    $template->process("account/created.html.tmpl", $vars)
-      || ThrowTemplateError($template->error());
+  $user->check_account_creation_enabled;
+
+  # Check the hash token to make sure this user actually submitted
+  # the create account form.
+  my $token = $cgi->param('token');
+  check_hash_token($token, ['create_account']);
+
+  $user->check_and_send_account_creation_confirmation($login, $email);
+  $vars->{'login'} = $login;
+  $vars->{'email'} = $email;
+
+  $template->process("account/created.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
 }
 else {
-    $user->check_account_creation_enabled;
-    # Show the standard "would you like to create an account?" form.
-    $template->process('account/create.html.tmpl', $vars)
-      || ThrowTemplateError($template->error());
+  $user->check_account_creation_enabled;
+
+  # Show the standard "would you like to create an account?" form.
+  $template->process('account/create.html.tmpl', $vars)
+    || ThrowTemplateError($template->error());
 }
index a81c8aa57e764390a173a35e7e9ee9d932b628c5..67d5ad26136c0f5d176eb4379bf39849af414289 100755 (executable)
@@ -19,10 +19,10 @@ use Bugzilla::Error;
 use Bugzilla::Classification;
 use Bugzilla::Product;
 
-my $user = Bugzilla->login();
-my $cgi = Bugzilla->cgi;
+my $user     = Bugzilla->login();
+my $cgi      = Bugzilla->cgi;
 my $template = Bugzilla->template;
-my $vars = {};
+my $vars     = {};
 
 print $cgi->header();
 
@@ -33,37 +33,41 @@ my $product_name = trim($cgi->param('product') || '');
 my $product = new Bugzilla::Product({'name' => $product_name});
 
 unless ($product && $user->can_access_product($product->name)) {
-    # Products which the user is allowed to see.
-    my @products = @{$user->get_accessible_products};
 
-    if (scalar(@products) == 0) {
-        ThrowUserError("no_products");
-    }
-    # If there is only one product available but the user entered
-    # another product name, we display a list with this single
-    # product only, to not confuse the user with components of a
-    # product they didn't request.
-    elsif (scalar(@products) > 1 || $product_name) {
-        $vars->{'classifications'} = sort_products_by_classification(\@products);
-        $vars->{'target'} = "describecomponents.cgi";
-        # If an invalid product name is given, or the user is not
-        # allowed to access that product, a message is displayed
-        # with a list of the products the user can choose from.
-        if ($product_name) {
-            $vars->{'message'} = "product_invalid";
-            # Do not use $product->name here, else you could use
-            # this way to determine whether the product exists or not.
-            $vars->{'product'} = $product_name;
-        }
-
-        $template->process("global/choose-product.html.tmpl", $vars)
-          || ThrowTemplateError($template->error());
-        exit;
+  # Products which the user is allowed to see.
+  my @products = @{$user->get_accessible_products};
+
+  if (scalar(@products) == 0) {
+    ThrowUserError("no_products");
+  }
+
+  # If there is only one product available but the user entered
+  # another product name, we display a list with this single
+  # product only, to not confuse the user with components of a
+  # product they didn't request.
+  elsif (scalar(@products) > 1 || $product_name) {
+    $vars->{'classifications'} = sort_products_by_classification(\@products);
+    $vars->{'target'}          = "describecomponents.cgi";
+
+    # If an invalid product name is given, or the user is not
+    # allowed to access that product, a message is displayed
+    # with a list of the products the user can choose from.
+    if ($product_name) {
+      $vars->{'message'} = "product_invalid";
+
+      # Do not use $product->name here, else you could use
+      # this way to determine whether the product exists or not.
+      $vars->{'product'} = $product_name;
     }
 
-    # If there is only one product available and the user didn't specify
-    # any product name, we show this product.
-    $product = $products[0];
+    $template->process("global/choose-product.html.tmpl", $vars)
+      || ThrowTemplateError($template->error());
+    exit;
+  }
+
+  # If there is only one product available and the user didn't specify
+  # any product name, we show this product.
+  $product = $products[0];
 }
 
 ######################################################################
index dd7c6f8817b21b134f14843ffc5a4089988d48c7..0ce3b2b3a6144c876f329cbfe3add11d0d10f6f8 100755 (executable)
@@ -18,16 +18,16 @@ use Bugzilla::Keyword;
 
 my $user = Bugzilla->login();
 
-my $cgi = Bugzilla->cgi;
+my $cgi      = Bugzilla->cgi;
 my $template = Bugzilla->template;
-my $vars = {};
+my $vars     = {};
 
 # Run queries against the shadow DB.
 Bugzilla->switch_to_shadow_db;
 
 $vars->{'keywords'} = Bugzilla::Keyword->get_all_with_bug_count();
 if (!@{$vars->{keywords}}) {
-    ThrowUserError("no_keywords");
+  ThrowUserError("no_keywords");
 }
 
 print $cgi->header();
index 047a27bc741bdd23511ce1775f385b88e615a6a6..e139f70813df6836c8b33d4107222a91844286f0 100644 (file)
@@ -16,53 +16,53 @@ use parent qw(Pod::Simple::HTML);
 # Without this constant, HTMLBatch will throw undef warnings.
 use constant VERSION    => $Pod::Simple::HTML::VERSION;
 use constant CODE_CLASS => ' class="code"';
-use constant META_CT  => '<meta http-equiv="Content-Type" content="text/html;'
-                         . ' charset=UTF-8">';
-use constant DOCTYPE  => '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01'
-    . ' Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">';
+use constant META_CT    => '<meta http-equiv="Content-Type" content="text/html;'
+  . ' charset=UTF-8">';
+use constant DOCTYPE => '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01'
+  . ' Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">';
 
 sub new {
-    my $self    = shift->SUPER::new(@_);
+  my $self = shift->SUPER::new(@_);
 
-    my $doctype      = $self->DOCTYPE;
-    my $content_type = $self->META_CT;
+  my $doctype      = $self->DOCTYPE;
+  my $content_type = $self->META_CT;
 
-    my $html_pre_title = <<END_HTML;
+  my $html_pre_title = <<END_HTML;
 $doctype
 <html>
   <head>
     <title>
 END_HTML
 
-    my $html_post_title = <<END_HTML;
+  my $html_post_title = <<END_HTML;
 </title>
     $content_type
   </head>
   <body id="pod">
 END_HTML
 
-    $self->html_header_before_title($html_pre_title);
-    $self->html_header_after_title($html_post_title);
+  $self->html_header_before_title($html_pre_title);
+  $self->html_header_after_title($html_post_title);
 
-    # Fix some tags to have classes so that we can adjust them.
-    my $code = CODE_CLASS;
-    $self->{'Tagmap'}->{'Verbatim'} = "\n<pre $code>";
-    $self->{'Tagmap'}->{'VerbatimFormatted'} = "\n<pre $code>";
-    $self->{'Tagmap'}->{'F'} = "<em $code>";
-    $self->{'Tagmap'}->{'C'} = "<code $code>";
+  # Fix some tags to have classes so that we can adjust them.
+  my $code = CODE_CLASS;
+  $self->{'Tagmap'}->{'Verbatim'}          = "\n<pre $code>";
+  $self->{'Tagmap'}->{'VerbatimFormatted'} = "\n<pre $code>";
+  $self->{'Tagmap'}->{'F'}                 = "<em $code>";
+  $self->{'Tagmap'}->{'C'}                 = "<code $code>";
 
-    # Don't put head4 tags into the Table of Contents. We have this
-    delete $Pod::Simple::HTML::ToIndex{'head4'};
+  # Don't put head4 tags into the Table of Contents. We have this
+  delete $Pod::Simple::HTML::ToIndex{'head4'};
 
-    return $self;
+  return $self;
 }
 
 # Override do_beginning to put the name of the module at the top
 sub do_beginning {
-    my $self = shift;
-    $self->SUPER::do_beginning(@_);
-    print {$self->{'output_fh'}} "<h1>" . $self->get_short_title . "</h1>";
-    return 1;
+  my $self = shift;
+  $self->SUPER::do_beginning(@_);
+  print {$self->{'output_fh'}} "<h1>" . $self->get_short_title . "</h1>";
+  return 1;
 }
 
 1;
index a4747eef0d165c2b67fc79e95328f9db8dc0b854..88e699a4e29f09ae6098e003ab782ffe43910cce 100644 (file)
@@ -21,90 +21,92 @@ BEGIN { *esc = \&Pod::Simple::HTML::esc }
 # Note that if you leave out a category here, it will not be indexed
 # in the contents file, even though its HTML POD will still exist.
 use constant FILE_TRANSLATION => {
-    Files      => ['importxml', 'contrib', 'checksetup', 'email_in', 
-                   'install-module', 'sanitycheck', 'jobqueue', 'migrate',
-                   'collectstats'],
-    Modules    => ['bugzilla'],
-    Extensions => ['extensions'],
+  Files => [
+    'importxml',      'contrib',     'checksetup', 'email_in',
+    'install-module', 'sanitycheck', 'jobqueue',   'migrate',
+    'collectstats'
+  ],
+  Modules    => ['bugzilla'],
+  Extensions => ['extensions'],
 };
 
 # This is basically copied from Pod::Simple::HTMLBatch, and overridden
 # so that we can format things more nicely.
 sub _write_contents_middle {
-    my ($self, $Contents, $outfile, $toplevel2submodules) = @_;
-
-    my $file_trans = FILE_TRANSLATION;
-
-    # For every top-level category...
-    foreach my $category (sort keys %$file_trans) {
-        # Get all of the HTMLBatch categories that should be in this
-        # category.
-        my @category_data;
-        foreach my $b_category (@{$file_trans->{$category}}) {
-            my $data = $toplevel2submodules->{$b_category};
-            push(@category_data, @$data) if $data;
-        }
-        next unless @category_data;
-
-        my @downlines = sort {$a->[-1] cmp $b->[-1]} @category_data;
-
-        # And finally, actually print out the table for this category. 
-        printf $Contents qq[<dt><a name="%s">%s</a></dt>\n<dd>\n],
-                esc($category), esc($category);
-        print $Contents '<table class="pod_desc_table">' . "\n";
-
-        # For every POD...
-        my $row_count = 0;
-        foreach my $e (@downlines) {
-            $row_count++;
-            my $even_or_odd = $row_count % 2 ? 'even' : 'odd';
-            my $name = esc($e->[0]);
-            my $path = join( "/", '.', esc(@{$e->[3]}) )
-               . $Pod::Simple::HTML::HTML_EXTENSION;
-            my $description = $self->{bugzilla_desc}->{$name} || '';
-            $description = esc($description);
-            my $html = <<END_HTML;
+  my ($self, $Contents, $outfile, $toplevel2submodules) = @_;
+
+  my $file_trans = FILE_TRANSLATION;
+
+  # For every top-level category...
+  foreach my $category (sort keys %$file_trans) {
+
+    # Get all of the HTMLBatch categories that should be in this
+    # category.
+    my @category_data;
+    foreach my $b_category (@{$file_trans->{$category}}) {
+      my $data = $toplevel2submodules->{$b_category};
+      push(@category_data, @$data) if $data;
+    }
+    next unless @category_data;
+
+    my @downlines = sort { $a->[-1] cmp $b->[-1] } @category_data;
+
+    # And finally, actually print out the table for this category.
+    printf $Contents qq[<dt><a name="%s">%s</a></dt>\n<dd>\n], esc($category),
+      esc($category);
+    print $Contents '<table class="pod_desc_table">' . "\n";
+
+    # For every POD...
+    my $row_count = 0;
+    foreach my $e (@downlines) {
+      $row_count++;
+      my $even_or_odd = $row_count % 2 ? 'even' : 'odd';
+      my $name = esc($e->[0]);
+      my $path = join("/", '.', esc(@{$e->[3]})) . $Pod::Simple::HTML::HTML_EXTENSION;
+      my $description = $self->{bugzilla_desc}->{$name} || '';
+      $description = esc($description);
+      my $html = <<END_HTML;
 <tr class="$even_or_odd">
   <th><a href="$path">$name</a></th>
   <td>$description</td>
 </tr>
 END_HTML
-      
-            print $Contents $html;
-        }
-        print $Contents "</table></dd>\n\n";
+
+      print $Contents $html;
     }
+    print $Contents "</table></dd>\n\n";
+  }
 
-    return 1;
+  return 1;
 }
 
 # This stores the name and description for each file, so that
 # we can get that information out later.
 sub note_for_contents_file {
-    my $self = shift;
-    my $retval = $self->SUPER::note_for_contents_file(@_);
-
-    my ($namelets, $infile) = @_;
-    my $parser   = $self->html_render_class->new;
-    $parser->set_source($infile);
-    my $full_title = $parser->get_title;
-    $full_title =~ /^\S+\s+-+\s+(.+)/;
-    my $description = $1;
-    
-    $self->{bugzilla_desc} ||= {};
-    $self->{bugzilla_desc}->{join('::', @$namelets)} = $description;
-
-    return $retval;
+  my $self   = shift;
+  my $retval = $self->SUPER::note_for_contents_file(@_);
+
+  my ($namelets, $infile) = @_;
+  my $parser = $self->html_render_class->new;
+  $parser->set_source($infile);
+  my $full_title = $parser->get_title;
+  $full_title =~ /^\S+\s+-+\s+(.+)/;
+  my $description = $1;
+
+  $self->{bugzilla_desc} ||= {};
+  $self->{bugzilla_desc}->{join('::', @$namelets)} = $description;
+
+  return $retval;
 }
 
 # Exclude modules being in lib/.
 sub find_all_pods {
-    my($self, $dirs) = @_;
-    my $mod2path = $self->SUPER::find_all_pods($dirs);
-    foreach my $mod (keys %$mod2path) {
-        delete $mod2path->{$mod} if $mod =~ /^lib::/;
-    }
-    return $mod2path;
+  my ($self, $dirs) = @_;
+  my $mod2path = $self->SUPER::find_all_pods($dirs);
+  foreach my $mod (keys %$mod2path) {
+    delete $mod2path->{$mod} if $mod =~ /^lib::/;
+  }
+  return $mod2path;
 }
 
 1;
index 93bdd5190a2a60b951323689086d58d4bcffa1f1..4d3e107aee71599160a0a28eede912c04d276e67 100755 (executable)
@@ -53,32 +53,30 @@ use File::Spec::Functions qw(:ALL);
 my $error_found = 0;
 
 sub MakeDocs {
-    my ($name, $cmdline) = @_;
+  my ($name, $cmdline) = @_;
 
-    say "Creating $name documentation ..." if defined $name;
-    system('make', $cmdline) == 0
-        or $error_found = 1;
-    print "\n";
+  say "Creating $name documentation ..." if defined $name;
+  system('make', $cmdline) == 0 or $error_found = 1;
+  print "\n";
 }
 
 # Compile all the POD and make it part of the API docs
 # Also duplicate some Extension docs into the User section, Admin section, and
 # WebService docs
 sub pod2rst {
-    my $path = shift;
+  my $path = shift;
 
-    say "Converting POD to RST...";
-    my $name2path = Pod::Simple::Search->new->inc(0)->verbose(0)
-        ->survey(@{ ['../'] });
+  say "Converting POD to RST...";
+  my $name2path
+    = Pod::Simple::Search->new->inc(0)->verbose(0)->survey(@{['../']});
 
-    my $ind_path = catdir($path, 'rst', 'integrating', 'internals');
-    rmtree($ind_path);
-    make_path($ind_path);
+  my $ind_path = catdir($path, 'rst', 'integrating', 'internals');
+  rmtree($ind_path);
+  make_path($ind_path);
 
-    my $FILE;
-    open($FILE, '>', catfile($ind_path, 'index.rst'))
-        || die("Can't open rst file");
-    print($FILE <<'EOI');
+  my $FILE;
+  open($FILE, '>', catfile($ind_path, 'index.rst')) || die("Can't open rst file");
+  print($FILE <<'EOI');
 .. highlight:: perl
 
 .. _developer
@@ -95,145 +93,139 @@ in Bugzilla.
 
 EOI
 
-    foreach my $mod (sort keys %$name2path) {
-        # Ignoring library files;
-        if ($mod =~ /^b?(lib::|local)/) {
-            delete $name2path->{$mod};
-            next;
+  foreach my $mod (sort keys %$name2path) {
+
+    # Ignoring library files;
+    if ($mod =~ /^b?(lib::|local)/) {
+      delete $name2path->{$mod};
+      next;
+    }
+
+    my $title = $mod;
+
+    my $ext = $title =~ s/^extensions:://;
+    $title =~ s/lib:://;
+    $title =~ s/::Extension$// if ($ext);
+    my $header = "=" x length($title);
+
+    my $abs_path = $name2path->{$mod};
+    my $fpath = abs2rel($abs_path, '..');
+    my ($volume, $directories, $file) = splitpath($fpath);
+    $directories =~ s/lib\///;
+    $directories = '.' if ($directories eq '');
+
+    my $dir_path = catdir($ind_path, $directories);
+    make_path($dir_path);
+    $file =~ s/\.[^.]*$//;
+    my $out_file = $file . '.rst';
+    my $full_out_file = catfile($dir_path, $out_file);
+
+    my $callbacks = {
+      link => sub {
+        my $text = shift;
+
+        my @results = parselink($text);
+
+        # A link to another page.
+        if (defined($results[1]) && defined($results[2])) {
+          if ($results[2] =~ /^http/) {
+            return ($results[2], $results[1]);
+          }
+          elsif ($results[2] =~ m/Bugzilla/) {
+            my $depth = scalar(split('/', $directories));
+            my @split = split('::', $results[2]);
+            my $base  = '../' x $depth;
+            my $url   = $base . join('/', @split);
+            $url .= '.html';
+            $url .= '#' . $results[3] if defined $results[3];
+            return ($url, $results[1]);
+          }
+          else {
+            # Not a Bugzilla package, link to CPAN.
+            my $url = 'http://search.cpan.org/search?query=' . $results[2] . '&mode=all';
+            return ($url, $results[1]);
+          }
         }
 
-        my $title = $mod;
-
-        my $ext = $title =~ s/^extensions:://;
-        $title =~ s/lib:://;
-        $title =~ s/::Extension$// if ($ext);
-        my $header = "=" x length($title);
-
-        my $abs_path = $name2path->{$mod};
-        my $fpath = abs2rel($abs_path, '..');
-        my ($volume, $directories, $file) = splitpath($fpath);
-        $directories =~ s/lib\///;
-        $directories = '.' if ($directories eq '');
-
-        my $dir_path = catdir($ind_path, $directories);
-        make_path($dir_path);
-        $file =~ s/\.[^.]*$//;
-        my $out_file = $file . '.rst';
-        my $full_out_file = catfile($dir_path, $out_file);
-
-        my $callbacks = {
-            link => sub {
-                my $text = shift;
-
-                my @results = parselink($text);
-
-                # A link to another page.
-                if (defined($results[1]) && defined($results[2])) {
-                    if ($results[2] =~ /^http/) {
-                        return ($results[2], $results[1]);
-                    }
-                    elsif ($results[2] =~ m/Bugzilla/) {
-                        my $depth = scalar(split('/', $directories));
-                        my @split = split('::', $results[2]);
-                        my $base  = '../' x $depth;
-                        my $url   = $base . join('/', @split);
-                        $url .= '.html';
-                        $url .= '#' . $results[3] if defined $results[3];
-                        return ($url, $results[1]);
-                    }
-                    else {
-                        # Not a Bugzilla package, link to CPAN.
-                        my $url
-                            = 'http://search.cpan.org/search?query='
-                            . $results[2]
-                            . '&mode=all';
-                        return ($url, $results[1]);
-                    }
-                }
-
-                # A Link within a page
-                elsif (defined($results[1]) && defined($results[4])) {
-                    my $anchor = $results[1];
-                    $anchor =~ s/"//g;
-                    return ("#$anchor", $anchor);
-                }
-                else {
-                    die "Don't know how to parse $text";
-                }
-            }
-        };
-        my $conv = Pod::POM::View::Restructured->new(
-                             { namespace => $title, callbacks => $callbacks });
-        my $rv = $conv->convert_file($abs_path, $title, $full_out_file,
-                                                                   $callbacks);
-        print($FILE "   ", catfile($directories, $out_file), "\n");
-
-        if ($ext) {
-            my $api_path = catdir($path, 'rst', $directories);
-            my $adminfile = catfile($api_path, "index-admin.rst");
-            my $userfile  = catfile($api_path, "index-user.rst");
-            my $apifile   = catfile($api_path, 'api', 'v1', 'index.rst');
-            make_path($api_path) unless -d $api_path;
-
-            # Add Core doc to User & Admin guides
-            if ($file eq 'Extension') {
-                my $FH;
-                open($FH, '<', $full_out_file) || die "$full_out_file: $!";
-                my @lines = <$FH>;
-                close $FH;
-
-                my @array;
-
-                # ensure out of order docs are at the top of the page without
-                # playing with file handles
-                tie @array, 'Tie::File', $adminfile or die "$adminfile: $!";
-                unshift @array, @lines, "", "", ".. toctree::", "", "";
-                untie @array;
-
-                tie @array, 'Tie::File', $userfile or die "$userfile: $!";
-                unshift @array, @lines, "", "", ".. toctree::", "", "";
-                untie @array;
-
-            }
-
-            # Add Config doc to Admin guide
-            elsif ($file eq 'Config') {
-                rcopy($full_out_file, $api_path);
-                `perl -E 'say "   $file.rst"' >> "$adminfile"`;
-            }
-
-            # Add WebServices doc to API docs
-            elsif ($file eq 'WebService') {
-                my $apidir = catdir($api_path, 'api', 'v1');
-                make_path($apidir)
-                    unless -d $apidir;
-                `perl -E 'say ".. toctree::\n\n"' >> $apifile`
-                    unless -f $apifile;
-
-                my $FH;
-                open($FH, '<', $full_out_file) || die "$full_out_file: $!";
-                my @lines = <$FH>;
-                close $FH;
-
-                my @array;
-
-                tie @array, 'Tie::File', $apifile or die "$apifile: $!";
-                unshift @array, @lines;
-                untie @array;
-            }
-            elsif ($api_path =~ /WebService\/$/) {
-                my $apidir = catdir($api_path, '..', 'api', 'v1');
-                $apifile = catfile($apidir, 'index.rst');
-                make_path($apidir) unless -d $apidir;
-                rcopy($full_out_file, $apidir);
-                `perl -E 'say ".. toctree::\n\n"' >> $apifile`
-                    unless -f $apifile;
-                `perl -E 'say "   $file.rst"' >> $apifile`;
-            }
+        # A Link within a page
+        elsif (defined($results[1]) && defined($results[4])) {
+          my $anchor = $results[1];
+          $anchor =~ s/"//g;
+          return ("#$anchor", $anchor);
         }
+        else {
+          die "Don't know how to parse $text";
+        }
+      }
+    };
+    my $conv = Pod::POM::View::Restructured->new(
+      {namespace => $title, callbacks => $callbacks});
+    my $rv = $conv->convert_file($abs_path, $title, $full_out_file, $callbacks);
+    print($FILE "   ", catfile($directories, $out_file), "\n");
+
+    if ($ext) {
+      my $api_path = catdir($path, 'rst', $directories);
+      my $adminfile = catfile($api_path, "index-admin.rst");
+      my $userfile  = catfile($api_path, "index-user.rst");
+      my $apifile   = catfile($api_path, 'api', 'v1', 'index.rst');
+      make_path($api_path) unless -d $api_path;
+
+      # Add Core doc to User & Admin guides
+      if ($file eq 'Extension') {
+        my $FH;
+        open($FH, '<', $full_out_file) || die "$full_out_file: $!";
+        my @lines = <$FH>;
+        close $FH;
+
+        my @array;
+
+        # ensure out of order docs are at the top of the page without
+        # playing with file handles
+        tie @array, 'Tie::File', $adminfile or die "$adminfile: $!";
+        unshift @array, @lines, "", "", ".. toctree::", "", "";
+        untie @array;
+
+        tie @array, 'Tie::File', $userfile or die "$userfile: $!";
+        unshift @array, @lines, "", "", ".. toctree::", "", "";
+        untie @array;
+
+      }
+
+      # Add Config doc to Admin guide
+      elsif ($file eq 'Config') {
+        rcopy($full_out_file, $api_path);
+        `perl -E 'say "   $file.rst"' >> "$adminfile"`;
+      }
+
+      # Add WebServices doc to API docs
+      elsif ($file eq 'WebService') {
+        my $apidir = catdir($api_path, 'api', 'v1');
+        make_path($apidir) unless -d $apidir;
+        `perl -E 'say ".. toctree::\n\n"' >> $apifile` unless -f $apifile;
+
+        my $FH;
+        open($FH, '<', $full_out_file) || die "$full_out_file: $!";
+        my @lines = <$FH>;
+        close $FH;
+
+        my @array;
+
+        tie @array, 'Tie::File', $apifile or die "$apifile: $!";
+        unshift @array, @lines;
+        untie @array;
+      }
+      elsif ($api_path =~ /WebService\/$/) {
+        my $apidir = catdir($api_path, '..', 'api', 'v1');
+        $apifile = catfile($apidir, 'index.rst');
+        make_path($apidir) unless -d $apidir;
+        rcopy($full_out_file, $apidir);
+        `perl -E 'say ".. toctree::\n\n"' >> $apifile` unless -f $apifile;
+        `perl -E 'say "   $file.rst"' >> $apifile`;
+      }
     }
+  }
 
-    close($FILE);
+  close($FILE);
 }
 
 ###############################################################################
@@ -245,45 +237,45 @@ my @langs;
 # search for sub directories which have a 'rst' sub-directory
 opendir(LANGS, './');
 foreach my $dir (readdir(LANGS)) {
-    next if (($dir eq '.') || ($dir eq '..') || (!-d $dir));
-    if (-d "$dir/rst") {
-        push(@langs, $dir);
-    }
+  next if (($dir eq '.') || ($dir eq '..') || (!-d $dir));
+  if (-d "$dir/rst") {
+    push(@langs, $dir);
+  }
 }
 closedir(LANGS);
 
 my $docparent = getcwd();
 foreach my $lang (@langs) {
 
-    rmtree("$lang/html", 0, 1);
-    rmtree("$lang/txt",  0, 1);
+  rmtree("$lang/html", 0, 1);
+  rmtree("$lang/txt",  0, 1);
 
-    my @sub_dirs = grep {-d} glob("$lang/rst/extensions/*");
-    rmtree(@sub_dirs, { verbose => 0, safe => 1 });
+  my @sub_dirs = grep {-d} glob("$lang/rst/extensions/*");
+  rmtree(@sub_dirs, {verbose => 0, safe => 1});
 
-    pod2rst("$docparent/$lang");
+  pod2rst("$docparent/$lang");
 
-    next if grep { $_ eq '--pod-only' } @ARGV;
+  next if grep { $_ eq '--pod-only' } @ARGV;
 
-    chdir "$docparent/$lang";
+  chdir "$docparent/$lang";
 
-    MakeDocs('HTML', 'html');
-    MakeDocs('TXT',  'text');
+  MakeDocs('HTML', 'html');
+  MakeDocs('TXT',  'text');
 
-    if (grep { $_ eq '--with-pdf' } @ARGV) {
-        if (which('pdflatex')) {
-            MakeDocs('PDF', 'latexpdf');
-        }
-        elsif (which('rst2pdf')) {
-            rmtree('pdf', 0, 1);
-            MakeDocs('PDF', 'pdf');
-        }
-        else {
-            say 'pdflatex or rst2pdf not found. Skipping PDF file creation';
-        }
+  if (grep { $_ eq '--with-pdf' } @ARGV) {
+    if (which('pdflatex')) {
+      MakeDocs('PDF', 'latexpdf');
+    }
+    elsif (which('rst2pdf')) {
+      rmtree('pdf', 0, 1);
+      MakeDocs('PDF', 'pdf');
+    }
+    else {
+      say 'pdflatex or rst2pdf not found. Skipping PDF file creation';
     }
+  }
 
-    rmtree('doctrees', 0, 1);
+  rmtree('doctrees', 0, 1);
 }
 
 die "Error occurred building the documentation\n" if $error_found;
index da00d8748c510e4ef8c5c14f26191a9e7b29c66c..d23051fd8d4112e2d4f63724bd3e106bd74608a5 100755 (executable)
@@ -21,21 +21,22 @@ use Bugzilla::Field;
 use Bugzilla::Product;
 
 use constant DEFAULTS => {
-    # We want to show bugs which:
-    # a) Aren't CLOSED; and
-    # b)  i) Aren't VERIFIED; OR
-    #    ii) Were resolved INVALID/WONTFIX
-    #
-    # The rationale behind this is that people will eventually stop
-    # reporting fixed bugs when they get newer versions of the software,
-    # but if the bug is determined to be erroneous, people will still
-    # keep reporting it, so we do need to show it here.
-    fully_exclude_status  => ['CLOSED'],
-    partly_exclude_status => ['VERIFIED'],
-    except_resolution => ['INVALID', 'WONTFIX'],
-    changedsince => 7,
-    maxrows      => 20,
-    sortby       => 'count',
+
+  # We want to show bugs which:
+  # a) Aren't CLOSED; and
+  # b)  i) Aren't VERIFIED; OR
+  #    ii) Were resolved INVALID/WONTFIX
+  #
+  # The rationale behind this is that people will eventually stop
+  # reporting fixed bugs when they get newer versions of the software,
+  # but if the bug is determined to be erroneous, people will still
+  # keep reporting it, so we do need to show it here.
+  fully_exclude_status  => ['CLOSED'],
+  partly_exclude_status => ['VERIFIED'],
+  except_resolution     => ['INVALID', 'WONTFIX'],
+  changedsince          => 7,
+  maxrows               => 20,
+  sortby                => 'count',
 };
 
 ###############
@@ -48,92 +49,93 @@ use constant DEFAULTS => {
 # in $count is a duplicate of another bug in $count, we add their counts
 # together under the target bug.
 sub add_indirect_dups {
-    my ($counts, $dups) = @_;
+  my ($counts, $dups) = @_;
 
-    foreach my $add_from (keys %$dups) {
-        my $add_to     = walk_dup_chain($dups, $add_from);
-        my $add_amount = delete $counts->{$add_from} || 0;
-        $counts->{$add_to} += $add_amount;
-    }
+  foreach my $add_from (keys %$dups) {
+    my $add_to = walk_dup_chain($dups, $add_from);
+    my $add_amount = delete $counts->{$add_from} || 0;
+    $counts->{$add_to} += $add_amount;
+  }
 }
 
 sub walk_dup_chain {
-    my ($dups, $from_id) = @_;
-    my $to_id = $dups->{$from_id};
-    my %seen;
-    while (my $bug_id = $dups->{$to_id}) {
-        if ($seen{$bug_id}) {
-            warn "Duplicate loop: $to_id -> $bug_id\n";
-            last;
-        }
-        $seen{$bug_id} = 1;
-        $to_id = $bug_id;
+  my ($dups, $from_id) = @_;
+  my $to_id = $dups->{$from_id};
+  my %seen;
+  while (my $bug_id = $dups->{$to_id}) {
+    if ($seen{$bug_id}) {
+      warn "Duplicate loop: $to_id -> $bug_id\n";
+      last;
     }
-    # Optimize for future calls to add_indirect_dups.
-    $dups->{$from_id} = $to_id;
-    return $to_id;
+    $seen{$bug_id} = 1;
+    $to_id = $bug_id;
+  }
+
+  # Optimize for future calls to add_indirect_dups.
+  $dups->{$from_id} = $to_id;
+  return $to_id;
 }
 
 # Get params from URL
 sub formvalue {
-    my ($name) = (@_);
-    my $cgi = Bugzilla->cgi;
-    if (defined $cgi->param($name)) {
-        return $cgi->param($name);
-    }
-    elsif (exists DEFAULTS->{$name}) {
-        return ref DEFAULTS->{$name} ? @{ DEFAULTS->{$name} } 
-                                     : DEFAULTS->{$name};
-    }
-    return undef;
+  my ($name) = (@_);
+  my $cgi = Bugzilla->cgi;
+  if (defined $cgi->param($name)) {
+    return $cgi->param($name);
+  }
+  elsif (exists DEFAULTS->{$name}) {
+    return ref DEFAULTS->{$name} ? @{DEFAULTS->{$name}} : DEFAULTS->{$name};
+  }
+  return undef;
 }
 
 sub sort_duplicates {
-    my ($a, $b, $sort_by) = @_;
-    if ($sort_by eq 'count' or $sort_by eq 'delta') {
-        return $a->{$sort_by} <=> $b->{$sort_by};
-    }
-    if ($sort_by =~ /^(bug_)?id$/) {
-        return $a->{'bug'}->$sort_by <=> $b->{'bug'}->$sort_by;
-    }
-    return $a->{'bug'}->$sort_by cmp $b->{'bug'}->$sort_by;
-    
+  my ($a, $b, $sort_by) = @_;
+  if ($sort_by eq 'count' or $sort_by eq 'delta') {
+    return $a->{$sort_by} <=> $b->{$sort_by};
+  }
+  if ($sort_by =~ /^(bug_)?id$/) {
+    return $a->{'bug'}->$sort_by <=> $b->{'bug'}->$sort_by;
+  }
+  return $a->{'bug'}->$sort_by cmp $b->{'bug'}->$sort_by;
+
 }
 
 ###############
 # Main Script #
 ###############
 
-my $cgi = Bugzilla->cgi;
+my $cgi      = Bugzilla->cgi;
 my $template = Bugzilla->template;
-my $user = Bugzilla->login();
+my $user     = Bugzilla->login();
 
 my $dbh = Bugzilla->switch_to_shadow_db();
 
 my $changedsince = formvalue("changedsince");
-my $maxrows = formvalue("maxrows");
-my $openonly = formvalue("openonly");
-my $sortby = formvalue("sortby");
+my $maxrows      = formvalue("maxrows");
+my $openonly     = formvalue("openonly");
+my $sortby       = formvalue("sortby");
 if (!grep(lc($_) eq lc($sortby), qw(count delta id))) {
-    Bugzilla::Field->check($sortby);
+  Bugzilla::Field->check($sortby);
 }
 my $reverse = formvalue("reverse");
+
 # Reverse count and delta by default.
 if (!defined $reverse) {
-    if ($sortby eq 'count' or $sortby eq 'delta') {
-        $reverse = 1;
-    }
-    else {
-        $reverse = 0;
-    }
+  if ($sortby eq 'count' or $sortby eq 'delta') {
+    $reverse = 1;
+  }
+  else {
+    $reverse = 0;
+  }
 }
 my @query_products = $cgi->multi_param('product');
-my $sortvisible = formvalue("sortvisible");
+my $sortvisible    = formvalue("sortvisible");
 my @bugs;
 if ($sortvisible) {
-    my @limit_to_ids = (split(/[:,]/, formvalue("bug_id") || ''));
-    @bugs = @{ Bugzilla::Bug->new_from_list(\@limit_to_ids) };
-    @bugs = @{ $user->visible_bugs(\@bugs) };
+  my @limit_to_ids = (split(/[:,]/, formvalue("bug_id") || ''));
+  @bugs = @{Bugzilla::Bug->new_from_list(\@limit_to_ids)};
+  @bugs = @{$user->visible_bugs(\@bugs)};
 }
 
 # Make sure all products are valid.
@@ -144,101 +146,117 @@ $sortby = "count" if $sortby eq "dup_count";
 
 my $origmaxrows = $maxrows;
 detaint_natural($maxrows)
-  || ThrowUserError("invalid_maxrows", { maxrows => $origmaxrows});
+  || ThrowUserError("invalid_maxrows", {maxrows => $origmaxrows});
 
 my $origchangedsince = $changedsince;
 detaint_natural($changedsince)
-  || ThrowUserError("invalid_changedsince", 
-                    { changedsince => $origchangedsince });
+  || ThrowUserError("invalid_changedsince",
+  {changedsince => $origchangedsince});
 
-my %total_dups = @{$dbh->selectcol_arrayref(
+my %total_dups = @{
+  $dbh->selectcol_arrayref(
     "SELECT dupe_of, COUNT(dupe)
        FROM duplicates
-   GROUP BY dupe_of", {Columns => [1,2]})};
+   GROUP BY dupe_of", {Columns => [1, 2]}
+  )
+};
 
-my %dupe_relation = @{$dbh->selectcol_arrayref(
+my %dupe_relation = @{
+  $dbh->selectcol_arrayref(
     "SELECT dupe, dupe_of FROM duplicates
-      WHERE dupe IN (SELECT dupe_of FROM duplicates)",
-    {Columns => [1,2]})};
+      WHERE dupe IN (SELECT dupe_of FROM duplicates)", {Columns => [1, 2]}
+  )
+};
 add_indirect_dups(\%total_dups, \%dupe_relation);
 
 my $reso_field_id = get_field_id('resolution');
-my %since_dups = @{$dbh->selectcol_arrayref(
+my %since_dups    = @{
+  $dbh->selectcol_arrayref(
     "SELECT dupe_of, COUNT(dupe)
        FROM duplicates INNER JOIN bugs_activity 
                        ON bugs_activity.bug_id = duplicates.dupe 
       WHERE added = 'DUPLICATE' AND fieldid = ? 
             AND bug_when >= "
-                . $dbh->sql_date_math('LOCALTIMESTAMP(0)', '-', '?', 'DAY') .
- " GROUP BY dupe_of", {Columns=>[1,2]},
-    $reso_field_id, $changedsince)};
+      . $dbh->sql_date_math('LOCALTIMESTAMP(0)', '-', '?', 'DAY')
+      . " GROUP BY dupe_of", {Columns => [1, 2]}, $reso_field_id, $changedsince
+  )
+};
 add_indirect_dups(\%since_dups, \%dupe_relation);
 
 # Enforce the MOST_FREQUENT_THRESHOLD constant and the "bug_id" cgi param.
 foreach my $id (keys %total_dups) {
-    if ($total_dups{$id} < MOST_FREQUENT_THRESHOLD) {
-        delete $total_dups{$id};
-        next;
-    }
-    if ($sortvisible and !grep($_->id == $id, @bugs)) {
-        delete $total_dups{$id};
-    }
+  if ($total_dups{$id} < MOST_FREQUENT_THRESHOLD) {
+    delete $total_dups{$id};
+    next;
+  }
+  if ($sortvisible and !grep($_->id == $id, @bugs)) {
+    delete $total_dups{$id};
+  }
 }
 
 if (!@bugs) {
-    @bugs = @{ Bugzilla::Bug->new_from_list([keys %total_dups]) };
-    @bugs = @{ $user->visible_bugs(\@bugs) };
+  @bugs = @{Bugzilla::Bug->new_from_list([keys %total_dups])};
+  @bugs = @{$user->visible_bugs(\@bugs)};
 }
 
-my @fully_exclude_status = formvalue('fully_exclude_status');
+my @fully_exclude_status  = formvalue('fully_exclude_status');
 my @partly_exclude_status = formvalue('partly_exclude_status');
-my @except_resolution = formvalue('except_resolution');
+my @except_resolution     = formvalue('except_resolution');
 
 # Filter bugs by criteria
 my @result_bugs;
 foreach my $bug (@bugs) {
-    # It's possible, if somebody specified a bug ID that wasn't a dup
-    # in the "buglist" parameter and specified $sortvisible that there
-    # would be bugs in the list with 0 dups, so we want to avoid that.
-    next if !$total_dups{$bug->id};
-
-    next if ($openonly and !$bug->isopened);
-    # If the bug has a status in @fully_exclude_status, we skip it,
-    # no question.
-    next if grep($_ eq $bug->bug_status, @fully_exclude_status);
-    # If the bug has a status in @partly_exclude_status, we skip it...
-    if (grep($_ eq $bug->bug_status, @partly_exclude_status)) {
-        # ...unless it has a resolution in @except_resolution.
-        next if !grep($_ eq $bug->resolution, @except_resolution);
-    }
 
-    if (scalar @query_products) {
-        next if !grep($_->id == $bug->product_id, @query_products);
-    }
+  # It's possible, if somebody specified a bug ID that wasn't a dup
+  # in the "buglist" parameter and specified $sortvisible that there
+  # would be bugs in the list with 0 dups, so we want to avoid that.
+  next if !$total_dups{$bug->id};
 
-    # Note: maximum row count is dealt with later.
-    push (@result_bugs, { bug => $bug,
-                          count => $total_dups{$bug->id},
-                          delta => $since_dups{$bug->id} || 0 });
+  next if ($openonly and !$bug->isopened);
+
+  # If the bug has a status in @fully_exclude_status, we skip it,
+  # no question.
+  next if grep($_ eq $bug->bug_status, @fully_exclude_status);
+
+  # If the bug has a status in @partly_exclude_status, we skip it...
+  if (grep($_ eq $bug->bug_status, @partly_exclude_status)) {
+
+    # ...unless it has a resolution in @except_resolution.
+    next if !grep($_ eq $bug->resolution, @except_resolution);
+  }
+
+  if (scalar @query_products) {
+    next if !grep($_->id == $bug->product_id, @query_products);
+  }
+
+  # Note: maximum row count is dealt with later.
+  push(
+    @result_bugs,
+    {
+      bug   => $bug,
+      count => $total_dups{$bug->id},
+      delta => $since_dups{$bug->id} || 0
+    }
+  );
 }
 @bugs = @result_bugs;
 @bugs = sort { sort_duplicates($a, $b, $sortby) } @bugs;
 if ($reverse) {
-    @bugs = reverse @bugs;
+  @bugs = reverse @bugs;
 }
-@bugs = @bugs[0..$maxrows-1] if scalar(@bugs) > $maxrows;
+@bugs = @bugs[0 .. $maxrows - 1] if scalar(@bugs) > $maxrows;
 
 my %vars = (
-    bugs     => \@bugs,
-    bug_ids  => [map { $_->{'bug'}->id } @bugs],
-    sortby   => $sortby,
-    openonly => $openonly,
-    maxrows  => $maxrows,
-    reverse  => $reverse,
-    format   => scalar $cgi->param('format'),
-    product  => [map { $_->name } @query_products],
-    sortvisible  => $sortvisible,
-    changedsince => $changedsince,
+  bugs         => \@bugs,
+  bug_ids      => [map { $_->{'bug'}->id } @bugs],
+  sortby       => $sortby,
+  openonly     => $openonly,
+  maxrows      => $maxrows,
+  reverse      => $reverse,
+  format       => scalar $cgi->param('format'),
+  product      => [map { $_->name } @query_products],
+  sortvisible  => $sortvisible,
+  changedsince => $changedsince,
 );
 
 my $format = $template->get_format("reports/duplicates", $vars{'format'});
index 1ae6e553c0abd121077d627b70debb4d8a817c14..b453a904c8bcc658baa27ac684c10e0323742b7e 100755 (executable)
@@ -20,26 +20,27 @@ use Bugzilla::Error;
 use Bugzilla::Classification;
 use Bugzilla::Token;
 
-my $dbh = Bugzilla->dbh;
-my $cgi = Bugzilla->cgi;
+my $dbh      = Bugzilla->dbh;
+my $cgi      = Bugzilla->cgi;
 my $template = Bugzilla->template;
 local our $vars = {};
 
 sub LoadTemplate {
-    my $action = shift;
-    my $template = Bugzilla->template;
-
-    $vars->{'classifications'} = [Bugzilla::Classification->get_all]
-      if ($action eq 'select');
-    # There is currently only one section about classifications,
-    # so all pages point to it. Let's define it here.
-    $vars->{'doc_section'} = 'administering/categorization.html#classifications';
-
-    $action =~ /(\w+)/;
-    $action = $1;
-    $template->process("admin/classifications/$action.html.tmpl", $vars)
-      || ThrowTemplateError($template->error());
-    exit;
+  my $action   = shift;
+  my $template = Bugzilla->template;
+
+  $vars->{'classifications'} = [Bugzilla::Classification->get_all]
+    if ($action eq 'select');
+
+  # There is currently only one section about classifications,
+  # so all pages point to it. Let's define it here.
+  $vars->{'doc_section'} = 'administering/categorization.html#classifications';
+
+  $action =~ /(\w+)/;
+  $action = $1;
+  $template->process("admin/classifications/$action.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
+  exit;
 }
 
 #
@@ -50,13 +51,12 @@ my $user = Bugzilla->login(LOGIN_REQUIRED);
 
 print $cgi->header();
 
-$user->in_group('editclassifications')
-  || ThrowUserError("auth_failure", {group  => "editclassifications",
-                                     action => "edit",
-                                     object => "classifications"});
+$user->in_group('editclassifications') || ThrowUserError("auth_failure",
+  {group => "editclassifications", action => "edit", object => "classifications"}
+);
 
-ThrowUserError("auth_classification_not_enabled") 
-    unless Bugzilla->params->{"useclassification"};
+ThrowUserError("auth_classification_not_enabled")
+  unless Bugzilla->params->{"useclassification"};
 
 #
 # often used variables
@@ -77,8 +77,8 @@ LoadTemplate('select') unless $action;
 #
 
 if ($action eq 'add') {
-    $vars->{'token'} = issue_session_token('add_classification');
-    LoadTemplate($action);
+  $vars->{'token'} = issue_session_token('add_classification');
+  LoadTemplate($action);
 }
 
 #
@@ -86,20 +86,21 @@ if ($action eq 'add') {
 #
 
 if ($action eq 'new') {
-    check_token_data($token, 'add_classification');
+  check_token_data($token, 'add_classification');
 
-    my $classification =
-      Bugzilla::Classification->create({name        => $class_name,
-                                        description => scalar $cgi->param('description'),
-                                        sortkey     => scalar $cgi->param('sortkey')});
+  my $classification = Bugzilla::Classification->create({
+    name        => $class_name,
+    description => scalar $cgi->param('description'),
+    sortkey     => scalar $cgi->param('sortkey')
+  });
 
-    delete_token($token);
+  delete_token($token);
 
-    $vars->{'message'} = 'classification_created';
-    $vars->{'classification'} = $classification;
-    $vars->{'classifications'} = [Bugzilla::Classification->get_all];
-    $vars->{'token'} = issue_session_token('reclassify_classifications');
-    LoadTemplate('reclassify');
+  $vars->{'message'}         = 'classification_created';
+  $vars->{'classification'}  = $classification;
+  $vars->{'classifications'} = [Bugzilla::Classification->get_all];
+  $vars->{'token'}           = issue_session_token('reclassify_classifications');
+  LoadTemplate('reclassify');
 }
 
 #
@@ -110,20 +111,20 @@ if ($action eq 'new') {
 
 if ($action eq 'del') {
 
-    my $classification = Bugzilla::Classification->check($class_name);
+  my $classification = Bugzilla::Classification->check($class_name);
 
-    if ($classification->id == 1) {
-        ThrowUserError("classification_not_deletable");
-    }
+  if ($classification->id == 1) {
+    ThrowUserError("classification_not_deletable");
+  }
 
-    if ($classification->product_count()) {
-        ThrowUserError("classification_has_products");
-    }
+  if ($classification->product_count()) {
+    ThrowUserError("classification_has_products");
+  }
 
-    $vars->{'classification'} = $classification;
-    $vars->{'token'} = issue_session_token('delete_classification');
+  $vars->{'classification'} = $classification;
+  $vars->{'token'}          = issue_session_token('delete_classification');
 
-    LoadTemplate($action);
+  LoadTemplate($action);
 }
 
 #
@@ -131,15 +132,15 @@ if ($action eq 'del') {
 #
 
 if ($action eq 'delete') {
-    check_token_data($token, 'delete_classification');
+  check_token_data($token, 'delete_classification');
 
-    my $classification = Bugzilla::Classification->check($class_name);
-    $classification->remove_from_db;
-    delete_token($token);
+  my $classification = Bugzilla::Classification->check($class_name);
+  $classification->remove_from_db;
+  delete_token($token);
 
-    $vars->{'message'} = 'classification_deleted';
-    $vars->{'classification'} = $classification;
-    LoadTemplate('select');
+  $vars->{'message'}        = 'classification_deleted';
+  $vars->{'classification'} = $classification;
+  LoadTemplate('select');
 }
 
 #
@@ -149,12 +150,12 @@ if ($action eq 'delete') {
 #
 
 if ($action eq 'edit') {
-    my $classification = Bugzilla::Classification->check($class_name);
+  my $classification = Bugzilla::Classification->check($class_name);
 
-    $vars->{'classification'} = $classification;
-    $vars->{'token'} = issue_session_token('edit_classification');
+  $vars->{'classification'} = $classification;
+  $vars->{'token'}          = issue_session_token('edit_classification');
 
-    LoadTemplate($action);
+  LoadTemplate($action);
 }
 
 #
@@ -162,24 +163,24 @@ if ($action eq 'edit') {
 #
 
 if ($action eq 'update') {
-    check_token_data($token, 'edit_classification');
+  check_token_data($token, 'edit_classification');
 
-    my $class_old_name = trim($cgi->param('classificationold') || '');
-    my $classification = Bugzilla::Classification->check($class_old_name);
+  my $class_old_name = trim($cgi->param('classificationold') || '');
+  my $classification = Bugzilla::Classification->check($class_old_name);
 
-    $classification->set_all({
-        name        => $class_name,
-        description => scalar $cgi->param('description'),
-        sortkey     => scalar $cgi->param('sortkey'),
-    });
+  $classification->set_all({
+    name        => $class_name,
+    description => scalar $cgi->param('description'),
+    sortkey     => scalar $cgi->param('sortkey'),
+  });
 
-    my $changes = $classification->update;
-    delete_token($token);
+  my $changes = $classification->update;
+  delete_token($token);
 
-    $vars->{'message'} = 'classification_updated';
-    $vars->{'classification'} = $classification;
-    $vars->{'changes'} = $changes;
-    LoadTemplate('select');
+  $vars->{'message'}        = 'classification_updated';
+  $vars->{'classification'} = $classification;
+  $vars->{'changes'}        = $changes;
+  LoadTemplate('select');
 }
 
 #
@@ -187,44 +188,47 @@ if ($action eq 'update') {
 #
 
 if ($action eq 'reclassify') {
-    my $classification = Bugzilla::Classification->check($class_name);
-
-    my $sth = $dbh->prepare("UPDATE products SET classification_id = ?
-                             WHERE name = ?");
-    my @names;
-
-    if (defined $cgi->param('add_products')) {
-        check_token_data($token, 'reclassify_classifications');
-        if (defined $cgi->param('prodlist')) {
-            foreach my $prod ($cgi->multi_param("prodlist")) {
-                trick_taint($prod);
-                $sth->execute($classification->id, $prod);
-                push @names, $prod;
-            }
-        }
-        delete_token($token);
-    } elsif (defined $cgi->param('remove_products')) {
-        check_token_data($token, 'reclassify_classifications');
-        if (defined $cgi->param('myprodlist')) {
-            foreach my $prod ($cgi->multi_param('myprodlist')) {
-                trick_taint($prod);
-                $sth->execute(1, $prod);
-                push @names, $prod;
-            }
-        }
-        delete_token($token);
+  my $classification = Bugzilla::Classification->check($class_name);
+
+  my $sth = $dbh->prepare(
+    "UPDATE products SET classification_id = ?
+                             WHERE name = ?"
+  );
+  my @names;
+
+  if (defined $cgi->param('add_products')) {
+    check_token_data($token, 'reclassify_classifications');
+    if (defined $cgi->param('prodlist')) {
+      foreach my $prod ($cgi->multi_param("prodlist")) {
+        trick_taint($prod);
+        $sth->execute($classification->id, $prod);
+        push @names, $prod;
+      }
+    }
+    delete_token($token);
+  }
+  elsif (defined $cgi->param('remove_products')) {
+    check_token_data($token, 'reclassify_classifications');
+    if (defined $cgi->param('myprodlist')) {
+      foreach my $prod ($cgi->multi_param('myprodlist')) {
+        trick_taint($prod);
+        $sth->execute(1, $prod);
+        push @names, $prod;
+      }
     }
+    delete_token($token);
+  }
 
-    $vars->{'classifications'} = [Bugzilla::Classification->get_all];
-    $vars->{'classification'} = $classification;
-    $vars->{'token'} = issue_session_token('reclassify_classifications');
+  $vars->{'classifications'} = [Bugzilla::Classification->get_all];
+  $vars->{'classification'}  = $classification;
+  $vars->{'token'}           = issue_session_token('reclassify_classifications');
 
-    foreach my $name (@names) {
-        Bugzilla->memcached->clear({ table => 'products', name => $name });
-    }
-    Bugzilla->memcached->clear_config();
+  foreach my $name (@names) {
+    Bugzilla->memcached->clear({table => 'products', name => $name});
+  }
+  Bugzilla->memcached->clear_config();
 
-    LoadTemplate($action);
+  LoadTemplate($action);
 }
 
 #
index ecbbd788f44a20ec558e9910f9695dd26c55ea86..b17feb06c0a708e0f59bbb45ed049910baffb6c6 100755 (executable)
@@ -20,9 +20,10 @@ use Bugzilla::User;
 use Bugzilla::Component;
 use Bugzilla::Token;
 
-my $cgi = Bugzilla->cgi;
+my $cgi      = Bugzilla->cgi;
 my $template = Bugzilla->template;
-my $vars = {};
+my $vars     = {};
+
 # There is only one section about components in the documentation,
 # so all actions point to the same page.
 $vars->{'doc_section'} = 'administering/categorization.html#components';
@@ -37,16 +38,15 @@ print $cgi->header();
 
 $user->in_group('editcomponents')
   || scalar(@{$user->get_products_by_permission('editcomponents')})
-  || ThrowUserError("auth_failure", {group  => "editcomponents",
-                                     action => "edit",
-                                     object => "components"});
+  || ThrowUserError("auth_failure",
+  {group => "editcomponents", action => "edit", object => "components"});
 
 #
 # often used variables
 #
-my $product_name  = trim($cgi->param('product')     || '');
-my $comp_name     = trim($cgi->param('component')   || '');
-my $action        = trim($cgi->param('action')      || '');
+my $product_name = trim($cgi->param('product')   || '');
+my $comp_name    = trim($cgi->param('component') || '');
+my $action       = trim($cgi->param('action')    || '');
 my $showbugcounts = (defined $cgi->param('showbugcounts'));
 my $token         = $cgi->param('token');
 
@@ -55,18 +55,19 @@ my $token         = $cgi->param('token');
 #
 
 unless ($product_name) {
-    my $selectable_products = $user->get_selectable_products;
-    # If the user has editcomponents privs for some products only,
-    # we have to restrict the list of products to display.
-    unless ($user->in_group('editcomponents')) {
-        $selectable_products = $user->get_products_by_permission('editcomponents');
-    }
-    $vars->{'products'} = $selectable_products;
-    $vars->{'showbugcounts'} = $showbugcounts;
-
-    $template->process("admin/components/select-product.html.tmpl", $vars)
-      || ThrowTemplateError($template->error());
-    exit;
+  my $selectable_products = $user->get_selectable_products;
+
+  # If the user has editcomponents privs for some products only,
+  # we have to restrict the list of products to display.
+  unless ($user->in_group('editcomponents')) {
+    $selectable_products = $user->get_products_by_permission('editcomponents');
+  }
+  $vars->{'products'}      = $selectable_products;
+  $vars->{'showbugcounts'} = $showbugcounts;
+
+  $template->process("admin/components/select-product.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
+  exit;
 }
 
 my $product = $user->check_can_admin_product($product_name);
@@ -76,11 +77,11 @@ my $product = $user->check_can_admin_product($product_name);
 #
 
 unless ($action) {
-    $vars->{'showbugcounts'} = $showbugcounts;
-    $vars->{'product'} = $product;
-    $template->process("admin/components/list.html.tmpl", $vars)
-        || ThrowTemplateError($template->error());
-    exit;
+  $vars->{'showbugcounts'} = $showbugcounts;
+  $vars->{'product'}       = $product;
+  $template->process("admin/components/list.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
+  exit;
 }
 
 #
@@ -90,11 +91,11 @@ unless ($action) {
 #
 
 if ($action eq 'add') {
-    $vars->{'token'} = issue_session_token('add_component');
-    $vars->{'product'} = $product;
-    $template->process("admin/components/create.html.tmpl", $vars)
-        || ThrowTemplateError($template->error());
-    exit;
+  $vars->{'token'}   = issue_session_token('add_component');
+  $vars->{'product'} = $product;
+  $template->process("admin/components/create.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
+  exit;
 }
 
 #
@@ -102,40 +103,42 @@ if ($action eq 'add') {
 #
 
 if ($action eq 'new') {
-    check_token_data($token, 'add_component');
-    # Do the user matching
-    Bugzilla::User::match_field ({
-        'initialowner'     => { 'type' => 'single' },
-        'initialqacontact' => { 'type' => 'single' },
-        'initialcc'        => { 'type' => 'multi'  },
-    });
-
-    my $default_assignee   = trim($cgi->param('initialowner')     || '');
-    my $default_qa_contact = trim($cgi->param('initialqacontact') || '');
-    my $description        = trim($cgi->param('description')      || '');
-    my @initial_cc         = $cgi->multi_param('initialcc');
-    my $isactive           = $cgi->param('isactive');
-
-    my $component = Bugzilla::Component->create({
-        name             => $comp_name,
-        product          => $product,
-        description      => $description,
-        initialowner     => $default_assignee,
-        initialqacontact => $default_qa_contact,
-        initial_cc       => \@initial_cc,
-        # XXX We should not be creating series for products that we
-        # didn't create series for.
-        create_series    => 1,
-   });
-
-    $vars->{'message'} = 'component_created';
-    $vars->{'comp'} = $component;
-    $vars->{'product'} = $product;
-    delete_token($token);
-
-    $template->process("admin/components/list.html.tmpl", $vars)
-      || ThrowTemplateError($template->error());
-    exit;
+  check_token_data($token, 'add_component');
+
+  # Do the user matching
+  Bugzilla::User::match_field({
+    'initialowner'     => {'type' => 'single'},
+    'initialqacontact' => {'type' => 'single'},
+    'initialcc'        => {'type' => 'multi'},
+  });
+
+  my $default_assignee   = trim($cgi->param('initialowner')     || '');
+  my $default_qa_contact = trim($cgi->param('initialqacontact') || '');
+  my $description        = trim($cgi->param('description')      || '');
+  my @initial_cc         = $cgi->multi_param('initialcc');
+  my $isactive           = $cgi->param('isactive');
+
+  my $component = Bugzilla::Component->create({
+    name             => $comp_name,
+    product          => $product,
+    description      => $description,
+    initialowner     => $default_assignee,
+    initialqacontact => $default_qa_contact,
+    initial_cc       => \@initial_cc,
+
+    # XXX We should not be creating series for products that we
+    # didn't create series for.
+    create_series => 1,
+  });
+
+  $vars->{'message'} = 'component_created';
+  $vars->{'comp'}    = $component;
+  $vars->{'product'} = $product;
+  delete_token($token);
+
+  $template->process("admin/components/list.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
+  exit;
 }
 
 #
@@ -145,14 +148,14 @@ if ($action eq 'new') {
 #
 
 if ($action eq 'del') {
-    $vars->{'token'} = issue_session_token('delete_component');
-    $vars->{'comp'} =
-      Bugzilla::Component->check({ product => $product, name => $comp_name });
-    $vars->{'product'} = $product;
-
-    $template->process("admin/components/confirm-delete.html.tmpl", $vars)
-        || ThrowTemplateError($template->error());
-    exit;
+  $vars->{'token'} = issue_session_token('delete_component');
+  $vars->{'comp'}
+    = Bugzilla::Component->check({product => $product, name => $comp_name});
+  $vars->{'product'} = $product;
+
+  $template->process("admin/components/confirm-delete.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
+  exit;
 }
 
 #
@@ -160,21 +163,21 @@ if ($action eq 'del') {
 #
 
 if ($action eq 'delete') {
-    check_token_data($token, 'delete_component');
-    my $component =
-        Bugzilla::Component->check({ product => $product, name => $comp_name });
+  check_token_data($token, 'delete_component');
+  my $component
+    = Bugzilla::Component->check({product => $product, name => $comp_name});
 
-    $component->remove_from_db;
+  $component->remove_from_db;
 
-    $vars->{'message'} = 'component_deleted';
-    $vars->{'comp'} = $component;
-    $vars->{'product'} = $product;
-    $vars->{'no_edit_component_link'} = 1;
-    delete_token($token);
+  $vars->{'message'}                = 'component_deleted';
+  $vars->{'comp'}                   = $component;
+  $vars->{'product'}                = $product;
+  $vars->{'no_edit_component_link'} = 1;
+  delete_token($token);
 
-    $template->process("admin/components/list.html.tmpl", $vars)
-      || ThrowTemplateError($template->error());
-    exit;
+  $template->process("admin/components/list.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
+  exit;
 }
 
 #
@@ -184,19 +187,19 @@ if ($action eq 'delete') {
 #
 
 if ($action eq 'edit') {
-    $vars->{'token'} = issue_session_token('edit_component');
-    my $component =
-        Bugzilla::Component->check({ product => $product, name => $comp_name });
-    $vars->{'comp'} = $component;
+  $vars->{'token'} = issue_session_token('edit_component');
+  my $component
+    = Bugzilla::Component->check({product => $product, name => $comp_name});
+  $vars->{'comp'} = $component;
 
-    $vars->{'initial_cc_names'} = 
-        join(', ', map($_->login, @{$component->initial_cc}));
+  $vars->{'initial_cc_names'}
+    = join(', ', map($_->login, @{$component->initial_cc}));
 
-    $vars->{'product'} = $product;
+  $vars->{'product'} = $product;
 
-    $template->process("admin/components/edit.html.tmpl", $vars)
-      || ThrowTemplateError($template->error());
-    exit;
+  $template->process("admin/components/edit.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
+  exit;
 }
 
 #
@@ -204,41 +207,42 @@ if ($action eq 'edit') {
 #
 
 if ($action eq 'update') {
-    check_token_data($token, 'edit_component');
-    # Do the user matching
-    Bugzilla::User::match_field ({
-        'initialowner'     => { 'type' => 'single' },
-        'initialqacontact' => { 'type' => 'single' },
-        'initialcc'        => { 'type' => 'multi'  },
-    });
-
-    my $comp_old_name         = trim($cgi->param('componentold')     || '');
-    my $default_assignee      = trim($cgi->param('initialowner')     || '');
-    my $default_qa_contact    = trim($cgi->param('initialqacontact') || '');
-    my $description           = trim($cgi->param('description')      || '');
-    my @initial_cc            = $cgi->multi_param('initialcc');
-    my $isactive              = $cgi->param('isactive');
-  
-    my $component =
-        Bugzilla::Component->check({ product => $product, name => $comp_old_name });
-
-    $component->set_name($comp_name);
-    $component->set_description($description);
-    $component->set_default_assignee($default_assignee);
-    $component->set_default_qa_contact($default_qa_contact);
-    $component->set_cc_list(\@initial_cc);
-    $component->set_is_active($isactive);
-    my $changes = $component->update();
-
-    $vars->{'message'} = 'component_updated';
-    $vars->{'comp'} = $component;
-    $vars->{'product'} = $product;
-    $vars->{'changes'} = $changes;
-    delete_token($token);
-
-    $template->process("admin/components/list.html.tmpl", $vars)
-      || ThrowTemplateError($template->error());
-    exit;
+  check_token_data($token, 'edit_component');
+
+  # Do the user matching
+  Bugzilla::User::match_field({
+    'initialowner'     => {'type' => 'single'},
+    'initialqacontact' => {'type' => 'single'},
+    'initialcc'        => {'type' => 'multi'},
+  });
+
+  my $comp_old_name      = trim($cgi->param('componentold')     || '');
+  my $default_assignee   = trim($cgi->param('initialowner')     || '');
+  my $default_qa_contact = trim($cgi->param('initialqacontact') || '');
+  my $description        = trim($cgi->param('description')      || '');
+  my @initial_cc         = $cgi->multi_param('initialcc');
+  my $isactive           = $cgi->param('isactive');
+
+  my $component
+    = Bugzilla::Component->check({product => $product, name => $comp_old_name});
+
+  $component->set_name($comp_name);
+  $component->set_description($description);
+  $component->set_default_assignee($default_assignee);
+  $component->set_default_qa_contact($default_qa_contact);
+  $component->set_cc_list(\@initial_cc);
+  $component->set_is_active($isactive);
+  my $changes = $component->update();
+
+  $vars->{'message'} = 'component_updated';
+  $vars->{'comp'}    = $component;
+  $vars->{'product'} = $product;
+  $vars->{'changes'} = $changes;
+  delete_token($token);
+
+  $template->process("admin/components/list.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
+  exit;
 }
 
 # No valid action found
index acb34e2df6594a432bfcefc732906109f2638323..e56134d3f1779cf640cdae2f378c773ef168bd40 100755 (executable)
@@ -19,153 +19,157 @@ use Bugzilla::Util;
 use Bugzilla::Field;
 use Bugzilla::Token;
 
-my $cgi = Bugzilla->cgi;
+my $cgi      = Bugzilla->cgi;
 my $template = Bugzilla->template;
-my $vars = {};
+my $vars     = {};
 
 # Make sure the user is logged in and is an administrator.
 my $user = Bugzilla->login(LOGIN_REQUIRED);
 $user->in_group('admin')
-  || ThrowUserError('auth_failure', {group  => 'admin',
-                                     action => 'edit',
-                                     object => 'custom_fields'});
+  || ThrowUserError('auth_failure',
+  {group => 'admin', action => 'edit', object => 'custom_fields'});
 
 my $action = trim($cgi->param('action') || '');
-my $token  = $cgi->param('token');
+my $token = $cgi->param('token');
 
 print $cgi->header();
 
 # List all existing custom fields if no action is given.
 if (!$action) {
-    $template->process('admin/custom_fields/list.html.tmpl', $vars)
-        || ThrowTemplateError($template->error());
+  $template->process('admin/custom_fields/list.html.tmpl', $vars)
+    || ThrowTemplateError($template->error());
 }
+
 # Interface to add a new custom field.
 elsif ($action eq 'add') {
-    $vars->{'token'} = issue_session_token('add_field');
+  $vars->{'token'} = issue_session_token('add_field');
 
-    $template->process('admin/custom_fields/create.html.tmpl', $vars)
-        || ThrowTemplateError($template->error());
+  $template->process('admin/custom_fields/create.html.tmpl', $vars)
+    || ThrowTemplateError($template->error());
 }
 elsif ($action eq 'new') {
-    check_token_data($token, 'add_field');
-    $vars->{'field'} = Bugzilla::Field->create({
-        name        => scalar $cgi->param('name'),
-        description => scalar $cgi->param('desc'),
-        long_desc   => scalar $cgi->param('long_desc'),
-        type        => scalar $cgi->param('type'),
-        sortkey     => scalar $cgi->param('sortkey'),
-        mailhead    => scalar $cgi->param('new_bugmail'),
-        enter_bug   => scalar $cgi->param('enter_bug'),
-        obsolete    => scalar $cgi->param('obsolete'),
-        custom      => 1,
-        buglist     => 1,
-        visibility_field_id => scalar $cgi->param('visibility_field_id'),
-        visibility_values => [ $cgi->multi_param('visibility_values') ],
-        value_field_id => scalar $cgi->param('value_field_id'),
-        reverse_desc => scalar $cgi->param('reverse_desc'),
-        is_mandatory => scalar $cgi->param('is_mandatory'),
-    });
-
-    delete_token($token);
-
-    $vars->{'message'} = 'custom_field_created';
-
-    $template->process('admin/custom_fields/list.html.tmpl', $vars)
-        || ThrowTemplateError($template->error());
+  check_token_data($token, 'add_field');
+  $vars->{'field'} = Bugzilla::Field->create({
+    name                => scalar $cgi->param('name'),
+    description         => scalar $cgi->param('desc'),
+    long_desc           => scalar $cgi->param('long_desc'),
+    type                => scalar $cgi->param('type'),
+    sortkey             => scalar $cgi->param('sortkey'),
+    mailhead            => scalar $cgi->param('new_bugmail'),
+    enter_bug           => scalar $cgi->param('enter_bug'),
+    obsolete            => scalar $cgi->param('obsolete'),
+    custom              => 1,
+    buglist             => 1,
+    visibility_field_id => scalar $cgi->param('visibility_field_id'),
+    visibility_values   => [$cgi->multi_param('visibility_values')],
+    value_field_id      => scalar $cgi->param('value_field_id'),
+    reverse_desc        => scalar $cgi->param('reverse_desc'),
+    is_mandatory        => scalar $cgi->param('is_mandatory'),
+  });
+
+  delete_token($token);
+
+  $vars->{'message'} = 'custom_field_created';
+
+  $template->process('admin/custom_fields/list.html.tmpl', $vars)
+    || ThrowTemplateError($template->error());
 }
 elsif ($action eq 'edit') {
-    my $name = $cgi->param('name') || ThrowUserError('field_missing_name');
-    # Custom field names must start with "cf_".
-    if ($name !~ /^cf_/) {
-        $name = 'cf_' . $name;
-    }
-    my $field = new Bugzilla::Field({'name' => $name});
-    $field || ThrowUserError('customfield_nonexistent', {'name' => $name});
-
-    $vars->{'field'} = $field;
-    $vars->{'token'} = issue_session_token('edit_field');
-
-    $template->process('admin/custom_fields/edit.html.tmpl', $vars)
-        || ThrowTemplateError($template->error());
+  my $name = $cgi->param('name') || ThrowUserError('field_missing_name');
+
+  # Custom field names must start with "cf_".
+  if ($name !~ /^cf_/) {
+    $name = 'cf_' . $name;
+  }
+  my $field = new Bugzilla::Field({'name' => $name});
+  $field || ThrowUserError('customfield_nonexistent', {'name' => $name});
+
+  $vars->{'field'} = $field;
+  $vars->{'token'} = issue_session_token('edit_field');
+
+  $template->process('admin/custom_fields/edit.html.tmpl', $vars)
+    || ThrowTemplateError($template->error());
 }
 elsif ($action eq 'update') {
-    check_token_data($token, 'edit_field');
-    my $name = $cgi->param('name');
-
-    # Validate fields.
-    $name || ThrowUserError('field_missing_name');
-    # Custom field names must start with "cf_".
-    if ($name !~ /^cf_/) {
-        $name = 'cf_' . $name;
-    }
-    my $field = new Bugzilla::Field({'name' => $name});
-    $field || ThrowUserError('customfield_nonexistent', {'name' => $name});
-
-    $field->set_description(scalar $cgi->param('desc'));
-    $field->set_long_desc(scalar $cgi->param('long_desc'));
-    $field->set_sortkey(scalar $cgi->param('sortkey'));
-    $field->set_in_new_bugmail(scalar $cgi->param('new_bugmail'));
-    $field->set_enter_bug(scalar $cgi->param('enter_bug'));
-    $field->set_obsolete(scalar $cgi->param('obsolete'));
-    $field->set_is_mandatory(scalar $cgi->param('is_mandatory'));
-    $field->set_visibility_field(scalar $cgi->param('visibility_field_id'));
-    $field->set_visibility_values([ $cgi->multi_param('visibility_values') ]);
-    $field->set_value_field(scalar $cgi->param('value_field_id'));
-    $field->set_reverse_desc(scalar $cgi->param('reverse_desc'));
-    $field->update();
-
-    delete_token($token);
-
-    $vars->{'field'}   = $field;
-    $vars->{'message'} = 'custom_field_updated';
-
-    $template->process('admin/custom_fields/list.html.tmpl', $vars)
-        || ThrowTemplateError($template->error());
+  check_token_data($token, 'edit_field');
+  my $name = $cgi->param('name');
+
+  # Validate fields.
+  $name || ThrowUserError('field_missing_name');
+
+  # Custom field names must start with "cf_".
+  if ($name !~ /^cf_/) {
+    $name = 'cf_' . $name;
+  }
+  my $field = new Bugzilla::Field({'name' => $name});
+  $field || ThrowUserError('customfield_nonexistent', {'name' => $name});
+
+  $field->set_description(scalar $cgi->param('desc'));
+  $field->set_long_desc(scalar $cgi->param('long_desc'));
+  $field->set_sortkey(scalar $cgi->param('sortkey'));
+  $field->set_in_new_bugmail(scalar $cgi->param('new_bugmail'));
+  $field->set_enter_bug(scalar $cgi->param('enter_bug'));
+  $field->set_obsolete(scalar $cgi->param('obsolete'));
+  $field->set_is_mandatory(scalar $cgi->param('is_mandatory'));
+  $field->set_visibility_field(scalar $cgi->param('visibility_field_id'));
+  $field->set_visibility_values([$cgi->multi_param('visibility_values')]);
+  $field->set_value_field(scalar $cgi->param('value_field_id'));
+  $field->set_reverse_desc(scalar $cgi->param('reverse_desc'));
+  $field->update();
+
+  delete_token($token);
+
+  $vars->{'field'}   = $field;
+  $vars->{'message'} = 'custom_field_updated';
+
+  $template->process('admin/custom_fields/list.html.tmpl', $vars)
+    || ThrowTemplateError($template->error());
 }
 elsif ($action eq 'del') {
-    my $name = $cgi->param('name');
-
-    # Validate field.
-    $name || ThrowUserError('field_missing_name');
-    # Custom field names must start with "cf_".
-    if ($name !~ /^cf_/) {
-        $name = 'cf_' . $name;
-    }
-    my $field = new Bugzilla::Field({'name' => $name});
-    $field || ThrowUserError('customfield_nonexistent', {'name' => $name});
-
-    $vars->{'field'} = $field;
-    $vars->{'token'} = issue_session_token('delete_field');
-
-    $template->process('admin/custom_fields/confirm-delete.html.tmpl', $vars)
-            || ThrowTemplateError($template->error());
+  my $name = $cgi->param('name');
+
+  # Validate field.
+  $name || ThrowUserError('field_missing_name');
+
+  # Custom field names must start with "cf_".
+  if ($name !~ /^cf_/) {
+    $name = 'cf_' . $name;
+  }
+  my $field = new Bugzilla::Field({'name' => $name});
+  $field || ThrowUserError('customfield_nonexistent', {'name' => $name});
+
+  $vars->{'field'} = $field;
+  $vars->{'token'} = issue_session_token('delete_field');
+
+  $template->process('admin/custom_fields/confirm-delete.html.tmpl', $vars)
+    || ThrowTemplateError($template->error());
 }
 elsif ($action eq 'delete') {
-    check_token_data($token, 'delete_field');
-    my $name = $cgi->param('name');
-
-    # Validate fields.
-    $name || ThrowUserError('field_missing_name');
-    # Custom field names must start with "cf_".
-    if ($name !~ /^cf_/) {
-        $name = 'cf_' . $name;
-    }
-    my $field = new Bugzilla::Field({'name' => $name});
-    $field || ThrowUserError('customfield_nonexistent', {'name' => $name});
-
-    # Calling remove_from_db will check if field can be deleted.
-    # If the field cannot be deleted, it will throw an error.
-    $field->remove_from_db();
-    
-    $vars->{'field'}   = $field;
-    $vars->{'message'} = 'custom_field_deleted';
-    
-    delete_token($token);
-
-    $template->process('admin/custom_fields/list.html.tmpl', $vars)
-        || ThrowTemplateError($template->error());
+  check_token_data($token, 'delete_field');
+  my $name = $cgi->param('name');
+
+  # Validate fields.
+  $name || ThrowUserError('field_missing_name');
+
+  # Custom field names must start with "cf_".
+  if ($name !~ /^cf_/) {
+    $name = 'cf_' . $name;
+  }
+  my $field = new Bugzilla::Field({'name' => $name});
+  $field || ThrowUserError('customfield_nonexistent', {'name' => $name});
+
+  # Calling remove_from_db will check if field can be deleted.
+  # If the field cannot be deleted, it will throw an error.
+  $field->remove_from_db();
+
+  $vars->{'field'}   = $field;
+  $vars->{'message'} = 'custom_field_deleted';
+
+  delete_token($token);
+
+  $template->process('admin/custom_fields/list.html.tmpl', $vars)
+    || ThrowTemplateError($template->error());
 }
 else {
-    ThrowUserError('unknown_action', {action => $action});
+  ThrowUserError('unknown_action', {action => $action});
 }
index c01bade4a43183f860f2bd082647bcc08f6d8ebc..70878d70edd60f8d9cc0b4f9e5c91a4c69bb287c 100755 (executable)
@@ -23,405 +23,430 @@ use Bugzilla::Product;
 use Bugzilla::Token;
 
 # Make sure the user is logged in and has the right privileges.
-my $user = Bugzilla->login(LOGIN_REQUIRED);
-my $cgi = Bugzilla->cgi;
+my $user     = Bugzilla->login(LOGIN_REQUIRED);
+my $cgi      = Bugzilla->cgi;
 my $template = Bugzilla->template;
 
 print $cgi->header();
 
 $user->in_group('editcomponents')
   || scalar(@{$user->get_products_by_permission('editcomponents')})
-  || ThrowUserError("auth_failure", {group  => "editcomponents",
-                                     action => "edit",
-                                     object => "flagtypes"});
+  || ThrowUserError("auth_failure",
+  {group => "editcomponents", action => "edit", object => "flagtypes"});
 
 # We need this everywhere.
-my $vars = get_products_and_components();
+my $vars     = get_products_and_components();
 my @products = @{$vars->{products}};
 
-my $action = $cgi->param('action') || 'list';
-my $token  = $cgi->param('token');
+my $action    = $cgi->param('action') || 'list';
+my $token     = $cgi->param('token');
 my $prod_name = $cgi->param('product');
 my $comp_name = $cgi->param('component');
-my $flag_id = $cgi->param('id');
+my $flag_id   = $cgi->param('id');
 
 my ($product, $component);
 
 if ($prod_name) {
-    # Make sure the user is allowed to view this product name.
-    # Users with global editcomponents privs can see all product names.
-    ($product) = grep { lc($_->name) eq lc($prod_name) } @products;
-    $product || ThrowUserError('product_access_denied', { name => $prod_name });
+
+  # Make sure the user is allowed to view this product name.
+  # Users with global editcomponents privs can see all product names.
+  ($product) = grep { lc($_->name) eq lc($prod_name) } @products;
+  $product || ThrowUserError('product_access_denied', {name => $prod_name});
 }
 
 if ($comp_name) {
-    $product || ThrowUserError('flag_type_component_without_product');
-    ($component) = grep { lc($_->name) eq lc($comp_name) } @{$product->components};
-    $component || ThrowUserError('product_unknown_component', { product => $product->name,
-                                                                comp => $comp_name });
+  $product || ThrowUserError('flag_type_component_without_product');
+  ($component) = grep { lc($_->name) eq lc($comp_name) } @{$product->components};
+  $component
+    || ThrowUserError('product_unknown_component',
+    {product => $product->name, comp => $comp_name});
 }
 
 # If 'categoryAction' is set, it has priority over 'action'.
-if (my ($category_action) = grep { $_ =~ /^categoryAction-(?:\w+)$/ } $cgi->multi_param()) {
-    $category_action =~ s/^categoryAction-//;
-
-    my @inclusions = $cgi->multi_param('inclusions');
-    my @exclusions = $cgi->multi_param('exclusions');
-    my @categories;
-    if ($category_action =~ /^(in|ex)clude$/) {
-        if (!$user->in_group('editcomponents') && !$product) {
-            # The user can only add the flag type to products they can administrate.
-            foreach my $prod (@products) {
-                push(@categories, $prod->id . ':0')
-            }
-        }
-        else {
-            my $category = ($product ? $product->id : 0) . ':' .
-                           ($component ? $component->id : 0);
-            push(@categories, $category);
-        }
+if (my ($category_action)
+  = grep { $_ =~ /^categoryAction-(?:\w+)$/ } $cgi->multi_param())
+{
+  $category_action =~ s/^categoryAction-//;
+
+  my @inclusions = $cgi->multi_param('inclusions');
+  my @exclusions = $cgi->multi_param('exclusions');
+  my @categories;
+  if ($category_action =~ /^(in|ex)clude$/) {
+    if (!$user->in_group('editcomponents') && !$product) {
+
+      # The user can only add the flag type to products they can administrate.
+      foreach my $prod (@products) {
+        push(@categories, $prod->id . ':0');
+      }
+    }
+    else {
+      my $category
+        = ($product ? $product->id : 0) . ':' . ($component ? $component->id : 0);
+      push(@categories, $category);
     }
+  }
 
-    if ($category_action eq 'include') {
-        foreach my $category (@categories) {
-            push(@inclusions, $category) unless grep($_ eq $category, @inclusions);
-        }
+  if ($category_action eq 'include') {
+    foreach my $category (@categories) {
+      push(@inclusions, $category) unless grep($_ eq $category, @inclusions);
     }
-    elsif ($category_action eq 'exclude') {
-        foreach my $category (@categories) {
-            push(@exclusions, $category) unless grep($_ eq $category, @exclusions);
-        }
+  }
+  elsif ($category_action eq 'exclude') {
+    foreach my $category (@categories) {
+      push(@exclusions, $category) unless grep($_ eq $category, @exclusions);
     }
-    elsif ($category_action eq 'removeInclusion') {
-        my @inclusion_to_remove = $cgi->multi_param('inclusion_to_remove');
-        foreach my $remove (@inclusion_to_remove) {
-            @inclusions = grep { $_ ne $remove } @inclusions;
-        }
+  }
+  elsif ($category_action eq 'removeInclusion') {
+    my @inclusion_to_remove = $cgi->multi_param('inclusion_to_remove');
+    foreach my $remove (@inclusion_to_remove) {
+      @inclusions = grep { $_ ne $remove } @inclusions;
     }
-    elsif ($category_action eq 'removeExclusion') {
-        my @exclusion_to_remove = $cgi->multi_param('exclusion_to_remove');
-        foreach my $remove (@exclusion_to_remove) {
-            @exclusions = grep { $_ ne $remove } @exclusions;
-        }
+  }
+  elsif ($category_action eq 'removeExclusion') {
+    my @exclusion_to_remove = $cgi->multi_param('exclusion_to_remove');
+    foreach my $remove (@exclusion_to_remove) {
+      @exclusions = grep { $_ ne $remove } @exclusions;
     }
-
-    $vars->{'groups'} = get_settable_groups();
-    $vars->{'action'} = $action;
-
-    my $type = {};
-    $type->{$_} = $cgi->param($_) foreach $cgi->param();
-    # Make sure boolean fields are defined, else they fall back to 1.
-    foreach my $boolean (qw(is_active is_requestable is_requesteeble is_multiplicable)) {
-        $type->{$boolean} ||= 0;
-    }
-
-    # That's what I call a big hack. The template expects to see a group object.
-    $type->{'grant_group'} = {};
-    $type->{'grant_group'}->{'name'} = $cgi->param('grant_group');
-    $type->{'request_group'} = {};
-    $type->{'request_group'}->{'name'} = $cgi->param('request_group');
-
-    $vars->{'inclusions'} = clusion_array_to_hash(\@inclusions, \@products);
-    $vars->{'exclusions'} = clusion_array_to_hash(\@exclusions, \@products);
-
-    $vars->{'type'} = $type;
-    $vars->{'token'} = $token;
-    $vars->{'check_clusions'} = 1;
-    $vars->{'can_fully_edit'} = $cgi->param('can_fully_edit');
-
-    $template->process("admin/flag-type/edit.html.tmpl", $vars)
-      || ThrowTemplateError($template->error());
-    exit;
+  }
+
+  $vars->{'groups'} = get_settable_groups();
+  $vars->{'action'} = $action;
+
+  my $type = {};
+  $type->{$_} = $cgi->param($_) foreach $cgi->param();
+
+  # Make sure boolean fields are defined, else they fall back to 1.
+  foreach
+    my $boolean (qw(is_active is_requestable is_requesteeble is_multiplicable))
+  {
+    $type->{$boolean} ||= 0;
+  }
+
+  # That's what I call a big hack. The template expects to see a group object.
+  $type->{'grant_group'}             = {};
+  $type->{'grant_group'}->{'name'}   = $cgi->param('grant_group');
+  $type->{'request_group'}           = {};
+  $type->{'request_group'}->{'name'} = $cgi->param('request_group');
+
+  $vars->{'inclusions'} = clusion_array_to_hash(\@inclusions, \@products);
+  $vars->{'exclusions'} = clusion_array_to_hash(\@exclusions, \@products);
+
+  $vars->{'type'}           = $type;
+  $vars->{'token'}          = $token;
+  $vars->{'check_clusions'} = 1;
+  $vars->{'can_fully_edit'} = $cgi->param('can_fully_edit');
+
+  $template->process("admin/flag-type/edit.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
+  exit;
 }
 
 if ($action eq 'list') {
-    my $product_id = $product ? $product->id : 0;
-    my $component_id = $component ? $component->id : 0;
-    my $show_flag_counts = $cgi->param('show_flag_counts') ? 1 : 0;
-    my $group_id = $cgi->param('group');
-    if ($group_id) {
-        detaint_natural($group_id) || ThrowUserError('invalid_group_ID');
-    }
+  my $product_id       = $product                        ? $product->id   : 0;
+  my $component_id     = $component                      ? $component->id : 0;
+  my $show_flag_counts = $cgi->param('show_flag_counts') ? 1              : 0;
+  my $group_id         = $cgi->param('group');
+  if ($group_id) {
+    detaint_natural($group_id) || ThrowUserError('invalid_group_ID');
+  }
+
+  my $bug_flagtypes;
+  my $attach_flagtypes;
+
+  # If a component is given, restrict the list to flag types available
+  # for this component.
+  if ($component) {
+    $bug_flagtypes    = $component->flag_types->{'bug'};
+    $attach_flagtypes = $component->flag_types->{'attachment'};
 
-    my $bug_flagtypes;
-    my $attach_flagtypes;
+    # Filter flag types if a group ID is given.
+    $bug_flagtypes    = filter_group($bug_flagtypes,    $group_id);
+    $attach_flagtypes = filter_group($attach_flagtypes, $group_id);
 
-    # If a component is given, restrict the list to flag types available
-    # for this component.
-    if ($component) {
-        $bug_flagtypes = $component->flag_types->{'bug'};
-        $attach_flagtypes = $component->flag_types->{'attachment'};
+  }
 
-        # Filter flag types if a group ID is given.
-        $bug_flagtypes = filter_group($bug_flagtypes, $group_id);
-        $attach_flagtypes = filter_group($attach_flagtypes, $group_id);
+  # If only a product is specified but no component, then restrict the list
+  # to flag types available in at least one component of that product.
+  elsif ($product) {
+    $bug_flagtypes    = $product->flag_types->{'bug'};
+    $attach_flagtypes = $product->flag_types->{'attachment'};
 
+    # Filter flag types if a group ID is given.
+    $bug_flagtypes    = filter_group($bug_flagtypes,    $group_id);
+    $attach_flagtypes = filter_group($attach_flagtypes, $group_id);
+  }
+
+  # If no product is given, then show all flag types available.
+  else {
+    my $flagtypes = get_editable_flagtypes(\@products, $group_id);
+    $bug_flagtypes    = [grep { $_->target_type eq 'bug' } @$flagtypes];
+    $attach_flagtypes = [grep { $_->target_type eq 'attachment' } @$flagtypes];
+  }
+
+  if ($show_flag_counts) {
+    my %bug_lists;
+    my %map = ('+' => 'granted', '-' => 'denied', '?' => 'pending');
+
+    foreach my $flagtype (@$bug_flagtypes, @$attach_flagtypes) {
+      $bug_lists{$flagtype->id} = {};
+      my $flags = Bugzilla::Flag->match({type_id => $flagtype->id});
+
+      # Build lists of bugs, triaged by flag status.
+      push(@{$bug_lists{$flagtype->id}->{$map{$_->status}}}, $_->bug_id)
+        foreach @$flags;
     }
-    # If only a product is specified but no component, then restrict the list
-    # to flag types available in at least one component of that product.
-    elsif ($product) {
-        $bug_flagtypes = $product->flag_types->{'bug'};
-        $attach_flagtypes = $product->flag_types->{'attachment'};
-
-        # Filter flag types if a group ID is given.
-        $bug_flagtypes = filter_group($bug_flagtypes, $group_id);
-        $attach_flagtypes = filter_group($attach_flagtypes, $group_id);
-    }
-    # If no product is given, then show all flag types available.
-    else {
-        my $flagtypes = get_editable_flagtypes(\@products, $group_id);
-        $bug_flagtypes = [grep { $_->target_type eq 'bug' } @$flagtypes];
-        $attach_flagtypes = [grep { $_->target_type eq 'attachment' } @$flagtypes];
-    }
-
-    if ($show_flag_counts) {
-        my %bug_lists;
-        my %map = ('+' => 'granted', '-' => 'denied', '?' => 'pending');
-
-        foreach my $flagtype (@$bug_flagtypes, @$attach_flagtypes) {
-            $bug_lists{$flagtype->id} = {};
-            my $flags = Bugzilla::Flag->match({type_id => $flagtype->id});
-            # Build lists of bugs, triaged by flag status.
-            push(@{$bug_lists{$flagtype->id}->{$map{$_->status}}}, $_->bug_id) foreach @$flags;
-        }
-        $vars->{'bug_lists'} = \%bug_lists;
-        $vars->{'show_flag_counts'} = 1;
-    }
-
-    $vars->{'selected_product'} = $product ? $product->name : '';
-    $vars->{'selected_component'} = $component ? $component->name : '';
-    $vars->{'bug_types'} = $bug_flagtypes;
-    $vars->{'attachment_types'} = $attach_flagtypes;
-
-    $template->process("admin/flag-type/list.html.tmpl", $vars)
-      || ThrowTemplateError($template->error());
-    exit;
+    $vars->{'bug_lists'}        = \%bug_lists;
+    $vars->{'show_flag_counts'} = 1;
+  }
+
+  $vars->{'selected_product'}   = $product   ? $product->name   : '';
+  $vars->{'selected_component'} = $component ? $component->name : '';
+  $vars->{'bug_types'}          = $bug_flagtypes;
+  $vars->{'attachment_types'}   = $attach_flagtypes;
+
+  $template->process("admin/flag-type/list.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
+  exit;
 }
 
 if ($action eq 'enter') {
-    my $type = $cgi->param('target_type');
-    ($type eq 'bug' || $type eq 'attachment')
-      || ThrowCodeError('flag_type_target_type_invalid', { target_type => $type });
-
-    $vars->{'action'} = 'insert';
-    $vars->{'token'} = issue_session_token('add_flagtype');
-    $vars->{'type'} = { 'target_type' => $type };
-    # Only users with global editcomponents privs can add a flagtype
-    # to all products.
-    $vars->{'inclusions'} = { '__Any__:__Any__' => '0:0' }
-      if $user->in_group('editcomponents');
-    $vars->{'can_fully_edit'} = 1;
-    # Get a list of groups available to restrict this flag type against.
-    $vars->{'groups'} = get_settable_groups();
-
-    $template->process("admin/flag-type/edit.html.tmpl", $vars)
-      || ThrowTemplateError($template->error());
-    exit;
+  my $type = $cgi->param('target_type');
+  ($type eq 'bug' || $type eq 'attachment')
+    || ThrowCodeError('flag_type_target_type_invalid', {target_type => $type});
+
+  $vars->{'action'} = 'insert';
+  $vars->{'token'}  = issue_session_token('add_flagtype');
+  $vars->{'type'}   = {'target_type' => $type};
+
+  # Only users with global editcomponents privs can add a flagtype
+  # to all products.
+  $vars->{'inclusions'} = {'__Any__:__Any__' => '0:0'}
+    if $user->in_group('editcomponents');
+  $vars->{'can_fully_edit'} = 1;
+
+  # Get a list of groups available to restrict this flag type against.
+  $vars->{'groups'} = get_settable_groups();
+
+  $template->process("admin/flag-type/edit.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
+  exit;
 }
 
 if ($action eq 'edit' || $action eq 'copy') {
-    my ($flagtype, $can_fully_edit) = $user->check_can_admin_flagtype($flag_id);
-    $vars->{'type'} = $flagtype;
-    $vars->{'can_fully_edit'} = $can_fully_edit;
-
-    if ($user->in_group('editcomponents')) {
-        $vars->{'inclusions'} = $flagtype->inclusions;
-        $vars->{'exclusions'} = $flagtype->exclusions;
-    }
-    else {
-        # Filter products the user shouldn't know about.
-        $vars->{'inclusions'} = clusion_array_to_hash([values %{$flagtype->inclusions}], \@products);
-        $vars->{'exclusions'} = clusion_array_to_hash([values %{$flagtype->exclusions}], \@products);
-    }
-
-    if ($action eq 'copy') {
-        $vars->{'action'} = "insert";
-        $vars->{'token'} = issue_session_token('add_flagtype');
-    }
-    else { 
-        $vars->{'action'} = "update";
-        $vars->{'token'} = issue_session_token('edit_flagtype');
-    }
-
-    # Get a list of groups available to restrict this flag type against.
-    $vars->{'groups'} = get_settable_groups();
-
-    $template->process("admin/flag-type/edit.html.tmpl", $vars)
-      || ThrowTemplateError($template->error());
-    exit;
+  my ($flagtype, $can_fully_edit) = $user->check_can_admin_flagtype($flag_id);
+  $vars->{'type'}           = $flagtype;
+  $vars->{'can_fully_edit'} = $can_fully_edit;
+
+  if ($user->in_group('editcomponents')) {
+    $vars->{'inclusions'} = $flagtype->inclusions;
+    $vars->{'exclusions'} = $flagtype->exclusions;
+  }
+  else {
+    # Filter products the user shouldn't know about.
+    $vars->{'inclusions'}
+      = clusion_array_to_hash([values %{$flagtype->inclusions}], \@products);
+    $vars->{'exclusions'}
+      = clusion_array_to_hash([values %{$flagtype->exclusions}], \@products);
+  }
+
+  if ($action eq 'copy') {
+    $vars->{'action'} = "insert";
+    $vars->{'token'}  = issue_session_token('add_flagtype');
+  }
+  else {
+    $vars->{'action'} = "update";
+    $vars->{'token'}  = issue_session_token('edit_flagtype');
+  }
+
+  # Get a list of groups available to restrict this flag type against.
+  $vars->{'groups'} = get_settable_groups();
+
+  $template->process("admin/flag-type/edit.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
+  exit;
 }
 
 if ($action eq 'insert') {
-    check_token_data($token, 'add_flagtype');
-
-    my $name             = $cgi->param('name');
-    my $description      = $cgi->param('description');
-    my $target_type      = $cgi->param('target_type');
-    my $cc_list          = $cgi->param('cc_list');
-    my $sortkey          = $cgi->param('sortkey');
-    my $is_active        = $cgi->param('is_active');
-    my $is_requestable   = $cgi->param('is_requestable');
-    my $is_specifically  = $cgi->param('is_requesteeble');
-    my $is_multiplicable = $cgi->param('is_multiplicable');
-    my $grant_group      = $cgi->param('grant_group');
-    my $request_group    = $cgi->param('request_group');
-    my @inclusions       = $cgi->multi_param('inclusions');
-    my @exclusions       = $cgi->multi_param('exclusions');
-
-    # Filter inclusion and exclusion lists to products the user can see.
-    unless ($user->in_group('editcomponents')) {
-        @inclusions = values %{clusion_array_to_hash(\@inclusions, \@products)};
-        @exclusions = values %{clusion_array_to_hash(\@exclusions, \@products)};
-    }
-
-    my $flagtype = Bugzilla::FlagType->create({
-        name        => $name,
-        description => $description,
-        target_type => $target_type,
-        cc_list     => $cc_list,
-        sortkey     => $sortkey,
-        is_active   => $is_active,
-        is_requestable   => $is_requestable,
-        is_requesteeble  => $is_specifically,
-        is_multiplicable => $is_multiplicable,
-        grant_group      => $grant_group,
-        request_group    => $request_group,
-        inclusions       => \@inclusions,
-        exclusions       => \@exclusions
-    });
-
-    delete_token($token);
-
-    $vars->{'name'} = $flagtype->name;
-    $vars->{'message'} = "flag_type_created";
-
-    my $flagtypes = get_editable_flagtypes(\@products);
-    $vars->{'bug_types'} = [grep { $_->target_type eq 'bug' } @$flagtypes];
-    $vars->{'attachment_types'} = [grep { $_->target_type eq 'attachment' } @$flagtypes];
-
-    $template->process("admin/flag-type/list.html.tmpl", $vars)
-      || ThrowTemplateError($template->error());
-    exit;
+  check_token_data($token, 'add_flagtype');
+
+  my $name             = $cgi->param('name');
+  my $description      = $cgi->param('description');
+  my $target_type      = $cgi->param('target_type');
+  my $cc_list          = $cgi->param('cc_list');
+  my $sortkey          = $cgi->param('sortkey');
+  my $is_active        = $cgi->param('is_active');
+  my $is_requestable   = $cgi->param('is_requestable');
+  my $is_specifically  = $cgi->param('is_requesteeble');
+  my $is_multiplicable = $cgi->param('is_multiplicable');
+  my $grant_group      = $cgi->param('grant_group');
+  my $request_group    = $cgi->param('request_group');
+  my @inclusions       = $cgi->multi_param('inclusions');
+  my @exclusions       = $cgi->multi_param('exclusions');
+
+  # Filter inclusion and exclusion lists to products the user can see.
+  unless ($user->in_group('editcomponents')) {
+    @inclusions = values %{clusion_array_to_hash(\@inclusions, \@products)};
+    @exclusions = values %{clusion_array_to_hash(\@exclusions, \@products)};
+  }
+
+  my $flagtype = Bugzilla::FlagType->create({
+    name             => $name,
+    description      => $description,
+    target_type      => $target_type,
+    cc_list          => $cc_list,
+    sortkey          => $sortkey,
+    is_active        => $is_active,
+    is_requestable   => $is_requestable,
+    is_requesteeble  => $is_specifically,
+    is_multiplicable => $is_multiplicable,
+    grant_group      => $grant_group,
+    request_group    => $request_group,
+    inclusions       => \@inclusions,
+    exclusions       => \@exclusions
+  });
+
+  delete_token($token);
+
+  $vars->{'name'}    = $flagtype->name;
+  $vars->{'message'} = "flag_type_created";
+
+  my $flagtypes = get_editable_flagtypes(\@products);
+  $vars->{'bug_types'} = [grep { $_->target_type eq 'bug' } @$flagtypes];
+  $vars->{'attachment_types'}
+    = [grep { $_->target_type eq 'attachment' } @$flagtypes];
+
+  $template->process("admin/flag-type/list.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
+  exit;
 }
 
 if ($action eq 'update') {
-    check_token_data($token, 'edit_flagtype');
-
-    my $name             = $cgi->param('name');
-    my $description      = $cgi->param('description');
-    my $cc_list          = $cgi->param('cc_list');
-    my $sortkey          = $cgi->param('sortkey');
-    my $is_active        = $cgi->param('is_active');
-    my $is_requestable   = $cgi->param('is_requestable');
-    my $is_specifically  = $cgi->param('is_requesteeble');
-    my $is_multiplicable = $cgi->param('is_multiplicable');
-    my $grant_group      = $cgi->param('grant_group');
-    my $request_group    = $cgi->param('request_group');
-    my @inclusions       = $cgi->multi_param('inclusions');
-    my @exclusions       = $cgi->multi_param('exclusions');
-
-    my ($flagtype, $can_fully_edit) = $user->check_can_admin_flagtype($flag_id);
-    if ($cgi->param('check_clusions') && !$user->in_group('editcomponents')) {
-        # Filter inclusion and exclusion lists to products the user can edit.
-        @inclusions = values %{clusion_array_to_hash(\@inclusions, \@products)};
-        @exclusions = values %{clusion_array_to_hash(\@exclusions, \@products)};
-        # Bring back the products the user cannot edit.
-        foreach my $item (values %{$flagtype->inclusions}) {
-            my ($prod_id, $comp_id) = split(':', $item);
-            push(@inclusions, $item) unless grep { $_->id == $prod_id } @products;
-        }
-        foreach my $item (values %{$flagtype->exclusions}) {
-            my ($prod_id, $comp_id) = split(':', $item);
-            push(@exclusions, $item) unless grep { $_->id == $prod_id } @products;
-        }
+  check_token_data($token, 'edit_flagtype');
+
+  my $name             = $cgi->param('name');
+  my $description      = $cgi->param('description');
+  my $cc_list          = $cgi->param('cc_list');
+  my $sortkey          = $cgi->param('sortkey');
+  my $is_active        = $cgi->param('is_active');
+  my $is_requestable   = $cgi->param('is_requestable');
+  my $is_specifically  = $cgi->param('is_requesteeble');
+  my $is_multiplicable = $cgi->param('is_multiplicable');
+  my $grant_group      = $cgi->param('grant_group');
+  my $request_group    = $cgi->param('request_group');
+  my @inclusions       = $cgi->multi_param('inclusions');
+  my @exclusions       = $cgi->multi_param('exclusions');
+
+  my ($flagtype, $can_fully_edit) = $user->check_can_admin_flagtype($flag_id);
+  if ($cgi->param('check_clusions') && !$user->in_group('editcomponents')) {
+
+    # Filter inclusion and exclusion lists to products the user can edit.
+    @inclusions = values %{clusion_array_to_hash(\@inclusions, \@products)};
+    @exclusions = values %{clusion_array_to_hash(\@exclusions, \@products)};
+
+    # Bring back the products the user cannot edit.
+    foreach my $item (values %{$flagtype->inclusions}) {
+      my ($prod_id, $comp_id) = split(':', $item);
+      push(@inclusions, $item) unless grep { $_->id == $prod_id } @products;
     }
-
-    if ($can_fully_edit) {
-        $flagtype->set_name($name);
-        $flagtype->set_description($description);
-        $flagtype->set_cc_list($cc_list);
-        $flagtype->set_sortkey($sortkey);
-        $flagtype->set_is_active($is_active);
-        $flagtype->set_is_requestable($is_requestable);
-        $flagtype->set_is_specifically_requestable($is_specifically);
-        $flagtype->set_is_multiplicable($is_multiplicable);
-        $flagtype->set_grant_group($grant_group);
-        $flagtype->set_request_group($request_group);
+    foreach my $item (values %{$flagtype->exclusions}) {
+      my ($prod_id, $comp_id) = split(':', $item);
+      push(@exclusions, $item) unless grep { $_->id == $prod_id } @products;
     }
-    $flagtype->set_clusions({ inclusions => \@inclusions, exclusions => \@exclusions})
-      if $cgi->param('check_clusions');
-    my $changes = $flagtype->update();
-
-    delete_token($token);
-
-    $vars->{'flagtype'} = $flagtype;
-    $vars->{'changes'} = $changes;
-    $vars->{'message'} = 'flag_type_updated';
-
-    my $flagtypes = get_editable_flagtypes(\@products);
-    $vars->{'bug_types'} = [grep { $_->target_type eq 'bug' } @$flagtypes];
-    $vars->{'attachment_types'} = [grep { $_->target_type eq 'attachment' } @$flagtypes];
-
-    $template->process("admin/flag-type/list.html.tmpl", $vars)
-      || ThrowTemplateError($template->error());
-    exit;
+  }
+
+  if ($can_fully_edit) {
+    $flagtype->set_name($name);
+    $flagtype->set_description($description);
+    $flagtype->set_cc_list($cc_list);
+    $flagtype->set_sortkey($sortkey);
+    $flagtype->set_is_active($is_active);
+    $flagtype->set_is_requestable($is_requestable);
+    $flagtype->set_is_specifically_requestable($is_specifically);
+    $flagtype->set_is_multiplicable($is_multiplicable);
+    $flagtype->set_grant_group($grant_group);
+    $flagtype->set_request_group($request_group);
+  }
+  $flagtype->set_clusions(
+    {inclusions => \@inclusions, exclusions => \@exclusions})
+    if $cgi->param('check_clusions');
+  my $changes = $flagtype->update();
+
+  delete_token($token);
+
+  $vars->{'flagtype'} = $flagtype;
+  $vars->{'changes'}  = $changes;
+  $vars->{'message'}  = 'flag_type_updated';
+
+  my $flagtypes = get_editable_flagtypes(\@products);
+  $vars->{'bug_types'} = [grep { $_->target_type eq 'bug' } @$flagtypes];
+  $vars->{'attachment_types'}
+    = [grep { $_->target_type eq 'attachment' } @$flagtypes];
+
+  $template->process("admin/flag-type/list.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
+  exit;
 }
 
 if ($action eq 'confirmdelete') {
-    my ($flagtype, $can_fully_edit) = $user->check_can_admin_flagtype($flag_id);
-    ThrowUserError('flag_type_cannot_delete', { flagtype => $flagtype }) unless $can_fully_edit;
+  my ($flagtype, $can_fully_edit) = $user->check_can_admin_flagtype($flag_id);
+  ThrowUserError('flag_type_cannot_delete', {flagtype => $flagtype})
+    unless $can_fully_edit;
 
-    $vars->{'flag_type'} = $flagtype;
-    $vars->{'token'} = issue_session_token('delete_flagtype');
+  $vars->{'flag_type'} = $flagtype;
+  $vars->{'token'}     = issue_session_token('delete_flagtype');
 
-    $template->process("admin/flag-type/confirm-delete.html.tmpl", $vars)
-      || ThrowTemplateError($template->error());
-    exit;
+  $template->process("admin/flag-type/confirm-delete.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
+  exit;
 }
 
 if ($action eq 'delete') {
-    check_token_data($token, 'delete_flagtype');
+  check_token_data($token, 'delete_flagtype');
 
-    my ($flagtype, $can_fully_edit) = $user->check_can_admin_flagtype($flag_id);
-    ThrowUserError('flag_type_cannot_delete', { flagtype => $flagtype }) unless $can_fully_edit;
+  my ($flagtype, $can_fully_edit) = $user->check_can_admin_flagtype($flag_id);
+  ThrowUserError('flag_type_cannot_delete', {flagtype => $flagtype})
+    unless $can_fully_edit;
 
-    $flagtype->remove_from_db();
+  $flagtype->remove_from_db();
 
-    delete_token($token);
+  delete_token($token);
 
-    $vars->{'name'} = $flagtype->name;
-    $vars->{'message'} = "flag_type_deleted";
+  $vars->{'name'}    = $flagtype->name;
+  $vars->{'message'} = "flag_type_deleted";
 
-    my @flagtypes = Bugzilla::FlagType->get_all;
-    $vars->{'bug_types'} = [grep { $_->target_type eq 'bug' } @flagtypes];
-    $vars->{'attachment_types'} = [grep { $_->target_type eq 'attachment' } @flagtypes];
+  my @flagtypes = Bugzilla::FlagType->get_all;
+  $vars->{'bug_types'} = [grep { $_->target_type eq 'bug' } @flagtypes];
+  $vars->{'attachment_types'}
+    = [grep { $_->target_type eq 'attachment' } @flagtypes];
 
-    $template->process("admin/flag-type/list.html.tmpl", $vars)
-      || ThrowTemplateError($template->error());
-    exit;
+  $template->process("admin/flag-type/list.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
+  exit;
 }
 
 if ($action eq 'deactivate') {
-    check_token_data($token, 'delete_flagtype');
+  check_token_data($token, 'delete_flagtype');
 
-    my ($flagtype, $can_fully_edit) = $user->check_can_admin_flagtype($flag_id);
-    ThrowUserError('flag_type_cannot_deactivate', { flagtype => $flagtype }) unless $can_fully_edit;
+  my ($flagtype, $can_fully_edit) = $user->check_can_admin_flagtype($flag_id);
+  ThrowUserError('flag_type_cannot_deactivate', {flagtype => $flagtype})
+    unless $can_fully_edit;
 
-    $flagtype->set_is_active(0);
-    $flagtype->update();
+  $flagtype->set_is_active(0);
+  $flagtype->update();
 
-    delete_token($token);
+  delete_token($token);
 
-    $vars->{'message'} = "flag_type_deactivated";
-    $vars->{'flag_type'} = $flagtype;
+  $vars->{'message'}   = "flag_type_deactivated";
+  $vars->{'flag_type'} = $flagtype;
 
-    my @flagtypes = Bugzilla::FlagType->get_all;
-    $vars->{'bug_types'} = [grep { $_->target_type eq 'bug' } @flagtypes];
-    $vars->{'attachment_types'} = [grep { $_->target_type eq 'attachment' } @flagtypes];
+  my @flagtypes = Bugzilla::FlagType->get_all;
+  $vars->{'bug_types'} = [grep { $_->target_type eq 'bug' } @flagtypes];
+  $vars->{'attachment_types'}
+    = [grep { $_->target_type eq 'attachment' } @flagtypes];
 
-    $template->process("admin/flag-type/list.html.tmpl", $vars)
-      || ThrowTemplateError($template->error());
-    exit;
+  $template->process("admin/flag-type/list.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
+  exit;
 }
 
 ThrowUserError('unknown_action', {action => $action});
@@ -431,115 +456,124 @@ ThrowUserError('unknown_action', {action => $action});
 #####################
 
 sub get_products_and_components {
-    my $vars = {};
-    my $user = Bugzilla->user;
-
-    my @products;
-    if ($user->in_group('editcomponents')) {
-        if (Bugzilla->params->{useclassification}) {
-            # We want products grouped by classifications.
-            @products = map { @{ $_->products } } Bugzilla::Classification->get_all;
-        }
-        else {
-            @products = Bugzilla::Product->get_all;
-        }
-    }
-    else {
-        @products = @{$user->get_products_by_permission('editcomponents')};
+  my $vars = {};
+  my $user = Bugzilla->user;
 
-        if (Bugzilla->params->{useclassification}) {
-            my %class;
-            push(@{$class{$_->classification_id}}, $_) foreach @products;
+  my @products;
+  if ($user->in_group('editcomponents')) {
+    if (Bugzilla->params->{useclassification}) {
 
-            # Let's sort the list by classifications.
-            @products = ();
-            push(@products, @{$class{$_->id} || []}) foreach Bugzilla::Classification->get_all;
-        }
+      # We want products grouped by classifications.
+      @products = map { @{$_->products} } Bugzilla::Classification->get_all;
     }
-
-    my %components;
-    foreach my $product (@products) {
-        $components{$_->name} = 1 foreach @{$product->components};
+    else {
+      @products = Bugzilla::Product->get_all;
     }
-    $vars->{'products'} = \@products;
-    $vars->{'components'} = [sort(keys %components)];
-    return $vars;
+  }
+  else {
+    @products = @{$user->get_products_by_permission('editcomponents')};
+
+    if (Bugzilla->params->{useclassification}) {
+      my %class;
+      push(@{$class{$_->classification_id}}, $_) foreach @products;
+
+      # Let's sort the list by classifications.
+      @products = ();
+      push(@products, @{$class{$_->id} || []})
+        foreach Bugzilla::Classification->get_all;
+    }
+  }
+
+  my %components;
+  foreach my $product (@products) {
+    $components{$_->name} = 1 foreach @{$product->components};
+  }
+  $vars->{'products'}   = \@products;
+  $vars->{'components'} = [sort(keys %components)];
+  return $vars;
 }
 
 sub get_editable_flagtypes {
-    my ($products, $group_id) = @_;
-    my $flagtypes;
+  my ($products, $group_id) = @_;
+  my $flagtypes;
 
-    if (Bugzilla->user->in_group('editcomponents')) {
-        $flagtypes = Bugzilla::FlagType::match({ group => $group_id });
-        return $flagtypes;
-    }
+  if (Bugzilla->user->in_group('editcomponents')) {
+    $flagtypes = Bugzilla::FlagType::match({group => $group_id});
+    return $flagtypes;
+  }
 
-    my %visible_flagtypes;
-    foreach my $product (@$products) {
-        foreach my $target ('bug', 'attachment') {
-            my $prod_flagtypes = $product->flag_types->{$target};
-            $visible_flagtypes{$_->id} ||= $_ foreach @$prod_flagtypes;
-        }
+  my %visible_flagtypes;
+  foreach my $product (@$products) {
+    foreach my $target ('bug', 'attachment') {
+      my $prod_flagtypes = $product->flag_types->{$target};
+      $visible_flagtypes{$_->id} ||= $_ foreach @$prod_flagtypes;
     }
-    @$flagtypes = sort { $a->sortkey <=> $b->sortkey || $a->name cmp $b->name }
-                    values %visible_flagtypes;
-    # Filter flag types if a group ID is given.
-    $flagtypes = filter_group($flagtypes, $group_id);
-    return $flagtypes;
+  }
+  @$flagtypes = sort { $a->sortkey <=> $b->sortkey || $a->name cmp $b->name }
+    values %visible_flagtypes;
+
+  # Filter flag types if a group ID is given.
+  $flagtypes = filter_group($flagtypes, $group_id);
+  return $flagtypes;
 }
 
 sub get_settable_groups {
-    my $user = Bugzilla->user;
-    my $groups = $user->in_group('editcomponents') ? [Bugzilla::Group->get_all] : $user->groups;
-    return $groups;
+  my $user = Bugzilla->user;
+  my $groups
+    = $user->in_group('editcomponents')
+    ? [Bugzilla::Group->get_all]
+    : $user->groups;
+  return $groups;
 }
 
 sub filter_group {
-    my ($flag_types, $gid) = @_;
-    return $flag_types unless $gid;
+  my ($flag_types, $gid) = @_;
+  return $flag_types unless $gid;
 
-    my @flag_types = grep {($_->grant_group && $_->grant_group->id == $gid)
-                           || ($_->request_group && $_->request_group->id == $gid)} @$flag_types;
+  my @flag_types = grep {
+         ($_->grant_group && $_->grant_group->id == $gid)
+      || ($_->request_group && $_->request_group->id == $gid)
+  } @$flag_types;
 
-    return \@flag_types;
+  return \@flag_types;
 }
 
 # Convert the array @clusions('prod_ID:comp_ID') back to a hash of
 # the form %clusions{'prod_name:comp_name'} = 'prod_ID:comp_ID'
 sub clusion_array_to_hash {
-    my ($array, $visible_products) = @_;
-    my $user = Bugzilla->user;
-    my $has_privs = $user->in_group('editcomponents');
-
-    my %hash;
-    my %products;
-    my %components;
-
-    foreach my $ids (@$array) {
-        my ($product_id, $component_id) = split(":", $ids);
-        my $product_name = "__Any__";
-        my $component_name = "__Any__";
-
-        if ($product_id) {
-            ($products{$product_id}) = grep { $_->id == $product_id } @$visible_products;
-            next unless $products{$product_id};
-            $product_name = $products{$product_id}->name;
-
-            if ($component_id) {
-                ($components{$component_id}) =
-                  grep { $_->id == $component_id } @{$products{$product_id}->components};
-                next unless $components{$component_id};
-                $component_name = $components{$component_id}->name;
-            }
-        }
-        else {
-            # Users with local editcomponents privs cannot use __Any__:__Any__.
-            next unless $has_privs;
-            # It's illegal to select a component without a product.
-            next if $component_id;
-        }
-        $hash{"$product_name:$component_name"} = $ids;
+  my ($array, $visible_products) = @_;
+  my $user      = Bugzilla->user;
+  my $has_privs = $user->in_group('editcomponents');
+
+  my %hash;
+  my %products;
+  my %components;
+
+  foreach my $ids (@$array) {
+    my ($product_id, $component_id) = split(":", $ids);
+    my $product_name   = "__Any__";
+    my $component_name = "__Any__";
+
+    if ($product_id) {
+      ($products{$product_id}) = grep { $_->id == $product_id } @$visible_products;
+      next unless $products{$product_id};
+      $product_name = $products{$product_id}->name;
+
+      if ($component_id) {
+        ($components{$component_id})
+          = grep { $_->id == $component_id } @{$products{$product_id}->components};
+        next unless $components{$component_id};
+        $component_name = $components{$component_id}->name;
+      }
+    }
+    else {
+      # Users with local editcomponents privs cannot use __Any__:__Any__.
+      next unless $has_privs;
+
+      # It's illegal to select a component without a product.
+      next if $component_id;
     }
-    return \%hash;
+    $hash{"$product_name:$component_name"} = $ids;
+  }
+  return \%hash;
 }
index 36c262fde32c0bfb2bcc143aafd4cb4a69781062..ed2a36011bb93dbc8bcde788f6a4f66c5774878d 100755 (executable)
@@ -22,36 +22,37 @@ use Bugzilla::Product;
 use Bugzilla::User;
 use Bugzilla::Token;
 
-my $cgi = Bugzilla->cgi;
-my $dbh = Bugzilla->dbh;
+my $cgi      = Bugzilla->cgi;
+my $dbh      = Bugzilla->dbh;
 my $template = Bugzilla->template;
-my $vars = {};
+my $vars     = {};
 
 my $user = Bugzilla->login(LOGIN_REQUIRED);
 
 print $cgi->header();
 
 $user->in_group('creategroups')
-  || ThrowUserError("auth_failure", {group  => "creategroups",
-                                     action => "edit",
-                                     object => "groups"});
+  || ThrowUserError("auth_failure",
+  {group => "creategroups", action => "edit", object => "groups"});
 
 my $action = trim($cgi->param('action') || '');
-my $token  = $cgi->param('token');
+my $token = $cgi->param('token');
 
 # CheckGroupID checks that a positive integer is given and is
 # actually a valid group ID. If all tests are successful, the
 # trimmed group ID is returned.
 
 sub CheckGroupID {
-    my ($group_id) = @_;
-    $group_id = trim($group_id || 0);
-    ThrowUserError("group_not_specified") unless $group_id;
-    (detaint_natural($group_id)
-      && Bugzilla->dbh->selectrow_array("SELECT id FROM groups WHERE id = ?",
-                                        undef, $group_id))
-      || ThrowUserError("invalid_group_ID");
-    return $group_id;
+  my ($group_id) = @_;
+  $group_id = trim($group_id || 0);
+  ThrowUserError("group_not_specified") unless $group_id;
+  (
+    detaint_natural($group_id) && Bugzilla->dbh->selectrow_array(
+      "SELECT id FROM groups WHERE id = ?",
+      undef, $group_id
+    )
+  ) || ThrowUserError("invalid_group_ID");
+  return $group_id;
 }
 
 # CheckGroupRegexp checks that the regular expression is valid
@@ -60,85 +61,86 @@ sub CheckGroupID {
 # is returned.
 
 sub CheckGroupRegexp {
-    my ($regexp) = @_;
-    $regexp = trim($regexp || '');
-    trick_taint($regexp);
-    ThrowUserError("invalid_regexp") unless (eval {qr/$regexp/});
-    return $regexp;
+  my ($regexp) = @_;
+  $regexp = trim($regexp || '');
+  trick_taint($regexp);
+  ThrowUserError("invalid_regexp") unless (eval {qr/$regexp/});
+  return $regexp;
 }
 
 # A helper for displaying the edit.html.tmpl template.
 sub get_current_and_available {
-    my ($group, $vars) = @_;
-
-    my @all_groups         = Bugzilla::Group->get_all;
-    my @members_current    = @{$group->grant_direct(GROUP_MEMBERSHIP)};
-    my @member_of_current  = @{$group->granted_by_direct(GROUP_MEMBERSHIP)};
-    my @bless_from_current = @{$group->grant_direct(GROUP_BLESS)};
-    my @bless_to_current   = @{$group->granted_by_direct(GROUP_BLESS)};
-    my (@visible_from_current, @visible_to_me_current);
+  my ($group, $vars) = @_;
+
+  my @all_groups         = Bugzilla::Group->get_all;
+  my @members_current    = @{$group->grant_direct(GROUP_MEMBERSHIP)};
+  my @member_of_current  = @{$group->granted_by_direct(GROUP_MEMBERSHIP)};
+  my @bless_from_current = @{$group->grant_direct(GROUP_BLESS)};
+  my @bless_to_current   = @{$group->granted_by_direct(GROUP_BLESS)};
+  my (@visible_from_current, @visible_to_me_current);
+  if (Bugzilla->params->{'usevisibilitygroups'}) {
+    @visible_from_current  = @{$group->grant_direct(GROUP_VISIBLE)};
+    @visible_to_me_current = @{$group->granted_by_direct(GROUP_VISIBLE)};
+  }
+
+  # Figure out what groups are not currently a member of this group,
+  # and what groups this group is not currently a member of.
+  my (
+    @members_available,  @member_of_available,    @bless_from_available,
+    @bless_to_available, @visible_from_available, @visible_to_me_available
+  );
+  foreach my $group_option (@all_groups) {
     if (Bugzilla->params->{'usevisibilitygroups'}) {
-        @visible_from_current  = @{$group->grant_direct(GROUP_VISIBLE)};
-        @visible_to_me_current = @{$group->granted_by_direct(GROUP_VISIBLE)};
+      push(@visible_from_available, $group_option)
+        if !grep($_->id == $group_option->id, @visible_from_current);
+      push(@visible_to_me_available, $group_option)
+        if !grep($_->id == $group_option->id, @visible_to_me_current);
     }
 
-    # Figure out what groups are not currently a member of this group,
-    # and what groups this group is not currently a member of.
-    my (@members_available, @member_of_available,
-        @bless_from_available, @bless_to_available,
-        @visible_from_available, @visible_to_me_available);
-    foreach my $group_option (@all_groups) {
-        if (Bugzilla->params->{'usevisibilitygroups'}) {
-            push(@visible_from_available, $group_option)
-                if !grep($_->id == $group_option->id, @visible_from_current);
-            push(@visible_to_me_available, $group_option)
-                if !grep($_->id == $group_option->id, @visible_to_me_current);
-        }
-
-        push(@bless_from_available, $group_option)
-            if !grep($_->id == $group_option->id, @bless_from_current);
-
-        # The group itself should never show up in the membership lists,
-        # and should show up in only one of the bless lists (otherwise
-        # you can try to allow it to bless itself twice, leading to a
-        # database unique constraint error).
-        next if $group_option->id == $group->id;
-
-        push(@members_available, $group_option)
-            if !grep($_->id == $group_option->id, @members_current);
-        push(@member_of_available, $group_option)
-            if !grep($_->id == $group_option->id, @member_of_current);
-        push(@bless_to_available, $group_option)
-           if !grep($_->id == $group_option->id, @bless_to_current);
-    }
-
-    $vars->{'members_current'}     = \@members_current;
-    $vars->{'members_available'}   = \@members_available;
-    $vars->{'member_of_current'}   = \@member_of_current;
-    $vars->{'member_of_available'} = \@member_of_available;
-
-    $vars->{'bless_from_current'}   = \@bless_from_current;
-    $vars->{'bless_from_available'} = \@bless_from_available;
-    $vars->{'bless_to_current'}     = \@bless_to_current;
-    $vars->{'bless_to_available'}   = \@bless_to_available;
-
-    if (Bugzilla->params->{'usevisibilitygroups'}) {
-        $vars->{'visible_from_current'}    = \@visible_from_current;
-        $vars->{'visible_from_available'}  = \@visible_from_available;
-        $vars->{'visible_to_me_current'}   = \@visible_to_me_current;
-        $vars->{'visible_to_me_available'} = \@visible_to_me_available;
-    }
+    push(@bless_from_available, $group_option)
+      if !grep($_->id == $group_option->id, @bless_from_current);
+
+    # The group itself should never show up in the membership lists,
+    # and should show up in only one of the bless lists (otherwise
+    # you can try to allow it to bless itself twice, leading to a
+    # database unique constraint error).
+    next if $group_option->id == $group->id;
+
+    push(@members_available, $group_option)
+      if !grep($_->id == $group_option->id, @members_current);
+    push(@member_of_available, $group_option)
+      if !grep($_->id == $group_option->id, @member_of_current);
+    push(@bless_to_available, $group_option)
+      if !grep($_->id == $group_option->id, @bless_to_current);
+  }
+
+  $vars->{'members_current'}     = \@members_current;
+  $vars->{'members_available'}   = \@members_available;
+  $vars->{'member_of_current'}   = \@member_of_current;
+  $vars->{'member_of_available'} = \@member_of_available;
+
+  $vars->{'bless_from_current'}   = \@bless_from_current;
+  $vars->{'bless_from_available'} = \@bless_from_available;
+  $vars->{'bless_to_current'}     = \@bless_to_current;
+  $vars->{'bless_to_available'}   = \@bless_to_available;
+
+  if (Bugzilla->params->{'usevisibilitygroups'}) {
+    $vars->{'visible_from_current'}    = \@visible_from_current;
+    $vars->{'visible_from_available'}  = \@visible_from_available;
+    $vars->{'visible_to_me_current'}   = \@visible_to_me_current;
+    $vars->{'visible_to_me_available'} = \@visible_to_me_available;
+  }
 }
 
 # If no action is specified, get a list of all groups available.
 
 unless ($action) {
-    my @groups = Bugzilla::Group->get_all;
-    $vars->{'groups'} = \@groups;
+  my @groups = Bugzilla::Group->get_all;
+  $vars->{'groups'} = \@groups;
 
-    $template->process("admin/groups/list.html.tmpl", $vars)
-      || ThrowTemplateError($template->error());
-    exit;
+  $template->process("admin/groups/list.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
+  exit;
 }
 
 #
@@ -148,17 +150,18 @@ unless ($action) {
 #
 
 if ($action eq 'changeform') {
-    # Check that an existing group ID is given
-    my $group_id = CheckGroupID(scalar $cgi->param('group'));
-    my $group = new Bugzilla::Group($group_id);
 
-    get_current_and_available($group, $vars);
-    $vars->{'group'} = $group;
-    $vars->{'token'} = issue_session_token('edit_group');
+  # Check that an existing group ID is given
+  my $group_id = CheckGroupID(scalar $cgi->param('group'));
+  my $group    = new Bugzilla::Group($group_id);
 
-    $template->process("admin/groups/edit.html.tmpl", $vars)
-      || ThrowTemplateError($template->error());
-    exit;
+  get_current_and_available($group, $vars);
+  $vars->{'group'} = $group;
+  $vars->{'token'} = issue_session_token('edit_group');
+
+  $template->process("admin/groups/edit.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
+  exit;
 }
 
 #
@@ -168,41 +171,40 @@ if ($action eq 'changeform') {
 #
 
 if ($action eq 'add') {
-    $vars->{'token'} = issue_session_token('add_group');
+  $vars->{'token'} = issue_session_token('add_group');
 
-    $template->process("admin/groups/create.html.tmpl", $vars)
-      || ThrowTemplateError($template->error());
-    exit;
+  $template->process("admin/groups/create.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
+  exit;
 }
 
 
-
 #
 # action='new' -> add group entered in the 'action=add' screen
 #
 
 if ($action eq 'new') {
-    check_token_data($token, 'add_group');
-    my $group = Bugzilla::Group->create({
-        name        => scalar $cgi->param('name'),
-        description => scalar $cgi->param('desc'),
-        userregexp  => scalar $cgi->param('regexp'),
-        isactive    => scalar $cgi->param('isactive'),
-        icon_url    => scalar $cgi->param('icon_url'),
-        isbuggroup  => 1,
-        use_in_all_products => scalar $cgi->param('insertnew'),
-    });
-
-    delete_token($token);
-
-    $vars->{'message'} = 'group_created';
-    $vars->{'group'} = $group;
-    get_current_and_available($group, $vars);
-    $vars->{'token'} = issue_session_token('edit_group');
-
-    $template->process("admin/groups/edit.html.tmpl", $vars)
-      || ThrowTemplateError($template->error());
-    exit;
+  check_token_data($token, 'add_group');
+  my $group = Bugzilla::Group->create({
+    name                => scalar $cgi->param('name'),
+    description         => scalar $cgi->param('desc'),
+    userregexp          => scalar $cgi->param('regexp'),
+    isactive            => scalar $cgi->param('isactive'),
+    icon_url            => scalar $cgi->param('icon_url'),
+    isbuggroup          => 1,
+    use_in_all_products => scalar $cgi->param('insertnew'),
+  });
+
+  delete_token($token);
+
+  $vars->{'message'} = 'group_created';
+  $vars->{'group'}   = $group;
+  get_current_and_available($group, $vars);
+  $vars->{'token'} = issue_session_token('edit_group');
+
+  $template->process("admin/groups/edit.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
+  exit;
 }
 
 #
@@ -212,20 +214,22 @@ if ($action eq 'new') {
 #
 
 if ($action eq 'del') {
-    # Check that an existing group ID is given
-    my $group = Bugzilla::Group->check({ id => scalar $cgi->param('group') });
-    $group->check_remove({ test_only => 1 });
-    $vars->{'shared_queries'} =
-        $dbh->selectrow_array('SELECT COUNT(*)
+
+  # Check that an existing group ID is given
+  my $group = Bugzilla::Group->check({id => scalar $cgi->param('group')});
+  $group->check_remove({test_only => 1});
+  $vars->{'shared_queries'} = $dbh->selectrow_array(
+    'SELECT COUNT(*)
                                  FROM namedquery_group_map
-                                WHERE group_id = ?', undef, $group->id);
+                                WHERE group_id = ?', undef, $group->id
+  );
 
-    $vars->{'group'} = $group;
-    $vars->{'token'} = issue_session_token('delete_group');
+  $vars->{'group'} = $group;
+  $vars->{'token'} = issue_session_token('delete_group');
 
-    $template->process("admin/groups/delete.html.tmpl", $vars)
-      || ThrowTemplateError($template->error());
-    exit;
+  $template->process("admin/groups/delete.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
+  exit;
 }
 
 #
@@ -233,24 +237,25 @@ if ($action eq 'del') {
 #
 
 if ($action eq 'delete') {
-    check_token_data($token, 'delete_group');
-    # Check that an existing group ID is given
-    my $group = Bugzilla::Group->check({ id => scalar $cgi->param('group') });
-    $vars->{'name'} = $group->name;
-    $group->remove_from_db({
-        remove_from_users => scalar $cgi->param('removeusers'),
-        remove_from_bugs  => scalar $cgi->param('removebugs'),
-        remove_from_flags => scalar $cgi->param('removeflags'),
-        remove_from_products => scalar $cgi->param('unbind'),
-    });
-    delete_token($token);
-
-    $vars->{'message'} = 'group_deleted';
-    $vars->{'groups'} = [Bugzilla::Group->get_all];
-
-    $template->process("admin/groups/list.html.tmpl", $vars)
-      || ThrowTemplateError($template->error());
-    exit;
+  check_token_data($token, 'delete_group');
+
+  # Check that an existing group ID is given
+  my $group = Bugzilla::Group->check({id => scalar $cgi->param('group')});
+  $vars->{'name'} = $group->name;
+  $group->remove_from_db({
+    remove_from_users    => scalar $cgi->param('removeusers'),
+    remove_from_bugs     => scalar $cgi->param('removebugs'),
+    remove_from_flags    => scalar $cgi->param('removeflags'),
+    remove_from_products => scalar $cgi->param('unbind'),
+  });
+  delete_token($token);
+
+  $vars->{'message'} = 'group_deleted';
+  $vars->{'groups'}  = [Bugzilla::Group->get_all];
+
+  $template->process("admin/groups/list.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
+  exit;
 }
 
 #
@@ -258,69 +263,71 @@ if ($action eq 'delete') {
 #
 
 if ($action eq 'postchanges') {
-    check_token_data($token, 'edit_group');
-    my $changes = doGroupChanges();
-    delete_token($token);
-
-    my $group = new Bugzilla::Group(scalar $cgi->param('group_id'));
-    get_current_and_available($group, $vars);
-    $vars->{'message'} = 'group_updated';
-    $vars->{'group'}   = $group;
-    $vars->{'changes'} = $changes;
-    $vars->{'token'} = issue_session_token('edit_group');
-
-    $template->process("admin/groups/edit.html.tmpl", $vars)
-      || ThrowTemplateError($template->error());
-    exit;
+  check_token_data($token, 'edit_group');
+  my $changes = doGroupChanges();
+  delete_token($token);
+
+  my $group = new Bugzilla::Group(scalar $cgi->param('group_id'));
+  get_current_and_available($group, $vars);
+  $vars->{'message'} = 'group_updated';
+  $vars->{'group'}   = $group;
+  $vars->{'changes'} = $changes;
+  $vars->{'token'}   = issue_session_token('edit_group');
+
+  $template->process("admin/groups/edit.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
+  exit;
 }
 
 if ($action eq 'confirm_remove') {
-    my $group = new Bugzilla::Group(CheckGroupID(scalar $cgi->param('group_id')));
-    $vars->{'group'} = $group;
-    $vars->{'regexp'} = CheckGroupRegexp(scalar $cgi->param('regexp'));
-    $vars->{'token'} = issue_session_token('remove_group_members');
-
-    $template->process('admin/groups/confirm-remove.html.tmpl', $vars)
-        || ThrowTemplateError($template->error());
-    exit;
+  my $group = new Bugzilla::Group(CheckGroupID(scalar $cgi->param('group_id')));
+  $vars->{'group'}  = $group;
+  $vars->{'regexp'} = CheckGroupRegexp(scalar $cgi->param('regexp'));
+  $vars->{'token'}  = issue_session_token('remove_group_members');
+
+  $template->process('admin/groups/confirm-remove.html.tmpl', $vars)
+    || ThrowTemplateError($template->error());
+  exit;
 }
 
 if ($action eq 'remove_regexp') {
-    check_token_data($token, 'remove_group_members');
-    # remove all explicit users from the group with
-    # gid = $cgi->param('group') that match the regular expression
-    # stored in the DB for that group or all of them period
-
-    my $group  = new Bugzilla::Group(CheckGroupID(scalar $cgi->param('group_id')));
-    my $regexp = CheckGroupRegexp(scalar $cgi->param('regexp'));
-
-    $dbh->bz_start_transaction();
-
-    my $users = $group->members_direct();
-    my $sth_delete = $dbh->prepare(
-        "DELETE FROM user_group_map
-           WHERE user_id = ? AND isbless = 0 AND group_id = ?");
-
-    my @deleted;
-    foreach my $member (@$users) {
-        if ($regexp eq '' || $member->email =~ m/$regexp/i) {
-            $sth_delete->execute($member->id, $group->id);
-            push(@deleted, $member);
-        }
+  check_token_data($token, 'remove_group_members');
+
+  # remove all explicit users from the group with
+  # gid = $cgi->param('group') that match the regular expression
+  # stored in the DB for that group or all of them period
+
+  my $group  = new Bugzilla::Group(CheckGroupID(scalar $cgi->param('group_id')));
+  my $regexp = CheckGroupRegexp(scalar $cgi->param('regexp'));
+
+  $dbh->bz_start_transaction();
+
+  my $users      = $group->members_direct();
+  my $sth_delete = $dbh->prepare(
+    "DELETE FROM user_group_map
+           WHERE user_id = ? AND isbless = 0 AND group_id = ?"
+  );
+
+  my @deleted;
+  foreach my $member (@$users) {
+    if ($regexp eq '' || $member->email =~ m/$regexp/i) {
+      $sth_delete->execute($member->id, $group->id);
+      push(@deleted, $member);
     }
-    $dbh->bz_commit_transaction();
+  }
+  $dbh->bz_commit_transaction();
 
-    $vars->{'users'}  = \@deleted;
-    $vars->{'regexp'} = $regexp;
-    delete_token($token);
+  $vars->{'users'}  = \@deleted;
+  $vars->{'regexp'} = $regexp;
+  delete_token($token);
 
-    $vars->{'message'} = 'group_membership_removed';
-    $vars->{'group'} = $group->name;
-    $vars->{'groups'} = [Bugzilla::Group->get_all];
+  $vars->{'message'} = 'group_membership_removed';
+  $vars->{'group'}   = $group->name;
+  $vars->{'groups'}  = [Bugzilla::Group->get_all];
 
-    $template->process("admin/groups/list.html.tmpl", $vars)
-      || ThrowTemplateError($template->error());
-    exit;
+  $template->process("admin/groups/list.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
+  exit;
 }
 
 # No valid action found
@@ -328,111 +335,120 @@ ThrowUserError('unknown_action', {action => $action});
 
 # Helper sub to handle the making of changes to a group
 sub doGroupChanges {
-    my $cgi = Bugzilla->cgi;
-    my $dbh = Bugzilla->dbh;
+  my $cgi = Bugzilla->cgi;
+  my $dbh = Bugzilla->dbh;
 
-    $dbh->bz_start_transaction();
+  $dbh->bz_start_transaction();
 
-    # Check that the given group ID is valid and make a Group.
-    my $group = new Bugzilla::Group(CheckGroupID(scalar $cgi->param('group_id')));
+  # Check that the given group ID is valid and make a Group.
+  my $group = new Bugzilla::Group(CheckGroupID(scalar $cgi->param('group_id')));
 
-    if (defined $cgi->param('regexp')) {
-        $group->set_user_regexp(scalar $cgi->param('regexp'));
-    }
+  if (defined $cgi->param('regexp')) {
+    $group->set_user_regexp(scalar $cgi->param('regexp'));
+  }
 
-    if ($group->is_bug_group) {
-        if (defined $cgi->param('name')) {
-            $group->set_name(scalar $cgi->param('name'));
-        }
-        if (defined $cgi->param('desc')) {
-            $group->set_description(scalar $cgi->param('desc'));
-        }
-        # Only set isactive if we came from the right form.
-        if (defined $cgi->param('regexp')) {
-            $group->set_is_active(scalar $cgi->param('isactive'));
-        }
+  if ($group->is_bug_group) {
+    if (defined $cgi->param('name')) {
+      $group->set_name(scalar $cgi->param('name'));
+    }
+    if (defined $cgi->param('desc')) {
+      $group->set_description(scalar $cgi->param('desc'));
     }
 
-    if (defined $cgi->param('icon_url')) {
-        $group->set_icon_url(scalar $cgi->param('icon_url'));
+    # Only set isactive if we came from the right form.
+    if (defined $cgi->param('regexp')) {
+      $group->set_is_active(scalar $cgi->param('isactive'));
     }
+  }
+
+  if (defined $cgi->param('icon_url')) {
+    $group->set_icon_url(scalar $cgi->param('icon_url'));
+  }
 
-    my $changes = $group->update();
+  my $changes = $group->update();
 
-    my $sth_insert = $dbh->prepare('INSERT INTO group_group_map
+  my $sth_insert = $dbh->prepare(
+    'INSERT INTO group_group_map
                                     (member_id, grantor_id, grant_type)
-                                    VALUES (?, ?, ?)');
+                                    VALUES (?, ?, ?)'
+  );
 
-    my $sth_delete = $dbh->prepare('DELETE FROM group_group_map
+  my $sth_delete = $dbh->prepare(
+    'DELETE FROM group_group_map
                                      WHERE member_id = ?
                                            AND grantor_id = ?
-                                           AND grant_type = ?');
-
-    # First item is the type, second is whether or not it's "reverse" 
-    # (granted_by) (see _do_add for more explanation).
-    my %fields = (
-        members       => [GROUP_MEMBERSHIP, 0],
-        bless_from    => [GROUP_BLESS, 0],
-        visible_from  => [GROUP_VISIBLE, 0],
-        member_of     => [GROUP_MEMBERSHIP, 1],
-        bless_to      => [GROUP_BLESS, 1],
-        visible_to_me => [GROUP_VISIBLE, 1]
-    );
-    while (my ($field, $data) = each %fields) {
-        _do_add($group, $changes, $sth_insert, "${field}_add", 
-                $data->[0], $data->[1]);
-        _do_remove($group, $changes, $sth_delete, "${field}_remove",
-                   $data->[0], $data->[1]);
-    }
-
-    $dbh->bz_commit_transaction();
-    return $changes;
+                                           AND grant_type = ?'
+  );
+
+  # First item is the type, second is whether or not it's "reverse"
+  # (granted_by) (see _do_add for more explanation).
+  my %fields = (
+    members       => [GROUP_MEMBERSHIP, 0],
+    bless_from    => [GROUP_BLESS,      0],
+    visible_from  => [GROUP_VISIBLE,    0],
+    member_of     => [GROUP_MEMBERSHIP, 1],
+    bless_to      => [GROUP_BLESS,      1],
+    visible_to_me => [GROUP_VISIBLE,    1]
+  );
+  while (my ($field, $data) = each %fields) {
+    _do_add($group, $changes, $sth_insert, "${field}_add", $data->[0], $data->[1]);
+    _do_remove($group, $changes, $sth_delete, "${field}_remove", $data->[0],
+      $data->[1]);
+  }
+
+  $dbh->bz_commit_transaction();
+  return $changes;
 }
 
 sub _do_add {
-    my ($group, $changes, $sth_insert, $field, $type, $reverse) = @_;
-    my $cgi = Bugzilla->cgi;
-
-    my $current;
-    # $reverse means we're doing a granted_by--that is, somebody else
-    # is granting us something.
-    if ($reverse) {
-        $current = $group->granted_by_direct($type);
-    }
-    else {
-        $current = $group->grant_direct($type);
-    }
+  my ($group, $changes, $sth_insert, $field, $type, $reverse) = @_;
+  my $cgi = Bugzilla->cgi;
 
-    my $add_items = Bugzilla::Group->new_from_list([$cgi->multi_param($field)]);
+  my $current;
 
-    foreach my $add (@$add_items) {
-        next if grep($_->id == $add->id, @$current);
+  # $reverse means we're doing a granted_by--that is, somebody else
+  # is granting us something.
+  if ($reverse) {
+    $current = $group->granted_by_direct($type);
+  }
+  else {
+    $current = $group->grant_direct($type);
+  }
 
-        $changes->{$field} ||= [];
-        push(@{$changes->{$field}}, $add->name);
-        # They go this direction for a normal "This group is granting
-        # $add something."
-        my @ids = ($add->id, $group->id);
-        # But they get reversed for "This group is being granted something
-        # by $add."
-        @ids = reverse @ids if $reverse;
-        $sth_insert->execute(@ids, $type);
-    }
+  my $add_items = Bugzilla::Group->new_from_list([$cgi->multi_param($field)]);
+
+  foreach my $add (@$add_items) {
+    next if grep($_->id == $add->id, @$current);
+
+    $changes->{$field} ||= [];
+    push(@{$changes->{$field}}, $add->name);
+
+    # They go this direction for a normal "This group is granting
+    # $add something."
+    my @ids = ($add->id, $group->id);
+
+    # But they get reversed for "This group is being granted something
+    # by $add."
+    @ids = reverse @ids if $reverse;
+    $sth_insert->execute(@ids, $type);
+  }
 }
 
 sub _do_remove {
-    my ($group, $changes, $sth_delete, $field, $type, $reverse) = @_;
-    my $cgi = Bugzilla->cgi;
-    my $remove_items = Bugzilla::Group->new_from_list([$cgi->multi_param($field)]);
-
-    foreach my $remove (@$remove_items) {
-        my @ids = ($remove->id, $group->id);
-        # See _do_add for an explanation of $reverse
-        @ids = reverse @ids if $reverse;
-        # Deletions always succeed and are harmless if they fail, so we
-        # don't need to do any checks.
-        $sth_delete->execute(@ids, $type);
-        $changes->{$field} ||= [];
-        push(@{$changes->{$field}}, $remove->name);
-    }
+  my ($group, $changes, $sth_delete, $field, $type, $reverse) = @_;
+  my $cgi          = Bugzilla->cgi;
+  my $remove_items = Bugzilla::Group->new_from_list([$cgi->multi_param($field)]);
+
+  foreach my $remove (@$remove_items) {
+    my @ids = ($remove->id, $group->id);
+
+    # See _do_add for an explanation of $reverse
+    @ids = reverse @ids if $reverse;
+
+    # Deletions always succeed and are harmless if they fail, so we
+    # don't need to do any checks.
+    $sth_delete->execute(@ids, $type);
+    $changes->{$field} ||= [];
+    push(@{$changes->{$field}}, $remove->name);
+  }
 }
index afa00ad7feaa69ffef20ff4ba3606125679e4722..a2ac45cb90b66a8715e67885773eb3e10ded2cea 100755 (executable)
@@ -19,21 +19,20 @@ use Bugzilla::Error;
 use Bugzilla::Keyword;
 use Bugzilla::Token;
 
-my $cgi = Bugzilla->cgi;
-my $dbh = Bugzilla->dbh;
+my $cgi      = Bugzilla->cgi;
+my $dbh      = Bugzilla->dbh;
 my $template = Bugzilla->template;
-my $vars = {};
+my $vars     = {};
 
 my $user = Bugzilla->login(LOGIN_REQUIRED);
 
 print $cgi->header();
 
 $user->in_group('editkeywords')
-  || ThrowUserError("auth_failure", {group  => "editkeywords",
-                                     action => "edit",
-                                     object => "keywords"});
+  || ThrowUserError("auth_failure",
+  {group => "editkeywords", action => "edit", object => "keywords"});
 
-my $action = trim($cgi->param('action')  || '');
+my $action = trim($cgi->param('action') || '');
 my $key_id = $cgi->param('id');
 my $token  = $cgi->param('token');
 
@@ -41,41 +40,40 @@ $vars->{'action'} = $action;
 
 
 if ($action eq "") {
-    $vars->{'keywords'} = Bugzilla::Keyword->get_all_with_bug_count();
+  $vars->{'keywords'} = Bugzilla::Keyword->get_all_with_bug_count();
 
-    $template->process("admin/keywords/list.html.tmpl", $vars)
-      || ThrowTemplateError($template->error());
-    exit;
+  $template->process("admin/keywords/list.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
+  exit;
 }
 
 if ($action eq 'add') {
-    $vars->{'token'} = issue_session_token('add_keyword');
+  $vars->{'token'} = issue_session_token('add_keyword');
 
-    $template->process("admin/keywords/create.html.tmpl", $vars)
-      || ThrowTemplateError($template->error());
-    exit;
+  $template->process("admin/keywords/create.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
+  exit;
 }
 
 #
 # action='new' -> add keyword entered in the 'action=add' screen
 #
 if ($action eq 'new') {
-    check_token_data($token, 'add_keyword');
-    my $name = $cgi->param('name') || '';
-    my $desc = $cgi->param('description')  || '';
+  check_token_data($token, 'add_keyword');
+  my $name = $cgi->param('name')        || '';
+  my $desc = $cgi->param('description') || '';
 
-    my $keyword = Bugzilla::Keyword->create(
-        { name => $name, description => $desc });
+  my $keyword = Bugzilla::Keyword->create({name => $name, description => $desc});
 
-    delete_token($token);
+  delete_token($token);
 
-    $vars->{'message'} = 'keyword_created';
-    $vars->{'name'} = $keyword->name;
-    $vars->{'keywords'} = Bugzilla::Keyword->get_all_with_bug_count();
+  $vars->{'message'}  = 'keyword_created';
+  $vars->{'name'}     = $keyword->name;
+  $vars->{'keywords'} = Bugzilla::Keyword->get_all_with_bug_count();
 
-    $template->process("admin/keywords/list.html.tmpl", $vars)
-      || ThrowTemplateError($template->error());
-    exit;
+  $template->process("admin/keywords/list.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
+  exit;
 }
 
 #
@@ -85,15 +83,15 @@ if ($action eq 'new') {
 #
 
 if ($action eq 'edit') {
-    my $keyword = new Bugzilla::Keyword($key_id)
-        || ThrowUserError('invalid_keyword_id', { id => $key_id });
+  my $keyword = new Bugzilla::Keyword($key_id)
+    || ThrowUserError('invalid_keyword_id', {id => $key_id});
 
-    $vars->{'keyword'} = $keyword;
-    $vars->{'token'} = issue_session_token('edit_keyword');
+  $vars->{'keyword'} = $keyword;
+  $vars->{'token'}   = issue_session_token('edit_keyword');
 
-    $template->process("admin/keywords/edit.html.tmpl", $vars)
-      || ThrowTemplateError($template->error());
-    exit;
+  $template->process("admin/keywords/edit.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
+  exit;
 }
 
 #
@@ -101,57 +99,57 @@ if ($action eq 'edit') {
 #
 
 if ($action eq 'update') {
-    check_token_data($token, 'edit_keyword');
-    my $keyword = new Bugzilla::Keyword($key_id)
-        || ThrowUserError('invalid_keyword_id', { id => $key_id });
-
-    $keyword->set_all({
-        name        => scalar $cgi->param('name'),
-        description => scalar $cgi->param('description'),
-        is_active   => scalar $cgi->param('is_active'),
-    });
-    my $changes = $keyword->update();
-
-    delete_token($token);
-
-    $vars->{'message'} = 'keyword_updated';
-    $vars->{'keyword'} = $keyword;
-    $vars->{'changes'} = $changes;
-    $vars->{'keywords'} = Bugzilla::Keyword->get_all_with_bug_count();
-
-    $template->process("admin/keywords/list.html.tmpl", $vars)
-      || ThrowTemplateError($template->error());
-    exit;
+  check_token_data($token, 'edit_keyword');
+  my $keyword = new Bugzilla::Keyword($key_id)
+    || ThrowUserError('invalid_keyword_id', {id => $key_id});
+
+  $keyword->set_all({
+    name        => scalar $cgi->param('name'),
+    description => scalar $cgi->param('description'),
+    is_active   => scalar $cgi->param('is_active'),
+  });
+  my $changes = $keyword->update();
+
+  delete_token($token);
+
+  $vars->{'message'}  = 'keyword_updated';
+  $vars->{'keyword'}  = $keyword;
+  $vars->{'changes'}  = $changes;
+  $vars->{'keywords'} = Bugzilla::Keyword->get_all_with_bug_count();
+
+  $template->process("admin/keywords/list.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
+  exit;
 }
 
 if ($action eq 'del') {
-    my $keyword =  new Bugzilla::Keyword($key_id)
-        || ThrowUserError('invalid_keyword_id', { id => $key_id });
+  my $keyword = new Bugzilla::Keyword($key_id)
+    || ThrowUserError('invalid_keyword_id', {id => $key_id});
 
-    $vars->{'keyword'} = $keyword;
-    $vars->{'token'} = issue_session_token('delete_keyword');
+  $vars->{'keyword'} = $keyword;
+  $vars->{'token'}   = issue_session_token('delete_keyword');
 
-    $template->process("admin/keywords/confirm-delete.html.tmpl", $vars)
-      || ThrowTemplateError($template->error());
-    exit;
+  $template->process("admin/keywords/confirm-delete.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
+  exit;
 }
 
 if ($action eq 'delete') {
-    check_token_data($token, 'delete_keyword');
-    my $keyword =  new Bugzilla::Keyword($key_id)
-        || ThrowUserError('invalid_keyword_id', { id => $key_id });
+  check_token_data($token, 'delete_keyword');
+  my $keyword = new Bugzilla::Keyword($key_id)
+    || ThrowUserError('invalid_keyword_id', {id => $key_id});
 
-    $keyword->remove_from_db();
+  $keyword->remove_from_db();
 
-    delete_token($token);
+  delete_token($token);
 
-    $vars->{'message'} = 'keyword_deleted';
-    $vars->{'keyword'} = $keyword;
-    $vars->{'keywords'} = Bugzilla::Keyword->get_all_with_bug_count();
+  $vars->{'message'}  = 'keyword_deleted';
+  $vars->{'keyword'}  = $keyword;
+  $vars->{'keywords'} = Bugzilla::Keyword->get_all_with_bug_count();
 
-    $template->process("admin/keywords/list.html.tmpl", $vars)
-      || ThrowTemplateError($template->error());
-    exit;
+  $template->process("admin/keywords/list.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
+  exit;
 }
 
 ThrowUserError('unknown_action', {action => $action});
index 2b6e041e75f7286dd0d02f310052bdc00f3668ba..5b184eb430688704bf11f3aa5335a0991fbe8b22 100755 (executable)
@@ -19,10 +19,11 @@ use Bugzilla::Error;
 use Bugzilla::Milestone;
 use Bugzilla::Token;
 
-my $cgi = Bugzilla->cgi;
-my $dbh = Bugzilla->dbh;
+my $cgi      = Bugzilla->cgi;
+my $dbh      = Bugzilla->dbh;
 my $template = Bugzilla->template;
-my $vars = {};
+my $vars     = {};
+
 # There is only one section about milestones in the documentation,
 # so all actions point to the same page.
 $vars->{'doc_section'} = 'administering/categorization.html#milestones';
@@ -37,18 +38,17 @@ print $cgi->header();
 
 $user->in_group('editcomponents')
   || scalar(@{$user->get_products_by_permission('editcomponents')})
-  || ThrowUserError("auth_failure", {group  => "editcomponents",
-                                     action => "edit",
-                                     object => "milestones"});
+  || ThrowUserError("auth_failure",
+  {group => "editcomponents", action => "edit", object => "milestones"});
 
 #
 # often used variables
 #
-my $product_name   = trim($cgi->param('product')     || '');
-my $milestone_name = trim($cgi->param('milestone')   || '');
-my $sortkey        = trim($cgi->param('sortkey')     || 0);
-my $action         = trim($cgi->param('action')      || '');
-my $showbugcounts = (defined $cgi->param('showbugcounts'));
+my $product_name   = trim($cgi->param('product')   || '');
+my $milestone_name = trim($cgi->param('milestone') || '');
+my $sortkey        = trim($cgi->param('sortkey')   || 0);
+my $action         = trim($cgi->param('action')    || '');
+my $showbugcounts  = (defined $cgi->param('showbugcounts'));
 my $token          = $cgi->param('token');
 my $isactive       = $cgi->param('isactive');
 
@@ -57,18 +57,19 @@ my $isactive       = $cgi->param('isactive');
 #
 
 unless ($product_name) {
-    my $selectable_products = $user->get_selectable_products;
-    # If the user has editcomponents privs for some products only,
-    # we have to restrict the list of products to display.
-    unless ($user->in_group('editcomponents')) {
-        $selectable_products = $user->get_products_by_permission('editcomponents');
-    }
-    $vars->{'products'} = $selectable_products;
-    $vars->{'showbugcounts'} = $showbugcounts;
-
-    $template->process("admin/milestones/select-product.html.tmpl", $vars)
-      || ThrowTemplateError($template->error());
-    exit;
+  my $selectable_products = $user->get_selectable_products;
+
+  # If the user has editcomponents privs for some products only,
+  # we have to restrict the list of products to display.
+  unless ($user->in_group('editcomponents')) {
+    $selectable_products = $user->get_products_by_permission('editcomponents');
+  }
+  $vars->{'products'}      = $selectable_products;
+  $vars->{'showbugcounts'} = $showbugcounts;
+
+  $template->process("admin/milestones/select-product.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
+  exit;
 }
 
 my $product = $user->check_can_admin_product($product_name);
@@ -79,11 +80,11 @@ my $product = $user->check_can_admin_product($product_name);
 
 unless ($action) {
 
-    $vars->{'showbugcounts'} = $showbugcounts;
-    $vars->{'product'} = $product;
-    $template->process("admin/milestones/list.html.tmpl", $vars)
-      || ThrowTemplateError($template->error());
-    exit;
+  $vars->{'showbugcounts'} = $showbugcounts;
+  $vars->{'product'}       = $product;
+  $template->process("admin/milestones/list.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
+  exit;
 }
 
 #
@@ -93,11 +94,11 @@ unless ($action) {
 #
 
 if ($action eq 'add') {
-    $vars->{'token'} = issue_session_token('add_milestone');
-    $vars->{'product'} = $product;
-    $template->process("admin/milestones/create.html.tmpl", $vars)
-      || ThrowTemplateError($template->error());
-    exit;
+  $vars->{'token'}   = issue_session_token('add_milestone');
+  $vars->{'product'} = $product;
+  $template->process("admin/milestones/create.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
+  exit;
 }
 
 #
@@ -105,20 +106,21 @@ if ($action eq 'add') {
 #
 
 if ($action eq 'new') {
-    check_token_data($token, 'add_milestone');
+  check_token_data($token, 'add_milestone');
 
-    my $milestone = Bugzilla::Milestone->create({ value    => $milestone_name,
-                                                  product  => $product,
-                                                  sortkey  => $sortkey });
+  my $milestone
+    = Bugzilla::Milestone->create({
+    value => $milestone_name, product => $product, sortkey => $sortkey
+    });
 
-    delete_token($token);
+  delete_token($token);
 
-    $vars->{'message'} = 'milestone_created';
-    $vars->{'milestone'} = $milestone;
-    $vars->{'product'} = $product;
-    $template->process("admin/milestones/list.html.tmpl", $vars)
-      || ThrowTemplateError($template->error());
-    exit;
+  $vars->{'message'}   = 'milestone_created';
+  $vars->{'milestone'} = $milestone;
+  $vars->{'product'}   = $product;
+  $template->process("admin/milestones/list.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
+  exit;
 }
 
 #
@@ -128,21 +130,21 @@ if ($action eq 'new') {
 #
 
 if ($action eq 'del') {
-    my $milestone = Bugzilla::Milestone->check({ product => $product,
-                                                 name    => $milestone_name });
-    
-    $vars->{'milestone'} = $milestone;
-    $vars->{'product'} = $product;
-
-    # The default milestone cannot be deleted.
-    if ($product->default_milestone eq $milestone->name) {
-        ThrowUserError("milestone_is_default", { milestone => $milestone });
-    }
-    $vars->{'token'} = issue_session_token('delete_milestone');
-
-    $template->process("admin/milestones/confirm-delete.html.tmpl", $vars)
-      || ThrowTemplateError($template->error());
-    exit;
+  my $milestone
+    = Bugzilla::Milestone->check({product => $product, name => $milestone_name});
+
+  $vars->{'milestone'} = $milestone;
+  $vars->{'product'}   = $product;
+
+  # The default milestone cannot be deleted.
+  if ($product->default_milestone eq $milestone->name) {
+    ThrowUserError("milestone_is_default", {milestone => $milestone});
+  }
+  $vars->{'token'} = issue_session_token('delete_milestone');
+
+  $template->process("admin/milestones/confirm-delete.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
+  exit;
 }
 
 #
@@ -150,20 +152,20 @@ if ($action eq 'del') {
 #
 
 if ($action eq 'delete') {
-    check_token_data($token, 'delete_milestone');
-    my $milestone = Bugzilla::Milestone->check({ product => $product,
-                                                 name    => $milestone_name });
-    $milestone->remove_from_db;
-    delete_token($token);
-
-    $vars->{'message'} = 'milestone_deleted';
-    $vars->{'milestone'} = $milestone;
-    $vars->{'product'} = $product;
-    $vars->{'no_edit_milestone_link'} = 1;
-
-    $template->process("admin/milestones/list.html.tmpl", $vars)
-      || ThrowTemplateError($template->error());
-    exit;
+  check_token_data($token, 'delete_milestone');
+  my $milestone
+    = Bugzilla::Milestone->check({product => $product, name => $milestone_name});
+  $milestone->remove_from_db;
+  delete_token($token);
+
+  $vars->{'message'}                = 'milestone_deleted';
+  $vars->{'milestone'}              = $milestone;
+  $vars->{'product'}                = $product;
+  $vars->{'no_edit_milestone_link'} = 1;
+
+  $template->process("admin/milestones/list.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
+  exit;
 }
 
 #
@@ -174,16 +176,16 @@ if ($action eq 'delete') {
 
 if ($action eq 'edit') {
 
-    my $milestone = Bugzilla::Milestone->check({ product => $product,
-                                                 name    => $milestone_name });
+  my $milestone
+    = Bugzilla::Milestone->check({product => $product, name => $milestone_name});
 
-    $vars->{'milestone'} = $milestone;
-    $vars->{'product'} = $product;
-    $vars->{'token'} = issue_session_token('edit_milestone');
+  $vars->{'milestone'} = $milestone;
+  $vars->{'product'}   = $product;
+  $vars->{'token'}     = issue_session_token('edit_milestone');
 
-    $template->process("admin/milestones/edit.html.tmpl", $vars)
-      || ThrowTemplateError($template->error());
-    exit;
+  $template->process("admin/milestones/edit.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
+  exit;
 }
 
 #
@@ -191,30 +193,29 @@ if ($action eq 'edit') {
 #
 
 if ($action eq 'update') {
-    check_token_data($token, 'edit_milestone');
-    my $milestone_old_name = trim($cgi->param('milestoneold') || '');
-    my $milestone = Bugzilla::Milestone->check({ product => $product,
-                                                 name    => $milestone_old_name });
-
-    $milestone->set_all({
-        name      => $milestone_name,
-        sortkey   => $sortkey,
-        is_active => $isactive,
-    });
-    my $changes = $milestone->update();
-    # Reloading the product since the default milestone name
-    # could have been changed.
-    $product = new Bugzilla::Product({ name => $product_name });
-
-    delete_token($token);
-
-    $vars->{'message'} = 'milestone_updated';
-    $vars->{'milestone'} = $milestone;
-    $vars->{'product'} = $product;
-    $vars->{'changes'} = $changes;
-    $template->process("admin/milestones/list.html.tmpl", $vars)
-      || ThrowTemplateError($template->error());
-    exit;
+  check_token_data($token, 'edit_milestone');
+  my $milestone_old_name = trim($cgi->param('milestoneold') || '');
+  my $milestone = Bugzilla::Milestone->check(
+    {product => $product, name => $milestone_old_name});
+
+  $milestone->set_all({
+    name => $milestone_name, sortkey => $sortkey, is_active => $isactive,
+  });
+  my $changes = $milestone->update();
+
+  # Reloading the product since the default milestone name
+  # could have been changed.
+  $product = new Bugzilla::Product({name => $product_name});
+
+  delete_token($token);
+
+  $vars->{'message'}   = 'milestone_updated';
+  $vars->{'milestone'} = $milestone;
+  $vars->{'product'}   = $product;
+  $vars->{'changes'}   = $changes;
+  $template->process("admin/milestones/list.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
+  exit;
 }
 
 # No valid action found
index c91235c061cc952e79f0158df000db7d60228306..88222881e58556ac0046f43b7beac7bd9d99fa51 100755 (executable)
@@ -24,144 +24,152 @@ use Bugzilla::User;
 use Bugzilla::User::Setting;
 use Bugzilla::Status;
 
-my $user = Bugzilla->login(LOGIN_REQUIRED);
-my $cgi = Bugzilla->cgi;
+my $user     = Bugzilla->login(LOGIN_REQUIRED);
+my $cgi      = Bugzilla->cgi;
 my $template = Bugzilla->template;
-my $vars = {};
+my $vars     = {};
 
 print $cgi->header();
 
 $user->in_group('tweakparams')
-  || ThrowUserError("auth_failure", {group  => "tweakparams",
-                                     action => "access",
-                                     object => "parameters"});
+  || ThrowUserError("auth_failure",
+  {group => "tweakparams", action => "access", object => "parameters"});
 
-my $action = trim($cgi->param('action') || '');
-my $token  = $cgi->param('token');
+my $action        = trim($cgi->param('action') || '');
+my $token         = $cgi->param('token');
 my $current_panel = $cgi->param('section') || 'core';
 $current_panel =~ /^([A-Za-z0-9_-]+)$/;
 $current_panel = $1;
 
 my $current_module;
-my @panels = ();
+my @panels       = ();
 my $param_panels = Bugzilla::Config::param_panels();
 foreach my $panel (keys %$param_panels) {
-    my $module = $param_panels->{$panel};
-    eval("require $module") || die $@;
-    my @module_param_list = "$module"->get_param_list();
-    my $item = { name => lc($panel),
-                 current => ($current_panel eq lc($panel)) ? 1 : 0,
-                 param_list => \@module_param_list,
-                 sortkey => eval "\$${module}::sortkey;"
-               };
-    defined($item->{'sortkey'}) || ($item->{'sortkey'} = 100000);
-    push(@panels, $item);
-    $current_module = $panel if ($current_panel eq lc($panel));
+  my $module = $param_panels->{$panel};
+  eval("require $module") || die $@;
+  my @module_param_list = "$module"->get_param_list();
+  my $item              = {
+    name       => lc($panel),
+    current    => ($current_panel eq lc($panel)) ? 1 : 0,
+    param_list => \@module_param_list,
+    sortkey    => eval "\$${module}::sortkey;"
+  };
+  defined($item->{'sortkey'}) || ($item->{'sortkey'} = 100000);
+  push(@panels, $item);
+  $current_module = $panel if ($current_panel eq lc($panel));
 }
 
-my %hook_panels = map { $_->{name} => { params => $_->{param_list} } }
-                      @panels;
+my %hook_panels = map { $_->{name} => {params => $_->{param_list}} } @panels;
+
 # Note that this hook is also called in Bugzilla::Config.
-Bugzilla::Hook::process('config_modify_panels', { panels => \%hook_panels });
+Bugzilla::Hook::process('config_modify_panels', {panels => \%hook_panels});
 
 $vars->{panels} = \@panels;
 
 if ($action eq 'save' && $current_module) {
-    check_token_data($token, 'edit_parameters');
-    my @changes = ();
-    my @module_param_list = @{ $hook_panels{lc($current_module)}->{params} };
-
-    foreach my $i (@module_param_list) {
-        my $name = $i->{'name'};
-        my $value = $cgi->param($name);
-
-        if (defined $cgi->param("reset-$name") && !$i->{'no_reset'}) {
-            $value = $i->{'default'};
-        } else {
-            if ($i->{'type'} eq 'm') {
-                # This simplifies the code below
-                $value = [ $cgi->param($name) ];
-            } else {
-                # Get rid of windows/mac-style line endings.
-                $value =~ s/\r\n?/\n/g;
-                # assume single linefeed is an empty string
-                $value =~ s/^\n$//;
-            }
-            # Stop complaining if the URL has no trailing slash.
-            # XXX - This hack can go away once bug 303662 is implemented.
-            if ($name =~ /base$/) {
-                $value = "$value/" if ($value && $value !~ m#/$#);
-            }
-        }
+  check_token_data($token, 'edit_parameters');
+  my @changes           = ();
+  my @module_param_list = @{$hook_panels{lc($current_module)}->{params}};
+
+  foreach my $i (@module_param_list) {
+    my $name  = $i->{'name'};
+    my $value = $cgi->param($name);
 
-        my $changed;
-        if ($i->{'type'} eq 'm') {
-            my @old = sort @{Bugzilla->params->{$name}};
-            my @new = sort @$value;
-            if (scalar(@old) != scalar(@new)) {
-                $changed = 1;
-            } else {
-                $changed = 0; # Assume not changed...
-                for (my $cnt = 0; $cnt < scalar(@old); ++$cnt) {
-                    if ($old[$cnt] ne $new[$cnt]) {
-                        # entry is different, therefore changed
-                        $changed = 1;
-                        last;
-                    }
-                }
-            }
-        } else {
-            $changed = ($value eq Bugzilla->params->{$name})? 0 : 1;
+    if (defined $cgi->param("reset-$name") && !$i->{'no_reset'}) {
+      $value = $i->{'default'};
+    }
+    else {
+      if ($i->{'type'} eq 'm') {
+
+        # This simplifies the code below
+        $value = [$cgi->param($name)];
+      }
+      else {
+        # Get rid of windows/mac-style line endings.
+        $value =~ s/\r\n?/\n/g;
+
+        # assume single linefeed is an empty string
+        $value =~ s/^\n$//;
+      }
+
+      # Stop complaining if the URL has no trailing slash.
+      # XXX - This hack can go away once bug 303662 is implemented.
+      if ($name =~ /base$/) {
+        $value = "$value/" if ($value && $value !~ m#/$#);
+      }
+    }
+
+    my $changed;
+    if ($i->{'type'} eq 'm') {
+      my @old = sort @{Bugzilla->params->{$name}};
+      my @new = sort @$value;
+      if (scalar(@old) != scalar(@new)) {
+        $changed = 1;
+      }
+      else {
+        $changed = 0;    # Assume not changed...
+        for (my $cnt = 0; $cnt < scalar(@old); ++$cnt) {
+          if ($old[$cnt] ne $new[$cnt]) {
+
+            # entry is different, therefore changed
+            $changed = 1;
+            last;
+          }
         }
+      }
+    }
+    else {
+      $changed = ($value eq Bugzilla->params->{$name}) ? 0 : 1;
+    }
 
-        if ($changed) {
-            if (exists $i->{'checker'}) {
-                my $ok = $i->{'checker'}->($value, $i);
-                if ($ok ne "") {
-                    ThrowUserError('invalid_parameter', { name => $name, err => $ok });
-                }
-            } elsif ($name eq 'globalwatchers') {
-                # can't check this as others, as Bugzilla::Config::Common
-                # cannot use Bugzilla::User
-                foreach my $watcher (split(/[,\s]+/, $value)) {
-                    ThrowUserError(
-                        'invalid_parameter',
-                        { name => $name, err => "no such user $watcher" }
-                    ) unless login_to_id($watcher);
-                }
-            }
-            push(@changes, $name);
-            SetParam($name, $value);
-            if (($name eq "shutdownhtml") && ($value ne "")) {
-                $vars->{'shutdown_is_active'} = 1;
-            }
+    if ($changed) {
+      if (exists $i->{'checker'}) {
+        my $ok = $i->{'checker'}->($value, $i);
+        if ($ok ne "") {
+          ThrowUserError('invalid_parameter', {name => $name, err => $ok});
+        }
+      }
+      elsif ($name eq 'globalwatchers') {
+
+        # can't check this as others, as Bugzilla::Config::Common
+        # cannot use Bugzilla::User
+        foreach my $watcher (split(/[,\s]+/, $value)) {
+          ThrowUserError('invalid_parameter',
+            {name => $name, err => "no such user $watcher"})
+            unless login_to_id($watcher);
         }
+      }
+      push(@changes, $name);
+      SetParam($name, $value);
+      if (($name eq "shutdownhtml") && ($value ne "")) {
+        $vars->{'shutdown_is_active'} = 1;
+      }
     }
+  }
 
-    call_param_onchange_handlers(\@changes);
+  call_param_onchange_handlers(\@changes);
 
-    $vars->{'message'} = 'parameters_updated';
-    $vars->{'param_changed'} = \@changes;
+  $vars->{'message'}       = 'parameters_updated';
+  $vars->{'param_changed'} = \@changes;
 
-    write_params();
-    delete_token($token);
+  write_params();
+  delete_token($token);
 }
 
-if ($cgi->param('save-and-test-mta-params') &&
-    $cgi->request_method() eq "POST")
+if ($cgi->param('save-and-test-mta-params') && $cgi->request_method() eq "POST")
 {
-    my $message;
-    my $mail_template = Bugzilla->template_inner($user->setting('lang'));
-    $mail_template->process('email/mta-test.txt.tmpl', undef, \$message);
-    require Bugzilla::Mailer;
+  my $message;
+  my $mail_template = Bugzilla->template_inner($user->setting('lang'));
+  $mail_template->process('email/mta-test.txt.tmpl', undef, \$message);
+  require Bugzilla::Mailer;
 
-    # This will throw all sorts of useful errors if mail is not working
-    Bugzilla::Mailer::MessageToMTA($message);
+  # This will throw all sorts of useful errors if mail is not working
+  Bugzilla::Mailer::MessageToMTA($message);
 
-    $vars->{'message'} = 'mta_param_tests_run';
+  $vars->{'message'} = 'mta_param_tests_run';
 }
 
 $vars->{'token'} = issue_session_token('edit_parameters');
 
 $template->process("admin/params/editparams.html.tmpl", $vars)
-    || ThrowTemplateError($template->error());
+  || ThrowTemplateError($template->error());
index 942e2073cf61854dd77092f139f80706ce618434..70b7250ded749584b269d3f1a64256d6fb9c323f 100755 (executable)
@@ -26,13 +26,14 @@ use Bugzilla::Token;
 # Preliminary checks:
 #
 
-my $user = Bugzilla->login(LOGIN_REQUIRED);
+my $user  = Bugzilla->login(LOGIN_REQUIRED);
 my $whoid = $user->id;
 
-my $dbh = Bugzilla->dbh;
-my $cgi = Bugzilla->cgi;
+my $dbh      = Bugzilla->dbh;
+my $cgi      = Bugzilla->cgi;
 my $template = Bugzilla->template;
-my $vars = {};
+my $vars     = {};
+
 # Remove this as soon as the documentation about products has been
 # improved and each action has its own section.
 $vars->{'doc_section'} = 'administering/categorization.html#products';
@@ -41,43 +42,42 @@ print $cgi->header();
 
 $user->in_group('editcomponents')
   || scalar(@{$user->get_products_by_permission('editcomponents')})
-  || ThrowUserError("auth_failure", {group  => "editcomponents",
-                                     action => "edit",
-                                     object => "products"});
+  || ThrowUserError("auth_failure",
+  {group => "editcomponents", action => "edit", object => "products"});
 
 #
 # often used variables
 #
 my $classification_name = trim($cgi->param('classification') || '');
-my $product_name = trim($cgi->param('product') || '');
-my $action  = trim($cgi->param('action')  || '');
-my $token = $cgi->param('token');
+my $product_name        = trim($cgi->param('product')        || '');
+my $action              = trim($cgi->param('action')         || '');
+my $token               = $cgi->param('token');
 
 #
 # product = '' -> Show nice list of classifications (if
 # classifications enabled)
 #
 
-if (Bugzilla->params->{'useclassification'} 
-    && !$classification_name
-    && !$product_name)
+if ( Bugzilla->params->{'useclassification'}
+  && !$classification_name
+  && !$product_name)
 {
-    my $class;
-    if ($user->in_group('editcomponents')) {
-        $class = [Bugzilla::Classification->get_all];
-    }
-    else {
-        # Only keep classifications containing at least one product
-        # which you can administer.
-        my $products = $user->get_products_by_permission('editcomponents');
-        my %class_ids = map { $_->classification_id => 1 } @$products;
-        $class = Bugzilla::Classification->new_from_list([keys %class_ids]);
-    }
-    $vars->{'classifications'} = $class;
-
-    $template->process("admin/products/list-classifications.html.tmpl", $vars)
-        || ThrowTemplateError($template->error());
-    exit;
+  my $class;
+  if ($user->in_group('editcomponents')) {
+    $class = [Bugzilla::Classification->get_all];
+  }
+  else {
+    # Only keep classifications containing at least one product
+    # which you can administer.
+    my $products = $user->get_products_by_permission('editcomponents');
+    my %class_ids = map { $_->classification_id => 1 } @$products;
+    $class = Bugzilla::Classification->new_from_list([keys %class_ids]);
+  }
+  $vars->{'classifications'} = $class;
+
+  $template->process("admin/products/list-classifications.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
+  exit;
 }
 
 
@@ -87,36 +87,35 @@ if (Bugzilla->params->{'useclassification'}
 #
 
 if (!$action && !$product_name) {
-    my $classification;
-    my $products;
-
+  my $classification;
+  my $products;
+
+  if (Bugzilla->params->{'useclassification'}) {
+    $classification = Bugzilla::Classification->check($classification_name);
+    $products       = $user->get_selectable_products($classification->id);
+    $vars->{'classification'} = $classification;
+  }
+  else {
+    $products = $user->get_selectable_products;
+  }
+
+  # If the user has editcomponents privs for some products only,
+  # we have to restrict the list of products to display.
+  unless ($user->in_group('editcomponents')) {
+    $products = $user->get_products_by_permission('editcomponents');
     if (Bugzilla->params->{'useclassification'}) {
-        $classification = Bugzilla::Classification->check($classification_name);
-        $products = $user->get_selectable_products($classification->id);
-        $vars->{'classification'} = $classification;
-    } else {
-        $products = $user->get_selectable_products;
-    }
-
-    # If the user has editcomponents privs for some products only,
-    # we have to restrict the list of products to display.
-    unless ($user->in_group('editcomponents')) {
-        $products = $user->get_products_by_permission('editcomponents');
-        if (Bugzilla->params->{'useclassification'}) {
-            @$products = grep {$_->classification_id == $classification->id} @$products;
-        }
+      @$products = grep { $_->classification_id == $classification->id } @$products;
     }
-    $vars->{'products'} = $products;
-    $vars->{'showbugcounts'} = $cgi->param('showbugcounts') ? 1 : 0;
+  }
+  $vars->{'products'} = $products;
+  $vars->{'showbugcounts'} = $cgi->param('showbugcounts') ? 1 : 0;
 
-    $template->process("admin/products/list.html.tmpl", $vars)
-      || ThrowTemplateError($template->error());
-    exit;
+  $template->process("admin/products/list.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
+  exit;
 }
 
 
-
-
 #
 # action='add' -> present form for parameters for new product
 #
@@ -124,23 +123,23 @@ if (!$action && !$product_name) {
 #
 
 if ($action eq 'add') {
-    # The user must have the global editcomponents privs to add
-    # new products.
-    $user->in_group('editcomponents')
-      || ThrowUserError("auth_failure", {group  => "editcomponents",
-                                         action => "add",
-                                         object => "products"});
 
-    if (Bugzilla->params->{'useclassification'}) {
-        my $classification = Bugzilla::Classification->check($classification_name);
-        $vars->{'classification'} = $classification;
-    }
-    $vars->{'token'} = issue_session_token('add_product');
+  # The user must have the global editcomponents privs to add
+  # new products.
+  $user->in_group('editcomponents')
+    || ThrowUserError("auth_failure",
+    {group => "editcomponents", action => "add", object => "products"});
 
-    $template->process("admin/products/create.html.tmpl", $vars)
-      || ThrowTemplateError($template->error());
+  if (Bugzilla->params->{'useclassification'}) {
+    my $classification = Bugzilla::Classification->check($classification_name);
+    $vars->{'classification'} = $classification;
+  }
+  $vars->{'token'} = issue_session_token('add_product');
+
+  $template->process("admin/products/create.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
 
-    exit;
+  exit;
 }
 
 
@@ -149,61 +148,62 @@ if ($action eq 'add') {
 #
 
 if ($action eq 'new') {
-    # The user must have the global editcomponents privs to add
-    # new products.
-    $user->in_group('editcomponents')
-      || ThrowUserError("auth_failure", {group  => "editcomponents",
-                                         action => "add",
-                                         object => "products"});
-
-    check_token_data($token, 'add_product');
-
-    Bugzilla::User::match_field ({
-        'initialowner'     => { 'type' => 'single' },
-        'initialqacontact' => { 'type' => 'single' },
-        'initialcc'        => { 'type' => 'multi'  },
-    });
-
-    my %product_create_params = (
-        classification   => $classification_name,
-        name             => $product_name,
-        description      => scalar $cgi->param('description'),
-        version          => scalar $cgi->param('version'),
-        defaultmilestone => scalar $cgi->param('defaultmilestone'),
-        isactive         => scalar $cgi->param('is_active'),
-        create_series    => scalar $cgi->param('createseries'),
-        allows_unconfirmed => scalar $cgi->param('allows_unconfirmed'),
-    );
-
-    $dbh->bz_start_transaction();
-    my $product = Bugzilla::Product->create(\%product_create_params);
 
-    my @initial_cc = $cgi->multi_param('initialcc');
-    my %component_create_params = (
-        product          => $product,
-        name             => trim($cgi->param('component') || ''),
-        description      => scalar $cgi->param('comp_desc'),
-        initialowner     => scalar $cgi->param('initialowner'),
-        initialqacontact => scalar $cgi->param('initialqacontact'),
-        initial_cc       => \@initial_cc,
-        create_series    => scalar $cgi->param('createseries'),
-   );
-
-    Bugzilla::Component->create(\%component_create_params);
-    $dbh->bz_commit_transaction();
-
-    delete_token($token);
-
-    $vars->{'message'} = 'product_created';
-    $vars->{'product'} = $product;
-    if (Bugzilla->params->{'useclassification'}) {
-        $vars->{'classification'} = new Bugzilla::Classification($product->classification_id);
-    }
-    $vars->{'token'} = issue_session_token('edit_product');
-
-    $template->process("admin/products/edit.html.tmpl", $vars)
-        || ThrowTemplateError($template->error());
-    exit;
+  # The user must have the global editcomponents privs to add
+  # new products.
+  $user->in_group('editcomponents')
+    || ThrowUserError("auth_failure",
+    {group => "editcomponents", action => "add", object => "products"});
+
+  check_token_data($token, 'add_product');
+
+  Bugzilla::User::match_field({
+    'initialowner'     => {'type' => 'single'},
+    'initialqacontact' => {'type' => 'single'},
+    'initialcc'        => {'type' => 'multi'},
+  });
+
+  my %product_create_params = (
+    classification     => $classification_name,
+    name               => $product_name,
+    description        => scalar $cgi->param('description'),
+    version            => scalar $cgi->param('version'),
+    defaultmilestone   => scalar $cgi->param('defaultmilestone'),
+    isactive           => scalar $cgi->param('is_active'),
+    create_series      => scalar $cgi->param('createseries'),
+    allows_unconfirmed => scalar $cgi->param('allows_unconfirmed'),
+  );
+
+  $dbh->bz_start_transaction();
+  my $product = Bugzilla::Product->create(\%product_create_params);
+
+  my @initial_cc              = $cgi->multi_param('initialcc');
+  my %component_create_params = (
+    product          => $product,
+    name             => trim($cgi->param('component') || ''),
+    description      => scalar $cgi->param('comp_desc'),
+    initialowner     => scalar $cgi->param('initialowner'),
+    initialqacontact => scalar $cgi->param('initialqacontact'),
+    initial_cc       => \@initial_cc,
+    create_series    => scalar $cgi->param('createseries'),
+  );
+
+  Bugzilla::Component->create(\%component_create_params);
+  $dbh->bz_commit_transaction();
+
+  delete_token($token);
+
+  $vars->{'message'} = 'product_created';
+  $vars->{'product'} = $product;
+  if (Bugzilla->params->{'useclassification'}) {
+    $vars->{'classification'}
+      = new Bugzilla::Classification($product->classification_id);
+  }
+  $vars->{'token'} = issue_session_token('edit_product');
+
+  $template->process("admin/products/edit.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
+  exit;
 }
 
 #
@@ -213,19 +213,20 @@ if ($action eq 'new') {
 #
 
 if ($action eq 'del') {
-    my $product = $user->check_can_admin_product($product_name);
+  my $product = $user->check_can_admin_product($product_name);
 
-    if (Bugzilla->params->{'useclassification'}) {
-        $vars->{'classification'} = new Bugzilla::Classification($product->classification_id);
-    }
-    $vars->{'product'} = $product;
-    $vars->{'token'} = issue_session_token('delete_product');
-    
-    Bugzilla::Hook::process('product_confirm_delete', { vars => $vars });
-    
-    $template->process("admin/products/confirm-delete.html.tmpl", $vars)
-        || ThrowTemplateError($template->error());
-    exit;
+  if (Bugzilla->params->{'useclassification'}) {
+    $vars->{'classification'}
+      = new Bugzilla::Classification($product->classification_id);
+  }
+  $vars->{'product'} = $product;
+  $vars->{'token'}   = issue_session_token('delete_product');
+
+  Bugzilla::Hook::process('product_confirm_delete', {vars => $vars});
+
+  $template->process("admin/products/confirm-delete.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
+  exit;
 }
 
 #
@@ -233,36 +234,40 @@ if ($action eq 'del') {
 #
 
 if ($action eq 'delete') {
-    my $product = $user->check_can_admin_product($product_name);
-    check_token_data($token, 'delete_product');
+  my $product = $user->check_can_admin_product($product_name);
+  check_token_data($token, 'delete_product');
 
-    $product->remove_from_db({ delete_series => scalar $cgi->param('delete_series')});
-    delete_token($token);
+  $product->remove_from_db(
+    {delete_series => scalar $cgi->param('delete_series')});
+  delete_token($token);
 
-    $vars->{'message'} = 'product_deleted';
-    $vars->{'product'} = $product;
-    $vars->{'no_edit_product_link'} = 1;
+  $vars->{'message'}              = 'product_deleted';
+  $vars->{'product'}              = $product;
+  $vars->{'no_edit_product_link'} = 1;
 
-    if (Bugzilla->params->{'useclassification'}) {
-        $vars->{'classifications'} = $user->in_group('editcomponents') ?
-          [Bugzilla::Classification->get_all] : $user->get_selectable_classifications;
+  if (Bugzilla->params->{'useclassification'}) {
+    $vars->{'classifications'}
+      = $user->in_group('editcomponents')
+      ? [Bugzilla::Classification->get_all]
+      : $user->get_selectable_classifications;
 
-        $template->process("admin/products/list-classifications.html.tmpl", $vars)
-          || ThrowTemplateError($template->error());
-    }
-    else {
-        my $products = $user->get_selectable_products;
-        # If the user has editcomponents privs for some products only,
-        # we have to restrict the list of products to display.
-        unless ($user->in_group('editcomponents')) {
-            $products = $user->get_products_by_permission('editcomponents');
-        }
-        $vars->{'products'} = $products;
-
-        $template->process("admin/products/list.html.tmpl", $vars)
-          || ThrowTemplateError($template->error());
+    $template->process("admin/products/list-classifications.html.tmpl", $vars)
+      || ThrowTemplateError($template->error());
+  }
+  else {
+    my $products = $user->get_selectable_products;
+
+    # If the user has editcomponents privs for some products only,
+    # we have to restrict the list of products to display.
+    unless ($user->in_group('editcomponents')) {
+      $products = $user->get_products_by_permission('editcomponents');
     }
-    exit;
+    $vars->{'products'} = $products;
+
+    $template->process("admin/products/list.html.tmpl", $vars)
+      || ThrowTemplateError($template->error());
+  }
+  exit;
 }
 
 #
@@ -273,48 +278,50 @@ if ($action eq 'delete') {
 #
 
 if ($action eq 'edit' || (!$action && $product_name)) {
-    my $product = $user->check_can_admin_product($product_name);
-
-    if (Bugzilla->params->{'useclassification'}) {
-        $vars->{'classification'} = new Bugzilla::Classification($product->classification_id);
-    }
-    $vars->{'product'} = $product;
-    $vars->{'token'} = issue_session_token('edit_product');
-
-    $template->process("admin/products/edit.html.tmpl", $vars)
-        || ThrowTemplateError($template->error());
-    exit;
+  my $product = $user->check_can_admin_product($product_name);
+
+  if (Bugzilla->params->{'useclassification'}) {
+    $vars->{'classification'}
+      = new Bugzilla::Classification($product->classification_id);
+  }
+  $vars->{'product'} = $product;
+  $vars->{'token'}   = issue_session_token('edit_product');
+
+  $template->process("admin/products/edit.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
+  exit;
 }
 
 #
 # action='update' -> update the product
 #
 if ($action eq 'update') {
-    check_token_data($token, 'edit_product');
-    my $product_old_name = trim($cgi->param('product_old_name') || '');
-    my $product = $user->check_can_admin_product($product_old_name);
-
-    $product->set_all({
-        name        => $product_name,
-        description => scalar $cgi->param('description'),
-        is_active   => scalar $cgi->param('is_active'),
-        allows_unconfirmed => scalar $cgi->param('allows_unconfirmed'),
-        default_milestone  => scalar $cgi->param('defaultmilestone'),
-    });
-
-    my $changes = $product->update();
-
-    delete_token($token);
-
-    if (Bugzilla->params->{'useclassification'}) {
-        $vars->{'classification'} = new Bugzilla::Classification($product->classification_id);
-    }
-    $vars->{'product'} = $product;
-    $vars->{'changes'} = $changes;
-
-    $template->process("admin/products/updated.html.tmpl", $vars)
-        || ThrowTemplateError($template->error());
-    exit;
+  check_token_data($token, 'edit_product');
+  my $product_old_name = trim($cgi->param('product_old_name') || '');
+  my $product = $user->check_can_admin_product($product_old_name);
+
+  $product->set_all({
+    name               => $product_name,
+    description        => scalar $cgi->param('description'),
+    is_active          => scalar $cgi->param('is_active'),
+    allows_unconfirmed => scalar $cgi->param('allows_unconfirmed'),
+    default_milestone  => scalar $cgi->param('defaultmilestone'),
+  });
+
+  my $changes = $product->update();
+
+  delete_token($token);
+
+  if (Bugzilla->params->{'useclassification'}) {
+    $vars->{'classification'}
+      = new Bugzilla::Classification($product->classification_id);
+  }
+  $vars->{'product'} = $product;
+  $vars->{'changes'} = $changes;
+
+  $template->process("admin/products/updated.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
+  exit;
 }
 
 #
@@ -322,14 +329,14 @@ if ($action eq 'update') {
 #
 
 if ($action eq 'editgroupcontrols') {
-    my $product = $user->check_can_admin_product($product_name);
+  my $product = $user->check_can_admin_product($product_name);
 
-    $vars->{'product'} = $product;
-    $vars->{'token'} = issue_session_token('edit_group_controls');
+  $vars->{'product'} = $product;
+  $vars->{'token'}   = issue_session_token('edit_group_controls');
 
-    $template->process("admin/products/groupcontrol/edit.html.tmpl", $vars)
-        || ThrowTemplateError($template->error());
-    exit;
+  $template->process("admin/products/groupcontrol/edit.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
+  exit;
 }
 
 #
@@ -337,44 +344,45 @@ if ($action eq 'editgroupcontrols') {
 #
 
 if ($action eq 'updategroupcontrols') {
-    my $product = $user->check_can_admin_product($product_name);
-    check_token_data($token, 'edit_group_controls');
-
-    my @now_na = ();
-    my @now_mandatory = ();
-    foreach my $f ($cgi->multi_param()) {
-        if ($f =~ /^membercontrol_(\d+)$/a) {
-            my $id = $1;
-            if ($cgi->param($f) == CONTROLMAPNA) {
-                push @now_na,$id;
-            } elsif ($cgi->param($f) == CONTROLMAPMANDATORY) {
-                push @now_mandatory,$id;
-            }
-        }
+  my $product = $user->check_can_admin_product($product_name);
+  check_token_data($token, 'edit_group_controls');
+
+  my @now_na        = ();
+  my @now_mandatory = ();
+  foreach my $f ($cgi->multi_param()) {
+    if ($f =~ /^membercontrol_(\d+)$/a) {
+      my $id = $1;
+      if ($cgi->param($f) == CONTROLMAPNA) {
+        push @now_na, $id;
+      }
+      elsif ($cgi->param($f) == CONTROLMAPMANDATORY) {
+        push @now_mandatory, $id;
+      }
     }
-    if (!defined $cgi->param('confirmed')) {
-        my $na_groups;
-        if (@now_na) {
-            $na_groups = $dbh->selectall_arrayref(
-                    'SELECT groups.name, COUNT(bugs.bug_id) AS count
+  }
+  if (!defined $cgi->param('confirmed')) {
+    my $na_groups;
+    if (@now_na) {
+      $na_groups = $dbh->selectall_arrayref(
+        'SELECT groups.name, COUNT(bugs.bug_id) AS count
                        FROM bugs
                  INNER JOIN bug_group_map
                          ON bug_group_map.bug_id = bugs.bug_id
                  INNER JOIN groups
                          ON bug_group_map.group_id = groups.id
                       WHERE groups.id IN (' . join(', ', @now_na) . ')
-                        AND bugs.product_id = ? ' .
-                       $dbh->sql_group_by('groups.name'),
-                   {'Slice' => {}}, $product->id);
-        }
-
-        # return the mandatory groups which need to have bug entries
-        # added to the bug_group_map and the corresponding bug count
-
-        my $mandatory_groups;
-        if (@now_mandatory) {
-            $mandatory_groups = $dbh->selectall_arrayref(
-                    'SELECT groups.name,
+                        AND bugs.product_id = ? ' . $dbh->sql_group_by('groups.name'),
+        {'Slice' => {}}, $product->id
+      );
+    }
+
+    # return the mandatory groups which need to have bug entries
+    # added to the bug_group_map and the corresponding bug count
+
+    my $mandatory_groups;
+    if (@now_mandatory) {
+      $mandatory_groups = $dbh->selectall_arrayref(
+        'SELECT groups.name,
                            (SELECT COUNT(bugs.bug_id)
                               FROM bugs
                              WHERE bugs.product_id = ?
@@ -384,46 +392,51 @@ if ($action eq 'updategroupcontrols') {
                            AS count
                       FROM groups
                      WHERE groups.id IN (' . join(', ', @now_mandatory) . ')
-                     ORDER BY groups.name',
-                   {'Slice' => {}}, $product->id);
-            # remove zero counts
-            @$mandatory_groups = grep { $_->{count} } @$mandatory_groups;
-
-        }
-        if (($na_groups && scalar(@$na_groups))
-            || ($mandatory_groups && scalar(@$mandatory_groups)))
-        {
-            $vars->{'product'} = $product;
-            $vars->{'na_groups'} = $na_groups;
-            $vars->{'mandatory_groups'} = $mandatory_groups;
-            $template->process("admin/products/groupcontrol/confirm-edit.html.tmpl", $vars)
-                || ThrowTemplateError($template->error());
-            exit;
-        }
-    }
+                     ORDER BY groups.name', {'Slice' => {}}, $product->id
+      );
+
+      # remove zero counts
+      @$mandatory_groups = grep { $_->{count} } @$mandatory_groups;
 
-    my $groups = Bugzilla::Group->match({isactive => 1, isbuggroup => 1});
-    foreach my $group (@$groups) {
-        my $group_id = $group->id;
-        $product->set_group_controls($group,
-                                     {entry          => scalar $cgi->param("entry_$group_id") || 0,
-                                      membercontrol  => scalar $cgi->param("membercontrol_$group_id") || CONTROLMAPNA,
-                                      othercontrol   => scalar $cgi->param("othercontrol_$group_id") || CONTROLMAPNA,
-                                      canedit        => scalar $cgi->param("canedit_$group_id") || 0,
-                                      editcomponents => scalar $cgi->param("editcomponents_$group_id") || 0,
-                                      editbugs       => scalar $cgi->param("editbugs_$group_id") || 0,
-                                      canconfirm     => scalar $cgi->param("canconfirm_$group_id") || 0});
     }
-    my $changes = $product->update;
+    if ( ($na_groups && scalar(@$na_groups))
+      || ($mandatory_groups && scalar(@$mandatory_groups)))
+    {
+      $vars->{'product'}          = $product;
+      $vars->{'na_groups'}        = $na_groups;
+      $vars->{'mandatory_groups'} = $mandatory_groups;
+      $template->process("admin/products/groupcontrol/confirm-edit.html.tmpl", $vars)
+        || ThrowTemplateError($template->error());
+      exit;
+    }
+  }
+
+  my $groups = Bugzilla::Group->match({isactive => 1, isbuggroup => 1});
+  foreach my $group (@$groups) {
+    my $group_id = $group->id;
+    $product->set_group_controls(
+      $group,
+      {
+        entry         => scalar $cgi->param("entry_$group_id")         || 0,
+        membercontrol => scalar $cgi->param("membercontrol_$group_id") || CONTROLMAPNA,
+        othercontrol  => scalar $cgi->param("othercontrol_$group_id")  || CONTROLMAPNA,
+        canedit       => scalar $cgi->param("canedit_$group_id")       || 0,
+        editcomponents => scalar $cgi->param("editcomponents_$group_id") || 0,
+        editbugs       => scalar $cgi->param("editbugs_$group_id")       || 0,
+        canconfirm     => scalar $cgi->param("canconfirm_$group_id")     || 0
+      }
+    );
+  }
+  my $changes = $product->update;
 
-    delete_token($token);
+  delete_token($token);
 
-    $vars->{'product'} = $product;
-    $vars->{'changes'} = $changes;
+  $vars->{'product'} = $product;
+  $vars->{'changes'} = $changes;
 
-    $template->process("admin/products/groupcontrol/updated.html.tmpl", $vars)
-        || ThrowTemplateError($template->error());
-    exit;
+  $template->process("admin/products/groupcontrol/updated.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
+  exit;
 }
 
 # No valid action found
index 5a30e68c9e3f83da1f7623448218f441c30b83a8..95a14b778ddd787b900e0233ebb9fd9464c64d0b 100755 (executable)
@@ -20,48 +20,47 @@ use Bugzilla::User::Setting;
 use Bugzilla::Token;
 
 my $template = Bugzilla->template;
-my $user = Bugzilla->login(LOGIN_REQUIRED);
-my $cgi = Bugzilla->cgi;
-my $vars = {};
+my $user     = Bugzilla->login(LOGIN_REQUIRED);
+my $cgi      = Bugzilla->cgi;
+my $vars     = {};
 
 print $cgi->header;
 
 $user->in_group('tweakparams')
-  || ThrowUserError("auth_failure", {group  => "tweakparams",
-                                     action => "modify",
-                                     object => "settings"});
+  || ThrowUserError("auth_failure",
+  {group => "tweakparams", action => "modify", object => "settings"});
 
 my $action = trim($cgi->param('action') || '');
 my $token = $cgi->param('token');
 
 if ($action eq 'update') {
-    check_token_data($token, 'edit_settings');
-    my $settings = Bugzilla::User::Setting::get_defaults();
-    my $changed = 0;
+  check_token_data($token, 'edit_settings');
+  my $settings = Bugzilla::User::Setting::get_defaults();
+  my $changed  = 0;
 
-    foreach my $name (keys %$settings) {
-        my $old_enabled = $settings->{$name}->{'is_enabled'};
-        my $old_value = $settings->{$name}->{'default_value'};
-        my $enabled = defined $cgi->param("${name}-enabled") || 0;
-        my $value = $cgi->param("${name}");
-        my $setting = new Bugzilla::User::Setting($name);
+  foreach my $name (keys %$settings) {
+    my $old_enabled = $settings->{$name}->{'is_enabled'};
+    my $old_value   = $settings->{$name}->{'default_value'};
+    my $enabled     = defined $cgi->param("${name}-enabled") || 0;
+    my $value       = $cgi->param("${name}");
+    my $setting     = new Bugzilla::User::Setting($name);
 
-        $setting->validate_value($value);
+    $setting->validate_value($value);
 
-        if ($old_enabled != $enabled || $old_value ne $value) {
-            Bugzilla::User::Setting::set_default($name, $value, $enabled);
-            $changed = 1;
-        }
+    if ($old_enabled != $enabled || $old_value ne $value) {
+      Bugzilla::User::Setting::set_default($name, $value, $enabled);
+      $changed = 1;
     }
-    $vars->{'message'} = 'default_settings_updated';
-    $vars->{'changes_saved'} = $changed;
-    Bugzilla->memcached->clear_config();
-    delete_token($token);
+  }
+  $vars->{'message'}       = 'default_settings_updated';
+  $vars->{'changes_saved'} = $changed;
+  Bugzilla->memcached->clear_config();
+  delete_token($token);
 }
 
 # Don't use $settings as defaults may have changed.
 $vars->{'settings'} = Bugzilla::User::Setting::get_defaults();
-$vars->{'token'} = issue_session_token('edit_settings');
+$vars->{'token'}    = issue_session_token('edit_settings');
 
 $template->process("admin/settings/edit.html.tmpl", $vars)
   || ThrowTemplateError($template->error());
index 13f7a963c7980b1d44ef81c392f5ba3ce1a43f30..c37eb56111b561e50595a46fe14deaa98bbfa031 100755 (executable)
@@ -32,15 +32,18 @@ my $template  = Bugzilla->template;
 my $dbh       = Bugzilla->dbh;
 my $userid    = $user->id;
 my $editusers = $user->in_group('editusers');
-local our $vars     = {};
+local our $vars = {};
 
 # Reject access if there is no sense in continuing.
-$editusers
-    || $user->can_bless()
-    || ThrowUserError("auth_failure", {group  => "editusers",
-                                       reason => "cant_bless",
-                                       action => "edit",
-                                       object => "users"});
+$editusers || $user->can_bless() || ThrowUserError(
+  "auth_failure",
+  {
+    group  => "editusers",
+    reason => "cant_bless",
+    action => "edit",
+    object => "users"
+  }
+);
 
 print $cgi->header();
 
@@ -55,564 +58,594 @@ $vars->{'editusers'} = $editusers;
 mirrorListSelectionValues();
 
 Bugzilla::Hook::process('admin_editusers_action',
-    { vars => $vars, user => $user, action => $action });
+  {vars => $vars, user => $user, action => $action});
 
 ###########################################################################
 if ($action eq 'search') {
-    # Allow to restrict the search to any group the user is allowed to bless.
-    $vars->{'restrictablegroups'} = $user->bless_groups();
-    $template->process('admin/users/search.html.tmpl', $vars)
-       || ThrowTemplateError($template->error());
 
-###########################################################################
-} elsif ($action eq 'list') {
-    my $matchvalue    = $cgi->param('matchvalue') || '';
-    my $matchstr      = trim(scalar $cgi->param('matchstr'));
-    my $matchtype     = $cgi->param('matchtype');
-    my $grouprestrict = $cgi->param('grouprestrict') || '0';
-    # 0 = disabled only, 1 = enabled only, 2 = everyone
-    my $is_enabled    = $cgi->param('is_enabled') // 2;
-    my $query = 'SELECT DISTINCT userid, login_name, email, realname, is_enabled, ' .
-                $dbh->sql_date_format('last_seen_date', '%Y-%m-%d') . ' AS last_seen_date ' .
-                'FROM profiles';
-    my @bindValues;
-    my $nextCondition;
-    my $visibleGroups;
-
-    # If a group ID is given, make sure it is a valid one.
-    my $group;
-    if ($grouprestrict) {
-        $group = new Bugzilla::Group(scalar $cgi->param('groupid'));
-        $group || ThrowUserError('invalid_group_ID');
-    }
-
-    if (!$editusers && Bugzilla->params->{'usevisibilitygroups'}) {
-        # Show only users in visible groups.
-        $visibleGroups = $user->visible_groups_as_string();
+  # Allow to restrict the search to any group the user is allowed to bless.
+  $vars->{'restrictablegroups'} = $user->bless_groups();
+  $template->process('admin/users/search.html.tmpl', $vars)
+    || ThrowTemplateError($template->error());
 
-        if ($visibleGroups) {
-            $query .= qq{, user_group_map AS ugm
+###########################################################################
+}
+elsif ($action eq 'list') {
+  my $matchvalue    = $cgi->param('matchvalue') || '';
+  my $matchstr      = trim(scalar $cgi->param('matchstr'));
+  my $matchtype     = $cgi->param('matchtype');
+  my $grouprestrict = $cgi->param('grouprestrict') || '0';
+
+  # 0 = disabled only, 1 = enabled only, 2 = everyone
+  my $is_enabled = $cgi->param('is_enabled') // 2;
+  my $query
+    = 'SELECT DISTINCT userid, login_name, email, realname, is_enabled, '
+    . $dbh->sql_date_format('last_seen_date', '%Y-%m-%d')
+    . ' AS last_seen_date '
+    . 'FROM profiles';
+  my @bindValues;
+  my $nextCondition;
+  my $visibleGroups;
+
+  # If a group ID is given, make sure it is a valid one.
+  my $group;
+  if ($grouprestrict) {
+    $group = new Bugzilla::Group(scalar $cgi->param('groupid'));
+    $group || ThrowUserError('invalid_group_ID');
+  }
+
+  if (!$editusers && Bugzilla->params->{'usevisibilitygroups'}) {
+
+    # Show only users in visible groups.
+    $visibleGroups = $user->visible_groups_as_string();
+
+    if ($visibleGroups) {
+      $query .= qq{, user_group_map AS ugm
                          WHERE ugm.user_id = profiles.userid
                            AND ugm.isbless = 0
                            AND ugm.group_id IN ($visibleGroups)
                         };
-            $nextCondition = 'AND';
-        }
+      $nextCondition = 'AND';
     }
-    else {
-        $visibleGroups = 1;
-        if ($grouprestrict eq '1') {
-            $query .= qq{, user_group_map AS ugm
+  }
+  else {
+    $visibleGroups = 1;
+    if ($grouprestrict eq '1') {
+      $query .= qq{, user_group_map AS ugm
                          WHERE ugm.user_id = profiles.userid
                            AND ugm.isbless = 0
                         };
-            $nextCondition = 'AND';
-        }
-        else {
-            $nextCondition = 'WHERE';
+      $nextCondition = 'AND';
+    }
+    else {
+      $nextCondition = 'WHERE';
+    }
+  }
+
+  if (!$visibleGroups) {
+    $vars->{'users'} = {};
+  }
+  else {
+    # Handle selection by login name, email, real name, or userid.
+    if (defined($matchtype)) {
+      $query .= " $nextCondition ";
+      my $expr = '';
+      if ($matchvalue eq 'userid') {
+        if ($matchstr) {
+          my $stored_matchstr = $matchstr;
+          detaint_natural($matchstr)
+            || ThrowUserError('illegal_user_id', {userid => $stored_matchstr});
         }
+        $expr = "profiles.userid";
+      }
+      elsif ($matchvalue eq 'email') {
+        $expr = 'profiles.email';
+      }
+      elsif ($matchvalue eq 'realname') {
+        $expr = "profiles.realname";
+      }
+      elsif ($matchvalue eq 'extern_id') {
+        $expr = "profiles.extern_id";
+      }
+      else {
+        $expr = "profiles.login_name";
+      }
+
+      if ($matchtype =~ /^(regexp|notregexp|exact)$/) {
+        $matchstr ||= '.';
+      }
+      else {
+        $matchstr = '' unless defined $matchstr;
+      }
+
+      # We can trick_taint because we use the value in a SELECT only,
+      # using a placeholder.
+      trick_taint($matchstr);
+
+      if ($matchtype eq 'regexp') {
+        $query .= $dbh->sql_regexp($expr, '?', 0, $dbh->quote($matchstr));
+      }
+      elsif ($matchtype eq 'notregexp') {
+        $query .= $dbh->sql_not_regexp($expr, '?', 0, $dbh->quote($matchstr));
+      }
+      elsif ($matchtype eq 'exact') {
+        $query .= $expr . ' = ?';
+      }
+      else {    # substr or unknown
+        $query .= $dbh->sql_iposition('?', $expr) . ' > 0';
+      }
+      $nextCondition = 'AND';
+      push(@bindValues, $matchstr);
     }
 
-    if (!$visibleGroups) {
-        $vars->{'users'} = {};
+    # Handle selection by group.
+    if ($grouprestrict eq '1') {
+      my $grouplist
+        = join(',', @{Bugzilla::Group->flatten_group_membership($group->id)});
+      $query .= " $nextCondition ugm.group_id IN($grouplist) ";
     }
-    else {
-        # Handle selection by login name, email, real name, or userid.
-        if (defined($matchtype)) {
-            $query .= " $nextCondition ";
-            my $expr = '';
-            if ($matchvalue eq 'userid') {
-                if ($matchstr) {
-                    my $stored_matchstr = $matchstr;
-                    detaint_natural($matchstr) 
-                        || ThrowUserError('illegal_user_id', {userid => $stored_matchstr});
-                }
-                $expr = "profiles.userid";
-            } elsif ($matchvalue eq 'email') {
-                $expr = 'profiles.email';
-            } elsif ($matchvalue eq 'realname') {
-                $expr = "profiles.realname";
-            } elsif ($matchvalue eq 'extern_id') {
-                $expr = "profiles.extern_id";
-            } else {
-                $expr = "profiles.login_name";
-            }
-
-            if ($matchtype =~ /^(regexp|notregexp|exact)$/) {
-                $matchstr ||= '.';
-            }
-            else {
-                $matchstr = '' unless defined $matchstr;
-            }
-            # We can trick_taint because we use the value in a SELECT only,
-            # using a placeholder.
-            trick_taint($matchstr);
-
-            if ($matchtype eq 'regexp') {
-                $query .= $dbh->sql_regexp($expr, '?', 0, $dbh->quote($matchstr));
-            } elsif ($matchtype eq 'notregexp') {
-                $query .= $dbh->sql_not_regexp($expr, '?', 0, $dbh->quote($matchstr));
-            } elsif ($matchtype eq 'exact') {
-                $query .= $expr . ' = ?';
-            } else { # substr or unknown
-                $query .= $dbh->sql_iposition('?', $expr) . ' > 0';
-            }
-            $nextCondition = 'AND';
-            push(@bindValues, $matchstr);
-        }
 
-        # Handle selection by group.
-        if ($grouprestrict eq '1') {
-            my $grouplist = join(',',
-                @{Bugzilla::Group->flatten_group_membership($group->id)});
-            $query .= " $nextCondition ugm.group_id IN($grouplist) ";
-        }
+    detaint_natural($is_enabled);
+    if ($is_enabled && ($is_enabled == 0 || $is_enabled == 1)) {
+      $query .= " $nextCondition profiles.is_enabled = ?";
+      $nextCondition = 'AND';
+      push(@bindValues, $is_enabled);
+    }
 
-        detaint_natural($is_enabled);
-        if ($is_enabled && ($is_enabled == 0 || $is_enabled == 1)) {
-            $query .= " $nextCondition profiles.is_enabled = ?";
-            $nextCondition = 'AND';
-            push(@bindValues, $is_enabled);
-        }
+    $query .= ' ORDER BY profiles.login_name';
 
-        $query .= ' ORDER BY profiles.login_name';
+    $vars->{'users'}
+      = $dbh->selectall_arrayref($query, {'Slice' => {}}, @bindValues);
 
-        $vars->{'users'} = $dbh->selectall_arrayref($query,
-                                                    {'Slice' => {}},
-                                                    @bindValues);
+  }
 
-    }
-
-    if ($matchtype && $matchtype eq 'exact' && scalar(@{$vars->{'users'}}) == 1) {
-        my $match_user_id = $vars->{'users'}[0]->{'userid'};
-        my $match_user = check_user($match_user_id);
-        edit_processing($match_user);
-    }
-    else {
-        $template->process('admin/users/list.html.tmpl', $vars)
-            || ThrowTemplateError($template->error());
-    }
+  if ($matchtype && $matchtype eq 'exact' && scalar(@{$vars->{'users'}}) == 1) {
+    my $match_user_id = $vars->{'users'}[0]->{'userid'};
+    my $match_user    = check_user($match_user_id);
+    edit_processing($match_user);
+  }
+  else {
+    $template->process('admin/users/list.html.tmpl', $vars)
+      || ThrowTemplateError($template->error());
+  }
 
 ###########################################################################
-} elsif ($action eq 'add') {
-    $editusers || ThrowUserError("auth_failure", {group  => "editusers",
-                                                  action => "add",
-                                                  object => "users"});
+}
+elsif ($action eq 'add') {
+  $editusers
+    || ThrowUserError("auth_failure",
+    {group => "editusers", action => "add", object => "users"});
 
-    $vars->{'token'} = issue_session_token('add_user');
+  $vars->{'token'} = issue_session_token('add_user');
 
-    $template->process('admin/users/create.html.tmpl', $vars)
-       || ThrowTemplateError($template->error());
+  $template->process('admin/users/create.html.tmpl', $vars)
+    || ThrowTemplateError($template->error());
 
 ###########################################################################
-} elsif ($action eq 'new') {
-    $editusers || ThrowUserError("auth_failure", {group  => "editusers",
-                                                  action => "add",
-                                                  object => "users"});
-
-    check_token_data($token, 'add_user');
-
-    # When e.g. the 'Env' auth method is used, the password field
-    # is not displayed. In that case, set the password to *.
-    my $password = $cgi->param('password');
-    $password = '*' if !defined $password;
-
-    my $login_name = $cgi->param('login');
-    my $email      = $cgi->param('email');
-
-    $login_name = $email if Bugzilla->params->{use_email_as_login};
-
-    my $new_user = Bugzilla::User->create({
-        login_name    => $login_name,
-        email         => $email,
-        cryptpassword => $password,
-        realname      => scalar $cgi->param('name'),
-        disabledtext  => scalar $cgi->param('disabledtext'),
-        disable_mail  => scalar $cgi->param('disable_mail'),
-        extern_id     => scalar $cgi->param('extern_id'),
-        });
-
-    userDataToVars($new_user->id);
-
-    delete_token($token);
-
-    if ($cgi->param('notify_user')) {
-        $vars->{'new_user'} = $new_user;
-        my $message;
-      
-        $template->process('email/new-user-details.txt.tmpl', $vars, \$message)
-            || ThrowTemplateError($template->error());
-        MessageToMTA($message);
-    }
+}
+elsif ($action eq 'new') {
+  $editusers
+    || ThrowUserError("auth_failure",
+    {group => "editusers", action => "add", object => "users"});
 
-    # We already display the updated page. We have to recreate a token now.
-    $vars->{'token'} = issue_session_token('edit_user');
-    $vars->{'message'} = 'account_created';
-    $template->process('admin/users/edit.html.tmpl', $vars)
-       || ThrowTemplateError($template->error());
+  check_token_data($token, 'add_user');
 
-###########################################################################
-} elsif ($action eq 'edit') {
-    my $otherUser = check_user($otherUserID, $otherUserLogin);
-    edit_processing($otherUser);
+  # When e.g. the 'Env' auth method is used, the password field
+  # is not displayed. In that case, set the password to *.
+  my $password = $cgi->param('password');
+  $password = '*' if !defined $password;
 
-###########################################################################
-} elsif ($action eq 'update') {
-    check_token_data($token, 'edit_user');
-    my $otherUser = check_user($otherUserID, $otherUserLogin);
-    $otherUserID = $otherUser->id;
-
-    # Lock tables during the check+update session.
-    $dbh->bz_start_transaction();
-
-    $editusers || $user->can_see_user($otherUser)
-        || ThrowUserError('auth_failure', {reason => "not_visible",
-                                           action => "modify",
-                                           object => "user"});
-
-    $vars->{'loginold'} = $otherUser->login;
-
-    # Update groups
-    my @group_ids = grep { s/group_// } keys %{ Bugzilla->cgi->Vars };
-    $otherUser->set_groups({ set => \@group_ids });
-
-    # Update profiles table entry; silently skip doing this if the user
-    # is not authorized.
-    my $changes = {};
-    if ($editusers) {
-        $otherUser->set_login(scalar $cgi->param('login'))
-            unless Bugzilla->params->{use_email_as_login};
-        $otherUser->set_email(scalar $cgi->param('email'));
-        $otherUser->set_name(scalar $cgi->param('name'));
-        $otherUser->set_password(scalar $cgi->param('password'))
-            if $cgi->param('password');
-        $otherUser->set_disabledtext(scalar $cgi->param('disabledtext'));
-        $otherUser->set_disable_mail(scalar $cgi->param('disable_mail'));
-        $otherUser->set_extern_id(scalar $cgi->param('extern_id'))
-            if defined $cgi->param('extern_id');
-
-        # Update bless groups
-        my @bless_ids = grep { s/bless_// } keys %{ Bugzilla->cgi->Vars };
-        $otherUser->set_bless_groups({ set => \@bless_ids });
-    }
-    $changes = $otherUser->update();
+  my $login_name = $cgi->param('login');
+  my $email      = $cgi->param('email');
 
-    $dbh->bz_commit_transaction();
+  $login_name = $email if Bugzilla->params->{use_email_as_login};
 
-    # XXX: userDataToVars may be off when editing ourselves.
-    userDataToVars($otherUserID);
-    delete_token($token);
+  my $new_user = Bugzilla::User->create({
+    login_name    => $login_name,
+    email         => $email,
+    cryptpassword => $password,
+    realname      => scalar $cgi->param('name'),
+    disabledtext  => scalar $cgi->param('disabledtext'),
+    disable_mail  => scalar $cgi->param('disable_mail'),
+    extern_id     => scalar $cgi->param('extern_id'),
+  });
 
-    $vars->{'message'} = 'account_updated';
-    $vars->{'changes'} = \%$changes;
-    # We already display the updated page. We have to recreate a token now.
-    $vars->{'token'} = issue_session_token('edit_user');
+  userDataToVars($new_user->id);
 
-    $template->process('admin/users/edit.html.tmpl', $vars)
-       || ThrowTemplateError($template->error());
+  delete_token($token);
+
+  if ($cgi->param('notify_user')) {
+    $vars->{'new_user'} = $new_user;
+    my $message;
+
+    $template->process('email/new-user-details.txt.tmpl', $vars, \$message)
+      || ThrowTemplateError($template->error());
+    MessageToMTA($message);
+  }
+
+  # We already display the updated page. We have to recreate a token now.
+  $vars->{'token'}   = issue_session_token('edit_user');
+  $vars->{'message'} = 'account_created';
+  $template->process('admin/users/edit.html.tmpl', $vars)
+    || ThrowTemplateError($template->error());
+
+###########################################################################
+}
+elsif ($action eq 'edit') {
+  my $otherUser = check_user($otherUserID, $otherUserLogin);
+  edit_processing($otherUser);
 
 ###########################################################################
-} elsif ($action eq 'del') {
-    my $otherUser = check_user($otherUserID, $otherUserLogin);
-    $otherUserID = $otherUser->id;
-
-    Bugzilla->params->{'allowuserdeletion'} 
-        || ThrowUserError('users_deletion_disabled');
-    $editusers || ThrowUserError('auth_failure', {group  => "editusers",
-                                                  action => "delete",
-                                                  object => "users"});
-    $vars->{'otheruser'}      = $otherUser;
-
-    # Find other cross references.
-    $vars->{'attachments'} = $dbh->selectrow_array(
-        'SELECT COUNT(*) FROM attachments WHERE submitter_id = ?',
-        undef, $otherUserID);
-    $vars->{'assignee_or_qa'} = $dbh->selectrow_array(
-        qq{SELECT COUNT(*)
+}
+elsif ($action eq 'update') {
+  check_token_data($token, 'edit_user');
+  my $otherUser = check_user($otherUserID, $otherUserLogin);
+  $otherUserID = $otherUser->id;
+
+  # Lock tables during the check+update session.
+  $dbh->bz_start_transaction();
+
+  $editusers
+    || $user->can_see_user($otherUser)
+    || ThrowUserError('auth_failure',
+    {reason => "not_visible", action => "modify", object => "user"});
+
+  $vars->{'loginold'} = $otherUser->login;
+
+  # Update groups
+  my @group_ids = grep {s/group_//} keys %{Bugzilla->cgi->Vars};
+  $otherUser->set_groups({set => \@group_ids});
+
+  # Update profiles table entry; silently skip doing this if the user
+  # is not authorized.
+  my $changes = {};
+  if ($editusers) {
+    $otherUser->set_login(scalar $cgi->param('login'))
+      unless Bugzilla->params->{use_email_as_login};
+    $otherUser->set_email(scalar $cgi->param('email'));
+    $otherUser->set_name(scalar $cgi->param('name'));
+    $otherUser->set_password(scalar $cgi->param('password'))
+      if $cgi->param('password');
+    $otherUser->set_disabledtext(scalar $cgi->param('disabledtext'));
+    $otherUser->set_disable_mail(scalar $cgi->param('disable_mail'));
+    $otherUser->set_extern_id(scalar $cgi->param('extern_id'))
+      if defined $cgi->param('extern_id');
+
+    # Update bless groups
+    my @bless_ids = grep {s/bless_//} keys %{Bugzilla->cgi->Vars};
+    $otherUser->set_bless_groups({set => \@bless_ids});
+  }
+  $changes = $otherUser->update();
+
+  $dbh->bz_commit_transaction();
+
+  # XXX: userDataToVars may be off when editing ourselves.
+  userDataToVars($otherUserID);
+  delete_token($token);
+
+  $vars->{'message'} = 'account_updated';
+  $vars->{'changes'} = \%$changes;
+
+  # We already display the updated page. We have to recreate a token now.
+  $vars->{'token'} = issue_session_token('edit_user');
+
+  $template->process('admin/users/edit.html.tmpl', $vars)
+    || ThrowTemplateError($template->error());
+
+###########################################################################
+}
+elsif ($action eq 'del') {
+  my $otherUser = check_user($otherUserID, $otherUserLogin);
+  $otherUserID = $otherUser->id;
+
+  Bugzilla->params->{'allowuserdeletion'}
+    || ThrowUserError('users_deletion_disabled');
+  $editusers
+    || ThrowUserError('auth_failure',
+    {group => "editusers", action => "delete", object => "users"});
+  $vars->{'otheruser'} = $otherUser;
+
+  # Find other cross references.
+  $vars->{'attachments'}
+    = $dbh->selectrow_array(
+    'SELECT COUNT(*) FROM attachments WHERE submitter_id = ?',
+    undef, $otherUserID);
+  $vars->{'assignee_or_qa'} = $dbh->selectrow_array(
+    qq{SELECT COUNT(*)
            FROM bugs
-           WHERE assigned_to = ? OR qa_contact = ?},
-        undef, ($otherUserID, $otherUserID));
-    $vars->{'reporter'} = $dbh->selectrow_array(
-        'SELECT COUNT(*) FROM bugs WHERE reporter = ?',
-        undef, $otherUserID);
-    $vars->{'cc'} = $dbh->selectrow_array(
-        'SELECT COUNT(*) FROM cc WHERE who = ?',
-        undef, $otherUserID);
-    $vars->{'bugs_activity'} = $dbh->selectrow_array(
-        'SELECT COUNT(*) FROM bugs_activity WHERE who = ?',
-        undef, $otherUserID);
-    $vars->{'component_cc'} = $dbh->selectrow_array(
-        'SELECT COUNT(*) FROM component_cc WHERE user_id = ?',
-        undef, $otherUserID);
-    $vars->{'email_setting'} = $dbh->selectrow_array(
-        'SELECT COUNT(*) FROM email_setting WHERE user_id = ?',
-        undef, $otherUserID);
-    $vars->{'flags'}{'requestee'} = $dbh->selectrow_array(
-        'SELECT COUNT(*) FROM flags WHERE requestee_id = ?',
-        undef, $otherUserID);
-    $vars->{'flags'}{'setter'} = $dbh->selectrow_array(
-        'SELECT COUNT(*) FROM flags WHERE setter_id = ?',
-        undef, $otherUserID);
-    $vars->{'longdescs'} = $dbh->selectrow_array(
-        'SELECT COUNT(*) FROM longdescs WHERE who = ?',
-        undef, $otherUserID);
-    my $namedquery_ids = $dbh->selectcol_arrayref(
-        'SELECT id FROM namedqueries WHERE userid = ?',
-        undef, $otherUserID);
-    $vars->{'namedqueries'} = scalar(@$namedquery_ids);
-    if (scalar(@$namedquery_ids)) {
-        $vars->{'namedquery_group_map'} = $dbh->selectrow_array(
-            'SELECT COUNT(*) FROM namedquery_group_map WHERE namedquery_id IN' .
-            ' (' . join(', ', @$namedquery_ids) . ')');
-    }
-    else {
-        $vars->{'namedquery_group_map'} = 0;
-    }
-    $vars->{'profile_setting'} = $dbh->selectrow_array(
-        'SELECT COUNT(*) FROM profile_setting WHERE user_id = ?',
-        undef, $otherUserID);
-    $vars->{'profiles_activity'} = $dbh->selectrow_array(
-        'SELECT COUNT(*) FROM profiles_activity WHERE who = ? AND userid != ?',
-        undef, ($otherUserID, $otherUserID));
-    $vars->{'quips'} = $dbh->selectrow_array(
-        'SELECT COUNT(*) FROM quips WHERE userid = ?',
-        undef, $otherUserID);
-    $vars->{'series'} = $dbh->selectrow_array(
-        'SELECT COUNT(*) FROM series WHERE creator = ?',
-        undef, $otherUserID);
-    $vars->{'watch'}{'watched'} = $dbh->selectrow_array(
-        'SELECT COUNT(*) FROM watch WHERE watched = ?',
-        undef, $otherUserID);
-    $vars->{'watch'}{'watcher'} = $dbh->selectrow_array(
-        'SELECT COUNT(*) FROM watch WHERE watcher = ?',
-        undef, $otherUserID);
-    $vars->{'whine_events'} = $dbh->selectrow_array(
-        'SELECT COUNT(*) FROM whine_events WHERE owner_userid = ?',
-        undef, $otherUserID);
-    $vars->{'whine_schedules'} = $dbh->selectrow_array(
-        qq{SELECT COUNT(distinct eventid)
+           WHERE assigned_to = ? OR qa_contact = ?}, undef,
+    ($otherUserID, $otherUserID)
+  );
+  $vars->{'reporter'}
+    = $dbh->selectrow_array('SELECT COUNT(*) FROM bugs WHERE reporter = ?',
+    undef, $otherUserID);
+  $vars->{'cc'} = $dbh->selectrow_array('SELECT COUNT(*) FROM cc WHERE who = ?',
+    undef, $otherUserID);
+  $vars->{'bugs_activity'}
+    = $dbh->selectrow_array('SELECT COUNT(*) FROM bugs_activity WHERE who = ?',
+    undef, $otherUserID);
+  $vars->{'component_cc'}
+    = $dbh->selectrow_array('SELECT COUNT(*) FROM component_cc WHERE user_id = ?',
+    undef, $otherUserID);
+  $vars->{'email_setting'}
+    = $dbh->selectrow_array(
+    'SELECT COUNT(*) FROM email_setting WHERE user_id = ?',
+    undef, $otherUserID);
+  $vars->{'flags'}{'requestee'}
+    = $dbh->selectrow_array('SELECT COUNT(*) FROM flags WHERE requestee_id = ?',
+    undef, $otherUserID);
+  $vars->{'flags'}{'setter'}
+    = $dbh->selectrow_array('SELECT COUNT(*) FROM flags WHERE setter_id = ?',
+    undef, $otherUserID);
+  $vars->{'longdescs'}
+    = $dbh->selectrow_array('SELECT COUNT(*) FROM longdescs WHERE who = ?',
+    undef, $otherUserID);
+  my $namedquery_ids
+    = $dbh->selectcol_arrayref('SELECT id FROM namedqueries WHERE userid = ?',
+    undef, $otherUserID);
+  $vars->{'namedqueries'} = scalar(@$namedquery_ids);
+
+  if (scalar(@$namedquery_ids)) {
+    $vars->{'namedquery_group_map'}
+      = $dbh->selectrow_array(
+          'SELECT COUNT(*) FROM namedquery_group_map WHERE namedquery_id IN' . ' ('
+        . join(', ', @$namedquery_ids)
+        . ')');
+  }
+  else {
+    $vars->{'namedquery_group_map'} = 0;
+  }
+  $vars->{'profile_setting'}
+    = $dbh->selectrow_array(
+    'SELECT COUNT(*) FROM profile_setting WHERE user_id = ?',
+    undef, $otherUserID);
+  $vars->{'profiles_activity'}
+    = $dbh->selectrow_array(
+    'SELECT COUNT(*) FROM profiles_activity WHERE who = ? AND userid != ?',
+    undef, ($otherUserID, $otherUserID));
+  $vars->{'quips'}
+    = $dbh->selectrow_array('SELECT COUNT(*) FROM quips WHERE userid = ?',
+    undef, $otherUserID);
+  $vars->{'series'}
+    = $dbh->selectrow_array('SELECT COUNT(*) FROM series WHERE creator = ?',
+    undef, $otherUserID);
+  $vars->{'watch'}{'watched'}
+    = $dbh->selectrow_array('SELECT COUNT(*) FROM watch WHERE watched = ?',
+    undef, $otherUserID);
+  $vars->{'watch'}{'watcher'}
+    = $dbh->selectrow_array('SELECT COUNT(*) FROM watch WHERE watcher = ?',
+    undef, $otherUserID);
+  $vars->{'whine_events'}
+    = $dbh->selectrow_array(
+    'SELECT COUNT(*) FROM whine_events WHERE owner_userid = ?',
+    undef, $otherUserID);
+  $vars->{'whine_schedules'} = $dbh->selectrow_array(
+    qq{SELECT COUNT(distinct eventid)
            FROM whine_schedules
            WHERE mailto = ?
            AND mailto_type = ?
-          },
-        undef, ($otherUserID, MAILTO_USER));
-    $vars->{'token'} = issue_session_token('delete_user');
+          }, undef, ($otherUserID, MAILTO_USER)
+  );
+  $vars->{'token'} = issue_session_token('delete_user');
 
-    $template->process('admin/users/confirm-delete.html.tmpl', $vars)
-       || ThrowTemplateError($template->error());
+  $template->process('admin/users/confirm-delete.html.tmpl', $vars)
+    || ThrowTemplateError($template->error());
 
 ###########################################################################
-} elsif ($action eq 'delete') {
-    check_token_data($token, 'delete_user');
-    my $otherUser = check_user($otherUserID, $otherUserLogin);
-    $otherUserID = $otherUser->id;
-
-    # Cache for user accounts.
-    my %usercache = (0 => new Bugzilla::User());
-    my %updatedbugs;
-
-    # Lock tables during the check+removal session.
-    # XXX: if there was some change on these tables after the deletion
-    #      confirmation checks, we may do something here we haven't warned
-    #      about.
-    $dbh->bz_start_transaction();
-
-    Bugzilla->params->{'allowuserdeletion'}
-        || ThrowUserError('users_deletion_disabled');
-    $editusers || ThrowUserError('auth_failure',
-                                 {group  => "editusers",
-                                  action => "delete",
-                                  object => "users"});
-    @{$otherUser->product_responsibilities()}
-        && ThrowUserError('user_has_responsibility');
-
-    Bugzilla->logout_user($otherUser);
-
-    # Get the named query list so we can delete namedquery_group_map entries.
-    my $namedqueries_as_string = join(', ', @{$dbh->selectcol_arrayref(
-        'SELECT id FROM namedqueries WHERE userid = ?', undef, $otherUserID)});
-
-    # Get the timestamp for LogActivityEntry.
-    my $timestamp = $dbh->selectrow_array('SELECT NOW()');
-
-    # When we update a bug_activity entry, we update the bug timestamp, too.
-    my $sth_set_bug_timestamp =
-        $dbh->prepare('UPDATE bugs SET delta_ts = ? WHERE bug_id = ?');
-
-    # Flags
-    my $flag_ids =
-      $dbh->selectcol_arrayref('SELECT id FROM flags WHERE requestee_id = ?',
-                                undef, $otherUserID);
-
-    my $flags = Bugzilla::Flag->new_from_list($flag_ids);
-
-    $dbh->do('UPDATE flags SET requestee_id = NULL, modification_date = ?
-              WHERE requestee_id = ?', undef, ($timestamp, $otherUserID));
-
-    # We want to remove the requestee but leave the requester alone,
-    # so we have to log these changes manually.
-    my %bugs;
-    push(@{$bugs{$_->bug_id}->{$_->attach_id || 0}}, $_) foreach @$flags;
-    foreach my $bug_id (keys %bugs) {
-        foreach my $attach_id (keys %{$bugs{$bug_id}}) {
-            my @old_summaries = Bugzilla::Flag->snapshot($bugs{$bug_id}->{$attach_id});
-            $_->_set_requestee() foreach @{$bugs{$bug_id}->{$attach_id}};
-            my @new_summaries = Bugzilla::Flag->snapshot($bugs{$bug_id}->{$attach_id});
-            my ($removed, $added) =
-                Bugzilla::Flag->update_activity(\@old_summaries, \@new_summaries);
-            LogActivityEntry($bug_id, 'flagtypes.name', $removed, $added,
-                $userid, $timestamp, undef, $attach_id);
-        }
-        $sth_set_bug_timestamp->execute($timestamp, $bug_id);
-        $updatedbugs{$bug_id} = 1;
-    }
-
-    # Simple deletions in referred tables.
-    $dbh->do('DELETE FROM email_setting WHERE user_id = ?', undef,
-             $otherUserID);
-    $dbh->do('DELETE FROM logincookies WHERE userid = ?', undef, $otherUserID);
-    $dbh->do('DELETE FROM namedqueries WHERE userid = ?', undef, $otherUserID);
-    $dbh->do('DELETE FROM namedqueries_link_in_footer WHERE user_id = ?', undef,
-             $otherUserID);
-    if ($namedqueries_as_string) {
-        $dbh->do('DELETE FROM namedquery_group_map WHERE namedquery_id IN ' .
-                 "($namedqueries_as_string)");
-    }
-    $dbh->do('DELETE FROM profile_setting WHERE user_id = ?', undef,
-             $otherUserID);
-    $dbh->do('DELETE FROM profiles_activity WHERE userid = ? OR who = ?', undef,
-             ($otherUserID, $otherUserID));
-    $dbh->do('DELETE FROM tokens WHERE userid = ?', undef, $otherUserID);
-    $dbh->do('DELETE FROM user_group_map WHERE user_id = ?', undef,
-             $otherUserID);
-    $dbh->do('DELETE FROM watch WHERE watcher = ? OR watched = ?', undef,
-             ($otherUserID, $otherUserID));
-
-    # Deletions in referred tables which need LogActivityEntry.
-    my $buglist = $dbh->selectcol_arrayref('SELECT bug_id FROM cc WHERE who = ?',
-                                            undef, $otherUserID);
-    $dbh->do('DELETE FROM cc WHERE who = ?', undef, $otherUserID);
-    foreach my $bug_id (@$buglist) {
-        LogActivityEntry($bug_id, 'cc', $otherUser->login, '', $userid,
-                         $timestamp);
-        $sth_set_bug_timestamp->execute($timestamp, $bug_id);
-        $updatedbugs{$bug_id} = 1;
-    }
-
-    # Even more complex deletions in referred tables.
-    my $id;
-
-    # 1) Series
-    my $sth_seriesid = $dbh->prepare(
-           'SELECT series_id FROM series WHERE creator = ?');
-    my $sth_deleteSeries = $dbh->prepare(
-           'DELETE FROM series WHERE series_id = ?');
-    my $sth_deleteSeriesData = $dbh->prepare(
-           'DELETE FROM series_data WHERE series_id = ?');
-
-    $sth_seriesid->execute($otherUserID);
-    while ($id = $sth_seriesid->fetchrow_array()) {
-        $sth_deleteSeriesData->execute($id);
-        $sth_deleteSeries->execute($id);
+}
+elsif ($action eq 'delete') {
+  check_token_data($token, 'delete_user');
+  my $otherUser = check_user($otherUserID, $otherUserLogin);
+  $otherUserID = $otherUser->id;
+
+  # Cache for user accounts.
+  my %usercache = (0 => new Bugzilla::User());
+  my %updatedbugs;
+
+  # Lock tables during the check+removal session.
+  # XXX: if there was some change on these tables after the deletion
+  #      confirmation checks, we may do something here we haven't warned
+  #      about.
+  $dbh->bz_start_transaction();
+
+  Bugzilla->params->{'allowuserdeletion'}
+    || ThrowUserError('users_deletion_disabled');
+  $editusers
+    || ThrowUserError('auth_failure',
+    {group => "editusers", action => "delete", object => "users"});
+  @{$otherUser->product_responsibilities()}
+    && ThrowUserError('user_has_responsibility');
+
+  Bugzilla->logout_user($otherUser);
+
+  # Get the named query list so we can delete namedquery_group_map entries.
+  my $namedqueries_as_string = join(
+    ', ',
+    @{
+      $dbh->selectcol_arrayref('SELECT id FROM namedqueries WHERE userid = ?',
+        undef, $otherUserID)
     }
-
-    # 2) Whines
-    my $sth_whineidFromEvents = $dbh->prepare(
-           'SELECT id FROM whine_events WHERE owner_userid = ?');
-    my $sth_deleteWhineEvent = $dbh->prepare(
-           'DELETE FROM whine_events WHERE id = ?');
-    my $sth_deleteWhineQuery = $dbh->prepare(
-           'DELETE FROM whine_queries WHERE eventid = ?');
-    my $sth_deleteWhineSchedule = $dbh->prepare(
-           'DELETE FROM whine_schedules WHERE eventid = ?');
-
-    $dbh->do('DELETE FROM whine_schedules WHERE mailto = ? AND mailto_type = ?',
-             undef, ($otherUserID, MAILTO_USER));
-
-    $sth_whineidFromEvents->execute($otherUserID);
-    while ($id = $sth_whineidFromEvents->fetchrow_array()) {
-        $sth_deleteWhineQuery->execute($id);
-        $sth_deleteWhineSchedule->execute($id);
-        $sth_deleteWhineEvent->execute($id);
+  );
+
+  # Get the timestamp for LogActivityEntry.
+  my $timestamp = $dbh->selectrow_array('SELECT NOW()');
+
+  # When we update a bug_activity entry, we update the bug timestamp, too.
+  my $sth_set_bug_timestamp
+    = $dbh->prepare('UPDATE bugs SET delta_ts = ? WHERE bug_id = ?');
+
+  # Flags
+  my $flag_ids
+    = $dbh->selectcol_arrayref('SELECT id FROM flags WHERE requestee_id = ?',
+    undef, $otherUserID);
+
+  my $flags = Bugzilla::Flag->new_from_list($flag_ids);
+
+  $dbh->do(
+    'UPDATE flags SET requestee_id = NULL, modification_date = ?
+              WHERE requestee_id = ?', undef, ($timestamp, $otherUserID)
+  );
+
+  # We want to remove the requestee but leave the requester alone,
+  # so we have to log these changes manually.
+  my %bugs;
+  push(@{$bugs{$_->bug_id}->{$_->attach_id || 0}}, $_) foreach @$flags;
+  foreach my $bug_id (keys %bugs) {
+    foreach my $attach_id (keys %{$bugs{$bug_id}}) {
+      my @old_summaries = Bugzilla::Flag->snapshot($bugs{$bug_id}->{$attach_id});
+      $_->_set_requestee() foreach @{$bugs{$bug_id}->{$attach_id}};
+      my @new_summaries = Bugzilla::Flag->snapshot($bugs{$bug_id}->{$attach_id});
+      my ($removed, $added)
+        = Bugzilla::Flag->update_activity(\@old_summaries, \@new_summaries);
+      LogActivityEntry($bug_id, 'flagtypes.name', $removed, $added, $userid,
+        $timestamp, undef, $attach_id);
     }
-
-    # 3) Bugs
-    # 3.1) fall back to the default assignee
-    $buglist = $dbh->selectall_arrayref(
-        'SELECT bug_id, initialowner
+    $sth_set_bug_timestamp->execute($timestamp, $bug_id);
+    $updatedbugs{$bug_id} = 1;
+  }
+
+  # Simple deletions in referred tables.
+  $dbh->do('DELETE FROM email_setting WHERE user_id = ?', undef, $otherUserID);
+  $dbh->do('DELETE FROM logincookies WHERE userid = ?',   undef, $otherUserID);
+  $dbh->do('DELETE FROM namedqueries WHERE userid = ?',   undef, $otherUserID);
+  $dbh->do('DELETE FROM namedqueries_link_in_footer WHERE user_id = ?',
+    undef, $otherUserID);
+  if ($namedqueries_as_string) {
+    $dbh->do('DELETE FROM namedquery_group_map WHERE namedquery_id IN '
+        . "($namedqueries_as_string)");
+  }
+  $dbh->do('DELETE FROM profile_setting WHERE user_id = ?', undef, $otherUserID);
+  $dbh->do('DELETE FROM profiles_activity WHERE userid = ? OR who = ?',
+    undef, ($otherUserID, $otherUserID));
+  $dbh->do('DELETE FROM tokens WHERE userid = ?',          undef, $otherUserID);
+  $dbh->do('DELETE FROM user_group_map WHERE user_id = ?', undef, $otherUserID);
+  $dbh->do('DELETE FROM watch WHERE watcher = ? OR watched = ?',
+    undef, ($otherUserID, $otherUserID));
+
+  # Deletions in referred tables which need LogActivityEntry.
+  my $buglist = $dbh->selectcol_arrayref('SELECT bug_id FROM cc WHERE who = ?',
+    undef, $otherUserID);
+  $dbh->do('DELETE FROM cc WHERE who = ?', undef, $otherUserID);
+  foreach my $bug_id (@$buglist) {
+    LogActivityEntry($bug_id, 'cc', $otherUser->login, '', $userid, $timestamp);
+    $sth_set_bug_timestamp->execute($timestamp, $bug_id);
+    $updatedbugs{$bug_id} = 1;
+  }
+
+  # Even more complex deletions in referred tables.
+  my $id;
+
+  # 1) Series
+  my $sth_seriesid
+    = $dbh->prepare('SELECT series_id FROM series WHERE creator = ?');
+  my $sth_deleteSeries = $dbh->prepare('DELETE FROM series WHERE series_id = ?');
+  my $sth_deleteSeriesData
+    = $dbh->prepare('DELETE FROM series_data WHERE series_id = ?');
+
+  $sth_seriesid->execute($otherUserID);
+  while ($id = $sth_seriesid->fetchrow_array()) {
+    $sth_deleteSeriesData->execute($id);
+    $sth_deleteSeries->execute($id);
+  }
+
+  # 2) Whines
+  my $sth_whineidFromEvents
+    = $dbh->prepare('SELECT id FROM whine_events WHERE owner_userid = ?');
+  my $sth_deleteWhineEvent
+    = $dbh->prepare('DELETE FROM whine_events WHERE id = ?');
+  my $sth_deleteWhineQuery
+    = $dbh->prepare('DELETE FROM whine_queries WHERE eventid = ?');
+  my $sth_deleteWhineSchedule
+    = $dbh->prepare('DELETE FROM whine_schedules WHERE eventid = ?');
+
+  $dbh->do('DELETE FROM whine_schedules WHERE mailto = ? AND mailto_type = ?',
+    undef, ($otherUserID, MAILTO_USER));
+
+  $sth_whineidFromEvents->execute($otherUserID);
+  while ($id = $sth_whineidFromEvents->fetchrow_array()) {
+    $sth_deleteWhineQuery->execute($id);
+    $sth_deleteWhineSchedule->execute($id);
+    $sth_deleteWhineEvent->execute($id);
+  }
+
+  # 3) Bugs
+  # 3.1) fall back to the default assignee
+  $buglist = $dbh->selectall_arrayref(
+    'SELECT bug_id, initialowner
          FROM bugs
          INNER JOIN components ON components.id = bugs.component_id
-         WHERE assigned_to = ?', undef, $otherUserID);
-
-    my $sth_updateAssignee = $dbh->prepare(
-        'UPDATE bugs SET assigned_to = ?, delta_ts = ? WHERE bug_id = ?');
-
-    foreach my $bug (@$buglist) {
-        my ($bug_id, $default_assignee_id) = @$bug;
-        $sth_updateAssignee->execute($default_assignee_id,
-                                     $timestamp, $bug_id);
-        $updatedbugs{$bug_id} = 1;
-        $default_assignee_id ||= 0;
-        $usercache{$default_assignee_id} ||=
-            new Bugzilla::User($default_assignee_id);
-        LogActivityEntry($bug_id, 'assigned_to', $otherUser->login,
-                         $usercache{$default_assignee_id}->login,
-                         $userid, $timestamp);
-    }
-
-    # 3.2) fall back to the default QA contact
-    $buglist = $dbh->selectall_arrayref(
-        'SELECT bug_id, initialqacontact
+         WHERE assigned_to = ?', undef, $otherUserID
+  );
+
+  my $sth_updateAssignee = $dbh->prepare(
+    'UPDATE bugs SET assigned_to = ?, delta_ts = ? WHERE bug_id = ?');
+
+  foreach my $bug (@$buglist) {
+    my ($bug_id, $default_assignee_id) = @$bug;
+    $sth_updateAssignee->execute($default_assignee_id, $timestamp, $bug_id);
+    $updatedbugs{$bug_id} = 1;
+    $default_assignee_id ||= 0;
+    $usercache{$default_assignee_id} ||= new Bugzilla::User($default_assignee_id);
+    LogActivityEntry($bug_id, 'assigned_to', $otherUser->login,
+      $usercache{$default_assignee_id}->login,
+      $userid, $timestamp);
+  }
+
+  # 3.2) fall back to the default QA contact
+  $buglist = $dbh->selectall_arrayref(
+    'SELECT bug_id, initialqacontact
          FROM bugs
          INNER JOIN components ON components.id = bugs.component_id
-         WHERE qa_contact = ?', undef, $otherUserID);
-
-    my $sth_updateQAcontact = $dbh->prepare(
-        'UPDATE bugs SET qa_contact = ?, delta_ts = ? WHERE bug_id = ?');
-
-    foreach my $bug (@$buglist) {
-        my ($bug_id, $default_qa_contact_id) = @$bug;
-        $sth_updateQAcontact->execute($default_qa_contact_id,
-                                      $timestamp, $bug_id);
-        $updatedbugs{$bug_id} = 1;
-        $default_qa_contact_id ||= 0;
-        $usercache{$default_qa_contact_id} ||=
-            new Bugzilla::User($default_qa_contact_id);
-        LogActivityEntry($bug_id, 'qa_contact', $otherUser->login,
-                         $usercache{$default_qa_contact_id}->login,
-                         $userid, $timestamp);
-    }
-
-    # Finally, remove the user account itself.
-    $dbh->do('DELETE FROM profiles WHERE userid = ?', undef, $otherUserID);
-
-    $dbh->bz_commit_transaction();
-    delete_token($token);
-
-    # It's complex to determine which items now need to be flushed from
-    # memcached.  As user deletion is expected to be a rare event, we just
-    # flush the entire cache when a user is deleted.
-    Bugzilla->memcached->clear_all();
-
-    $vars->{'message'} = 'account_deleted';
-    $vars->{'otheruser'}{'login'} = $otherUser->login;
-    $vars->{'restrictablegroups'} = $user->bless_groups();
-    $template->process('admin/users/search.html.tmpl', $vars)
-       || ThrowTemplateError($template->error());
-
-    # Send mail about what we've done to bugs.
-    # The deleted user is not notified of the changes.
-    foreach (keys(%updatedbugs)) {
-        Bugzilla::BugMail::Send($_, {'changer' => $user} );
-    }
+         WHERE qa_contact = ?', undef, $otherUserID
+  );
+
+  my $sth_updateQAcontact = $dbh->prepare(
+    'UPDATE bugs SET qa_contact = ?, delta_ts = ? WHERE bug_id = ?');
+
+  foreach my $bug (@$buglist) {
+    my ($bug_id, $default_qa_contact_id) = @$bug;
+    $sth_updateQAcontact->execute($default_qa_contact_id, $timestamp, $bug_id);
+    $updatedbugs{$bug_id} = 1;
+    $default_qa_contact_id ||= 0;
+    $usercache{$default_qa_contact_id}
+      ||= new Bugzilla::User($default_qa_contact_id);
+    LogActivityEntry($bug_id, 'qa_contact', $otherUser->login,
+      $usercache{$default_qa_contact_id}->login,
+      $userid, $timestamp);
+  }
+
+  # Finally, remove the user account itself.
+  $dbh->do('DELETE FROM profiles WHERE userid = ?', undef, $otherUserID);
+
+  $dbh->bz_commit_transaction();
+  delete_token($token);
+
+  # It's complex to determine which items now need to be flushed from
+  # memcached.  As user deletion is expected to be a rare event, we just
+  # flush the entire cache when a user is deleted.
+  Bugzilla->memcached->clear_all();
+
+  $vars->{'message'}            = 'account_deleted';
+  $vars->{'otheruser'}{'login'} = $otherUser->login;
+  $vars->{'restrictablegroups'} = $user->bless_groups();
+  $template->process('admin/users/search.html.tmpl', $vars)
+    || ThrowTemplateError($template->error());
+
+  # Send mail about what we've done to bugs.
+  # The deleted user is not notified of the changes.
+  foreach (keys(%updatedbugs)) {
+    Bugzilla::BugMail::Send($_, {'changer' => $user});
+  }
 
 ###########################################################################
-} elsif ($action eq 'activity') {
-    my $otherUser = check_user($otherUserID, $otherUserLogin);
+}
+elsif ($action eq 'activity') {
+  my $otherUser = check_user($otherUserID, $otherUserLogin);
 
-    $vars->{'profile_changes'} = $dbh->selectall_arrayref(
-        "SELECT profiles.login_name AS who, " .
-                $dbh->sql_date_format('profiles_activity.profiles_when') . " AS activity_when,
+  $vars->{'profile_changes'} = $dbh->selectall_arrayref(
+        "SELECT profiles.login_name AS who, "
+      . $dbh->sql_date_format('profiles_activity.profiles_when')
+      . " AS activity_when,
                 fielddefs.name AS what,
                 profiles_activity.oldvalue AS removed,
                 profiles_activity.newvalue AS added
@@ -620,18 +653,18 @@ if ($action eq 'search') {
          INNER JOIN profiles ON profiles_activity.who = profiles.userid
          INNER JOIN fielddefs ON fielddefs.id = profiles_activity.fieldid
          WHERE profiles_activity.userid = ?
-         ORDER BY profiles_activity.profiles_when",
-        {'Slice' => {}},
-        $otherUser->id);
+         ORDER BY profiles_activity.profiles_when", {'Slice' => {}}, $otherUser->id
+  );
 
-    $vars->{'otheruser'} = $otherUser;
+  $vars->{'otheruser'} = $otherUser;
 
-    $template->process("account/profile-activity.html.tmpl", $vars)
-        || ThrowTemplateError($template->error());
+  $template->process("account/profile-activity.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
 
 ###########################################################################
-} else {
-    ThrowUserError('unknown_action', {action => $action});
+}
+else {
+  ThrowUserError('unknown_action', {action => $action});
 }
 
 exit;
@@ -643,52 +676,54 @@ exit;
 # Try to build a user object using its ID, else its login name, and throw
 # an error if the user does not exist.
 sub check_user {
-    my ($otherUserID, $otherUserLogin) = @_;
-
-    my $otherUser;
-    my $vars = {};
-
-    if ($otherUserID) {
-        $otherUser = Bugzilla::User->new($otherUserID);
-        $vars->{'user_id'} = $otherUserID;
-    }
-    elsif ($otherUserLogin) {
-        $otherUser = new Bugzilla::User({ name => $otherUserLogin });
-        $vars->{'user_login'} = $otherUserLogin;
-    }
-    ($otherUser && $otherUser->id) || ThrowUserError('invalid_user', $vars);
-
-    return $otherUser;
+  my ($otherUserID, $otherUserLogin) = @_;
+
+  my $otherUser;
+  my $vars = {};
+
+  if ($otherUserID) {
+    $otherUser = Bugzilla::User->new($otherUserID);
+    $vars->{'user_id'} = $otherUserID;
+  }
+  elsif ($otherUserLogin) {
+    $otherUser = new Bugzilla::User({name => $otherUserLogin});
+    $vars->{'user_login'} = $otherUserLogin;
+  }
+  ($otherUser && $otherUser->id) || ThrowUserError('invalid_user', $vars);
+
+  return $otherUser;
 }
 
 # Copy incoming list selection values from CGI params to template variables.
 sub mirrorListSelectionValues {
-    my $cgi = Bugzilla->cgi;
-    if (defined($cgi->param('matchtype'))) {
-        foreach ('matchvalue', 'matchstr', 'matchtype',
-                 'grouprestrict', 'groupid', 'is_enabled')
-        {
-            $vars->{'listselectionvalues'}{$_} = $cgi->param($_);
-        }
+  my $cgi = Bugzilla->cgi;
+  if (defined($cgi->param('matchtype'))) {
+    foreach (
+      'matchvalue',    'matchstr', 'matchtype',
+      'grouprestrict', 'groupid',  'is_enabled'
+      )
+    {
+      $vars->{'listselectionvalues'}{$_} = $cgi->param($_);
     }
+  }
 }
 
 # Retrieve user data for the user editing form. User creation and user
 # editing code rely on this to call derive_groups().
 sub userDataToVars {
-    my $otheruserid = shift;
-    my $otheruser = new Bugzilla::User($otheruserid);
-    my $query;
-    my $user = Bugzilla->user;
-    my $dbh = Bugzilla->dbh;
+  my $otheruserid = shift;
+  my $otheruser   = new Bugzilla::User($otheruserid);
+  my $query;
+  my $user = Bugzilla->user;
+  my $dbh  = Bugzilla->dbh;
 
-    my $grouplist = $otheruser->groups_as_string;
+  my $grouplist = $otheruser->groups_as_string;
 
-    $vars->{'otheruser'} = $otheruser;
-    $vars->{'groups'} = $user->bless_groups();
+  $vars->{'otheruser'} = $otheruser;
+  $vars->{'groups'}    = $user->bless_groups();
 
-    $vars->{'permissions'} = $dbh->selectall_hashref(
-        qq{SELECT id,
+  $vars->{'permissions'} = $dbh->selectall_hashref(
+    qq{SELECT id,
                   COUNT(directmember.group_id) AS directmember,
                   COUNT(regexpmember.group_id) AS regexpmember,
                   (CASE WHEN (groups.id IN ($grouplist)
@@ -714,38 +749,40 @@ sub userDataToVars {
                  AND directbless.isbless = 1
                  AND directbless.grant_type = ?
           } . $dbh->sql_group_by('id'),
-        'id', undef,
-        ($otheruserid, GRANT_DIRECT,
-         $otheruserid, GRANT_REGEXP,
-         $otheruserid, GRANT_DIRECT));
-
-    # Find indirect bless permission.
-    $query = qq{SELECT groups.id
+    'id', undef,
+    (
+      $otheruserid, GRANT_DIRECT, $otheruserid, GRANT_REGEXP,
+      $otheruserid, GRANT_DIRECT
+    )
+  );
+
+  # Find indirect bless permission.
+  $query = qq{SELECT groups.id
                 FROM groups, group_group_map AS ggm
                 WHERE groups.id = ggm.grantor_id
                   AND ggm.member_id IN ($grouplist)
                   AND ggm.grant_type = ?
                } . $dbh->sql_group_by('id');
-    foreach (@{$dbh->selectall_arrayref($query, undef,
-                                        (GROUP_BLESS))}) {
-        # Merge indirect bless permissions into permission variable.
-        $vars->{'permissions'}{${$_}[0]}{'indirectbless'} = 1;
-    }
+  foreach (@{$dbh->selectall_arrayref($query, undef, (GROUP_BLESS))}) {
+
+    # Merge indirect bless permissions into permission variable.
+    $vars->{'permissions'}{${$_}[0]}{'indirectbless'} = 1;
+  }
 }
 
 sub edit_processing {
-    my $otherUser = shift;
-    my $user = Bugzilla->user;
-    my $template = Bugzilla->template;
+  my $otherUser = shift;
+  my $user      = Bugzilla->user;
+  my $template  = Bugzilla->template;
 
-    $user->in_group('editusers') || $user->can_see_user($otherUser)
-        || ThrowUserError('auth_failure', {reason => "not_visible",
-                                           action => "modify",
-                                           object => "user"});
+  $user->in_group('editusers')
+    || $user->can_see_user($otherUser)
+    || ThrowUserError('auth_failure',
+    {reason => "not_visible", action => "modify", object => "user"});
 
-    userDataToVars($otherUser->id);
-    $vars->{'token'} = issue_session_token('edit_user');
+  userDataToVars($otherUser->id);
+  $vars->{'token'} = issue_session_token('edit_user');
 
-    $template->process('admin/users/edit.html.tmpl', $vars)
-       || ThrowTemplateError($template->error());
+  $template->process('admin/users/edit.html.tmpl', $vars)
+    || ThrowTemplateError($template->error());
 }
index 2b276df4bddbb6445c42188b2fc1300ab54383ae..4d9f094a48b83acfbfe807ab205c6a65cd42e09e 100755 (executable)
@@ -25,12 +25,12 @@ use Bugzilla::Field::Choice;
 ###############
 
 sub display_field_values {
-    my $vars = shift;
-    my $template = Bugzilla->template;
-    $vars->{'values'} = $vars->{'field'}->legal_values;
-    $template->process("admin/fieldvalues/list.html.tmpl", $vars)
-      || ThrowTemplateError($template->error());
-    exit;
+  my $vars     = shift;
+  my $template = Bugzilla->template;
+  $vars->{'values'} = $vars->{'field'}->legal_values;
+  $template->process("admin/fieldvalues/list.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
+  exit;
 }
 
 ######################################################################
@@ -43,7 +43,7 @@ my $user = Bugzilla->login(LOGIN_REQUIRED);
 my $dbh      = Bugzilla->dbh;
 my $cgi      = Bugzilla->cgi;
 my $template = Bugzilla->template;
-my $vars = {};
+my $vars     = {};
 
 # Replace this entry by separate entries in templates when
 # the documentation about legal values becomes bigger.
@@ -52,33 +52,31 @@ $vars->{'doc_section'} = 'administering/field-values.html';
 print $cgi->header();
 
 $user->in_group('admin')
-  || ThrowUserError('auth_failure', {group  => "admin",
-                                     action => "edit",
-                                     object => "field_values"});
+  || ThrowUserError('auth_failure',
+  {group => "admin", action => "edit", object => "field_values"});
 
 #
 # often-used variables
 #
-my $action = trim($cgi->param('action')  || '');
-my $token  = $cgi->param('token');
+my $action = trim($cgi->param('action') || '');
+my $token = $cgi->param('token');
 
 #
 # field = '' -> Show nice list of fields
 #
 if (!$cgi->param('field')) {
-    my @field_list =
-        @{ Bugzilla->fields({ is_select => 1, is_abnormal => 0 }) };
+  my @field_list = @{Bugzilla->fields({is_select => 1, is_abnormal => 0})};
 
-    $vars->{'fields'} = \@field_list;
-    $template->process("admin/fieldvalues/select-field.html.tmpl", $vars)
-      || ThrowTemplateError($template->error());
-    exit;
+  $vars->{'fields'} = \@field_list;
+  $template->process("admin/fieldvalues/select-field.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
+  exit;
 }
 
 # At this point, the field must be defined.
 my $field = Bugzilla::Field->check(scalar $cgi->param('field'));
 if (!$field->is_select || $field->is_abnormal) {
-    ThrowUserError('fieldname_invalid', { field => $field });
+  ThrowUserError('fieldname_invalid', {field => $field});
 }
 $vars->{'field'} = $field;
 
@@ -92,34 +90,35 @@ display_field_values($vars) unless $action;
 # (next action will be 'new')
 #
 if ($action eq 'add') {
-    $vars->{'token'} = issue_session_token('add_field_value');
-    $template->process("admin/fieldvalues/create.html.tmpl", $vars)
-      || ThrowTemplateError($template->error());
-    exit;
+  $vars->{'token'} = issue_session_token('add_field_value');
+  $template->process("admin/fieldvalues/create.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
+  exit;
 }
 
 #
 # action='new' -> add field value entered in the 'action=add' screen
 #
 if ($action eq 'new') {
-    check_token_data($token, 'add_field_value');
+  check_token_data($token, 'add_field_value');
 
-    my $created_value = Bugzilla::Field::Choice->type($field)->create({
-        value   => scalar $cgi->param('value'), 
-        sortkey => scalar $cgi->param('sortkey'),
-        is_open => scalar $cgi->param('is_open'),
-        visibility_value_id => scalar $cgi->param('visibility_value_id'),
-    });
+  my $created_value = Bugzilla::Field::Choice->type($field)->create({
+    value               => scalar $cgi->param('value'),
+    sortkey             => scalar $cgi->param('sortkey'),
+    is_open             => scalar $cgi->param('is_open'),
+    visibility_value_id => scalar $cgi->param('visibility_value_id'),
+  });
 
-    delete_token($token);
+  delete_token($token);
 
-    $vars->{'message'} = 'field_value_created';
-    $vars->{'value'} = $created_value;
-    display_field_values($vars);
+  $vars->{'message'} = 'field_value_created';
+  $vars->{'value'}   = $created_value;
+  display_field_values($vars);
 }
 
 # After this, we always have a value
-my $value = Bugzilla::Field::Choice->type($field)->check(scalar $cgi->param('value'));
+my $value
+  = Bugzilla::Field::Choice->type($field)->check(scalar $cgi->param('value'));
 $vars->{'value'} = $value;
 
 #
@@ -127,16 +126,17 @@ $vars->{'value'} = $value;
 # (next action would be 'delete')
 #
 if ($action eq 'del') {
-    # If the value cannot be deleted, throw an error.
-    if ($value->is_static) {
-        ThrowUserError('fieldvalue_not_deletable', $vars);
-    }
-    $vars->{'token'} = issue_session_token('delete_field_value');
 
-    $template->process("admin/fieldvalues/confirm-delete.html.tmpl", $vars)
-      || ThrowTemplateError($template->error());
+  # If the value cannot be deleted, throw an error.
+  if ($value->is_static) {
+    ThrowUserError('fieldvalue_not_deletable', $vars);
+  }
+  $vars->{'token'} = issue_session_token('delete_field_value');
 
-    exit;
+  $template->process("admin/fieldvalues/confirm-delete.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
+
+  exit;
 }
 
 
@@ -144,12 +144,12 @@ if ($action eq 'del') {
 # action='delete' -> really delete the field value
 #
 if ($action eq 'delete') {
-    check_token_data($token, 'delete_field_value');
-    $value->remove_from_db();
-    delete_token($token);
-    $vars->{'message'} = 'field_value_deleted';
-    $vars->{'no_edit_link'} = 1;
-    display_field_values($vars);
+  check_token_data($token, 'delete_field_value');
+  $value->remove_from_db();
+  delete_token($token);
+  $vars->{'message'}      = 'field_value_deleted';
+  $vars->{'no_edit_link'} = 1;
+  display_field_values($vars);
 }
 
 
@@ -158,11 +158,11 @@ if ($action eq 'delete') {
 # (next action would be 'update')
 #
 if ($action eq 'edit') {
-    $vars->{'token'} = issue_session_token('edit_field_value');
-    $template->process("admin/fieldvalues/edit.html.tmpl", $vars)
-      || ThrowTemplateError($template->error());
+  $vars->{'token'} = issue_session_token('edit_field_value');
+  $template->process("admin/fieldvalues/edit.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
 
-    exit;
+  exit;
 }
 
 
@@ -170,21 +170,21 @@ if ($action eq 'edit') {
 # action='update' -> update the field value
 #
 if ($action eq 'update') {
-    check_token_data($token, 'edit_field_value');
-    $vars->{'value_old'} = $value->name;
-    my %params = (
-        name    => scalar $cgi->param('value_new'),
-        sortkey => scalar $cgi->param('sortkey'),
-        visibility_value => scalar $cgi->param('visibility_value_id'),
-    );
-    if ($cgi->should_set('is_active')) {
-        $params{is_active} = $cgi->param('is_active');
-    }
-    $value->set_all(\%params);
-    $vars->{'changes'} = $value->update();
-    delete_token($token);
-    $vars->{'message'} = 'field_value_updated';
-    display_field_values($vars);
+  check_token_data($token, 'edit_field_value');
+  $vars->{'value_old'} = $value->name;
+  my %params = (
+    name             => scalar $cgi->param('value_new'),
+    sortkey          => scalar $cgi->param('sortkey'),
+    visibility_value => scalar $cgi->param('visibility_value_id'),
+  );
+  if ($cgi->should_set('is_active')) {
+    $params{is_active} = $cgi->param('is_active');
+  }
+  $value->set_all(\%params);
+  $vars->{'changes'} = $value->update();
+  delete_token($token);
+  $vars->{'message'} = 'field_value_updated';
+  display_field_values($vars);
 }
 
 # No valid action found
index 4a102c438bc039c8d58240878f2697a5a7cf187b..691764f76b20bd8852634d6f5d63b7b3ef51910c 100755 (executable)
@@ -19,10 +19,11 @@ use Bugzilla::Error;
 use Bugzilla::Version;
 use Bugzilla::Token;
 
-my $cgi = Bugzilla->cgi;
-my $dbh = Bugzilla->dbh;
+my $cgi      = Bugzilla->cgi;
+my $dbh      = Bugzilla->dbh;
 my $template = Bugzilla->template;
-my $vars = {};
+my $vars     = {};
+
 # There is only one section about versions in the documentation,
 # so all actions point to the same page.
 $vars->{'doc_section'} = 'administering/categorization.html#versions';
@@ -37,9 +38,8 @@ print $cgi->header();
 
 $user->in_group('editcomponents')
   || scalar(@{$user->get_products_by_permission('editcomponents')})
-  || ThrowUserError("auth_failure", {group  => "editcomponents",
-                                     action => "edit",
-                                     object => "versions"});
+  || ThrowUserError("auth_failure",
+  {group => "editcomponents", action => "edit", object => "versions"});
 
 #
 # often used variables
@@ -48,26 +48,27 @@ my $product_name = trim($cgi->param('product') || '');
 my $version_name = trim($cgi->param('version') || '');
 my $action       = trim($cgi->param('action')  || '');
 my $showbugcounts = (defined $cgi->param('showbugcounts'));
-my $token        = $cgi->param('token');
-my $isactive     = $cgi->param('isactive');
+my $token         = $cgi->param('token');
+my $isactive      = $cgi->param('isactive');
 
 #
 # product = '' -> Show nice list of products
 #
 
 unless ($product_name) {
-    my $selectable_products = $user->get_selectable_products;
-    # If the user has editcomponents privs for some products only,
-    # we have to restrict the list of products to display.
-    unless ($user->in_group('editcomponents')) {
-        $selectable_products = $user->get_products_by_permission('editcomponents');
-    }
-    $vars->{'products'} = $selectable_products;
-    $vars->{'showbugcounts'} = $showbugcounts;
-
-    $template->process("admin/versions/select-product.html.tmpl", $vars)
-      || ThrowTemplateError($template->error());
-    exit;
+  my $selectable_products = $user->get_selectable_products;
+
+  # If the user has editcomponents privs for some products only,
+  # we have to restrict the list of products to display.
+  unless ($user->in_group('editcomponents')) {
+    $selectable_products = $user->get_products_by_permission('editcomponents');
+  }
+  $vars->{'products'}      = $selectable_products;
+  $vars->{'showbugcounts'} = $showbugcounts;
+
+  $template->process("admin/versions/select-product.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
+  exit;
 }
 
 my $product = $user->check_can_admin_product($product_name);
@@ -77,12 +78,12 @@ my $product = $user->check_can_admin_product($product_name);
 #
 
 unless ($action) {
-    $vars->{'showbugcounts'} = $showbugcounts;
-    $vars->{'product'} = $product;
-    $template->process("admin/versions/list.html.tmpl", $vars)
-      || ThrowTemplateError($template->error());
+  $vars->{'showbugcounts'} = $showbugcounts;
+  $vars->{'product'}       = $product;
+  $template->process("admin/versions/list.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
 
-    exit;
+  exit;
 }
 
 #
@@ -92,12 +93,12 @@ unless ($action) {
 #
 
 if ($action eq 'add') {
-    $vars->{'token'} = issue_session_token('add_version');
-    $vars->{'product'} = $product;
-    $template->process("admin/versions/create.html.tmpl", $vars)
-      || ThrowTemplateError($template->error());
+  $vars->{'token'}   = issue_session_token('add_version');
+  $vars->{'product'} = $product;
+  $template->process("admin/versions/create.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
 
-    exit;
+  exit;
 }
 
 #
@@ -105,18 +106,18 @@ if ($action eq 'add') {
 #
 
 if ($action eq 'new') {
-    check_token_data($token, 'add_version');
-    my $version = Bugzilla::Version->create(
-        { value => $version_name, product => $product });
-    delete_token($token);
-
-    $vars->{'message'} = 'version_created';
-    $vars->{'version'} = $version;
-    $vars->{'product'} = $product;
-    $template->process("admin/versions/list.html.tmpl", $vars)
-      || ThrowTemplateError($template->error());
-
-    exit;
+  check_token_data($token, 'add_version');
+  my $version
+    = Bugzilla::Version->create({value => $version_name, product => $product});
+  delete_token($token);
+
+  $vars->{'message'} = 'version_created';
+  $vars->{'version'} = $version;
+  $vars->{'product'} = $product;
+  $template->process("admin/versions/list.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
+
+  exit;
 }
 
 #
@@ -126,15 +127,15 @@ if ($action eq 'new') {
 #
 
 if ($action eq 'del') {
-    my $version = Bugzilla::Version->check({ product => $product,
-                                             name    => $version_name });
-    $vars->{'version'} = $version;
-    $vars->{'product'} = $product;
-    $vars->{'token'} = issue_session_token('delete_version');
-    $template->process("admin/versions/confirm-delete.html.tmpl", $vars)
-      || ThrowTemplateError($template->error());
-
-    exit;
+  my $version
+    = Bugzilla::Version->check({product => $product, name => $version_name});
+  $vars->{'version'} = $version;
+  $vars->{'product'} = $product;
+  $vars->{'token'}   = issue_session_token('delete_version');
+  $template->process("admin/versions/confirm-delete.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
+
+  exit;
 }
 
 #
@@ -142,21 +143,21 @@ if ($action eq 'del') {
 #
 
 if ($action eq 'delete') {
-    check_token_data($token, 'delete_version');
-    my $version = Bugzilla::Version->check({ product => $product, 
-                                             name    => $version_name });
-    $version->remove_from_db;
-    delete_token($token);
+  check_token_data($token, 'delete_version');
+  my $version
+    = Bugzilla::Version->check({product => $product, name => $version_name});
+  $version->remove_from_db;
+  delete_token($token);
 
-    $vars->{'message'} = 'version_deleted';
-    $vars->{'version'} = $version;
-    $vars->{'product'} = $product;
-    $vars->{'no_edit_version_link'} = 1;
+  $vars->{'message'}              = 'version_deleted';
+  $vars->{'version'}              = $version;
+  $vars->{'product'}              = $product;
+  $vars->{'no_edit_version_link'} = 1;
 
-    $template->process("admin/versions/list.html.tmpl", $vars)
-      || ThrowTemplateError($template->error());
+  $template->process("admin/versions/list.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
 
-    exit;
+  exit;
 }
 
 #
@@ -166,16 +167,16 @@ if ($action eq 'delete') {
 #
 
 if ($action eq 'edit') {
-    my $version = Bugzilla::Version->check({ product => $product,
-                                             name    => $version_name });
-    $vars->{'version'} = $version;
-    $vars->{'product'} = $product;
-    $vars->{'token'} = issue_session_token('edit_version');
+  my $version
+    = Bugzilla::Version->check({product => $product, name => $version_name});
+  $vars->{'version'} = $version;
+  $vars->{'product'} = $product;
+  $vars->{'token'}   = issue_session_token('edit_version');
 
-    $template->process("admin/versions/edit.html.tmpl", $vars)
-      || ThrowTemplateError($template->error());
+  $template->process("admin/versions/edit.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
 
-    exit;
+  exit;
 }
 
 #
@@ -183,30 +184,27 @@ if ($action eq 'edit') {
 #
 
 if ($action eq 'update') {
-    check_token_data($token, 'edit_version');
-    my $version_old_name = trim($cgi->param('versionold') || '');
-    my $version = Bugzilla::Version->check({ product => $product,
-                                             name   => $version_old_name });
-
-    $dbh->bz_start_transaction();
-
-    $version->set_all({
-        value    =>  $version_name,
-        isactive =>  $isactive
-    });
-    my $changes = $version->update();
-
-    $dbh->bz_commit_transaction();
-    delete_token($token);
-
-    $vars->{'message'} = 'version_updated';
-    $vars->{'version'} = $version;
-    $vars->{'product'} = $product;
-    $vars->{'changes'} = $changes;
-    $template->process("admin/versions/list.html.tmpl", $vars)
-      || ThrowTemplateError($template->error());
-
-    exit;
+  check_token_data($token, 'edit_version');
+  my $version_old_name = trim($cgi->param('versionold') || '');
+  my $version
+    = Bugzilla::Version->check({product => $product, name => $version_old_name});
+
+  $dbh->bz_start_transaction();
+
+  $version->set_all({value => $version_name, isactive => $isactive});
+  my $changes = $version->update();
+
+  $dbh->bz_commit_transaction();
+  delete_token($token);
+
+  $vars->{'message'} = 'version_updated';
+  $vars->{'version'} = $version;
+  $vars->{'product'} = $product;
+  $vars->{'changes'} = $changes;
+  $template->process("admin/versions/list.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
+
+  exit;
 }
 
 # No valid action found
index d545845b15884aabd0805142b687cdc614faaaae..749c92688184b8a382c3609ee63a1dd0fa17d3cb 100755 (executable)
@@ -41,9 +41,9 @@ my $template = Bugzilla->template;
 my $vars     = {};
 my $dbh      = Bugzilla->dbh;
 
-my $userid   = $user->id;
-my $token    = $cgi->param('token');
-my $sth; # database statement handle
+my $userid = $user->id;
+my $token  = $cgi->param('token');
+my $sth;    # database statement handle
 
 # $events is a hash ref of Bugzilla::Whine objects keyed by event id,
 # that stores the active user's events.
@@ -66,9 +66,8 @@ my $events = get_events($userid);
 
 # First see if this user may use whines
 $user->in_group('bz_canusewhines')
-  || ThrowUserError("auth_failure", {group  => "bz_canusewhines",
-                                     action => "schedule",
-                                     object => "reports"});
+  || ThrowUserError("auth_failure",
+  {group => "bz_canusewhines", action => "schedule", object => "reports"});
 
 # May this user send mail to other users?
 my $can_mail_others = $user->in_group('bz_canusewhineatothers');
@@ -77,240 +76,245 @@ my $can_mail_others = $user->in_group('bz_canusewhineatothers');
 # removed, then what was altered.
 
 if ($cgi->param('update')) {
-    check_token_data($token, 'edit_whine');
-
-    if ($cgi->param("add_event")) {
-        # we create a new event
-        $sth = $dbh->prepare("INSERT INTO whine_events " .
-                             "(owner_userid) " .
-                             "VALUES (?)");
-        $sth->execute($userid);
-    }
-    else {
-        for my $eventid (keys %{$events}) {
-            # delete an entire event
-            if ($cgi->param("remove_event_$eventid")) {
-                # We need to make sure these belong to the same user,
-                # otherwise we could simply delete whatever matched that ID.
-                #
-                # schedules
-                my $schedules = Bugzilla::Whine::Schedule->match({ eventid => $eventid });
-                $sth = $dbh->prepare("DELETE FROM whine_schedules "
-                    . "WHERE id=?");
-                foreach my $schedule (@$schedules) {                    
-                    $sth->execute($schedule->id);
-                }
-
-                # queries
-                $sth = $dbh->prepare("SELECT whine_queries.id " .
-                                     "FROM whine_queries " .
-                                     "LEFT JOIN whine_events " .
-                                     "ON whine_events.id = " .
-                                     "whine_queries.eventid " .
-                                     "WHERE whine_events.id = ? " .
-                                     "AND whine_events.owner_userid = ?");
-                $sth->execute($eventid, $userid);
-                my @ids = @{$sth->fetchall_arrayref};
-                $sth = $dbh->prepare("DELETE FROM whine_queries " .
-                                     "WHERE id=?");
-                for (@ids) {
-                    my $delete_id = $_->[0];
-                    $sth->execute($delete_id);
-                }
-
-                # events
-                $sth = $dbh->prepare("DELETE FROM whine_events " .
-                                     "WHERE id=? AND owner_userid=?");
-                $sth->execute($eventid, $userid);
-            }
-            else {
-                # check the subject, body and mailifnobugs for changes
-                my $subject = $cgi->param("event_${eventid}_subject") // '';
-                my $body = $cgi->param("event_${eventid}_body") // '';
-                my $mailifnobugs = $cgi->param("event_${eventid}_mailifnobugs") ? 1 : 0;
-
-                trick_taint($subject) if $subject;
-                trick_taint($body)    if $body;
-
-                if ( ($subject ne $events->{$eventid}->subject)
-                  || ($mailifnobugs != $events->{$eventid}->mail_if_no_bugs)
-                  || ($body    ne $events->{$eventid}->body) ) {
-
-                    $sth = $dbh->prepare("UPDATE whine_events " .
-                                         "SET subject=?, body=?, mailifnobugs=? " .
-                                         "WHERE id=?");
-                    $sth->execute($subject, $body, $mailifnobugs, $eventid);
-                }
-
-                # add a schedule
-                if ($cgi->param("add_schedule_$eventid")) {
-                    # the schedule table must be locked before altering
-                    $sth = $dbh->prepare("INSERT INTO whine_schedules " .
-                                         "(eventid, mailto_type, mailto, " .
-                                         "run_day, run_time) " .
-                                         "VALUES (?, ?, ?, 'Sun', 2)");
-                    $sth->execute($eventid, MAILTO_USER, $userid);
-                }
-                # add a query
-                elsif ($cgi->param("add_query_$eventid")) {
-                    $sth = $dbh->prepare("INSERT INTO whine_queries "
-                        . "(eventid) "
-                        . "VALUES (?)");
-                    $sth->execute($eventid);
-                }
-            }
+  check_token_data($token, 'edit_whine');
+
+  if ($cgi->param("add_event")) {
+
+    # we create a new event
+    $sth = $dbh->prepare(
+      "INSERT INTO whine_events " . "(owner_userid) " . "VALUES (?)");
+    $sth->execute($userid);
+  }
+  else {
+    for my $eventid (keys %{$events}) {
+
+      # delete an entire event
+      if ($cgi->param("remove_event_$eventid")) {
+
+        # We need to make sure these belong to the same user,
+        # otherwise we could simply delete whatever matched that ID.
+        #
+        # schedules
+        my $schedules = Bugzilla::Whine::Schedule->match({eventid => $eventid});
+        $sth = $dbh->prepare("DELETE FROM whine_schedules " . "WHERE id=?");
+        foreach my $schedule (@$schedules) {
+          $sth->execute($schedule->id);
+        }
+
+        # queries
+        $sth
+          = $dbh->prepare("SELECT whine_queries.id "
+            . "FROM whine_queries "
+            . "LEFT JOIN whine_events "
+            . "ON whine_events.id = "
+            . "whine_queries.eventid "
+            . "WHERE whine_events.id = ? "
+            . "AND whine_events.owner_userid = ?");
+        $sth->execute($eventid, $userid);
+        my @ids = @{$sth->fetchall_arrayref};
+        $sth = $dbh->prepare("DELETE FROM whine_queries " . "WHERE id=?");
+        for (@ids) {
+          my $delete_id = $_->[0];
+          $sth->execute($delete_id);
+        }
 
-            # now check all of the schedules and queries to see if they need
-            # to be altered or deleted
+        # events
+        $sth = $dbh->prepare(
+          "DELETE FROM whine_events " . "WHERE id=? AND owner_userid=?");
+        $sth->execute($eventid, $userid);
+      }
+      else {
+        # check the subject, body and mailifnobugs for changes
+        my $subject      = $cgi->param("event_${eventid}_subject") // '';
+        my $body         = $cgi->param("event_${eventid}_body") // '';
+        my $mailifnobugs = $cgi->param("event_${eventid}_mailifnobugs") ? 1 : 0;
+
+        trick_taint($subject) if $subject;
+        trick_taint($body)    if $body;
+
+        if ( ($subject ne $events->{$eventid}->subject)
+          || ($mailifnobugs != $events->{$eventid}->mail_if_no_bugs)
+          || ($body ne $events->{$eventid}->body))
+        {
+
+          $sth
+            = $dbh->prepare("UPDATE whine_events "
+              . "SET subject=?, body=?, mailifnobugs=? "
+              . "WHERE id=?");
+          $sth->execute($subject, $body, $mailifnobugs, $eventid);
+        }
 
-            # Check schedules for changes
-            my $schedules = Bugzilla::Whine::Schedule->match({ eventid => $eventid });
-            my @scheduleids = ();
-            foreach my $schedule (@$schedules) {
-                push @scheduleids, $schedule->id;
-            }
+        # add a schedule
+        if ($cgi->param("add_schedule_$eventid")) {
 
-            # we need to double-check all of the user IDs in mailto to make
-            # sure they exist
-            my $arglist = {};   # args for match_field
-            for my $sid (@scheduleids) {
-                if ($cgi->param("mailto_type_$sid") == MAILTO_USER) {
-                    $arglist->{"mailto_$sid"} = {
-                        'type' => 'single',
-                    };
-                }
-            }
-            if (scalar %{$arglist}) {
-                Bugzilla::User::match_field($arglist);
-            }
+          # the schedule table must be locked before altering
+          $sth
+            = $dbh->prepare("INSERT INTO whine_schedules "
+              . "(eventid, mailto_type, mailto, "
+              . "run_day, run_time) "
+              . "VALUES (?, ?, ?, 'Sun', 2)");
+          $sth->execute($eventid, MAILTO_USER, $userid);
+        }
 
-            for my $sid (@scheduleids) {
-                if ($cgi->param("remove_schedule_$sid")) {
-                    # having the assignee id in here is a security failsafe
-                    $sth = $dbh->prepare("SELECT whine_schedules.id " .
-                                         "FROM whine_schedules " .
-                                         "LEFT JOIN whine_events " .
-                                         "ON whine_events.id = " .
-                                         "whine_schedules.eventid " .
-                                         "WHERE whine_events.owner_userid=? " .
-                                         "AND whine_schedules.id =?");
-                    $sth->execute($userid, $sid);
-
-                    my @ids = @{$sth->fetchall_arrayref};
-                    for (@ids) {
-                        $sth = $dbh->prepare("DELETE FROM whine_schedules " .
-                                             "WHERE id=?");
-                        $sth->execute($_->[0]);
-                    }
-                }
-                else {
-                    my $o_day         = $cgi->param("orig_day_$sid") || '';
-                    my $day           = $cgi->param("day_$sid") || '';
-                    my $o_time        = $cgi->param("orig_time_$sid") || 0;
-                    my $time          = $cgi->param("time_$sid") || 0;
-                    my $o_mailto      = $cgi->param("orig_mailto_$sid") || '';
-                    my $mailto        = $cgi->param("mailto_$sid") || '';
-                    my $o_mailto_type = $cgi->param("orig_mailto_type_$sid") || 0;
-                    my $mailto_type   = $cgi->param("mailto_type_$sid") || 0;
-
-                    my $mailto_id = $userid;
-
-                    # get an id for the mailto address
-                    if ($can_mail_others && $mailto) {
-                        if ($mailto_type == MAILTO_USER) {
-                            $mailto_id = login_to_id($mailto);
-                        }
-                        elsif ($mailto_type == MAILTO_GROUP) {
-                            # The group name is used in a placeholder.
-                            trick_taint($mailto);
-                            $mailto_id = Bugzilla::Group::ValidateGroupName($mailto, ($user))
-                                           || ThrowUserError('invalid_group_name', { name => $mailto });
-                        }
-                        else {
-                            # bad value, so it will just mail to the whine
-                            # owner.  $mailto_id was already set above.
-                            $mailto_type = MAILTO_USER;
-                        }
-                    }
-
-                    detaint_natural($mailto_type);
-
-                    if ( ($o_day  ne $day) ||
-                         ($o_time ne $time) ||
-                         ($o_mailto ne $mailto) ||
-                         ($o_mailto_type != $mailto_type) ){
-
-                        trick_taint($day);
-                        trick_taint($time);
-
-                        # the schedule table must be locked
-                        $sth = $dbh->prepare("UPDATE whine_schedules " .
-                                             "SET run_day=?, run_time=?, " .
-                                             "mailto_type=?, mailto=?, " .
-                                             "run_next=NULL " .
-                                             "WHERE id=?");
-                        $sth->execute($day, $time, $mailto_type,
-                                      $mailto_id, $sid);
-                    }
-                }
+        # add a query
+        elsif ($cgi->param("add_query_$eventid")) {
+          $sth
+            = $dbh->prepare("INSERT INTO whine_queries " . "(eventid) " . "VALUES (?)");
+          $sth->execute($eventid);
+        }
+      }
+
+      # now check all of the schedules and queries to see if they need
+      # to be altered or deleted
+
+      # Check schedules for changes
+      my $schedules = Bugzilla::Whine::Schedule->match({eventid => $eventid});
+      my @scheduleids = ();
+      foreach my $schedule (@$schedules) {
+        push @scheduleids, $schedule->id;
+      }
+
+      # we need to double-check all of the user IDs in mailto to make
+      # sure they exist
+      my $arglist = {};    # args for match_field
+      for my $sid (@scheduleids) {
+        if ($cgi->param("mailto_type_$sid") == MAILTO_USER) {
+          $arglist->{"mailto_$sid"} = {'type' => 'single',};
+        }
+      }
+      if (scalar %{$arglist}) {
+        Bugzilla::User::match_field($arglist);
+      }
+
+      for my $sid (@scheduleids) {
+        if ($cgi->param("remove_schedule_$sid")) {
+
+          # having the assignee id in here is a security failsafe
+          $sth
+            = $dbh->prepare("SELECT whine_schedules.id "
+              . "FROM whine_schedules "
+              . "LEFT JOIN whine_events "
+              . "ON whine_events.id = "
+              . "whine_schedules.eventid "
+              . "WHERE whine_events.owner_userid=? "
+              . "AND whine_schedules.id =?");
+          $sth->execute($userid, $sid);
+
+          my @ids = @{$sth->fetchall_arrayref};
+          for (@ids) {
+            $sth = $dbh->prepare("DELETE FROM whine_schedules " . "WHERE id=?");
+            $sth->execute($_->[0]);
+          }
+        }
+        else {
+          my $o_day         = $cgi->param("orig_day_$sid")         || '';
+          my $day           = $cgi->param("day_$sid")              || '';
+          my $o_time        = $cgi->param("orig_time_$sid")        || 0;
+          my $time          = $cgi->param("time_$sid")             || 0;
+          my $o_mailto      = $cgi->param("orig_mailto_$sid")      || '';
+          my $mailto        = $cgi->param("mailto_$sid")           || '';
+          my $o_mailto_type = $cgi->param("orig_mailto_type_$sid") || 0;
+          my $mailto_type   = $cgi->param("mailto_type_$sid")      || 0;
+
+          my $mailto_id = $userid;
+
+          # get an id for the mailto address
+          if ($can_mail_others && $mailto) {
+            if ($mailto_type == MAILTO_USER) {
+              $mailto_id = login_to_id($mailto);
             }
+            elsif ($mailto_type == MAILTO_GROUP) {
 
-            # Check queries for changes
-            my $queries = Bugzilla::Whine::Query->match({ eventid => $eventid });
-            for my $query (@$queries) {
-                my $qid = $query->id;
-                if ($cgi->param("remove_query_$qid")) {
-
-                    $sth = $dbh->prepare("SELECT whine_queries.id " .
-                                         "FROM whine_queries " .
-                                         "LEFT JOIN whine_events " .
-                                         "ON whine_events.id = " .
-                                         "whine_queries.eventid " .
-                                         "WHERE whine_events.owner_userid=? " .
-                                         "AND whine_queries.id =?");
-                    $sth->execute($userid, $qid);
-
-                    for (@{$sth->fetchall_arrayref}) {
-                        $sth = $dbh->prepare("DELETE FROM whine_queries " .
-                                             "WHERE id=?");
-                        $sth->execute($_->[0]);
-                    }
-                }
-                else {
-                    my $o_sort      = $cgi->param("orig_query_sort_$qid") || 0;
-                    my $sort        = $cgi->param("query_sort_$qid") || 0;
-                    my $o_queryname = $cgi->param("orig_query_name_$qid") || '';
-                    my $queryname   = $cgi->param("query_name_$qid") || '';
-                    my $o_title     = $cgi->param("orig_query_title_$qid") || '';
-                    my $title       = $cgi->param("query_title_$qid") || '';
-                    my $o_onemailperbug =
-                            $cgi->param("orig_query_onemailperbug_$qid") || 0;
-                    my $onemailperbug   =
-                            $cgi->param("query_onemailperbug_$qid") ? 1 : 0;
-
-                    if ( ($o_sort != $sort) ||
-                         ($o_queryname ne $queryname) ||
-                         ($o_onemailperbug != $onemailperbug) ||
-                         ($o_title ne $title) ){
-
-                        detaint_natural($sort);
-                        trick_taint($queryname);
-                        trick_taint($title);
-
-                        $sth = $dbh->prepare("UPDATE whine_queries " .
-                                             "SET sortkey=?, " .
-                                             "query_name=?, " .
-                                             "title=?, " .
-                                             "onemailperbug=? " .
-                                             "WHERE id=?");
-                        $sth->execute($sort, $queryname, $title,
-                                      $onemailperbug, $qid);
-                    }
-                }
+              # The group name is used in a placeholder.
+              trick_taint($mailto);
+              $mailto_id = Bugzilla::Group::ValidateGroupName($mailto, ($user))
+                || ThrowUserError('invalid_group_name', {name => $mailto});
             }
+            else {
+              # bad value, so it will just mail to the whine
+              # owner.  $mailto_id was already set above.
+              $mailto_type = MAILTO_USER;
+            }
+          }
+
+          detaint_natural($mailto_type);
+
+          if ( ($o_day ne $day)
+            || ($o_time ne $time)
+            || ($o_mailto ne $mailto)
+            || ($o_mailto_type != $mailto_type))
+          {
+
+            trick_taint($day);
+            trick_taint($time);
+
+            # the schedule table must be locked
+            $sth
+              = $dbh->prepare("UPDATE whine_schedules "
+                . "SET run_day=?, run_time=?, "
+                . "mailto_type=?, mailto=?, "
+                . "run_next=NULL "
+                . "WHERE id=?");
+            $sth->execute($day, $time, $mailto_type, $mailto_id, $sid);
+          }
+        }
+      }
+
+      # Check queries for changes
+      my $queries = Bugzilla::Whine::Query->match({eventid => $eventid});
+      for my $query (@$queries) {
+        my $qid = $query->id;
+        if ($cgi->param("remove_query_$qid")) {
+
+          $sth
+            = $dbh->prepare("SELECT whine_queries.id "
+              . "FROM whine_queries "
+              . "LEFT JOIN whine_events "
+              . "ON whine_events.id = "
+              . "whine_queries.eventid "
+              . "WHERE whine_events.owner_userid=? "
+              . "AND whine_queries.id =?");
+          $sth->execute($userid, $qid);
+
+          for (@{$sth->fetchall_arrayref}) {
+            $sth = $dbh->prepare("DELETE FROM whine_queries " . "WHERE id=?");
+            $sth->execute($_->[0]);
+          }
         }
+        else {
+          my $o_sort          = $cgi->param("orig_query_sort_$qid")          || 0;
+          my $sort            = $cgi->param("query_sort_$qid")               || 0;
+          my $o_queryname     = $cgi->param("orig_query_name_$qid")          || '';
+          my $queryname       = $cgi->param("query_name_$qid")               || '';
+          my $o_title         = $cgi->param("orig_query_title_$qid")         || '';
+          my $title           = $cgi->param("query_title_$qid")              || '';
+          my $o_onemailperbug = $cgi->param("orig_query_onemailperbug_$qid") || 0;
+          my $onemailperbug = $cgi->param("query_onemailperbug_$qid") ? 1 : 0;
+
+          if ( ($o_sort != $sort)
+            || ($o_queryname ne $queryname)
+            || ($o_onemailperbug != $onemailperbug)
+            || ($o_title ne $title))
+          {
+
+            detaint_natural($sort);
+            trick_taint($queryname);
+            trick_taint($title);
+
+            $sth
+              = $dbh->prepare("UPDATE whine_queries "
+                . "SET sortkey=?, "
+                . "query_name=?, "
+                . "title=?, "
+                . "onemailperbug=? "
+                . "WHERE id=?");
+            $sth->execute($sort, $queryname, $title, $onemailperbug, $qid);
+          }
+        }
+      }
     }
-    delete_token($token);
+  }
+  delete_token($token);
 }
 
 $vars->{'mail_others'} = $can_mail_others;
@@ -336,44 +340,43 @@ $events = get_events($userid);
 #
 # build the whine list by event id
 for my $event_id (keys %{$events}) {
-    $events->{$event_id}->{'schedule'} = [];
-    $events->{$event_id}->{'queries'} = [];
-
-    # schedules
-    my $schedules = Bugzilla::Whine::Schedule->match({ eventid => $event_id });
-    foreach my $schedule (@$schedules) {
-        my $mailto_type = $schedule->mailto_is_group ? MAILTO_GROUP 
-                                                     : MAILTO_USER;
-        my $mailto = '';
-        if ($mailto_type == MAILTO_USER) {
-            $mailto = $schedule->mailto->login;
-        }
-        elsif ($mailto_type == MAILTO_GROUP) {
-            $mailto = $schedule->mailto->name;
-        }
-
-        push @{$events->{$event_id}->{'schedule'}},
-             {
-                 'day'         => $schedule->run_day,
-                 'time'        => $schedule->run_time,
-                 'mailto_type' => $mailto_type,
-                 'mailto'      => $mailto,
-                 'id'          => $schedule->id,
-             };
+  $events->{$event_id}->{'schedule'} = [];
+  $events->{$event_id}->{'queries'}  = [];
+
+  # schedules
+  my $schedules = Bugzilla::Whine::Schedule->match({eventid => $event_id});
+  foreach my $schedule (@$schedules) {
+    my $mailto_type = $schedule->mailto_is_group ? MAILTO_GROUP : MAILTO_USER;
+    my $mailto = '';
+    if ($mailto_type == MAILTO_USER) {
+      $mailto = $schedule->mailto->login;
     }
-
-    # queries
-    my $queries = Bugzilla::Whine::Query->match({ eventid => $event_id });
-    for my $query (@$queries) {
-        push @{$events->{$event_id}->{'queries'}}, 
-             {
-                 'name'          => $query->name,
-                 'title'         => $query->title,
-                 'sort'          => $query->sortkey,
-                 'id'            => $query->id,
-                 'onemailperbug' => $query->one_email_per_bug,
-             };
+    elsif ($mailto_type == MAILTO_GROUP) {
+      $mailto = $schedule->mailto->name;
     }
+
+    push @{$events->{$event_id}->{'schedule'}},
+      {
+      'day'         => $schedule->run_day,
+      'time'        => $schedule->run_time,
+      'mailto_type' => $mailto_type,
+      'mailto'      => $mailto,
+      'id'          => $schedule->id,
+      };
+  }
+
+  # queries
+  my $queries = Bugzilla::Whine::Query->match({eventid => $event_id});
+  for my $query (@$queries) {
+    push @{$events->{$event_id}->{'queries'}},
+      {
+      'name'          => $query->name,
+      'title'         => $query->title,
+      'sort'          => $query->sortkey,
+      'id'            => $query->id,
+      'onemailperbug' => $query->one_email_per_bug,
+      };
+  }
 }
 
 $vars->{'events'} = $events;
@@ -384,10 +387,11 @@ $sth->execute($userid);
 
 $vars->{'available_queries'} = [];
 while (my ($query) = $sth->fetchrow_array) {
-    push @{$vars->{'available_queries'}}, $query;
+  push @{$vars->{'available_queries'}}, $query;
 }
 $vars->{'token'} = issue_session_token('edit_whine');
-$vars->{'local_timezone'} = Bugzilla->local_timezone->short_name_for_datetime(DateTime->now());
+$vars->{'local_timezone'}
+  = Bugzilla->local_timezone->short_name_for_datetime(DateTime->now());
 
 $template->process("whine/schedule.html.tmpl", $vars)
   || ThrowTemplateError($template->error());
@@ -395,9 +399,9 @@ $template->process("whine/schedule.html.tmpl", $vars)
 # get_events takes a userid and returns a hash of
 # Bugzilla::Whine objects keyed by event ID.
 sub get_events {
-    my $userid = shift;
-    my $event_rows = Bugzilla::Whine->match({ owner_userid => $userid });
-    my %events = map { $_->{id} => $_ } @$event_rows;
+  my $userid     = shift;
+  my $event_rows = Bugzilla::Whine->match({owner_userid => $userid});
+  my %events     = map { $_->{id} => $_ } @$event_rows;
 
-    return \%events;
+  return \%events;
 }
index 583bce6889945fad466c117598405610646444ec..46f41803115590f80c0741d570dd15da61c69b8a 100755 (executable)
@@ -18,123 +18,134 @@ use Bugzilla::Error;
 use Bugzilla::Token;
 use Bugzilla::Status;
 
-my $cgi = Bugzilla->cgi;
-my $dbh = Bugzilla->dbh;
+my $cgi  = Bugzilla->cgi;
+my $dbh  = Bugzilla->dbh;
 my $user = Bugzilla->login(LOGIN_REQUIRED);
 
 print $cgi->header();
 
 $user->in_group('admin')
-  || ThrowUserError('auth_failure', {group  => 'admin',
-                                     action => 'modify',
-                                     object => 'workflow'});
+  || ThrowUserError('auth_failure',
+  {group => 'admin', action => 'modify', object => 'workflow'});
 
 my $action = $cgi->param('action') || 'edit';
 my $token = $cgi->param('token');
 
 sub get_workflow {
-    my $dbh = Bugzilla->dbh;
-    my $workflow = $dbh->selectall_arrayref('SELECT old_status, new_status, require_comment
-                                             FROM status_workflow');
-    my %workflow;
-    foreach my $row (@$workflow) {
-        my ($old, $new, $type) = @$row;
-        $workflow{$old || 0}{$new} = $type;
-    }
-    return \%workflow;
+  my $dbh      = Bugzilla->dbh;
+  my $workflow = $dbh->selectall_arrayref(
+    'SELECT old_status, new_status, require_comment
+                                             FROM status_workflow'
+  );
+  my %workflow;
+  foreach my $row (@$workflow) {
+    my ($old, $new, $type) = @$row;
+    $workflow{$old || 0}{$new} = $type;
+  }
+  return \%workflow;
 }
 
 sub load_template {
-    my ($filename, $message) = @_;
-    my $template = Bugzilla->template;
-    my $vars = {};
-
-    $vars->{'statuses'} = [Bugzilla::Status->get_all];
-    $vars->{'workflow'} = get_workflow();
-    $vars->{'token'} = issue_session_token("workflow_$filename");
-    $vars->{'message'} = $message;
-
-    $template->process("admin/workflow/$filename.html.tmpl", $vars)
-      || ThrowTemplateError($template->error());
-    exit;
+  my ($filename, $message) = @_;
+  my $template = Bugzilla->template;
+  my $vars     = {};
+
+  $vars->{'statuses'} = [Bugzilla::Status->get_all];
+  $vars->{'workflow'} = get_workflow();
+  $vars->{'token'}    = issue_session_token("workflow_$filename");
+  $vars->{'message'}  = $message;
+
+  $template->process("admin/workflow/$filename.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
+  exit;
 }
 
 if ($action eq 'edit') {
-    load_template('edit');
+  load_template('edit');
 }
 elsif ($action eq 'update') {
-    check_token_data($token, 'workflow_edit');
-    my $statuses = [Bugzilla::Status->get_all];
-    my $workflow = get_workflow();
-
-    my $sth_insert = $dbh->prepare('INSERT INTO status_workflow (old_status, new_status)
-                                    VALUES (?, ?)');
-    my $sth_delete = $dbh->prepare('DELETE FROM status_workflow
-                                    WHERE old_status = ? AND new_status = ?');
-    my $sth_delnul = $dbh->prepare('DELETE FROM status_workflow
-                                    WHERE old_status IS NULL AND new_status = ?');
-
-    # Part 1: Initial bug statuses.
-    foreach my $new (@$statuses) {
-        if ($cgi->param('w_0_' . $new->id)) {
-            $sth_insert->execute(undef, $new->id)
-              unless defined $workflow->{0}->{$new->id};
-        }
-        else {
-            $sth_delnul->execute($new->id);
-        }
+  check_token_data($token, 'workflow_edit');
+  my $statuses = [Bugzilla::Status->get_all];
+  my $workflow = get_workflow();
+
+  my $sth_insert = $dbh->prepare(
+    'INSERT INTO status_workflow (old_status, new_status)
+                                    VALUES (?, ?)'
+  );
+  my $sth_delete = $dbh->prepare(
+    'DELETE FROM status_workflow
+                                    WHERE old_status = ? AND new_status = ?'
+  );
+  my $sth_delnul = $dbh->prepare(
+    'DELETE FROM status_workflow
+                                    WHERE old_status IS NULL AND new_status = ?'
+  );
+
+  # Part 1: Initial bug statuses.
+  foreach my $new (@$statuses) {
+    if ($cgi->param('w_0_' . $new->id)) {
+      $sth_insert->execute(undef, $new->id) unless defined $workflow->{0}->{$new->id};
     }
+    else {
+      $sth_delnul->execute($new->id);
+    }
+  }
 
-    # Part 2: Bug status changes.
-    foreach my $old (@$statuses) {
-        foreach my $new (@$statuses) {
-            next if $old->id == $new->id;
-
-            # All transitions to 'duplicate_or_move_bug_status' must be valid.
-            if ($cgi->param('w_' . $old->id . '_' . $new->id)
-                || ($new->name eq Bugzilla->params->{'duplicate_or_move_bug_status'}))
-            {
-                $sth_insert->execute($old->id, $new->id)
-                  unless defined $workflow->{$old->id}->{$new->id};
-            }
-            else {
-                $sth_delete->execute($old->id, $new->id);
-            }
-        }
+  # Part 2: Bug status changes.
+  foreach my $old (@$statuses) {
+    foreach my $new (@$statuses) {
+      next if $old->id == $new->id;
+
+      # All transitions to 'duplicate_or_move_bug_status' must be valid.
+      if ($cgi->param('w_' . $old->id . '_' . $new->id)
+        || ($new->name eq Bugzilla->params->{'duplicate_or_move_bug_status'}))
+      {
+        $sth_insert->execute($old->id, $new->id)
+          unless defined $workflow->{$old->id}->{$new->id};
+      }
+      else {
+        $sth_delete->execute($old->id, $new->id);
+      }
     }
-    delete_token($token);
-    load_template('edit', 'workflow_updated');
+  }
+  delete_token($token);
+  load_template('edit', 'workflow_updated');
 }
 elsif ($action eq 'edit_comment') {
-    load_template('comment');
+  load_template('comment');
 }
 elsif ($action eq 'update_comment') {
-    check_token_data($token, 'workflow_comment');
-    my $workflow = get_workflow();
-
-    my $sth_update = $dbh->prepare('UPDATE status_workflow SET require_comment = ?
-                                    WHERE old_status = ? AND new_status = ?');
-    my $sth_updnul = $dbh->prepare('UPDATE status_workflow SET require_comment = ?
-                                    WHERE old_status IS NULL AND new_status = ?');
-
-    foreach my $old (keys %$workflow) {
-        # Hashes cannot have undef as a key, so we use 0. But the DB
-        # must store undef, for referential integrity.
-        my $old_id_for_db = $old || undef;
-        foreach my $new (keys %{$workflow->{$old}}) {
-            my $comment_required = $cgi->param("c_${old}_$new") ? 1 : 0;
-            next if ($workflow->{$old}->{$new} == $comment_required);
-            if ($old_id_for_db) {
-                $sth_update->execute($comment_required, $old_id_for_db, $new);
-            }
-            else {
-                $sth_updnul->execute($comment_required, $new);
-            }
-        }
+  check_token_data($token, 'workflow_comment');
+  my $workflow = get_workflow();
+
+  my $sth_update = $dbh->prepare(
+    'UPDATE status_workflow SET require_comment = ?
+                                    WHERE old_status = ? AND new_status = ?'
+  );
+  my $sth_updnul = $dbh->prepare(
+    'UPDATE status_workflow SET require_comment = ?
+                                    WHERE old_status IS NULL AND new_status = ?'
+  );
+
+  foreach my $old (keys %$workflow) {
+
+    # Hashes cannot have undef as a key, so we use 0. But the DB
+    # must store undef, for referential integrity.
+    my $old_id_for_db = $old || undef;
+    foreach my $new (keys %{$workflow->{$old}}) {
+      my $comment_required = $cgi->param("c_${old}_$new") ? 1 : 0;
+      next if ($workflow->{$old}->{$new} == $comment_required);
+      if ($old_id_for_db) {
+        $sth_update->execute($comment_required, $old_id_for_db, $new);
+      }
+      else {
+        $sth_updnul->execute($comment_required, $new);
+      }
     }
-    delete_token($token);
-    load_template('comment', 'workflow_updated');
+  }
+  delete_token($token);
+  load_template('comment', 'workflow_updated');
 }
 else {
-    ThrowUserError('unknown_action', {action => $action});
+  ThrowUserError('unknown_action', {action => $action});
 }
index cb3c4883e8d99874a8cf715a19821561624800f0..c9b417ad25775a2f3ed7d69989f875c51b0747d7 100755 (executable)
@@ -14,10 +14,11 @@ use warnings;
 # run from this one so that it can find its modules.
 use Cwd qw(abs_path);
 use File::Basename qw(dirname);
+
 BEGIN {
-    # Untaint the abs_path.
-    my ($a) = abs_path($0) =~ /^(.*)$/;
-    chdir dirname($a);
+  # Untaint the abs_path.
+  my ($a) = abs_path($0) =~ /^(.*)$/;
+  chdir dirname($a);
 }
 
 use lib qw(. lib local/lib/perl5);
@@ -57,10 +58,10 @@ use constant SIGNATURE_DELIMITER => '-- ';
 # These MIME types represent a "body" of an email if they have an
 # "inline" Content-Disposition (or no content disposition).
 use constant BODY_TYPES => qw(
-    text/plain
-    text/html
-    application/xhtml+xml
-    multipart/alternative
+  text/plain
+  text/html
+  application/xhtml+xml
+  multipart/alternative
 );
 
 # $input_email is a global so that it can be used in die_handler.
@@ -71,239 +72,249 @@ our ($input_email, %switch);
 ####################
 
 sub parse_mail {
-    my ($mail_text) = @_;
-    debug_print('Parsing Email');
-    $input_email = Email::MIME->new($mail_text);
-    
-    my %fields = %{ $switch{'default'} || {} };
-    Bugzilla::Hook::process('email_in_before_parse', { mail => $input_email,
-                                                       fields => \%fields });
-
-    my $summary = $input_email->header('Subject');
-    if ($summary =~ /\[\S+ (\d+)\](.*)/i) {
-        $fields{'bug_id'} = $1;
-        $summary = trim($2);
+  my ($mail_text) = @_;
+  debug_print('Parsing Email');
+  $input_email = Email::MIME->new($mail_text);
+
+  my %fields = %{$switch{'default'} || {}};
+  Bugzilla::Hook::process('email_in_before_parse',
+    {mail => $input_email, fields => \%fields});
+
+  my $summary = $input_email->header('Subject');
+  if ($summary =~ /\[\S+ (\d+)\](.*)/i) {
+    $fields{'bug_id'} = $1;
+    $summary = trim($2);
+  }
+
+  # Ignore automatic replies.
+  # XXX - Improve the way to detect such subjects in different languages.
+  my $auto_submitted = $input_email->header('Auto-Submitted') || '';
+  if ($summary =~ /out of( the)? office/i || $auto_submitted eq 'auto-replied') {
+    debug_print("Automatic reply detected: $summary");
+    exit;
+  }
+
+  my ($body, $attachments) = get_body_and_attachments($input_email);
+
+  debug_print("Body:\n" . $body, 3);
+
+  $body = remove_leading_blank_lines($body);
+  my @body_lines = split(/\r?\n/s, $body);
+
+  # If there are fields specified.
+  if ($body =~ /^\s*@/s) {
+    my $current_field;
+    while (my $line = shift @body_lines) {
+
+      # If the sig is starting, we want to keep this in the
+      # @body_lines so that we don't keep the sig as part of the
+      # comment down below.
+      if ($line eq SIGNATURE_DELIMITER) {
+        unshift(@body_lines, $line);
+        last;
+      }
+
+      # Otherwise, we stop parsing fields on the first blank line.
+      $line = trim($line);
+      last if !$line;
+      if ($line =~ /^\@(\w+)\s*(?:=|\s|$)\s*(.*)\s*/) {
+        $current_field = lc($1);
+        $fields{$current_field} = $2;
+      }
+      else {
+        $fields{$current_field} .= " $line";
+      }
     }
-
-    # Ignore automatic replies.
-    # XXX - Improve the way to detect such subjects in different languages.
-    my $auto_submitted = $input_email->header('Auto-Submitted') || '';
-    if ($summary =~ /out of( the)? office/i || $auto_submitted eq 'auto-replied') {
-        debug_print("Automatic reply detected: $summary");
-        exit;
+  }
+
+  %fields = %{Bugzilla::Bug::map_fields(\%fields)};
+
+  my ($reporter) = Email::Address->parse($input_email->header('From'));
+  $fields{'reporter'} = $reporter->address;
+
+  # The summary line only affects us if we're doing a post_bug.
+  # We have to check it down here because there might have been
+  # a bug_id specified in the body of the email.
+  if (!$fields{'bug_id'} && !$fields{'short_desc'}) {
+    $fields{'short_desc'} = $summary;
+  }
+
+  # The Importance/X-Priority headers are only used when creating a new bug.
+  # 1) If somebody specifies a priority, use it.
+  # 2) If there is an Importance or X-Priority header, use it as
+  #    something that is relative to the default priority.
+  #    If the value is High or 1, increase the priority by 1.
+  #    If the value is Low or 5, decrease the priority by 1.
+  # 3) Otherwise, use the default priority.
+  # Note: this will only work if the 'letsubmitterchoosepriority'
+  # parameter is enabled.
+  my $importance
+    = $input_email->header('Importance') || $input_email->header('X-Priority');
+  if (!$fields{'bug_id'} && !$fields{'priority'} && $importance) {
+    my @legal_priorities = @{get_legal_field_values('priority')};
+    my $i
+      = firstidx { $_ eq Bugzilla->params->{'defaultpriority'} } @legal_priorities;
+    if ($importance =~ /(high|[12])/i) {
+      $i-- unless $i == 0;
     }
-
-    my ($body, $attachments) = get_body_and_attachments($input_email);
-
-    debug_print("Body:\n" . $body, 3);
-
-    $body = remove_leading_blank_lines($body);
-    my @body_lines = split(/\r?\n/s, $body);
-
-    # If there are fields specified.
-    if ($body =~ /^\s*@/s) {
-        my $current_field;
-        while (my $line = shift @body_lines) {
-            # If the sig is starting, we want to keep this in the 
-            # @body_lines so that we don't keep the sig as part of the 
-            # comment down below.
-            if ($line eq SIGNATURE_DELIMITER) {
-                unshift(@body_lines, $line);
-                last;
-            }
-            # Otherwise, we stop parsing fields on the first blank line.
-            $line = trim($line);
-            last if !$line;
-            if ($line =~ /^\@(\w+)\s*(?:=|\s|$)\s*(.*)\s*/) {
-                $current_field = lc($1);
-                $fields{$current_field} = $2;
-            }
-            else {
-                $fields{$current_field} .= " $line";
-            }
-        }
+    elsif ($importance =~ /(low|[45])/i) {
+      $i++ unless $i == $#legal_priorities;
     }
+    $fields{'priority'} = $legal_priorities[$i];
+  }
 
-    %fields = %{ Bugzilla::Bug::map_fields(\%fields) };
+  my $comment = '';
 
-    my ($reporter) = Email::Address->parse($input_email->header('From'));
-    $fields{'reporter'} = $reporter->address;
+  # Get the description, except the signature.
+  foreach my $line (@body_lines) {
+    last if $line eq SIGNATURE_DELIMITER;
+    $comment .= "$line\n";
+  }
+  $fields{'comment'} = $comment;
 
-    # The summary line only affects us if we're doing a post_bug.
-    # We have to check it down here because there might have been
-    # a bug_id specified in the body of the email.
-    if (!$fields{'bug_id'} && !$fields{'short_desc'}) {
-        $fields{'short_desc'} = $summary;
-    }
+  my %override = %{$switch{'override'} || {}};
+  foreach my $key (keys %override) {
+    $fields{$key} = $override{$key};
+  }
 
-    # The Importance/X-Priority headers are only used when creating a new bug.
-    # 1) If somebody specifies a priority, use it.
-    # 2) If there is an Importance or X-Priority header, use it as
-    #    something that is relative to the default priority.
-    #    If the value is High or 1, increase the priority by 1.
-    #    If the value is Low or 5, decrease the priority by 1.
-    # 3) Otherwise, use the default priority.
-    # Note: this will only work if the 'letsubmitterchoosepriority'
-    # parameter is enabled.
-    my $importance = $input_email->header('Importance')
-                     || $input_email->header('X-Priority');
-    if (!$fields{'bug_id'} && !$fields{'priority'} && $importance) {
-        my @legal_priorities = @{get_legal_field_values('priority')};
-        my $i = firstidx { $_ eq Bugzilla->params->{'defaultpriority'} } @legal_priorities;
-        if ($importance =~ /(high|[12])/i) {
-            $i-- unless $i == 0;
-        }
-        elsif ($importance =~ /(low|[45])/i) {
-            $i++ unless $i == $#legal_priorities;
-        }
-        $fields{'priority'} = $legal_priorities[$i];
-    }
+  debug_print("Parsed Fields:\n" . Dumper(\%fields), 2);
 
-    my $comment = '';
-    # Get the description, except the signature.
-    foreach my $line (@body_lines) {
-        last if $line eq SIGNATURE_DELIMITER;
-        $comment .= "$line\n";
-    }
-    $fields{'comment'} = $comment;
+  debug_print("Attachments:\n" . Dumper($attachments), 3);
+  if (@$attachments) {
+    $fields{'attachments'} = $attachments;
+  }
 
-    my %override = %{ $switch{'override'} || {} };
-    foreach my $key (keys %override) {
-        $fields{$key} = $override{$key};
-    }
-
-    debug_print("Parsed Fields:\n" . Dumper(\%fields), 2);
-
-    debug_print("Attachments:\n" . Dumper($attachments), 3);
-    if (@$attachments) {
-        $fields{'attachments'} = $attachments;
-    }
-
-    return \%fields;
+  return \%fields;
 }
 
 sub check_email_fields {
-    my ($fields) = @_;
-
-    my ($retval, $non_conclusive_fields) =
-      Bugzilla::User::match_field({
-        'assigned_to'   => { 'type' => 'single' },
-        'qa_contact'    => { 'type' => 'single' },
-        'cc'            => { 'type' => 'multi'  },
-        'newcc'         => { 'type' => 'multi'  }
-      }, $fields, MATCH_SKIP_CONFIRM);
-
-    if ($retval != USER_MATCH_SUCCESS) {
-        ThrowUserError('user_match_too_many', {fields => $non_conclusive_fields});
-    }
+  my ($fields) = @_;
+
+  my ($retval, $non_conclusive_fields) = Bugzilla::User::match_field(
+    {
+      'assigned_to' => {'type' => 'single'},
+      'qa_contact'  => {'type' => 'single'},
+      'cc'          => {'type' => 'multi'},
+      'newcc'       => {'type' => 'multi'}
+    },
+    $fields,
+    MATCH_SKIP_CONFIRM
+  );
+
+  if ($retval != USER_MATCH_SUCCESS) {
+    ThrowUserError('user_match_too_many', {fields => $non_conclusive_fields});
+  }
 }
 
 sub post_bug {
-    my ($fields) = @_;
-    debug_print('Posting a new bug...');
+  my ($fields) = @_;
+  debug_print('Posting a new bug...');
 
-    my $user = Bugzilla->user;
+  my $user = Bugzilla->user;
 
-    check_email_fields($fields);
+  check_email_fields($fields);
 
-    my $bug = Bugzilla::Bug->create($fields);
-    debug_print("Created bug " . $bug->id);
-    return ($bug, $bug->comments->[0]);
+  my $bug = Bugzilla::Bug->create($fields);
+  debug_print("Created bug " . $bug->id);
+  return ($bug, $bug->comments->[0]);
 }
 
 sub process_bug {
-    my ($fields_in) = @_; 
-    my %fields = %$fields_in;
-
-    my $bug_id = $fields{'bug_id'};
-    $fields{'id'} = $bug_id;
-    delete $fields{'bug_id'};
-
-    debug_print("Updating Bug $fields{id}...");
-
-    my $bug = Bugzilla::Bug->check($bug_id);
-
-    if ($fields{'bug_status'}) {
-        $fields{'knob'} = $fields{'bug_status'};
-    }
-    # If no status is given, then we only want to change the resolution.
-    elsif ($fields{'resolution'}) {
-        $fields{'knob'} = 'change_resolution';
-        $fields{'resolution_knob_change_resolution'} = $fields{'resolution'};
-    }
-    if ($fields{'dup_id'}) {
-        $fields{'knob'} = 'duplicate';
-    }
-
-    # Move @cc to @newcc as @cc is used by process_bug.cgi to remove
-    # users from the CC list when @removecc is set.
-    $fields{'newcc'} = delete $fields{'cc'} if $fields{'cc'};
-
-    # Make it possible to remove CCs.
-    if ($fields{'removecc'}) {
-        $fields{'cc'} = [split(',', $fields{'removecc'})];
-        $fields{'removecc'} = 1;
-    }
-
-    check_email_fields(\%fields);
-
-    my $cgi = Bugzilla->cgi;
-    foreach my $field (keys %fields) {
-        $cgi->param(-name => $field, -value => $fields{$field});
-    }
-    $cgi->param('token', issue_hash_token([$bug->id, $bug->delta_ts]));
-
-    require 'process_bug.cgi';
-    debug_print("Bug processed.");
-
-    my $added_comment;
-    if (trim($fields{'comment'})) {
-        # The "old" bug object doesn't contain the comment we just added.
-        $added_comment = Bugzilla::Bug->check($bug_id)->comments->[-1];
-    }
-    return ($bug, $added_comment);
+  my ($fields_in) = @_;
+  my %fields = %$fields_in;
+
+  my $bug_id = $fields{'bug_id'};
+  $fields{'id'} = $bug_id;
+  delete $fields{'bug_id'};
+
+  debug_print("Updating Bug $fields{id}...");
+
+  my $bug = Bugzilla::Bug->check($bug_id);
+
+  if ($fields{'bug_status'}) {
+    $fields{'knob'} = $fields{'bug_status'};
+  }
+
+  # If no status is given, then we only want to change the resolution.
+  elsif ($fields{'resolution'}) {
+    $fields{'knob'}                              = 'change_resolution';
+    $fields{'resolution_knob_change_resolution'} = $fields{'resolution'};
+  }
+  if ($fields{'dup_id'}) {
+    $fields{'knob'} = 'duplicate';
+  }
+
+  # Move @cc to @newcc as @cc is used by process_bug.cgi to remove
+  # users from the CC list when @removecc is set.
+  $fields{'newcc'} = delete $fields{'cc'} if $fields{'cc'};
+
+  # Make it possible to remove CCs.
+  if ($fields{'removecc'}) {
+    $fields{'cc'} = [split(',', $fields{'removecc'})];
+    $fields{'removecc'} = 1;
+  }
+
+  check_email_fields(\%fields);
+
+  my $cgi = Bugzilla->cgi;
+  foreach my $field (keys %fields) {
+    $cgi->param(-name => $field, -value => $fields{$field});
+  }
+  $cgi->param('token', issue_hash_token([$bug->id, $bug->delta_ts]));
+
+  require 'process_bug.cgi';
+  debug_print("Bug processed.");
+
+  my $added_comment;
+  if (trim($fields{'comment'})) {
+
+    # The "old" bug object doesn't contain the comment we just added.
+    $added_comment = Bugzilla::Bug->check($bug_id)->comments->[-1];
+  }
+  return ($bug, $added_comment);
 }
 
 sub handle_attachments {
-    my ($bug, $attachments, $comment) = @_;
-    return if !$attachments;
-    debug_print("Handling attachments...");
-    my $dbh = Bugzilla->dbh;
-    $dbh->bz_start_transaction();
-    my ($update_comment, $update_bug);
-    foreach my $attachment (@$attachments) {
-        debug_print("Inserting Attachment: " . Dumper($attachment), 3);
-        my $type = $attachment->content_type || 'application/octet-stream';
-        # MUAs add stuff like "name=" to content-type that we really don't
-        # want.
-        $type =~ s/;.*//;
-        my $obj = Bugzilla::Attachment->create({
-            bug         => $bug,
-            description => $attachment->filename(1),
-            filename    => $attachment->filename(1),
-            mimetype    => $type,
-            data        => $attachment->body,
-        });
-        # If we added a comment, and our comment does not already have a type,
-        # and this is our first attachment, then we make the comment an 
-        # "attachment created" comment.
-        if ($comment and !$comment->type and !$update_comment) {
-            $comment->set_all({ type       => CMT_ATTACHMENT_CREATED, 
-                                extra_data => $obj->id });
-            $update_comment = 1;
-        }
-        else {
-            $bug->add_comment('', { type => CMT_ATTACHMENT_CREATED,
-                                    extra_data => $obj->id });
-            $update_bug = 1;
-        }
+  my ($bug, $attachments, $comment) = @_;
+  return if !$attachments;
+  debug_print("Handling attachments...");
+  my $dbh = Bugzilla->dbh;
+  $dbh->bz_start_transaction();
+  my ($update_comment, $update_bug);
+  foreach my $attachment (@$attachments) {
+    debug_print("Inserting Attachment: " . Dumper($attachment), 3);
+    my $type = $attachment->content_type || 'application/octet-stream';
+
+    # MUAs add stuff like "name=" to content-type that we really don't
+    # want.
+    $type =~ s/;.*//;
+    my $obj = Bugzilla::Attachment->create({
+      bug         => $bug,
+      description => $attachment->filename(1),
+      filename    => $attachment->filename(1),
+      mimetype    => $type,
+      data        => $attachment->body,
+    });
+
+    # If we added a comment, and our comment does not already have a type,
+    # and this is our first attachment, then we make the comment an
+    # "attachment created" comment.
+    if ($comment and !$comment->type and !$update_comment) {
+      $comment->set_all({type => CMT_ATTACHMENT_CREATED, extra_data => $obj->id});
+      $update_comment = 1;
     }
-    # We only update the comments and bugs at the end of the transaction,
-    # because doing so modifies bugs_fulltext, which is a non-transactional
-    # table.
-    $bug->update() if $update_bug;
-    $comment->update() if $update_comment;
-    $dbh->bz_commit_transaction();
+    else {
+      $bug->add_comment('', {type => CMT_ATTACHMENT_CREATED, extra_data => $obj->id});
+      $update_bug = 1;
+    }
+  }
+
+  # We only update the comments and bugs at the end of the transaction,
+  # because doing so modifies bugs_fulltext, which is a non-transactional
+  # table.
+  $bug->update()     if $update_bug;
+  $comment->update() if $update_comment;
+  $dbh->bz_commit_transaction();
 }
 
 ######################
@@ -311,178 +322,196 @@ sub handle_attachments {
 ######################
 
 sub debug_print {
-    my ($str, $level) = @_;
-    $level ||= 1;
-    print STDERR "$str\n" if $level <= $switch{'verbose'};
+  my ($str, $level) = @_;
+  $level ||= 1;
+  print STDERR "$str\n" if $level <= $switch{'verbose'};
 }
 
 sub get_body_and_attachments {
-    my ($email) = @_;
-
-    my $ct = $email->content_type || 'text/plain';
-    debug_print("Splitting Body and Attachments [Type: $ct]...", 2);
-
-    my ($bodies, $attachments) = split_body_and_attachments($email);
-    debug_print(scalar(@$bodies) . " body part(s) and " . scalar(@$attachments)
-                . " attachment part(s).");
-    debug_print('Bodies: ' . Dumper($bodies), 3);
-
-    # Get the first part of the email that contains a text body,
-    # and make all the other pieces into attachments. (This handles
-    # people or MUAs who accidentally attach text files as an "inline"
-    # attachment.)
-    my $body;
-    while (@$bodies) {
-        my $possible = shift @$bodies;
-        $body = get_text_alternative($possible);
-        if (defined $body) {
-            unshift(@$attachments, @$bodies);
-            last;
-        }
+  my ($email) = @_;
+
+  my $ct = $email->content_type || 'text/plain';
+  debug_print("Splitting Body and Attachments [Type: $ct]...", 2);
+
+  my ($bodies, $attachments) = split_body_and_attachments($email);
+  debug_print(
+        scalar(@$bodies)
+      . " body part(s) and "
+      . scalar(@$attachments)
+      . " attachment part(s).");
+  debug_print('Bodies: ' . Dumper($bodies), 3);
+
+  # Get the first part of the email that contains a text body,
+  # and make all the other pieces into attachments. (This handles
+  # people or MUAs who accidentally attach text files as an "inline"
+  # attachment.)
+  my $body;
+  while (@$bodies) {
+    my $possible = shift @$bodies;
+    $body = get_text_alternative($possible);
+    if (defined $body) {
+      unshift(@$attachments, @$bodies);
+      last;
     }
+  }
 
-    if (!defined $body) {
-        # Note that this only happens if the email does not contain any
-        # text/plain parts. If the email has an empty text/plain part,
-        # you're fine, and this message does NOT get thrown.
-        ThrowUserError('email_no_body');
-    }
+  if (!defined $body) {
+
+    # Note that this only happens if the email does not contain any
+    # text/plain parts. If the email has an empty text/plain part,
+    # you're fine, and this message does NOT get thrown.
+    ThrowUserError('email_no_body');
+  }
 
-    debug_print("Picked Body:\n$body", 2);
-    
-    return ($body, $attachments);
+  debug_print("Picked Body:\n$body", 2);
+
+  return ($body, $attachments);
 }
 
 sub get_text_alternative {
-    my ($email) = @_;
-
-    my @parts = $email->parts;
-    my $body;
-    foreach my $part (@parts) {
-        my $ct = $part->content_type || 'text/plain';
-        my $charset = 'iso-8859-1';
-        # The charset may be quoted.
-        if ($ct =~ /charset="?([^;"]+)/) {
-            $charset= $1;
-        }
-        debug_print("Alternative Part Content-Type: $ct", 2);
-        debug_print("Alternative Part Character Encoding: $charset", 2);
-        # If we find a text/plain body here, return it immediately.
-        if (!$ct || $ct =~ m{^text/plain}i) {
-            return _decode_body($charset, $part->body);
-        }
-        # If we find a text/html body, decode it, but don't return
-        # it immediately, because there might be a text/plain alternative
-        # later. This could be any HTML type.
-        if ($ct =~ m{^application/xhtml\+xml}i or $ct =~ m{text/html}i) {
-            my $parser = HTML::FormatText::WithLinks->new(
-                # Put footnnote indicators after the text, not before it.
-                before_link => '',
-                after_link  => '[%n]',
-                # Convert bold and italics, use "*" for bold instead of "_".
-                with_emphasis => 1,
-                bold_marker => '*',
-                # If the same link appears multiple times, only create
-                # one footnote.
-                unique_links => 1,
-                # If the link text is the URL, don't create a footnote.
-                skip_linked_urls => 1,
-            );
-            $body = _decode_body($charset, $part->body);
-            $body = $parser->parse($body);
-        }
+  my ($email) = @_;
+
+  my @parts = $email->parts;
+  my $body;
+  foreach my $part (@parts) {
+    my $ct = $part->content_type || 'text/plain';
+    my $charset = 'iso-8859-1';
+
+    # The charset may be quoted.
+    if ($ct =~ /charset="?([^;"]+)/) {
+      $charset = $1;
     }
+    debug_print("Alternative Part Content-Type: $ct",            2);
+    debug_print("Alternative Part Character Encoding: $charset", 2);
 
-    return $body;
+    # If we find a text/plain body here, return it immediately.
+    if (!$ct || $ct =~ m{^text/plain}i) {
+      return _decode_body($charset, $part->body);
+    }
+
+    # If we find a text/html body, decode it, but don't return
+    # it immediately, because there might be a text/plain alternative
+    # later. This could be any HTML type.
+    if ($ct =~ m{^application/xhtml\+xml}i or $ct =~ m{text/html}i) {
+      my $parser = HTML::FormatText::WithLinks->new(
+
+        # Put footnnote indicators after the text, not before it.
+        before_link => '',
+        after_link  => '[%n]',
+
+        # Convert bold and italics, use "*" for bold instead of "_".
+        with_emphasis => 1,
+        bold_marker   => '*',
+
+        # If the same link appears multiple times, only create
+        # one footnote.
+        unique_links => 1,
+
+        # If the link text is the URL, don't create a footnote.
+        skip_linked_urls => 1,
+      );
+      $body = _decode_body($charset, $part->body);
+      $body = $parser->parse($body);
+    }
+  }
+
+  return $body;
 }
 
 sub _decode_body {
-    my ($charset, $body) = @_;
-    return utf8::is_utf8($body) ? $body : Encode::decode($charset, $body);
+  my ($charset, $body) = @_;
+  return utf8::is_utf8($body) ? $body : Encode::decode($charset, $body);
 }
 
 sub remove_leading_blank_lines {
-    my ($text) = @_;
-    $text =~ s/^(\s*\n)+//s;
-    return $text;
+  my ($text) = @_;
+  $text =~ s/^(\s*\n)+//s;
+  return $text;
 }
 
 sub html_strip {
-    my ($var) = @_;
-    # Trivial HTML tag remover (this is just for error messages, really.)
-    $var =~ s/<[^>]*>//g;
-    # And this basically reverses the Template-Toolkit html filter.
-    $var =~ s/\&amp;/\&/g;
-    $var =~ s/\&lt;/</g;
-    $var =~ s/\&gt;/>/g;
-    $var =~ s/\&quot;/\"/g;
-    $var =~ s/&#64;/@/g;
-    # Also remove undesired newlines and consecutive spaces.
-    $var =~ s/[\n\s]+/ /gms;
-    return $var;
+  my ($var) = @_;
+
+  # Trivial HTML tag remover (this is just for error messages, really.)
+  $var =~ s/<[^>]*>//g;
+
+  # And this basically reverses the Template-Toolkit html filter.
+  $var =~ s/\&amp;/\&/g;
+  $var =~ s/\&lt;/</g;
+  $var =~ s/\&gt;/>/g;
+  $var =~ s/\&quot;/\"/g;
+  $var =~ s/&#64;/@/g;
+
+  # Also remove undesired newlines and consecutive spaces.
+  $var =~ s/[\n\s]+/ /gms;
+  return $var;
 }
 
 sub split_body_and_attachments {
-    my ($email) = @_;
-
-    my (@body, @attachments);
-    foreach my $part ($email->parts) {
-        my $ct = lc($part->content_type || 'text/plain');
-        my $disposition = lc($part->header('Content-Disposition') || 'inline');
-        # Remove the charset, etc. from the content-type, we don't care here.
-        $ct =~ s/;.*//;
-        debug_print("Part Content-Type: [$ct]", 2);
-        debug_print("Part Disposition: [$disposition]", 2);
-
-        if ($disposition eq 'inline' and grep($_ eq $ct, BODY_TYPES)) {
-            push(@body, $part);
-            next;
-        }
-        
-        if (scalar($part->parts) == 1) {
-            push(@attachments, $part);
-            next;
-        }
-
-        # If this part has sub-parts, analyze them similarly to how we
-        # did above and return the relevant pieces.
-        my ($add_body, $add_attachments) = split_body_and_attachments($part);
-        push(@body, @$add_body);
-        push(@attachments, @$add_attachments);
+  my ($email) = @_;
+
+  my (@body, @attachments);
+  foreach my $part ($email->parts) {
+    my $ct = lc($part->content_type || 'text/plain');
+    my $disposition = lc($part->header('Content-Disposition') || 'inline');
+
+    # Remove the charset, etc. from the content-type, we don't care here.
+    $ct =~ s/;.*//;
+    debug_print("Part Content-Type: [$ct]",         2);
+    debug_print("Part Disposition: [$disposition]", 2);
+
+    if ($disposition eq 'inline' and grep($_ eq $ct, BODY_TYPES)) {
+      push(@body, $part);
+      next;
+    }
+
+    if (scalar($part->parts) == 1) {
+      push(@attachments, $part);
+      next;
     }
 
-    return (\@body, \@attachments);
+    # If this part has sub-parts, analyze them similarly to how we
+    # did above and return the relevant pieces.
+    my ($add_body, $add_attachments) = split_body_and_attachments($part);
+    push(@body,        @$add_body);
+    push(@attachments, @$add_attachments);
+  }
+
+  return (\@body, \@attachments);
 }
 
 
 sub die_handler {
-    my ($msg) = @_;
-
-    # In Template-Toolkit, [% RETURN %] is implemented as a call to "die".
-    # But of course, we really don't want to actually *die* just because
-    # the user-error or code-error template ended. So we don't really die.
-    return if blessed($msg) && $msg->isa('Template::Exception')
-              && $msg->type eq 'return';
-
-    # If this is inside an eval, then we should just act like...we're
-    # in an eval (instead of printing the error and exiting).
-    die @_ if ($^S // Bugzilla->request_cache->{in_eval});
-
-    # We can't depend on the MTA to send an error message, so we have
-    # to generate one properly.
-    if ($input_email) {
-       $msg =~ s/at .+ line.*$//ms;
-       $msg =~ s/^Compilation failed in require.+$//ms;
-       $msg = html_strip($msg);
-       my $from = Bugzilla->params->{'mailfrom'};
-       my $reply = reply(to => $input_email, from => $from, top_post => 1, 
-                         body => "$msg\n");
-       MessageToMTA($reply->as_string);
-    }
-    print STDERR "$msg\n";
-    # We exit with a successful value, because we don't want the MTA
-    # to *also* send a failure notice.
-    exit;
+  my ($msg) = @_;
+
+  # In Template-Toolkit, [% RETURN %] is implemented as a call to "die".
+  # But of course, we really don't want to actually *die* just because
+  # the user-error or code-error template ended. So we don't really die.
+  return
+       if blessed($msg)
+    && $msg->isa('Template::Exception')
+    && $msg->type eq 'return';
+
+  # If this is inside an eval, then we should just act like...we're
+  # in an eval (instead of printing the error and exiting).
+  die @_ if ($^S // Bugzilla->request_cache->{in_eval});
+
+  # We can't depend on the MTA to send an error message, so we have
+  # to generate one properly.
+  if ($input_email) {
+    $msg =~ s/at .+ line.*$//ms;
+    $msg =~ s/^Compilation failed in require.+$//ms;
+    $msg = html_strip($msg);
+    my $from = Bugzilla->params->{'mailfrom'};
+    my $reply
+      = reply(to => $input_email, from => $from, top_post => 1, body => "$msg\n");
+    MessageToMTA($reply->as_string);
+  }
+  print STDERR "$msg\n";
+
+  # We exit with a successful value, because we don't want the MTA
+  # to *also* send a failure notice.
+  exit;
 }
 
 ###############
@@ -499,11 +528,11 @@ pod2usage({-verbose => 0, -exitval => 1}) if $switch{'help'};
 
 Bugzilla->usage_mode(USAGE_MODE_EMAIL);
 
-my @mail_lines = <STDIN>;
-my $mail_text = join("", @mail_lines);
+my @mail_lines  = <STDIN>;
+my $mail_text   = join("", @mail_lines);
 my $mail_fields = parse_mail($mail_text);
 
-Bugzilla::Hook::process('email_in_after_parse', { fields => $mail_fields });
+Bugzilla::Hook::process('email_in_after_parse', {fields => $mail_fields});
 
 my $attachments = delete $mail_fields->{'attachments'};
 
@@ -514,10 +543,10 @@ Bugzilla->set_user($user);
 
 my ($bug, $comment);
 if ($mail_fields->{'bug_id'}) {
-    ($bug, $comment) = process_bug($mail_fields);
+  ($bug, $comment) = process_bug($mail_fields);
 }
 else {
-    ($bug, $comment) = post_bug($mail_fields);
+  ($bug, $comment) = post_bug($mail_fields);
 }
 
 handle_attachments($bug, $attachments, $comment);
@@ -529,7 +558,7 @@ handle_attachments($bug, $attachments, $comment);
 # to wait for $bug->update() to be fully used in email_in.pl first. So
 # currently, process_bug.cgi does the mail sending for bugs, and this does
 # any mail sending for attachments after the first one.
-Bugzilla::BugMail::Send($bug->id, { changer => Bugzilla->user });
+Bugzilla::BugMail::Send($bug->id, {changer => Bugzilla->user});
 debug_print("Sent bugmail");
 
 
index d7f8e60a1aa117e15ad4b5c1ae3b6c295c634476..46efeaaf0c1e8175d7090e2e941836655104e34b 100755 (executable)
@@ -10,8 +10,8 @@
 #
 # enter_bug.cgi
 # -------------
-# Displays bug entry form. Bug fields are specified through popup menus, 
-# drop-down lists, or text fields. Default for these values can be 
+# Displays bug entry form. Bug fields are specified through popup menus,
+# drop-down lists, or text fields. Default for these values can be
 # passed in as parameters to the cgi.
 #
 ##############################################################################
@@ -41,81 +41,90 @@ my $user = Bugzilla->login(LOGIN_REQUIRED);
 my $cloned_bug;
 my $cloned_bug_id;
 
-my $cgi = Bugzilla->cgi;
-my $dbh = Bugzilla->dbh;
+my $cgi      = Bugzilla->cgi;
+my $dbh      = Bugzilla->dbh;
 my $template = Bugzilla->template;
-my $vars = {};
+my $vars     = {};
 
 # All pages point to the same part of the documentation.
 $vars->{'doc_section'} = 'using/filing.html';
 
 my $product_name = trim($cgi->param('product') || '');
+
 # Will contain the product object the bug is created in.
 my $product;
 
 if ($product_name eq '') {
-    # If the user cannot enter bugs in any product, stop here.
-    my @enterable_products = @{$user->get_enterable_products};
-    ThrowUserError('no_products') unless scalar(@enterable_products);
 
-    my $classification = Bugzilla->params->{'useclassification'} ?
-        scalar($cgi->param('classification')) : '__all';
+  # If the user cannot enter bugs in any product, stop here.
+  my @enterable_products = @{$user->get_enterable_products};
+  ThrowUserError('no_products') unless scalar(@enterable_products);
 
-    # Unless a real classification name is given, we sort products
-    # by classification.
-    my @classifications;
+  my $classification
+    = Bugzilla->params->{'useclassification'}
+    ? scalar($cgi->param('classification'))
+    : '__all';
 
-    unless ($classification && $classification ne '__all') {
-        @classifications = @{sort_products_by_classification(\@enterable_products)};
-    }
+  # Unless a real classification name is given, we sort products
+  # by classification.
+  my @classifications;
 
-    unless ($classification) {
-        # We know there is at least one classification available,
-        # else we would have stopped earlier.
-        if (scalar(@classifications) > 1) {
-            # We only need classification objects.
-            $vars->{'classifications'} = [map {$_->{'object'}} @classifications];
-
-            $vars->{'target'} = "enter_bug.cgi";
-
-            print $cgi->header();
-            $template->process("global/choose-classification.html.tmpl", $vars)
-               || ThrowTemplateError($template->error());
-            exit;
-        }
-        # If we come here, then there is only one classification available.
-        $classification = $classifications[0]->{'object'}->name;
-    }
+  unless ($classification && $classification ne '__all') {
+    @classifications = @{sort_products_by_classification(\@enterable_products)};
+  }
+
+  unless ($classification) {
+
+    # We know there is at least one classification available,
+    # else we would have stopped earlier.
+    if (scalar(@classifications) > 1) {
+
+      # We only need classification objects.
+      $vars->{'classifications'} = [map { $_->{'object'} } @classifications];
+
+      $vars->{'target'} = "enter_bug.cgi";
 
-    # Keep only enterable products which are in the specified classification.
-    if ($classification ne "__all") {
-        my $class = new Bugzilla::Classification({'name' => $classification});
-        # If the classification doesn't exist, then there is no product in it.
-        if ($class) {
-            @enterable_products
-              = grep {$_->classification_id == $class->id} @enterable_products;
-            @classifications = ({object => $class, products => \@enterable_products});
-        }
-        else {
-            @enterable_products = ();
-        }
+      print $cgi->header();
+      $template->process("global/choose-classification.html.tmpl", $vars)
+        || ThrowTemplateError($template->error());
+      exit;
     }
 
-    if (scalar(@enterable_products) == 0) {
-        ThrowUserError('no_products');
+    # If we come here, then there is only one classification available.
+    $classification = $classifications[0]->{'object'}->name;
+  }
+
+  # Keep only enterable products which are in the specified classification.
+  if ($classification ne "__all") {
+    my $class = new Bugzilla::Classification({'name' => $classification});
+
+    # If the classification doesn't exist, then there is no product in it.
+    if ($class) {
+      @enterable_products
+        = grep { $_->classification_id == $class->id } @enterable_products;
+      @classifications = ({object => $class, products => \@enterable_products});
     }
-    elsif (scalar(@enterable_products) > 1) {
-        $vars->{'classifications'} = \@classifications;
-        $vars->{'target'} = "enter_bug.cgi";
-
-        print $cgi->header();
-        $template->process("global/choose-product.html.tmpl", $vars)
-          || ThrowTemplateError($template->error());
-        exit;
-    } else {
-        # Only one product exists.
-        $product = $enterable_products[0];
+    else {
+      @enterable_products = ();
     }
+  }
+
+  if (scalar(@enterable_products) == 0) {
+    ThrowUserError('no_products');
+  }
+  elsif (scalar(@enterable_products) > 1) {
+    $vars->{'classifications'} = \@classifications;
+    $vars->{'target'}          = "enter_bug.cgi";
+
+    print $cgi->header();
+    $template->process("global/choose-product.html.tmpl", $vars)
+      || ThrowTemplateError($template->error());
+    exit;
+  }
+  else {
+    # Only one product exists.
+    $product = $enterable_products[0];
+  }
 }
 
 # We need to check and make sure that the user has permission
@@ -126,8 +135,8 @@ $product = $user->can_enter_product($product || $product_name, THROW_ERROR);
 # Useful Subroutines
 ##############################################################################
 sub formvalue {
-    my ($name, $default) = (@_);
-    return Bugzilla->cgi->param($name) || $default || "";
+  my ($name, $default) = (@_);
+  return Bugzilla->cgi->param($name) || $default || "";
 }
 
 ##############################################################################
@@ -142,139 +151,143 @@ my $has_editbugs = $user->in_group('editbugs', $product->id);
 $cloned_bug_id = $cgi->param('cloned_bug_id');
 
 if ($cloned_bug_id) {
-    $cloned_bug = Bugzilla::Bug->check($cloned_bug_id);
-    $cloned_bug_id = $cloned_bug->id;
+  $cloned_bug    = Bugzilla::Bug->check($cloned_bug_id);
+  $cloned_bug_id = $cloned_bug->id;
 }
 
 # If there is only one active component, choose it
 my @active = grep { $_->is_active } @{$product->components};
 if (scalar(@active) == 1) {
-    $cgi->param('component', $active[0]->name);
+  $cgi->param('component', $active[0]->name);
 }
 
 # If there is only one active version, choose it
 @active = grep { $_->is_active } @{$product->versions};
 if (scalar(@active) == 1) {
-    $cgi->param('version', $active[0]->name);
+  $cgi->param('version', $active[0]->name);
 }
 
 my %default;
 
-$vars->{'product'}               = $product;
+$vars->{'product'} = $product;
 
-$vars->{'priority'}              = get_legal_field_values('priority');
-$vars->{'bug_severity'}          = get_legal_field_values('bug_severity');
-$vars->{'rep_platform'}          = get_legal_field_values('rep_platform');
-$vars->{'op_sys'}                = get_legal_field_values('op_sys');
+$vars->{'priority'}     = get_legal_field_values('priority');
+$vars->{'bug_severity'} = get_legal_field_values('bug_severity');
+$vars->{'rep_platform'} = get_legal_field_values('rep_platform');
+$vars->{'op_sys'}       = get_legal_field_values('op_sys');
 
-$vars->{'assigned_to'}           = formvalue('assigned_to');
-$vars->{'assigned_to_disabled'}  = !$has_editbugs;
-$vars->{'cc_disabled'}           = 0;
+$vars->{'assigned_to'}          = formvalue('assigned_to');
+$vars->{'assigned_to_disabled'} = !$has_editbugs;
+$vars->{'cc_disabled'}          = 0;
 
-$vars->{'qa_contact'}           = formvalue('qa_contact');
-$vars->{'qa_contact_disabled'}  = !$has_editbugs;
+$vars->{'qa_contact'}          = formvalue('qa_contact');
+$vars->{'qa_contact_disabled'} = !$has_editbugs;
 
-$vars->{'cloned_bug_id'}         = $cloned_bug_id;
+$vars->{'cloned_bug_id'} = $cloned_bug_id;
 
 $vars->{'token'} = issue_session_token('create_bug');
 
 
 my @enter_bug_fields = grep { $_->enter_bug } Bugzilla->active_custom_fields;
 foreach my $field (@enter_bug_fields) {
-    my $cf_name = $field->name;
-    my $cf_value = $cgi->param($cf_name);
-    if (defined $cf_value) {
-        if ($field->type == FIELD_TYPE_MULTI_SELECT) {
-            $cf_value = [$cgi->multi_param($cf_name)];
-        }
-        $default{$cf_name} = $vars->{$cf_name} = $cf_value;
+  my $cf_name  = $field->name;
+  my $cf_value = $cgi->param($cf_name);
+  if (defined $cf_value) {
+    if ($field->type == FIELD_TYPE_MULTI_SELECT) {
+      $cf_value = [$cgi->multi_param($cf_name)];
     }
+    $default{$cf_name} = $vars->{$cf_name} = $cf_value;
+  }
 }
 
 # This allows the Field visibility and value controls to work with the
 # Classification and Product fields as a parent.
 $default{'classification'} = $product->classification->name;
-$default{'product'} = $product->name;
+$default{'product'}        = $product->name;
 
 if ($cloned_bug_id) {
 
-    $default{'component_'}    = $cloned_bug->component;
-    $default{'priority'}      = $cloned_bug->priority;
-    $default{'bug_severity'}  = $cloned_bug->bug_severity;
-    $default{'rep_platform'}  = $cloned_bug->rep_platform;
-    $default{'op_sys'}        = $cloned_bug->op_sys;
-
-    $vars->{'short_desc'}     = $cloned_bug->short_desc;
-    $vars->{'bug_file_loc'}   = $cloned_bug->bug_file_loc;
-    $vars->{'keywords'}       = $cloned_bug->keywords;
-    $vars->{'dependson'}      = join (", ", $cloned_bug_id, @{$cloned_bug->dependson});
-    $vars->{'blocked'}        = join (", ", @{$cloned_bug->blocked});
-    $vars->{'deadline'}       = $cloned_bug->deadline;
-    $vars->{'estimated_time'} = $cloned_bug->estimated_time;
-
-    if (scalar @{$cloned_bug->cc}) {
-        $vars->{'cc'}         = join (", ", @{$cloned_bug->cc});
-    } else {
-        $vars->{'cc'}         = formvalue('cc');
+  $default{'component_'}   = $cloned_bug->component;
+  $default{'priority'}     = $cloned_bug->priority;
+  $default{'bug_severity'} = $cloned_bug->bug_severity;
+  $default{'rep_platform'} = $cloned_bug->rep_platform;
+  $default{'op_sys'}       = $cloned_bug->op_sys;
+
+  $vars->{'short_desc'}   = $cloned_bug->short_desc;
+  $vars->{'bug_file_loc'} = $cloned_bug->bug_file_loc;
+  $vars->{'keywords'}     = $cloned_bug->keywords;
+  $vars->{'dependson'}    = join(", ", $cloned_bug_id, @{$cloned_bug->dependson});
+  $vars->{'blocked'}      = join(", ", @{$cloned_bug->blocked});
+  $vars->{'deadline'}     = $cloned_bug->deadline;
+  $vars->{'estimated_time'} = $cloned_bug->estimated_time;
+
+  if (scalar @{$cloned_bug->cc}) {
+    $vars->{'cc'} = join(", ", @{$cloned_bug->cc});
+  }
+  else {
+    $vars->{'cc'} = formvalue('cc');
+  }
+
+  foreach my $role (qw(reporter assigned_to qa_contact)) {
+    if ( defined($cloned_bug->$role)
+      && $cloned_bug->$role->id != $user->id
+      && none { $_ eq $cloned_bug->$role->login } @{$cloned_bug->cc})
+    {
+      $vars->{'cc'} = join(", ", $cloned_bug->$role->login, $vars->{'cc'});
     }
+  }
 
-    foreach my $role (qw(reporter assigned_to qa_contact)) {
-        if (defined($cloned_bug->$role)
-            && $cloned_bug->$role->id != $user->id
-            && none { $_ eq $cloned_bug->$role->login } @{$cloned_bug->cc})
-        {
-            $vars->{'cc'} = join (", ", $cloned_bug->$role->login, $vars->{'cc'});
-        }
-    }
+  foreach my $field (@enter_bug_fields) {
+    my $field_name = $field->name;
+    $vars->{$field_name} = $cloned_bug->$field_name;
+  }
 
-    foreach my $field (@enter_bug_fields) {
-        my $field_name = $field->name;
-        $vars->{$field_name} = $cloned_bug->$field_name;
-    }
+  # We need to ensure that we respect the 'insider' status of
+  # the first comment, if it has one. Either way, make a note
+  # that this bug was cloned from another bug.
+  my $bug_desc = $cloned_bug->comments({order => 'oldest_to_newest'})->[0];
+  my $isprivate = $bug_desc->is_private;
 
-    # We need to ensure that we respect the 'insider' status of
-    # the first comment, if it has one. Either way, make a note
-    # that this bug was cloned from another bug.
-    my $bug_desc = $cloned_bug->comments({ order => 'oldest_to_newest' })->[0];
-    my $isprivate = $bug_desc->is_private;
+  $vars->{'comment'}            = "";
+  $vars->{'comment_is_private'} = 0;
 
-    $vars->{'comment'} = "";
-    $vars->{'comment_is_private'} = 0;
+  if (!$isprivate || $user->is_insider) {
 
-    if (!$isprivate || $user->is_insider) {
-        # We use "body" to avoid any format_comment text, which would be
-        # pointless to clone.
-        $vars->{'comment'} = $bug_desc->body;
-        $vars->{'comment_is_private'} = $isprivate;
-    }
+    # We use "body" to avoid any format_comment text, which would be
+    # pointless to clone.
+    $vars->{'comment'}            = $bug_desc->body;
+    $vars->{'comment_is_private'} = $isprivate;
+  }
 
-} # end of cloned bug entry form
+}    # end of cloned bug entry form
 
 else {
-    $default{'component_'}    = formvalue('component');
-    $default{'priority'}      = formvalue('priority', Bugzilla->params->{'defaultpriority'});
-    $default{'bug_severity'}  = formvalue('bug_severity', Bugzilla->params->{'defaultseverity'});
-    $default{'rep_platform'}  = formvalue('rep_platform', 
-                                          Bugzilla->params->{'defaultplatform'} || detect_platform());
-    $default{'op_sys'}        = formvalue('op_sys', 
-                                          Bugzilla->params->{'defaultopsys'} || detect_op_sys());
-
-    $vars->{'alias'}          = formvalue('alias');
-    $vars->{'short_desc'}     = formvalue('short_desc');
-    $vars->{'bug_file_loc'}   = formvalue('bug_file_loc', "http://");
-    $vars->{'keywords'}       = formvalue('keywords');
-    $vars->{'dependson'}      = formvalue('dependson');
-    $vars->{'blocked'}        = formvalue('blocked');
-    $vars->{'deadline'}       = formvalue('deadline');
-    $vars->{'estimated_time'} = formvalue('estimated_time');
-    $vars->{'see_also'}       = formvalue('see_also');
-
-    $vars->{'cc'}             = join(', ', $cgi->multi_param('cc'));
-
-    $vars->{'comment'}        = formvalue('comment');
-    $vars->{'comment_is_private'} = formvalue('comment_is_private');
-
-} # end of normal/bookmarked entry form
+  $default{'component_'} = formvalue('component');
+  $default{'priority'}
+    = formvalue('priority', Bugzilla->params->{'defaultpriority'});
+  $default{'bug_severity'}
+    = formvalue('bug_severity', Bugzilla->params->{'defaultseverity'});
+  $default{'rep_platform'} = formvalue('rep_platform',
+    Bugzilla->params->{'defaultplatform'} || detect_platform());
+  $default{'op_sys'}
+    = formvalue('op_sys', Bugzilla->params->{'defaultopsys'} || detect_op_sys());
+
+  $vars->{'alias'}          = formvalue('alias');
+  $vars->{'short_desc'}     = formvalue('short_desc');
+  $vars->{'bug_file_loc'}   = formvalue('bug_file_loc', "http://");
+  $vars->{'keywords'}       = formvalue('keywords');
+  $vars->{'dependson'}      = formvalue('dependson');
+  $vars->{'blocked'}        = formvalue('blocked');
+  $vars->{'deadline'}       = formvalue('deadline');
+  $vars->{'estimated_time'} = formvalue('estimated_time');
+  $vars->{'see_also'}       = formvalue('see_also');
+
+  $vars->{'cc'} = join(', ', $cgi->multi_param('cc'));
+
+  $vars->{'comment'}            = formvalue('comment');
+  $vars->{'comment_is_private'} = formvalue('comment_is_private');
+
+}    # end of normal/bookmarked entry form
 
 
 # IF this is a cloned bug,
@@ -294,31 +307,35 @@ $vars->{'version'} = $product->versions;
 
 my $version_cookie = $cgi->cookie("VERSION-" . $product->name);
 
-if ( ($cloned_bug_id) &&
-     ($product->name eq $cloned_bug->product ) ) {
-    $default{'version'} = $cloned_bug->version;
-} elsif (formvalue('version')) {
-    $default{'version'} = formvalue('version');
-} elsif (defined $version_cookie
-         and grep { $_->name eq $version_cookie } @{ $vars->{'version'} })
+if (($cloned_bug_id) && ($product->name eq $cloned_bug->product)) {
+  $default{'version'} = $cloned_bug->version;
+}
+elsif (formvalue('version')) {
+  $default{'version'} = formvalue('version');
+}
+elsif (defined $version_cookie
+  and grep { $_->name eq $version_cookie } @{$vars->{'version'}})
 {
-    $default{'version'} = $version_cookie;
-} else {
-    $default{'version'} = $vars->{'version'}->[$#{$vars->{'version'}}]->name;
+  $default{'version'} = $version_cookie;
+}
+else {
+  $default{'version'} = $vars->{'version'}->[$#{$vars->{'version'}}]->name;
 }
 
 # Get list of milestones.
-if ( Bugzilla->params->{'usetargetmilestone'} ) {
-    $vars->{'target_milestone'} = $product->milestones;
-    if (formvalue('target_milestone')) {
-       $default{'target_milestone'} = formvalue('target_milestone');
-    } else {
-       $default{'target_milestone'} = $product->default_milestone;
-    }
+if (Bugzilla->params->{'usetargetmilestone'}) {
+  $vars->{'target_milestone'} = $product->milestones;
+  if (formvalue('target_milestone')) {
+    $default{'target_milestone'} = formvalue('target_milestone');
+  }
+  else {
+    $default{'target_milestone'} = $product->default_milestone;
+  }
 }
 
 # Construct the list of allowable statuses.
-my @statuses = @{ Bugzilla::Bug->new_bug_statuses($product) };
+my @statuses = @{Bugzilla::Bug->new_bug_statuses($product)};
+
 # Exclude closed states from the UI, even if the workflow allows them.
 # The back-end code will still accept them, though.
 # XXX We should remove this when the UI accepts closed statuses and update
@@ -335,28 +352,32 @@ $vars->{'bug_status'} = \@statuses;
 
 my $picked_status = formvalue('bug_status');
 if ($picked_status and grep($_->name eq $picked_status, @statuses)) {
-    $default{'bug_status'} = formvalue('bug_status');
-} else {
-    $default{'bug_status'} = Bugzilla::Bug->default_bug_status(@statuses);
+  $default{'bug_status'} = formvalue('bug_status');
+}
+else {
+  $default{'bug_status'} = Bugzilla::Bug->default_bug_status(@statuses);
 }
 
 my @groups = $cgi->multi_param('groups');
 if ($cloned_bug) {
-    my @clone_groups = map { $_->name } @{ $cloned_bug->groups_in };
-    # It doesn't matter if there are duplicate names, since all we check
-    # for in the template is whether or not the group is set.
-    push(@groups, @clone_groups);
+  my @clone_groups = map { $_->name } @{$cloned_bug->groups_in};
+
+  # It doesn't matter if there are duplicate names, since all we check
+  # for in the template is whether or not the group is set.
+  push(@groups, @clone_groups);
 }
 $default{'groups'} = \@groups;
 
-Bugzilla::Hook::process('enter_bug_entrydefaultvars', { vars => $vars });
+Bugzilla::Hook::process('enter_bug_entrydefaultvars', {vars => $vars});
 
 $vars->{'default'} = \%default;
 
-my $format = $template->get_format("bug/create/create",
-                                   scalar $cgi->param('format'), 
-                                   scalar $cgi->param('ctype'));
+my $format = $template->get_format(
+  "bug/create/create",
+  scalar $cgi->param('format'),
+  scalar $cgi->param('ctype')
+);
 
 print $cgi->header($format->{'ctype'});
 $template->process($format->{'template'}, $vars)
-  || ThrowTemplateError($template->error());          
+  || ThrowTemplateError($template->error());
index 3c8e792abd5286c0076385238b8701157ad08481..aabb8142e0c06100e2e3f5fe891d8c1bb3f3f022 100644 (file)
@@ -12,12 +12,7 @@ use strict;
 use warnings;
 
 use constant NAME => 'BmpConvert';
-use constant REQUIRED_MODULES => [
-  {
-      package => 'PerlMagick',
-      module  => 'Image::Magick',
-      version => 0,
-  },
-];
+use constant REQUIRED_MODULES =>
+  [{package => 'PerlMagick', module => 'Image::Magick', version => 0,},];
 
 __PACKAGE__->NAME;
index 407ab3aaa7ca998dd1322033f16b49ca54bb0d58..8bab7620c4f838c8b7c9ed98cc51f58550b5783f 100644 (file)
@@ -18,29 +18,30 @@ use Image::Magick;
 our $VERSION = '1.0';
 
 sub attachment_process_data {
-    my ($self, $args) = @_;
-    return unless $args->{attributes}->{mimetype} eq 'image/bmp';
-
-    my $data = ${$args->{data}};
-    my $img = Image::Magick->new(magick => 'bmp');
-
-    # $data is a filehandle.
-    if (ref $data) {
-        $img->Read(file => \*$data);
-        $img->set(magick => 'png');
-        $img->Write(file => \*$data);
-    }
-    # $data is a blob.
-    else {
-        $img->BlobToImage($data);
-        $img->set(magick => 'png');
-        $data = $img->ImageToBlob();
-    }
-    undef $img;
-
-    ${$args->{data}} = $data;
-    $args->{attributes}->{mimetype} = 'image/png';
-    $args->{attributes}->{filename} =~ s/^(.+)\.bmp$/$1.png/i;
+  my ($self, $args) = @_;
+  return unless $args->{attributes}->{mimetype} eq 'image/bmp';
+
+  my $data = ${$args->{data}};
+  my $img = Image::Magick->new(magick => 'bmp');
+
+  # $data is a filehandle.
+  if (ref $data) {
+    $img->Read(file => \*$data);
+    $img->set(magick => 'png');
+    $img->Write(file => \*$data);
+  }
+
+  # $data is a blob.
+  else {
+    $img->BlobToImage($data);
+    $img->set(magick => 'png');
+    $data = $img->ImageToBlob();
+  }
+  undef $img;
+
+  ${$args->{data}} = $data;
+  $args->{attributes}->{mimetype} = 'image/png';
+  $args->{attributes}->{filename} =~ s/^(.+)\.bmp$/$1.png/i;
 }
 
- __PACKAGE__->NAME;
+__PACKAGE__->NAME;
index 38000fbdb94f850b5c948355efce7c52b5fa75d5..38a2c3b98e1ff47c94f721b3130135d14c93252c 100644 (file)
@@ -18,29 +18,21 @@ use Bugzilla::Error;
 #############
 
 use constant READ_ONLY => qw(
-    hello
-    throw_an_error
+  hello
+  throw_an_error
 );
 
 use constant PUBLIC_METHODS => qw(
-    hello
-    throw_an_error
+  hello
+  throw_an_error
 );
 
 sub REST_RESOURCES {
-    my $rest_resources = [
-        qr{^/hello$}, {
-            GET  => {
-                method => 'hello'
-            }
-        },
-        qr{^/throw_an_error$}, {
-            GET => {
-                method => 'throw_an_error'
-            }
-        }
-    ];
-    return $rest_resources;
+  my $rest_resources = [
+    qr{^/hello$},          {GET => {method => 'hello'}},
+    qr{^/throw_an_error$}, {GET => {method => 'throw_an_error'}}
+  ];
+  return $rest_resources;
 }
 
 ###########
@@ -49,9 +41,7 @@ sub REST_RESOURCES {
 
 # This can be called as Example.hello() from the WebService.
 sub hello {
-    return {
-        message => 'Hello!'
-    };
+  return {message => 'Hello!'};
 }
 
 sub throw_an_error { ThrowUserError('example_my_error') }
index 5f95d0b8bd674100fc37ff364c56c38e4acf78b2..727d1dd6308c652c475159b446810332471d435a 100644 (file)
@@ -12,29 +12,21 @@ use strict;
 use warnings;
 
 use constant NAME => 'Example';
-use constant REQUIRED_MODULES => [
-    {
-        package => 'Data-Dumper',
-        module  => 'Data::Dumper',
-        version => 0,
-    },
-];
+use constant REQUIRED_MODULES =>
+  [{package => 'Data-Dumper', module => 'Data::Dumper', version => 0,},];
 
 use constant OPTIONAL_MODULES => [
-    {
-        package => 'Acme',
-        module  => 'Acme',
-        version => 1.11,
-        feature => ['example_acme'],
-    },
+  {
+    package => 'Acme',
+    module  => 'Acme',
+    version => 1.11,
+    feature => ['example_acme'],
+  },
 ];
 
 # The map determines which version of
 # the Core API an extension's API modules
 # were written to work with.
-use constant API_VERSION_MAP => {
-    '1_0' => '1_0',
-    '2_0' => '1_0'
-};
+use constant API_VERSION_MAP => {'1_0' => '1_0', '2_0' => '1_0'};
 
 __PACKAGE__->NAME;
index ef27200ac74ae592747ca28955f87b80d9380a3d..8da0ea07b9d15f3d5c3742e41eee414ca8585d8e 100644 (file)
@@ -35,368 +35,385 @@ use constant REL_EXAMPLE => -127;
 our $VERSION = '1.0';
 
 sub user_can_administer {
-    my ($self, $args) = @_;
-    my $can_administer = $args->{can_administer};
+  my ($self, $args) = @_;
+  my $can_administer = $args->{can_administer};
 
-    # If you add an option to the admin pages (e.g. by using the Hooks in
-    # template/en/default/admin/admin.html.tmpl), you may want to allow
-    # users in another group view admin.cgi
-    #if (Bugzilla->user->in_group('other_group')) {
-    #    $$can_administer = 1;
-    #}
+  # If you add an option to the admin pages (e.g. by using the Hooks in
+  # template/en/default/admin/admin.html.tmpl), you may want to allow
+  # users in another group view admin.cgi
+  #if (Bugzilla->user->in_group('other_group')) {
+  #    $$can_administer = 1;
+  #}
 }
 
 sub admin_editusers_action {
-    my ($self, $args) = @_;
-    my ($vars, $action, $user) = @$args{qw(vars action user)};
-    my $template = Bugzilla->template;
-
-    if ($action eq 'my_action') {
-        # Allow to restrict the search to any group the user is allowed to bless.
-        $vars->{'restrictablegroups'} = $user->bless_groups();
-        $template->process('admin/users/search.html.tmpl', $vars)
-            || ThrowTemplateError($template->error());
-        exit;
-    }
+  my ($self, $args) = @_;
+  my ($vars, $action, $user) = @$args{qw(vars action user)};
+  my $template = Bugzilla->template;
+
+  if ($action eq 'my_action') {
+
+    # Allow to restrict the search to any group the user is allowed to bless.
+    $vars->{'restrictablegroups'} = $user->bless_groups();
+    $template->process('admin/users/search.html.tmpl', $vars)
+      || ThrowTemplateError($template->error());
+    exit;
+  }
 }
 
 sub attachment_process_data {
-    my ($self, $args) = @_;
-    my $type     = $args->{attributes}->{mimetype};
-    my $filename = $args->{attributes}->{filename};
-
-    # Make sure images have the correct extension.
-    # Uncomment the two lines below to make this check effective.
-    if ($type =~ /^image\/(\w+)$/) {
-        my $format = $1;
-        if ($filename =~ /^(.+)(:?\.[^\.]+)$/) {
-            my $name = $1;
-            #$args->{attributes}->{filename} = "${name}.$format";
-        }
-        else {
-            # The file has no extension. We append it.
-            #$args->{attributes}->{filename} .= ".$format";
-        }
+  my ($self, $args) = @_;
+  my $type     = $args->{attributes}->{mimetype};
+  my $filename = $args->{attributes}->{filename};
+
+  # Make sure images have the correct extension.
+  # Uncomment the two lines below to make this check effective.
+  if ($type =~ /^image\/(\w+)$/) {
+    my $format = $1;
+    if ($filename =~ /^(.+)(:?\.[^\.]+)$/) {
+      my $name = $1;
+
+      #$args->{attributes}->{filename} = "${name}.$format";
+    }
+    else {
+      # The file has no extension. We append it.
+      #$args->{attributes}->{filename} .= ".$format";
     }
+  }
 }
 
 sub auth_login_methods {
-    my ($self, $args) = @_;
-    my $modules = $args->{modules};
-    if (exists $modules->{Example}) {
-        $modules->{Example} = 'Bugzilla/Extension/Example/Auth/Login.pm';
-    }
+  my ($self, $args) = @_;
+  my $modules = $args->{modules};
+  if (exists $modules->{Example}) {
+    $modules->{Example} = 'Bugzilla/Extension/Example/Auth/Login.pm';
+  }
 }
 
 sub auth_verify_methods {
-    my ($self, $args) = @_;
-    my $modules = $args->{modules};
-    if (exists $modules->{Example}) {
-        $modules->{Example} = 'Bugzilla/Extension/Example/Auth/Verify.pm';
-    }
+  my ($self, $args) = @_;
+  my $modules = $args->{modules};
+  if (exists $modules->{Example}) {
+    $modules->{Example} = 'Bugzilla/Extension/Example/Auth/Verify.pm';
+  }
 }
 
 sub bug_check_can_change_field {
-    my ($self, $args) = @_;
-
-    my ($bug, $field, $new_value, $old_value, $priv_results)
-        = @$args{qw(bug field new_value old_value priv_results)};
-
-    my $user = Bugzilla->user;
-
-    # Disallow a bug from being reopened if currently closed unless user 
-    # is in 'admin' group
-    if ($field eq 'bug_status' && $bug->product_obj->name eq 'Example') {
-        if (!is_open_state($old_value) && is_open_state($new_value) 
-            && !$user->in_group('admin')) 
-        {
-            push(@$priv_results, PRIVILEGES_REQUIRED_EMPOWERED);
-            return;
-        }
-    }
+  my ($self, $args) = @_;
 
-    # Disallow a bug's keywords from being edited unless user is the
-    # reporter of the bug 
-    if ($field eq 'keywords' && $bug->product_obj->name eq 'Example' 
-        && $user->login ne $bug->reporter->login) 
-    {
-        push(@$priv_results, PRIVILEGES_REQUIRED_REPORTER);
-        return;
-    }
+  my ($bug, $field, $new_value, $old_value, $priv_results)
+    = @$args{qw(bug field new_value old_value priv_results)};
+
+  my $user = Bugzilla->user;
 
-    # Allow updating of priority even if user cannot normally edit the bug 
-    # and they are in group 'engineering'
-    if ($field eq 'priority' && $bug->product_obj->name eq 'Example'
-        && $user->in_group('engineering')) 
+  # Disallow a bug from being reopened if currently closed unless user
+  # is in 'admin' group
+  if ($field eq 'bug_status' && $bug->product_obj->name eq 'Example') {
+    if (!is_open_state($old_value)
+      && is_open_state($new_value)
+      && !$user->in_group('admin'))
     {
-        push(@$priv_results, PRIVILEGES_REQUIRED_NONE);
-        return;
+      push(@$priv_results, PRIVILEGES_REQUIRED_EMPOWERED);
+      return;
     }
+  }
+
+  # Disallow a bug's keywords from being edited unless user is the
+  # reporter of the bug
+  if ( $field eq 'keywords'
+    && $bug->product_obj->name eq 'Example'
+    && $user->login ne $bug->reporter->login)
+  {
+    push(@$priv_results, PRIVILEGES_REQUIRED_REPORTER);
+    return;
+  }
+
+  # Allow updating of priority even if user cannot normally edit the bug
+  # and they are in group 'engineering'
+  if ( $field eq 'priority'
+    && $bug->product_obj->name eq 'Example'
+    && $user->in_group('engineering'))
+  {
+    push(@$priv_results, PRIVILEGES_REQUIRED_NONE);
+    return;
+  }
 }
 
 sub bug_columns {
-    my ($self, $args) = @_;
-    my $columns = $args->{'columns'};
-    push (@$columns, "delta_ts AS example")
+  my ($self, $args) = @_;
+  my $columns = $args->{'columns'};
+  push(@$columns, "delta_ts AS example");
 }
 
 sub bug_end_of_create {
-    my ($self, $args) = @_;
+  my ($self, $args) = @_;
+
+  # This code doesn't actually *do* anything, it's just here to show you
+  # how to use this hook.
+  my $bug       = $args->{'bug'};
+  my $timestamp = $args->{'timestamp'};
+
+  my $bug_id = $bug->id;
 
-    # This code doesn't actually *do* anything, it's just here to show you
-    # how to use this hook.
-    my $bug = $args->{'bug'};
-    my $timestamp = $args->{'timestamp'};
-    
-    my $bug_id = $bug->id;
-    # Uncomment this line to see a line in your webserver's error log whenever
-    # you file a bug.
-    # warn "Bug $bug_id has been filed!";
+  # Uncomment this line to see a line in your webserver's error log whenever
+  # you file a bug.
+  # warn "Bug $bug_id has been filed!";
 }
 
 sub bug_end_of_create_validators {
-    my ($self, $args) = @_;
-    
-    # This code doesn't actually *do* anything, it's just here to show you
-    # how to use this hook.
-    my $bug_params = $args->{'params'};
-    
-    # Uncomment this line below to see a line in your webserver's error log
-    # containing all validated bug field values every time you file a bug.
-    # warn Dumper($bug_params);
-    
-    # This would remove all ccs from the bug, preventing ANY ccs from being
-    # added on bug creation.
-    # $bug_params->{cc} = [];
-}
+  my ($self, $args) = @_;
 
-sub bug_start_of_update {
-    my ($self, $args) = @_;
+  # This code doesn't actually *do* anything, it's just here to show you
+  # how to use this hook.
+  my $bug_params = $args->{'params'};
 
-    # This code doesn't actually *do* anything, it's just here to show you
-    # how to use this hook.
-    my ($bug, $old_bug, $timestamp, $changes) = 
-        @$args{qw(bug old_bug timestamp changes)};
+  # Uncomment this line below to see a line in your webserver's error log
+  # containing all validated bug field values every time you file a bug.
+  # warn Dumper($bug_params);
 
-    foreach my $field (keys %$changes) {
-        my $used_to_be = $changes->{$field}->[0];
-        my $now_it_is  = $changes->{$field}->[1];
-    }
+  # This would remove all ccs from the bug, preventing ANY ccs from being
+  # added on bug creation.
+  # $bug_params->{cc} = [];
+}
 
-    my $old_summary = $old_bug->short_desc;
-
-    my $status_message;
-    if (my $status_change = $changes->{'bug_status'}) {
-        my $old_status = new Bugzilla::Status({ name => $status_change->[0] });
-        my $new_status = new Bugzilla::Status({ name => $status_change->[1] });
-        if ($new_status->is_open && !$old_status->is_open) {
-            $status_message = "Bug re-opened!";
-        }
-        if (!$new_status->is_open && $old_status->is_open) {
-            $status_message = "Bug closed!";
-        }
+sub bug_start_of_update {
+  my ($self, $args) = @_;
+
+  # This code doesn't actually *do* anything, it's just here to show you
+  # how to use this hook.
+  my ($bug, $old_bug, $timestamp, $changes)
+    = @$args{qw(bug old_bug timestamp changes)};
+
+  foreach my $field (keys %$changes) {
+    my $used_to_be = $changes->{$field}->[0];
+    my $now_it_is  = $changes->{$field}->[1];
+  }
+
+  my $old_summary = $old_bug->short_desc;
+
+  my $status_message;
+  if (my $status_change = $changes->{'bug_status'}) {
+    my $old_status = new Bugzilla::Status({name => $status_change->[0]});
+    my $new_status = new Bugzilla::Status({name => $status_change->[1]});
+    if ($new_status->is_open && !$old_status->is_open) {
+      $status_message = "Bug re-opened!";
+    }
+    if (!$new_status->is_open && $old_status->is_open) {
+      $status_message = "Bug closed!";
     }
+  }
 
-    my $bug_id = $bug->id;
-    my $num_changes = scalar keys %$changes;
-    my $result = "There were $num_changes changes to fields on bug $bug_id"
-                 . " at $timestamp.";
-    # Uncomment this line to see $result in your webserver's error log whenever
-    # you update a bug.
-    # warn $result;
+  my $bug_id      = $bug->id;
+  my $num_changes = scalar keys %$changes;
+  my $result      = "There were $num_changes changes to fields on bug $bug_id"
+    . " at $timestamp.";
+
+  # Uncomment this line to see $result in your webserver's error log whenever
+  # you update a bug.
+  # warn $result;
 }
 
 sub bug_end_of_update {
-    my ($self, $args) = @_;
-    
-    # This code doesn't actually *do* anything, it's just here to show you
-    # how to use this hook.
-    my ($bug, $old_bug, $timestamp, $changes) = 
-        @$args{qw(bug old_bug timestamp changes)};
-    
-    foreach my $field (keys %$changes) {
-        my $used_to_be = $changes->{$field}->[0];
-        my $now_it_is  = $changes->{$field}->[1];
+  my ($self, $args) = @_;
+
+  # This code doesn't actually *do* anything, it's just here to show you
+  # how to use this hook.
+  my ($bug, $old_bug, $timestamp, $changes)
+    = @$args{qw(bug old_bug timestamp changes)};
+
+  foreach my $field (keys %$changes) {
+    my $used_to_be = $changes->{$field}->[0];
+    my $now_it_is  = $changes->{$field}->[1];
+  }
+
+  my $old_summary = $old_bug->short_desc;
+
+  my $status_message;
+  if (my $status_change = $changes->{'bug_status'}) {
+    my $old_status = new Bugzilla::Status({name => $status_change->[0]});
+    my $new_status = new Bugzilla::Status({name => $status_change->[1]});
+    if ($new_status->is_open && !$old_status->is_open) {
+      $status_message = "Bug re-opened!";
     }
-
-    my $old_summary = $old_bug->short_desc;
-
-    my $status_message;
-    if (my $status_change = $changes->{'bug_status'}) {
-        my $old_status = new Bugzilla::Status({ name => $status_change->[0] });
-        my $new_status = new Bugzilla::Status({ name => $status_change->[1] });
-        if ($new_status->is_open && !$old_status->is_open) {
-            $status_message = "Bug re-opened!";
-        }
-        if (!$new_status->is_open && $old_status->is_open) {
-            $status_message = "Bug closed!";
-        }
+    if (!$new_status->is_open && $old_status->is_open) {
+      $status_message = "Bug closed!";
     }
-    
-    my $bug_id = $bug->id;
-    my $num_changes = scalar keys %$changes;
-    my $result = "There were $num_changes changes to fields on bug $bug_id"
-                 . " at $timestamp.";
-    # Uncomment this line to see $result in your webserver's error log whenever
-    # you update a bug.
-    # warn $result;
+  }
+
+  my $bug_id      = $bug->id;
+  my $num_changes = scalar keys %$changes;
+  my $result      = "There were $num_changes changes to fields on bug $bug_id"
+    . " at $timestamp.";
+
+  # Uncomment this line to see $result in your webserver's error log whenever
+  # you update a bug.
+  # warn $result;
 }
 
 sub bug_fields {
-    my ($self, $args) = @_;
+  my ($self, $args) = @_;
 
-    my $fields = $args->{'fields'};
-    push (@$fields, "example")
+  my $fields = $args->{'fields'};
+  push(@$fields, "example");
 }
 
 sub bug_format_comment {
-    my ($self, $args) = @_;
-    
-    # This replaces every occurrence of the word "foo" with the word
-    # "bar"
-    
-    my $regexes = $args->{'regexes'};
-    push(@$regexes, { match => qr/\bfoo\b/, replace => 'bar' });
-    
-    # And this links every occurrence of the word "bar" to example.com,
-    # but it won't affect "foo"s that have already been turned into "bar"
-    # above (because each regex is run in order, and later regexes don't modify
-    # earlier matches, due to some cleverness in Bugzilla's internals).
-    #
-    # For example, the phrase "foo bar" would become:
-    # bar <a href="http://example.com/bar">bar</a>
-    my $bar_match = qr/\b(bar)\b/;
-    push(@$regexes, { match => $bar_match, replace => \&_replace_bar });
+  my ($self, $args) = @_;
+
+  # This replaces every occurrence of the word "foo" with the word
+  # "bar"
+
+  my $regexes = $args->{'regexes'};
+  push(@$regexes, {match => qr/\bfoo\b/, replace => 'bar'});
+
+  # And this links every occurrence of the word "bar" to example.com,
+  # but it won't affect "foo"s that have already been turned into "bar"
+  # above (because each regex is run in order, and later regexes don't modify
+  # earlier matches, due to some cleverness in Bugzilla's internals).
+  #
+  # For example, the phrase "foo bar" would become:
+  # bar <a href="http://example.com/bar">bar</a>
+  my $bar_match = qr/\b(bar)\b/;
+  push(@$regexes, {match => $bar_match, replace => \&_replace_bar});
 }
 
 # Used by bug_format_comment--see its code for an explanation.
 sub _replace_bar {
-    my $args = shift;
-    # $match is the first parentheses match in the $bar_match regex 
-    # in bug-format_comment.pl. We get up to 10 regex matches as 
-    # arguments to this function.
-    my $match = $args->{matches}->[0];
-    # Remember, you have to HTML-escape any data that you are returning!
-    $match = html_quote($match);
-    return qq{<a href="http://example.com/">$match</a>};
-};
+  my $args = shift;
+
+  # $match is the first parentheses match in the $bar_match regex
+  # in bug-format_comment.pl. We get up to 10 regex matches as
+  # arguments to this function.
+  my $match = $args->{matches}->[0];
+
+  # Remember, you have to HTML-escape any data that you are returning!
+  $match = html_quote($match);
+  return qq{<a href="http://example.com/">$match</a>};
+}
 
 sub buglist_columns {
-    my ($self, $args) = @_;
-    
-    my $columns = $args->{'columns'};
-    $columns->{'example'} = { 'name' => 'bugs.delta_ts' , 'title' => 'Example' };
-    $columns->{'product_desc'} = { 'name'  => 'prod_desc.description',
-                                   'title' => 'Product Description' };
+  my ($self, $args) = @_;
+
+  my $columns = $args->{'columns'};
+  $columns->{'example'} = {'name' => 'bugs.delta_ts', 'title' => 'Example'};
+  $columns->{'product_desc'}
+    = {'name' => 'prod_desc.description', 'title' => 'Product Description'};
 }
 
 sub buglist_column_joins {
-    my ($self, $args) = @_;
-    my $joins = $args->{'column_joins'};
+  my ($self, $args) = @_;
+  my $joins = $args->{'column_joins'};
 
-    # This column is added using the "buglist_columns" hook
-    $joins->{'product_desc'} = {
-        from  => 'product_id',
-        to    => 'id',
-        table => 'products',
-        as    => 'prod_desc',
-        join  => 'INNER',
-    };
+  # This column is added using the "buglist_columns" hook
+  $joins->{'product_desc'} = {
+    from  => 'product_id',
+    to    => 'id',
+    table => 'products',
+    as    => 'prod_desc',
+    join  => 'INNER',
+  };
 }
 
 sub buglist_format {
-    my ($self, $args) = @_;
-    my $vars = $args->{'vars'};
-    my $format = $args->{'format'};
-    my $params = $args->{'params'};
+  my ($self, $args) = @_;
+  my $vars   = $args->{'vars'};
+  my $format = $args->{'format'};
+  my $params = $args->{'params'};
 
-    if ($format->{'template'} eq "list/list-example.html.tmpl") {
-        $vars->{'example'} = $params->param('example');
-    }
+  if ($format->{'template'} eq "list/list-example.html.tmpl") {
+    $vars->{'example'} = $params->param('example');
+  }
 }
 
 sub search_operator_field_override {
-    my ($self, $args) = @_;
-    
-    my $operators = $args->{'operators'};
+  my ($self, $args) = @_;
 
-    my $original = $operators->{component}->{_non_changed};
-    $operators->{component} = {
-        _non_changed => sub { _component_nonchanged($original, @_) }
-    };
+  my $operators = $args->{'operators'};
+
+  my $original = $operators->{component}->{_non_changed};
+  $operators->{component} = {
+    _non_changed => sub { _component_nonchanged($original, @_) }
+  };
 }
 
 sub _component_nonchanged {
-    my $original = shift;
-    my ($invocant, $args) = @_;
+  my $original = shift;
+  my ($invocant, $args) = @_;
+
+  $invocant->$original($args);
 
-    $invocant->$original($args);
-    # Actually, it does not change anything in the result,
-    # just an example.
-    $args->{term} = $args->{term} . " OR 1=2";
+  # Actually, it does not change anything in the result,
+  # just an example.
+  $args->{term} = $args->{term} . " OR 1=2";
 }
 
 sub bugmail_recipients {
-    my ($self, $args) = @_;
-    my $recipients = $args->{recipients};
-    my $bug = $args->{bug};
-
-    my $user = 
-        new Bugzilla::User({ name => Bugzilla->params->{'maintainer'} });
-
-    if ($bug->id == 1) {
-        # Uncomment the line below to add the maintainer to the recipients
-        # list of every bugmail from bug 1 as though that the maintainer
-        # were on the CC list.
-        #$recipients->{$user->id}->{+REL_CC} = 1;
-
-        # And this line adds the maintainer as though they had the
-        # "REL_EXAMPLE" relationship from the bugmail_relationships hook below.
-        #$recipients->{$user->id}->{+REL_EXAMPLE} = 1;
-    }
+  my ($self, $args) = @_;
+  my $recipients = $args->{recipients};
+  my $bug        = $args->{bug};
+
+  my $user = new Bugzilla::User({name => Bugzilla->params->{'maintainer'}});
+
+  if ($bug->id == 1) {
+
+    # Uncomment the line below to add the maintainer to the recipients
+    # list of every bugmail from bug 1 as though that the maintainer
+    # were on the CC list.
+    #$recipients->{$user->id}->{+REL_CC} = 1;
+
+    # And this line adds the maintainer as though they had the
+    # "REL_EXAMPLE" relationship from the bugmail_relationships hook below.
+    #$recipients->{$user->id}->{+REL_EXAMPLE} = 1;
+  }
 }
 
 sub bugmail_relationships {
-    my ($self, $args) = @_;
-    my $relationships = $args->{relationships};
-    $relationships->{+REL_EXAMPLE} = 'Example';
+  my ($self, $args) = @_;
+  my $relationships = $args->{relationships};
+  $relationships->{+REL_EXAMPLE} = 'Example';
 }
 
 sub cgi_headers {
-    my ($self, $args) = @_;
-    my $headers = $args->{'headers'};
+  my ($self, $args) = @_;
+  my $headers = $args->{'headers'};
 
-    $headers->{'-x_test_header'} = "Test header from Example extension";
+  $headers->{'-x_test_header'} = "Test header from Example extension";
 }
 
 sub config_add_panels {
-    my ($self, $args) = @_;
-    
-    my $modules = $args->{panel_modules};
-    $modules->{Example} = "Bugzilla::Extension::Example::Config";
+  my ($self, $args) = @_;
+
+  my $modules = $args->{panel_modules};
+  $modules->{Example} = "Bugzilla::Extension::Example::Config";
 }
 
 sub config_modify_panels {
-    my ($self, $args) = @_;
-    
-    my $panels = $args->{panels};
-    
-    # Add the "Example" auth methods.
-    my $auth_params = $panels->{'auth'}->{params};
-    my ($info_class)   = grep($_->{name} eq 'user_info_class', @$auth_params);
-    my ($verify_class) = grep($_->{name} eq 'user_verify_class', @$auth_params);
+  my ($self, $args) = @_;
+
+  my $panels = $args->{panels};
 
-    push(@{ $info_class->{choices} },   'CGI,Example');
-    push(@{ $verify_class->{choices} }, 'Example');
+  # Add the "Example" auth methods.
+  my $auth_params = $panels->{'auth'}->{params};
+  my ($info_class)   = grep($_->{name} eq 'user_info_class',   @$auth_params);
+  my ($verify_class) = grep($_->{name} eq 'user_verify_class', @$auth_params);
 
-    push(@$auth_params, { name => 'param_example',
-                          type => 't',
-                          default => 0,
-                          checker => \&check_numeric });    
+  push(@{$info_class->{choices}},   'CGI,Example');
+  push(@{$verify_class->{choices}}, 'Example');
+
+  push(
+    @$auth_params,
+    {
+      name    => 'param_example',
+      type    => 't',
+      default => 0,
+      checker => \&check_numeric
+    }
+  );
 }
 
 sub db_schema_abstract_schema {
-    my ($self, $args) = @_;
+  my ($self, $args) = @_;
+
 #    $args->{'schema'}->{'example_table'} = {
 #        FIELDS => [
 #            id       => {TYPE => 'SMALLSERIAL', NOTNULL => 1,
@@ -415,676 +432,704 @@ sub db_schema_abstract_schema {
 }
 
 sub email_in_before_parse {
-    my ($self, $args) = @_;
+  my ($self, $args) = @_;
 
-    my $subject = $args->{mail}->header('Subject');
-    # Correctly extract the bug ID from email subjects of the form [Bug comp/NNN].
-    if ($subject =~ /\[.*(\d+)\].*/) {
-        $args->{fields}->{bug_id} = $1;
-    }
+  my $subject = $args->{mail}->header('Subject');
+
+  # Correctly extract the bug ID from email subjects of the form [Bug comp/NNN].
+  if ($subject =~ /\[.*(\d+)\].*/) {
+    $args->{fields}->{bug_id} = $1;
+  }
 }
 
 sub email_in_after_parse {
-    my ($self, $args) = @_;
-    my $reporter = $args->{fields}->{reporter};
-    my $dbh = Bugzilla->dbh;
-
-    # No other check needed if this is a valid regular user.
-    return if login_to_id($reporter);
-
-    # The reporter is not a regular user. We create an account for them,
-    # but they can only comment on existing bugs.
-    # This is useful for people who reply by email to bugmails received
-    # in mailing-lists.
-    if ($args->{fields}->{bug_id}) {
-        # WARNING: we return now to skip the remaining code below.
-        # You must understand that removing this line would make the code
-        # below effective! Do it only if you are OK with the behavior
-        # described here.
-        return;
-
-        Bugzilla::User->create({ login_name => $reporter, cryptpassword => '*' });
-
-        # For security reasons, delete all fields unrelated to comments.
-        foreach my $field (keys %{$args->{fields}}) {
-            next if $field =~ /^(?:bug_id|comment|reporter)$/;
-            delete $args->{fields}->{$field};
-        }
-    }
-    else {
-        ThrowUserError('invalid_username', { name => $reporter });
+  my ($self, $args) = @_;
+  my $reporter = $args->{fields}->{reporter};
+  my $dbh      = Bugzilla->dbh;
+
+  # No other check needed if this is a valid regular user.
+  return if login_to_id($reporter);
+
+  # The reporter is not a regular user. We create an account for them,
+  # but they can only comment on existing bugs.
+  # This is useful for people who reply by email to bugmails received
+  # in mailing-lists.
+  if ($args->{fields}->{bug_id}) {
+
+    # WARNING: we return now to skip the remaining code below.
+    # You must understand that removing this line would make the code
+    # below effective! Do it only if you are OK with the behavior
+    # described here.
+    return;
+
+    Bugzilla::User->create({login_name => $reporter, cryptpassword => '*'});
+
+    # For security reasons, delete all fields unrelated to comments.
+    foreach my $field (keys %{$args->{fields}}) {
+      next if $field =~ /^(?:bug_id|comment|reporter)$/;
+      delete $args->{fields}->{$field};
     }
+  }
+  else {
+    ThrowUserError('invalid_username', {name => $reporter});
+  }
 }
 
 sub enter_bug_entrydefaultvars {
-    my ($self, $args) = @_;
-    
-    my $vars = $args->{vars};
-    $vars->{'example'} = 1;
+  my ($self, $args) = @_;
+
+  my $vars = $args->{vars};
+  $vars->{'example'} = 1;
 }
 
 sub error_catch {
-    my ($self, $args) = @_;
-    # Customize the error message displayed when someone tries to access
-    # page.cgi with an invalid page ID, and keep track of this attempt
-    # in the web server log.
-    return unless Bugzilla->error_mode == ERROR_MODE_WEBPAGE;
-    return unless $args->{error} eq 'bad_page_cgi_id';
+  my ($self, $args) = @_;
+
+  # Customize the error message displayed when someone tries to access
+  # page.cgi with an invalid page ID, and keep track of this attempt
+  # in the web server log.
+  return unless Bugzilla->error_mode == ERROR_MODE_WEBPAGE;
+  return unless $args->{error} eq 'bad_page_cgi_id';
 
-    my $page_id = $args->{vars}->{page_id};
-    my $login = Bugzilla->user->identity || "Someone";
-    warn "$login attempted to access page.cgi with id = $page_id";
+  my $page_id = $args->{vars}->{page_id};
+  my $login = Bugzilla->user->identity || "Someone";
+  warn "$login attempted to access page.cgi with id = $page_id";
 
-    my $page = $args->{message};
-    my $new_error_msg = "Ah ah, you tried to access $page_id? Good try!";
-    $new_error_msg = html_quote($new_error_msg);
-    # There are better tools to parse an HTML page, but it's just an example.
-    $$page =~ s/<div id="error_msg" class="throw_error">\K(.*)/$new_error_msg<br>$1/siaa;
+  my $page          = $args->{message};
+  my $new_error_msg = "Ah ah, you tried to access $page_id? Good try!";
+  $new_error_msg = html_quote($new_error_msg);
+
+  # There are better tools to parse an HTML page, but it's just an example.
+  $$page
+    =~ s/<div id="error_msg" class="throw_error">\K(.*)/$new_error_msg<br>$1/siaa;
 }
 
 sub flag_end_of_update {
-    my ($self, $args) = @_;
-    
-    # This code doesn't actually *do* anything, it's just here to show you
-    # how to use this hook.
-    my $flag_params = $args;
-    my ($object, $timestamp, $old_flags, $new_flags) =
-        @$flag_params{qw(object timestamp old_flags new_flags)};
-    my ($removed, $added) = diff_arrays($old_flags, $new_flags);
-    my ($granted, $denied) = (0, 0);
-    foreach my $new_flag (@$added) {
-        $granted++ if $new_flag =~ /\+$/;
-        $denied++ if $new_flag =~ /-$/;
-    }
-    my $bug_id = $object->isa('Bugzilla::Bug') ? $object->id 
-                                               : $object->bug_id;
-    my $result = "$granted flags were granted and $denied flags were denied"
-                 . " on bug $bug_id at $timestamp.";
-    # Uncomment this line to see $result in your webserver's error log whenever
-    # you update flags.
-    # warn $result;
+  my ($self, $args) = @_;
+
+  # This code doesn't actually *do* anything, it's just here to show you
+  # how to use this hook.
+  my $flag_params = $args;
+  my ($object, $timestamp, $old_flags, $new_flags)
+    = @$flag_params{qw(object timestamp old_flags new_flags)};
+  my ($removed, $added) = diff_arrays($old_flags, $new_flags);
+  my ($granted, $denied) = (0, 0);
+  foreach my $new_flag (@$added) {
+    $granted++ if $new_flag =~ /\+$/;
+    $denied++  if $new_flag =~ /-$/;
+  }
+  my $bug_id = $object->isa('Bugzilla::Bug') ? $object->id : $object->bug_id;
+  my $result = "$granted flags were granted and $denied flags were denied"
+    . " on bug $bug_id at $timestamp.";
+
+  # Uncomment this line to see $result in your webserver's error log whenever
+  # you update flags.
+  # warn $result;
 }
 
 sub group_before_delete {
-    my ($self, $args) = @_;
-    # This code doesn't actually *do* anything, it's just here to show you
-    # how to use this hook.
+  my ($self, $args) = @_;
+
+  # This code doesn't actually *do* anything, it's just here to show you
+  # how to use this hook.
+
+  my $group    = $args->{'group'};
+  my $group_id = $group->id;
 
-    my $group = $args->{'group'};
-    my $group_id = $group->id;
-    # Uncomment this line to see a line in your webserver's error log whenever
-    # you file a bug.
-    # warn "Group $group_id is about to be deleted!";
+  # Uncomment this line to see a line in your webserver's error log whenever
+  # you file a bug.
+  # warn "Group $group_id is about to be deleted!";
 }
 
 sub group_end_of_create {
-    my ($self, $args) = @_;
-    # This code doesn't actually *do* anything, it's just here to show you
-    # how to use this hook.
-    my $group = $args->{'group'};
+  my ($self, $args) = @_;
 
-    my $group_id = $group->id;
-    # Uncomment this line to see a line in your webserver's error log whenever
-    # you create a new group.
-    #warn "Group $group_id has been created!";
+  # This code doesn't actually *do* anything, it's just here to show you
+  # how to use this hook.
+  my $group = $args->{'group'};
+
+  my $group_id = $group->id;
+
+  # Uncomment this line to see a line in your webserver's error log whenever
+  # you create a new group.
+  #warn "Group $group_id has been created!";
 }
 
 sub group_end_of_update {
-    my ($self, $args) = @_;
-    # This code doesn't actually *do* anything, it's just here to show you
-    # how to use this hook.
+  my ($self, $args) = @_;
 
-    my ($group, $changes) = @$args{qw(group changes)};
+  # This code doesn't actually *do* anything, it's just here to show you
+  # how to use this hook.
 
-    foreach my $field (keys %$changes) {
-        my $used_to_be = $changes->{$field}->[0];
-        my $now_it_is  = $changes->{$field}->[1];
-    }
+  my ($group, $changes) = @$args{qw(group changes)};
+
+  foreach my $field (keys %$changes) {
+    my $used_to_be = $changes->{$field}->[0];
+    my $now_it_is  = $changes->{$field}->[1];
+  }
 
-    my $group_id = $group->id;
-    my $num_changes = scalar keys %$changes;
-    my $result = 
-        "There were $num_changes changes to fields on group $group_id.";
-    # Uncomment this line to see $result in your webserver's error log whenever
-    # you update a group.
-    #warn $result;
+  my $group_id    = $group->id;
+  my $num_changes = scalar keys %$changes;
+  my $result = "There were $num_changes changes to fields on group $group_id.";
+
+  # Uncomment this line to see $result in your webserver's error log whenever
+  # you update a group.
+  #warn $result;
 }
 
 sub install_before_final_checks {
-    my ($self, $args) = @_;
-    print "Install-before_final_checks hook\n" unless $args->{silent};
-    
-    # Add a new user setting like this:
-    #
-    # add_setting('product_chooser',           # setting name
-    #             ['pretty', 'full', 'small'], # options
-    #             'pretty');                   # default
-    #
-    # To add descriptions for the setting and choices, add extra values to 
-    # the hash defined in global/setting-descs.none.tmpl. Do this in a hook: 
-    # hook/global/setting-descs-settings.none.tmpl .
+  my ($self, $args) = @_;
+  print "Install-before_final_checks hook\n" unless $args->{silent};
+
+  # Add a new user setting like this:
+  #
+  # add_setting('product_chooser',           # setting name
+  #             ['pretty', 'full', 'small'], # options
+  #             'pretty');                   # default
+  #
+  # To add descriptions for the setting and choices, add extra values to
+  # the hash defined in global/setting-descs.none.tmpl. Do this in a hook:
+  # hook/global/setting-descs-settings.none.tmpl .
 }
 
 sub install_filesystem {
-    my ($self, $args) = @_;
-    my $create_dirs  = $args->{'create_dirs'};
-    my $recurse_dirs = $args->{'recurse_dirs'};
-    my $htaccess     = $args->{'htaccess'};
-
-    # Create a new directory in datadir specifically for this extension.
-    # The directory will need to allow files to be created by the extension
-    # code as well as allow the webserver to server content from it.
-    # my $data_path = bz_locations->{'datadir'} . "/" . __PACKAGE__->NAME;
-    # $create_dirs->{$data_path} = Bugzilla::Install::Filesystem::DIR_CGI_WRITE;
-   
-    # Update the permissions of any files and directories that currently reside
-    # in the extension's directory. 
-    # $recurse_dirs->{$data_path} = {
-    #     files => Bugzilla::Install::Filesystem::CGI_READ,
-    #     dirs  => Bugzilla::Install::Filesystem::DIR_CGI_WRITE
-    # };
-    
-    # Create a htaccess file that allows specific content to be served from the 
-    # extension's directory.
-    # $htaccess->{"$data_path/.htaccess"} = {
-    #     perms    => Bugzilla::Install::Filesystem::WS_SERVE,
-    #     contents => Bugzilla::Install::Filesystem::HT_DEFAULT_DENY
-    # };
+  my ($self, $args) = @_;
+  my $create_dirs  = $args->{'create_dirs'};
+  my $recurse_dirs = $args->{'recurse_dirs'};
+  my $htaccess     = $args->{'htaccess'};
+
+  # Create a new directory in datadir specifically for this extension.
+  # The directory will need to allow files to be created by the extension
+  # code as well as allow the webserver to server content from it.
+  # my $data_path = bz_locations->{'datadir'} . "/" . __PACKAGE__->NAME;
+  # $create_dirs->{$data_path} = Bugzilla::Install::Filesystem::DIR_CGI_WRITE;
+
+  # Update the permissions of any files and directories that currently reside
+  # in the extension's directory.
+  # $recurse_dirs->{$data_path} = {
+  #     files => Bugzilla::Install::Filesystem::CGI_READ,
+  #     dirs  => Bugzilla::Install::Filesystem::DIR_CGI_WRITE
+  # };
+
+  # Create a htaccess file that allows specific content to be served from the
+  # extension's directory.
+  # $htaccess->{"$data_path/.htaccess"} = {
+  #     perms    => Bugzilla::Install::Filesystem::WS_SERVE,
+  #     contents => Bugzilla::Install::Filesystem::HT_DEFAULT_DENY
+  # };
 }
 
 sub install_update_db {
-    my $dbh = Bugzilla->dbh;
+  my $dbh = Bugzilla->dbh;
+
 #    $dbh->bz_add_column('example', 'new_column',
 #                        {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0});
 #    $dbh->bz_add_index('example', 'example_new_column_idx', [qw(value)]);
 }
 
 sub install_update_db_fielddefs {
-    my $dbh = Bugzilla->dbh;
-#    $dbh->bz_add_column('fielddefs', 'example_column', 
+  my $dbh = Bugzilla->dbh;
+
+#    $dbh->bz_add_column('fielddefs', 'example_column',
 #                        {TYPE => 'MEDIUMTEXT', NOTNULL => 1, DEFAULT => ''});
 }
 
 sub job_map {
-    my ($self, $args) = @_;
-    
-    my $job_map = $args->{job_map};
-    
-    # This adds the named class (an instance of TheSchwartz::Worker) as a
-    # handler for when a job is added with the name "some_task".
-    $job_map->{'some_task'} = 'Bugzilla::Extension::Example::Job::SomeClass';
-    
-    # Schedule a job like this:
-    # my $queue = Bugzilla->job_queue();
-    # $queue->insert('some_task', { some_parameter => $some_variable });
+  my ($self, $args) = @_;
+
+  my $job_map = $args->{job_map};
+
+  # This adds the named class (an instance of TheSchwartz::Worker) as a
+  # handler for when a job is added with the name "some_task".
+  $job_map->{'some_task'} = 'Bugzilla::Extension::Example::Job::SomeClass';
+
+  # Schedule a job like this:
+  # my $queue = Bugzilla->job_queue();
+  # $queue->insert('some_task', { some_parameter => $some_variable });
 }
 
 sub mailer_before_send {
-    my ($self, $args) = @_;
-    
-    my $email = $args->{email};
-    # If you add a header to an email, it's best to start it with
-    # 'X-Bugzilla-<Extension>' so that you don't conflict with
-    # other extensions.
-    $email->header_set('X-Bugzilla-Example-Header', 'Example');
+  my ($self, $args) = @_;
+
+  my $email = $args->{email};
+
+  # If you add a header to an email, it's best to start it with
+  # 'X-Bugzilla-<Extension>' so that you don't conflict with
+  # other extensions.
+  $email->header_set('X-Bugzilla-Example-Header', 'Example');
 }
 
 sub migrate_modules {
-    my ($self, $args) = @_;
-    $args->{path} = bz_locations->{'extensionsdir'} . "/Example/lib/Migrate";
-    $args->{prefix} = "Bugzilla::Extension::Example::Migrate";
+  my ($self, $args) = @_;
+  $args->{path}   = bz_locations->{'extensionsdir'} . "/Example/lib/Migrate";
+  $args->{prefix} = "Bugzilla::Extension::Example::Migrate";
 }
 
 sub object_before_create {
-    my ($self, $args) = @_;
-    
-    my $class = $args->{'class'};
-    my $object_params = $args->{'params'};
-    
-    # Note that this is a made-up class, for this example.
-    if ($class->isa('Bugzilla::ExampleObject')) {
-        warn "About to create an ExampleObject!";
-        warn "Got the following parameters: " 
-             . join(', ', keys(%$object_params));
-    }
+  my ($self, $args) = @_;
+
+  my $class         = $args->{'class'};
+  my $object_params = $args->{'params'};
+
+  # Note that this is a made-up class, for this example.
+  if ($class->isa('Bugzilla::ExampleObject')) {
+    warn "About to create an ExampleObject!";
+    warn "Got the following parameters: " . join(', ', keys(%$object_params));
+  }
 }
 
 sub object_before_delete {
-    my ($self, $args) = @_;
+  my ($self, $args) = @_;
 
-    my $object = $args->{'object'};
+  my $object = $args->{'object'};
 
-    # Note that this is a made-up class, for this example.
-    if ($object->isa('Bugzilla::ExampleObject')) {
-        my $id = $object->id;
-        warn "An object with id $id is about to be deleted!";
-    } 
+  # Note that this is a made-up class, for this example.
+  if ($object->isa('Bugzilla::ExampleObject')) {
+    my $id = $object->id;
+    warn "An object with id $id is about to be deleted!";
+  }
 }
 
 sub object_before_set {
-    my ($self, $args) = @_;
-    
-    my ($object, $field, $value) = @$args{qw(object field value)};
-    
-    # Note that this is a made-up class, for this example.
-    if ($object->isa('Bugzilla::ExampleObject')) {
-        warn "The field $field is changing from " . $object->{$field} 
-             . " to $value!";
-    }
+  my ($self, $args) = @_;
+
+  my ($object, $field, $value) = @$args{qw(object field value)};
+
+  # Note that this is a made-up class, for this example.
+  if ($object->isa('Bugzilla::ExampleObject')) {
+    warn "The field $field is changing from " . $object->{$field} . " to $value!";
+  }
 }
 
 sub object_columns {
-    my ($self, $args) = @_;
-    my ($class, $columns) = @$args{qw(class columns)};
+  my ($self,  $args)    = @_;
+  my ($class, $columns) = @$args{qw(class columns)};
 
-    if ($class->isa('Bugzilla::ExampleObject')) {
-        push(@$columns, 'example');
-    }
+  if ($class->isa('Bugzilla::ExampleObject')) {
+    push(@$columns, 'example');
+  }
 }
 
 sub object_end_of_create {
-    my ($self, $args) = @_;
-    
-    my $class  = $args->{'class'};
-    my $object = $args->{'object'};
+  my ($self, $args) = @_;
+
+  my $class  = $args->{'class'};
+  my $object = $args->{'object'};
 
-    warn "Created a new $class object!";
+  warn "Created a new $class object!";
 }
 
 sub object_end_of_create_validators {
-    my ($self, $args) = @_;
-    
-    my $class = $args->{'class'};
-    my $object_params = $args->{'params'};
-    
-    # Note that this is a made-up class, for this example.
-    if ($class->isa('Bugzilla::ExampleObject')) {
-        # Always set example_field to 1, even if the validators said otherwise.
-        $object_params->{example_field} = 1;
-    }
-    
+  my ($self, $args) = @_;
+
+  my $class         = $args->{'class'};
+  my $object_params = $args->{'params'};
+
+  # Note that this is a made-up class, for this example.
+  if ($class->isa('Bugzilla::ExampleObject')) {
+
+    # Always set example_field to 1, even if the validators said otherwise.
+    $object_params->{example_field} = 1;
+  }
+
 }
 
 sub object_end_of_set {
-    my ($self, $args) = @_;
+  my ($self, $args) = @_;
 
-    my ($object, $field) = @$args{qw(object field)};
+  my ($object, $field) = @$args{qw(object field)};
 
-    # Note that this is a made-up class, for this example.
-    if ($object->isa('Bugzilla::ExampleObject')) {
-        warn "The field $field has changed to " . $object->{$field};
-    }
+  # Note that this is a made-up class, for this example.
+  if ($object->isa('Bugzilla::ExampleObject')) {
+    warn "The field $field has changed to " . $object->{$field};
+  }
 }
 
 sub object_end_of_set_all {
-    my ($self, $args) = @_;
-    
-    my $object = $args->{'object'};
-    my $object_params = $args->{'params'};
-    
-    # Note that this is a made-up class, for this example.
-    if ($object->isa('Bugzilla::ExampleObject')) {
-        if ($object_params->{example_field} == 1) {
-            $object->{example_field} = 1;
-        }
+  my ($self, $args) = @_;
+
+  my $object        = $args->{'object'};
+  my $object_params = $args->{'params'};
+
+  # Note that this is a made-up class, for this example.
+  if ($object->isa('Bugzilla::ExampleObject')) {
+    if ($object_params->{example_field} == 1) {
+      $object->{example_field} = 1;
     }
-    
+  }
+
 }
 
 sub object_end_of_update {
-    my ($self, $args) = @_;
-    
-    my ($object, $old_object, $changes) = 
-        @$args{qw(object old_object changes)};
-    
-    # Note that this is a made-up class, for this example.
-    if ($object->isa('Bugzilla::ExampleObject')) {
-        if (defined $changes->{'name'}) {
-            my ($old, $new) = @{ $changes->{'name'} };
-            print "The name field changed from $old to $new!";
-        }
+  my ($self, $args) = @_;
+
+  my ($object, $old_object, $changes) = @$args{qw(object old_object changes)};
+
+  # Note that this is a made-up class, for this example.
+  if ($object->isa('Bugzilla::ExampleObject')) {
+    if (defined $changes->{'name'}) {
+      my ($old, $new) = @{$changes->{'name'}};
+      print "The name field changed from $old to $new!";
     }
+  }
 }
 
 sub object_update_columns {
-    my ($self, $args) = @_;
-    my ($object, $columns) = @$args{qw(object columns)};
+  my ($self,   $args)    = @_;
+  my ($object, $columns) = @$args{qw(object columns)};
 
-    if ($object->isa('Bugzilla::ExampleObject')) {
-        push(@$columns, 'example');
-    }
+  if ($object->isa('Bugzilla::ExampleObject')) {
+    push(@$columns, 'example');
+  }
 }
 
 sub object_validators {
-    my ($self, $args) = @_;
-    my ($class, $validators) = @$args{qw(class validators)};
-
-    if ($class->isa('Bugzilla::Bug')) {
-        # This is an example of adding a new validator.
-        # See the _check_example subroutine below.
-        $validators->{example} = \&_check_example;
-
-        # This is an example of overriding an existing validator.
-        # See the check_short_desc validator below.
-        my $original = $validators->{short_desc};
-        $validators->{short_desc} = sub { _check_short_desc($original, @_) };
-    }
+  my ($self,  $args)       = @_;
+  my ($class, $validators) = @$args{qw(class validators)};
+
+  if ($class->isa('Bugzilla::Bug')) {
+
+    # This is an example of adding a new validator.
+    # See the _check_example subroutine below.
+    $validators->{example} = \&_check_example;
+
+    # This is an example of overriding an existing validator.
+    # See the check_short_desc validator below.
+    my $original = $validators->{short_desc};
+    $validators->{short_desc} = sub { _check_short_desc($original, @_) };
+  }
 }
 
 sub _check_example {
-    my ($invocant, $value, $field) = @_;
-    warn "I was called to validate the value of $field.";
-    warn "The value of $field that I was passed in is: $value";
+  my ($invocant, $value, $field) = @_;
+  warn "I was called to validate the value of $field.";
+  warn "The value of $field that I was passed in is: $value";
 
-    # Make the value always be 1.
-    my $fixed_value = 1;
-    return $fixed_value;
+  # Make the value always be 1.
+  my $fixed_value = 1;
+  return $fixed_value;
 }
 
 sub _check_short_desc {
-    my $original = shift;
-    my $invocant = shift;
-    my $value = $invocant->$original(@_);
-    if ($value !~ /example/i) {
-        # Use this line to make Bugzilla throw an error every time
-        # you try to file a bug or update a bug without the word "example"
-        # in the summary.
-        if (0) {
-            ThrowUserError('example_short_desc_invalid');
-        }
+  my $original = shift;
+  my $invocant = shift;
+  my $value    = $invocant->$original(@_);
+  if ($value !~ /example/i) {
+
+    # Use this line to make Bugzilla throw an error every time
+    # you try to file a bug or update a bug without the word "example"
+    # in the summary.
+    if (0) {
+      ThrowUserError('example_short_desc_invalid');
     }
-    return $value;
+  }
+  return $value;
 }
 
 sub page_before_template {
-    my ($self, $args) = @_;
-    
-    my ($vars, $page) = @$args{qw(vars page_id)};
-    
-    # You can see this hook in action by loading page.cgi?id=example.html
-    if ($page eq 'example.html') {
-        $vars->{cgi_variables} = { Bugzilla->cgi->Vars };
-    }
+  my ($self, $args) = @_;
+
+  my ($vars, $page) = @$args{qw(vars page_id)};
+
+  # You can see this hook in action by loading page.cgi?id=example.html
+  if ($page eq 'example.html') {
+    $vars->{cgi_variables} = {Bugzilla->cgi->Vars};
+  }
 }
 
 sub path_info_whitelist {
-    my ($self, $args) = @_;
-    my $whitelist = $args->{whitelist};
-    push(@$whitelist, "page.cgi");
+  my ($self, $args) = @_;
+  my $whitelist = $args->{whitelist};
+  push(@$whitelist, "page.cgi");
 }
 
 sub post_bug_after_creation {
-    my ($self, $args) = @_;
-    
-    my $vars = $args->{vars};
-    $vars->{'example'} = 1;
+  my ($self, $args) = @_;
+
+  my $vars = $args->{vars};
+  $vars->{'example'} = 1;
 }
 
 sub product_confirm_delete {
-    my ($self, $args) = @_;
-    
-    my $vars = $args->{vars};
-    $vars->{'example'} = 1;
+  my ($self, $args) = @_;
+
+  my $vars = $args->{vars};
+  $vars->{'example'} = 1;
 }
 
 
 sub product_end_of_create {
-    my ($self, $args) = @_;
+  my ($self, $args) = @_;
+
+  my $product = $args->{product};
 
-    my $product = $args->{product};
+  # For this example, any lines of code that actually make changes to your
+  # database have been commented out.
 
-    # For this example, any lines of code that actually make changes to your
-    # database have been commented out.
+  # This section will take a group that exists in your installation
+  # (possible called test_group) and automatically makes the new
+  # product hidden to only members of the group. Just remove
+  # the restriction if you want the new product to be public.
 
-    # This section will take a group that exists in your installation
-    # (possible called test_group) and automatically makes the new
-    # product hidden to only members of the group. Just remove
-    # the restriction if you want the new product to be public.
+  my $example_group = new Bugzilla::Group({name => 'example_group'});
 
-    my $example_group = new Bugzilla::Group({ name => 'example_group' });
+  if ($example_group) {
+    $product->set_group_controls(
+      $example_group,
+      {
+        entry         => 1,
+        membercontrol => CONTROLMAPMANDATORY,
+        othercontrol  => CONTROLMAPMANDATORY
+      }
+    );
 
-    if ($example_group) {
-        $product->set_group_controls($example_group, 
-                { entry          => 1,
-                  membercontrol  => CONTROLMAPMANDATORY,
-                  othercontrol   => CONTROLMAPMANDATORY });
 #        $product->update();
-    }
+  }
+
+  # This section will automatically add a default component
+  # to the new product called 'No Component'.
 
-    # This section will automatically add a default component
-    # to the new product called 'No Component'.
+  my $default_assignee
+    = new Bugzilla::User({name => Bugzilla->params->{maintainer}});
 
-    my $default_assignee = new Bugzilla::User(
-        { name => Bugzilla->params->{maintainer} });
+  if ($default_assignee) {
 
-    if ($default_assignee) {
 #        Bugzilla::Component->create(
 #            { name             => 'No Component',
 #              product          => $product,
-#              description      => 'Select this component if one does not ' . 
+#              description      => 'Select this component if one does not ' .
 #                                  'exist in the current list of components',
 #              initialowner     => $default_assignee });
-    }
+  }
 }
 
 sub query_format {
-    my ($self, $args) = @_;
-    my $vars = $args->{'vars'};
-    my $default = $vars->{'default'};
-    my $format = $args->{'format'};
-
-    # change some default values
-    if ($format->{'template'} eq "search/search-example.html.tmpl") {
-        $default->{'example'}[0] = $default->{'example'}[0] || "example";
-    }
+  my ($self, $args) = @_;
+  my $vars    = $args->{'vars'};
+  my $default = $vars->{'default'};
+  my $format  = $args->{'format'};
+
+  # change some default values
+  if ($format->{'template'} eq "search/search-example.html.tmpl") {
+    $default->{'example'}[0] = $default->{'example'}[0] || "example";
+  }
 }
 
 sub quicksearch_map {
-    my ($self, $args) = @_;
-    my $map = $args->{'map'};
+  my ($self, $args) = @_;
+  my $map = $args->{'map'};
 
-    # This demonstrates adding a shorter alias for a long custom field name.
-    $map->{'impact'} = $map->{'cf_long_field_name_for_impact_field'};
+  # This demonstrates adding a shorter alias for a long custom field name.
+  $map->{'impact'} = $map->{'cf_long_field_name_for_impact_field'};
 }
 
 sub sanitycheck_check {
-    my ($self, $args) = @_;
-    
-    my $dbh = Bugzilla->dbh;
-    my $sth;
-    
-    my $status = $args->{'status'};
-    
-    # Check that all users are Australian
-    $status->('example_check_au_user');
-    
-    $sth = $dbh->prepare("SELECT userid, login_name
+  my ($self, $args) = @_;
+
+  my $dbh = Bugzilla->dbh;
+  my $sth;
+
+  my $status = $args->{'status'};
+
+  # Check that all users are Australian
+  $status->('example_check_au_user');
+
+  $sth = $dbh->prepare(
+    "SELECT userid, login_name
                             FROM profiles
-                           WHERE login_name NOT LIKE '%.au'");
-    $sth->execute;
-    
-    my $seen_nonau = 0;
-    while (my ($userid, $login, $numgroups) = $sth->fetchrow_array) {
-        $status->('example_check_au_user_alert',
-                  { userid => $userid, login => $login },
-                  'alert');
-        $seen_nonau = 1;
-    }
-    
-    $status->('example_check_au_user_prompt') if $seen_nonau;
+                           WHERE login_name NOT LIKE '%.au'"
+  );
+  $sth->execute;
+
+  my $seen_nonau = 0;
+  while (my ($userid, $login, $numgroups) = $sth->fetchrow_array) {
+    $status->(
+      'example_check_au_user_alert', {userid => $userid, login => $login}, 'alert'
+    );
+    $seen_nonau = 1;
+  }
+
+  $status->('example_check_au_user_prompt') if $seen_nonau;
 }
 
 sub sanitycheck_repair {
-    my ($self, $args) = @_;
-    
-    my $cgi = Bugzilla->cgi;
-    my $dbh = Bugzilla->dbh;
-    
-    my $status = $args->{'status'};
-    
-    if ($cgi->param('example_repair_au_user')) {
-        $status->('example_repair_au_user_start');
-    
-        #$dbh->do("UPDATE profiles
-        #             SET login_name = CONCAT(login_name, '.au')
-        #           WHERE login_name NOT LIKE '%.au'");
-    
-        $status->('example_repair_au_user_end');
-    }
+  my ($self, $args) = @_;
+
+  my $cgi = Bugzilla->cgi;
+  my $dbh = Bugzilla->dbh;
+
+  my $status = $args->{'status'};
+
+  if ($cgi->param('example_repair_au_user')) {
+    $status->('example_repair_au_user_start');
+
+    #$dbh->do("UPDATE profiles
+    #             SET login_name = CONCAT(login_name, '.au')
+    #           WHERE login_name NOT LIKE '%.au'");
+
+    $status->('example_repair_au_user_end');
+  }
 }
 
 sub template_before_create {
-    my ($self, $args) = @_;
-    
-    my $config = $args->{'config'};
-    # This will be accessible as "example_global_variable" in every
-    # template in Bugzilla. See Bugzilla/Template.pm's create() function
-    # for more things that you can set.
-    $config->{VARIABLES}->{example_global_variable} = sub { return 'value' };
+  my ($self, $args) = @_;
+
+  my $config = $args->{'config'};
+
+  # This will be accessible as "example_global_variable" in every
+  # template in Bugzilla. See Bugzilla/Template.pm's create() function
+  # for more things that you can set.
+  $config->{VARIABLES}->{example_global_variable} = sub { return 'value' };
 }
 
 sub template_before_process {
-    my ($self, $args) = @_;
-    
-    my ($vars, $file, $context) = @$args{qw(vars file context)};
+  my ($self, $args) = @_;
 
-    if ($file eq 'bug/edit.html.tmpl') {
-        $vars->{'viewing_the_bug_form'} = 1;
-    }
+  my ($vars, $file, $context) = @$args{qw(vars file context)};
+
+  if ($file eq 'bug/edit.html.tmpl') {
+    $vars->{'viewing_the_bug_form'} = 1;
+  }
 }
 
 sub user_check_account_creation {
-    my ($self, $args) = @_;
+  my ($self, $args) = @_;
 
-    my $login = $args->{login};
-    my $ip = remote_ip();
+  my $login = $args->{login};
+  my $ip    = remote_ip();
 
-    # Log all requests.
-    warn "USER ACCOUNT CREATION REQUEST FOR $login ($ip)";
+  # Log all requests.
+  warn "USER ACCOUNT CREATION REQUEST FOR $login ($ip)";
 
-    # Reject requests based on their email address.
-    if ($login =~ /\@evil\.com$/) {
-        ThrowUserError('account_creation_restricted');
-    }
+  # Reject requests based on their email address.
+  if ($login =~ /\@evil\.com$/) {
+    ThrowUserError('account_creation_restricted');
+  }
 
-    # Reject requests based on their IP address.
-    if ($ip =~ /^192\.168\./) {
-        ThrowUserError('account_creation_restricted');
-    }
+  # Reject requests based on their IP address.
+  if ($ip =~ /^192\.168\./) {
+    ThrowUserError('account_creation_restricted');
+  }
 }
 
 sub user_preferences {
-    my ($self, $args) = @_;
-    my $tab = $args->{current_tab};
-    my $save = $args->{save_changes};
-    my $handled = $args->{handled};
+  my ($self, $args) = @_;
+  my $tab     = $args->{current_tab};
+  my $save    = $args->{save_changes};
+  my $handled = $args->{handled};
 
-    return unless $tab eq 'my_tab';
+  return unless $tab eq 'my_tab';
 
-    my $value = Bugzilla->input_params->{'example_pref'};
-    if ($save) {
-        # Validate your data and update the DB accordingly.
-        $value =~ s/\s+/:/g;
-    }
-    $args->{'vars'}->{example_pref} = $value;
+  my $value = Bugzilla->input_params->{'example_pref'};
+  if ($save) {
 
-    # Set the 'handled' scalar reference to true so that the caller
-    # knows the panel name is valid and that an extension took care of it.
-    $$handled = 1;
+    # Validate your data and update the DB accordingly.
+    $value =~ s/\s+/:/g;
+  }
+  $args->{'vars'}->{example_pref} = $value;
+
+  # Set the 'handled' scalar reference to true so that the caller
+  # knows the panel name is valid and that an extension took care of it.
+  $$handled = 1;
 }
 
 sub webservice {
-    my ($self, $args) = @_;
+  my ($self, $args) = @_;
 
-    my $dispatch = $args->{dispatch};
-    $dispatch->{Example} = "Bugzilla::Extension::Example::WebService";
+  my $dispatch = $args->{dispatch};
+  $dispatch->{Example} = "Bugzilla::Extension::Example::WebService";
 }
 
 sub webservice_error_codes {
-    my ($self, $args) = @_;
+  my ($self, $args) = @_;
 
-    my $error_map = $args->{error_map};
-    $error_map->{'example_my_error'} = 10001;
+  my $error_map = $args->{error_map};
+  $error_map->{'example_my_error'} = 10001;
 }
 
 sub webservice_status_code_map {
-    my ($self, $args) = @_;
+  my ($self, $args) = @_;
+
+  my $status_code_map = $args->{status_code_map};
 
-    my $status_code_map = $args->{status_code_map};
-    # Uncomment this line to override the status code for the
-    # error 'object_does_not_exist' to STATUS_BAD_REQUEST
-    #$status_code_map->{51} = STATUS_BAD_REQUEST;
+  # Uncomment this line to override the status code for the
+  # error 'object_does_not_exist' to STATUS_BAD_REQUEST
+  #$status_code_map->{51} = STATUS_BAD_REQUEST;
 }
 
 sub webservice_before_call {
-    my ($self, $args) = @_;
+  my ($self, $args) = @_;
 
-    # This code doesn't actually *do* anything, it's just here to show you
-    # how to use this hook.
-    my $method      = $args->{method};
-    my $full_method = $args->{full_method};
+  # This code doesn't actually *do* anything, it's just here to show you
+  # how to use this hook.
+  my $method      = $args->{method};
+  my $full_method = $args->{full_method};
 
-    # Uncomment this line to see a line in your webserver's error log whenever
-    # a webservice call is made
-    #warn "RPC call $full_method made by ",
-    #   Bugzilla->user->login || 'an anonymous user', "\n";
+  # Uncomment this line to see a line in your webserver's error log whenever
+  # a webservice call is made
+  #warn "RPC call $full_method made by ",
+  #   Bugzilla->user->login || 'an anonymous user', "\n";
 }
 
 sub webservice_fix_credentials {
-    my ($self, $args) = @_;
-    my $rpc    = $args->{'rpc'};
-    my $params = $args->{'params'};
-    # Allow user to pass in username=foo&password=bar
-    if (exists $params->{'username'} && exists $params->{'password'}) {
-        $params->{'Bugzilla_login'}    = $params->{'username'};
-        $params->{'Bugzilla_password'} = $params->{'password'};
-    }
+  my ($self, $args) = @_;
+  my $rpc    = $args->{'rpc'};
+  my $params = $args->{'params'};
+
+  # Allow user to pass in username=foo&password=bar
+  if (exists $params->{'username'} && exists $params->{'password'}) {
+    $params->{'Bugzilla_login'}    = $params->{'username'};
+    $params->{'Bugzilla_password'} = $params->{'password'};
+  }
 }
 
 sub webservice_rest_request {
-    my ($self, $args) = @_;
-    my $rpc    = $args->{'rpc'};
-    my $params = $args->{'params'};
-    # Internally we may have a field called 'cf_test_field' but we allow users
-    # to use the shorter 'test_field' name.
-    if (exists $params->{'test_field'}) {
-        $params->{'test_field'} = delete $params->{'cf_test_field'};
-    }
+  my ($self, $args) = @_;
+  my $rpc    = $args->{'rpc'};
+  my $params = $args->{'params'};
+
+  # Internally we may have a field called 'cf_test_field' but we allow users
+  # to use the shorter 'test_field' name.
+  if (exists $params->{'test_field'}) {
+    $params->{'test_field'} = delete $params->{'cf_test_field'};
+  }
 }
 
 sub webservice_rest_resources {
-    my ($self, $args) = @_;
-    my $rpc = $args->{'rpc'};
-    my $resources = $args->{'resources'};
-    # Add a new resource that allows for /rest/example/hello
-    # to call Example.hello
-    #$resources->{'Bugzilla::Extension::Example::WebService'} = [
-    #    qr{^/example/hello$}, {
-    #        GET => {
-    #            method => 'hello',
-    #        }
-    #    }
-    #];
+  my ($self, $args) = @_;
+  my $rpc       = $args->{'rpc'};
+  my $resources = $args->{'resources'};
+
+  # Add a new resource that allows for /rest/example/hello
+  # to call Example.hello
+  #$resources->{'Bugzilla::Extension::Example::WebService'} = [
+  #    qr{^/example/hello$}, {
+  #        GET => {
+  #            method => 'hello',
+  #        }
+  #    }
+  #];
 }
 
 sub webservice_rest_result {
-    my ($self, $args) = @_;
-    my $result = $args->{'result'};
-    # Convert a list of bug hashes to a single bug hash if only one is
-    # being returned.
-    if (ref $$result eq 'HASH'
-        && exists $$result->{'bugs'}
-        && ref $$result->{'bugs'} eq 'ARRAY'
-        && scalar @{ $$result->{'bugs'} } == 1)
-    {
-        $$result = $$result->{'bugs'}->[0];
-    }
+  my ($self, $args) = @_;
+  my $result = $args->{'result'};
+
+  # Convert a list of bug hashes to a single bug hash if only one is
+  # being returned.
+  if ( ref $$result eq 'HASH'
+    && exists $$result->{'bugs'}
+    && ref $$result->{'bugs'} eq 'ARRAY'
+    && scalar @{$$result->{'bugs'}} == 1)
+  {
+    $$result = $$result->{'bugs'}->[0];
+  }
 }
 
 sub webservice_rest_response {
-    my ($self, $args) = @_;
-    my $response = $args->{'response'};
-    $response->header('X-Example-Header', 'This is an example header');
+  my ($self, $args) = @_;
+  my $response = $args->{'response'};
+  $response->header('X-Example-Header', 'This is an example header');
 }
 
 # This must be the last line of your extension.
index 77a113722e1fcfb3d0be03a34dd8ec8c154c98fe..3764f2d3d232e596493d8adb45f1b15b4ea11a84 100644 (file)
@@ -17,7 +17,7 @@ use Bugzilla::Constants;
 
 # Always returns no data.
 sub get_login_info {
-    return { failure => AUTH_NODATA };
+  return {failure => AUTH_NODATA};
 }
 
 1;
index 8cda4a9e86d3a45d3172ac413cb23c5eacd1aa8a..26d5d6e81d73129b15bdfd1e4c30f6dc1bb97506 100644 (file)
@@ -16,7 +16,7 @@ use Bugzilla::Constants;
 
 # A verifier that always fails.
 sub check_credentials {
-    return { failure => AUTH_NO_SUCH_USER };
+  return {failure => AUTH_NO_SUCH_USER};
 }
 
 1;
index c183b0efcc4376ef70908e481f68d4d7d3585215..79195ba9f18cc9b1ecaf080b11a06cd8d515ce71 100644 (file)
@@ -16,67 +16,60 @@ use Bugzilla::Config::Common;
 our $sortkey = 5000;
 
 sub get_param_list {
-    my ($class) = @_;
+  my ($class) = @_;
 
-    my @param_list = (
+  my @param_list = (
+    {name => 'example_string', type => 't', default => 'Bugzilla is powerful'},
     {
-        name => 'example_string',
-        type => 't',
-        default => 'Bugzilla is powerful'
+      name    => 'example_constrained_string',
+      type    => 't',
+      default => '12-xfdd-5',
+      checker => sub {
+        $_[0] =~ /^\d{2}\-[a-zA-Z]{4}\-\d$/
+          ? ''
+          : "$_[0] must be of the form NN-XXXX-N";
+      }
     },
     {
-        name => 'example_constrained_string',
-        type => 't',
-        default => '12-xfdd-5',
-        checker => sub { $_[0] =~ /^\d{2}\-[a-zA-Z]{4}\-\d$/ ?
-                         '' : "$_[0] must be of the form NN-XXXX-N"; }
+      name    => 'example_number',
+      type    => 't',
+      default => '905',
+      checker => \&check_numeric
     },
+    {name => 'example_password', type => 'p', default => '1234'},
     {
-        name => 'example_number',
-        type => 't',
-        default => '905',
-        checker => \&check_numeric
-    },
-    {
-        name => 'example_password',
-        type => 'p',
-        default => '1234'
-    },
-    {
-        name => 'example_multi_lines',
-        type => 'l',
-        default => "This text can be very long.\n\nVery very long!"
+      name    => 'example_multi_lines',
+      type    => 'l',
+      default => "This text can be very long.\n\nVery very long!"
     },
+
     # Default can only be 0 or 1.
+    {name => 'example_boolean', type => 'b', default => 0},
     {
-        name => 'example_boolean',
-        type => 'b',
-        default => 0
-    },
-    {
-        name => 'example_single_select',
-        type => 's',
-        choices => [ 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday' ],
-        default => 'Thursday',
-        checker => \&check_multi
+      name    => 'example_single_select',
+      type    => 's',
+      choices => ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'],
+      default => 'Thursday',
+      checker => \&check_multi
     },
     {
-        name => 'example_multi_select',
-        type => 'm',
-        choices => [ 'Mercury', 'Venus', 'Mars', 'Jupiter', 'Saturn' ],
-        default => [ 'Venus', 'Saturn' ],
-        checker => \&check_multi
+      name    => 'example_multi_select',
+      type    => 'm',
+      choices => ['Mercury', 'Venus', 'Mars', 'Jupiter', 'Saturn'],
+      default => ['Venus', 'Saturn'],
+      checker => \&check_multi
     },
+
     # This one lets you order selected items.
     {
-        name => 'example_multi_ordered',
-        type => 'o',
-        choices => [ 'Perl', 'Python', 'PHP', 'C++', 'Java' ],
-        default => 'Perl,C++',
-        checker => \&check_multi
+      name    => 'example_multi_ordered',
+      type    => 'o',
+      choices => ['Perl', 'Python', 'PHP', 'C++', 'Java'],
+      default => 'Perl,C++',
+      checker => \&check_multi
     },
-    );
-    return @param_list;
+  );
+  return @param_list;
 }
 
 1;
index 3160c91a85934a2d6a104384aef0a736fa36735b..af82b238b11f8468177c5ab83b31364c59e43147 100644 (file)
@@ -19,8 +19,7 @@ implementation see L<Bugzilla::Migrate::Gnats>.
 
 package Bugzilla::Extension::Example::Migrate::ImportBugs;
 
-use 5.14.0
-use strict;
+use 5.14.0 use strict;
 use warnings;
 
 use parent qw(Bugzilla::Migrate);
@@ -30,41 +29,32 @@ use Bugzilla::Install::Util qw(indicate_progress);
 use Bugzilla::Util qw(format_time trim generate_random_password);
 
 use constant REQUIRED_MODULES => [
-    {
-        package => 'Email-Simple-FromHandle',
-        module  => 'Email::Simple::FromHandle',
-        version => 0.050,
-    },
+  {
+    package => 'Email-Simple-FromHandle',
+    module  => 'Email::Simple::FromHandle',
+    version => 0.050,
+  },
 ];
 
-use constant FIELD_MAP => {
-    'Number'         => 'bug_id',
-    'Category'       => 'product',
-};
+use constant FIELD_MAP => {'Number' => 'bug_id', 'Category' => 'product',};
 
 use constant VALUE_MAP => {
-    bug_severity => {
-        'serious'      => 'major',
-        'non-critical' => 'normal',
-    },
-    bug_status => {
-        'feedback'  => 'RESOLVED',
-        'released'  => 'VERIFIED',
-    },
+  bug_severity => {'serious'  => 'major',    'non-critical' => 'normal',},
+  bug_status   => {'feedback' => 'RESOLVED', 'released'     => 'VERIFIED',},
 };
 
 use constant IMPORTBUGS_CONFIG_VARS => (
-    {
-        name    => 'default_email_domain',
-        default => 'example.com',
-        desc    => <<'END',
+  {
+    name    => 'default_email_domain',
+    default => 'example.com',
+    desc    => <<'END',
 # Some 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
-    },
+  },
 );
 
 #########
@@ -72,7 +62,7 @@ END
 #########
 
 sub before_insert {
-    my $self = shift;
+  my $self = shift;
 }
 
 #########
@@ -80,7 +70,7 @@ sub before_insert {
 #########
 
 sub _read_users {
-    my $self = shift;
+  my $self = shift;
 }
 
 ############
@@ -88,7 +78,7 @@ sub _read_users {
 ############
 
 sub _read_products {
-    my $self = shift;
+  my $self = shift;
 }
 
 ################
@@ -96,11 +86,11 @@ sub _read_products {
 ################
 
 sub _read_bugs {
-    my $self = shift;
+  my $self = shift;
 }
 
 sub _parse_project {
-    my ($self, $directory) = @_;
+  my ($self, $directory) = @_;
 }
 
 1;
index 4dc7dd2e52483156fa269a34b96db3763179c88e..e9cba24610789002a62a3d3ea1c1601794f6bc91 100644 (file)
@@ -14,8 +14,8 @@ use parent qw(Bugzilla::WebService);
 use Bugzilla::Error;
 
 use constant PUBLIC_METHODS => qw(
-    hello
-    throw_an_error
+  hello
+  throw_an_error
 );
 
 # This can be called as Example.hello() from the WebService.
index 98550547e7e729ea2f467e5b13404fb43fdedab1..e2fc5245201e848911f107584cceb01b7812bca1 100644 (file)
@@ -5,8 +5,6 @@
 # This Source Code Form is "Incompatible With Secondary Licenses", as
 # defined by the Mozilla Public License, v. 2.0.
 
-%strings = (
-  feature_example_acme => 'Example Extension: Acme Feature',
-);
+%strings = (feature_example_acme => 'Example Extension: Acme Feature',);
 
 1;
index a93f350ff5a0aaccce536e5971e923d70acd1b79..0fea92c0fe0203f141287b5d4037746fcc3a065a 100644 (file)
@@ -13,10 +13,8 @@ use warnings;
 
 use constant NAME => 'MoreBugUrl';
 
-use constant REQUIRED_MODULES => [
-];
+use constant REQUIRED_MODULES => [];
 
-use constant OPTIONAL_MODULES => [
-];
+use constant OPTIONAL_MODULES => [];
 
 __PACKAGE__->NAME;
index d5fd661f6bb8f5992384af710835693e7290e337..9675e5b4d1f56556b4de99c9258e9d4fc41bb3cd 100644 (file)
@@ -14,41 +14,44 @@ use warnings;
 use parent qw(Bugzilla::Extension);
 
 use constant MORE_SUB_CLASSES => qw(
-    Bugzilla::Extension::MoreBugUrl::BitBucket
-    Bugzilla::Extension::MoreBugUrl::ReviewBoard
-    Bugzilla::Extension::MoreBugUrl::Rietveld
-    Bugzilla::Extension::MoreBugUrl::RT
-    Bugzilla::Extension::MoreBugUrl::GetSatisfaction
-    Bugzilla::Extension::MoreBugUrl::PHP
-    Bugzilla::Extension::MoreBugUrl::Redmine
-    Bugzilla::Extension::MoreBugUrl::Savane
-    Bugzilla::Extension::MoreBugUrl::Phabricator
+  Bugzilla::Extension::MoreBugUrl::BitBucket
+  Bugzilla::Extension::MoreBugUrl::ReviewBoard
+  Bugzilla::Extension::MoreBugUrl::Rietveld
+  Bugzilla::Extension::MoreBugUrl::RT
+  Bugzilla::Extension::MoreBugUrl::GetSatisfaction
+  Bugzilla::Extension::MoreBugUrl::PHP
+  Bugzilla::Extension::MoreBugUrl::Redmine
+  Bugzilla::Extension::MoreBugUrl::Savane
+  Bugzilla::Extension::MoreBugUrl::Phabricator
 );
 
 # We need to update bug_see_also table because both
 # Rietveld and ReviewBoard were originally under Bugzilla/BugUrl/.
 sub install_update_db {
-    my $dbh = Bugzilla->dbh;
+  my $dbh = Bugzilla->dbh;
 
-    my $should_rename = $dbh->selectrow_array(
-        q{SELECT 1 FROM bug_see_also
+  my $should_rename = $dbh->selectrow_array(
+    q{SELECT 1 FROM bug_see_also
           WHERE class IN ('Bugzilla::BugUrl::Rietveld', 
-                          'Bugzilla::BugUrl::ReviewBoard')});
-
-    if ($should_rename) {
-        my $sth = $dbh->prepare('UPDATE bug_see_also SET class = ?
-                                 WHERE class = ?');
-        $sth->execute('Bugzilla::Extension::MoreBugUrl::ReviewBoard',
-                      'Bugzilla::BugUrl::ReviewBoard');
-
-        $sth->execute('Bugzilla::Extension::MoreBugUrl::Rietveld',
-                      'Bugzilla::BugUrl::Rietveld');
-    }
+                          'Bugzilla::BugUrl::ReviewBoard')}
+  );
+
+  if ($should_rename) {
+    my $sth = $dbh->prepare(
+      'UPDATE bug_see_also SET class = ?
+                                 WHERE class = ?'
+    );
+    $sth->execute('Bugzilla::Extension::MoreBugUrl::ReviewBoard',
+      'Bugzilla::BugUrl::ReviewBoard');
+
+    $sth->execute('Bugzilla::Extension::MoreBugUrl::Rietveld',
+      'Bugzilla::BugUrl::Rietveld');
+  }
 }
 
 sub bug_url_sub_classes {
-    my ($self, $args) = @_;
-    push @{ $args->{sub_classes} }, MORE_SUB_CLASSES;
+  my ($self, $args) = @_;
+  push @{$args->{sub_classes}}, MORE_SUB_CLASSES;
 }
 
 __PACKAGE__->NAME;
index 8b01f0bb1809fe789288ce924b2f4d411bb2cd40..a0bf48a9065cd184e60659795cb90027445d4086 100644 (file)
@@ -18,23 +18,23 @@ use parent qw(Bugzilla::BugUrl);
 ###############################
 
 sub should_handle {
-    my ($class, $uri) = @_;
+  my ($class, $uri) = @_;
 
-    # BitBucket issues have the form of
-    # bitbucket.org/user/project/issue/1234
-    return (lc($uri->authority) eq "bitbucket.org"
-            && $uri->path =~ m|[^/]+/[^/]+/issue/\d+|i) ? 1 : 0;
+  # BitBucket issues have the form of
+  # bitbucket.org/user/project/issue/1234
+  return (lc($uri->authority) eq "bitbucket.org"
+      && $uri->path =~ m|[^/]+/[^/]+/issue/\d+|i) ? 1 : 0;
 }
 
 sub _check_value {
-    my $class = shift;
+  my $class = shift;
 
-    my $uri = $class->SUPER::_check_value(@_);
+  my $uri = $class->SUPER::_check_value(@_);
 
-    my ($path) = $uri->path =~ m|([^/]+/[^/]+/issue/\d+)|i;
-    $uri = new URI("https://bitbucket.org/$path");
+  my ($path) = $uri->path =~ m|([^/]+/[^/]+/issue/\d+)|i;
+  $uri = new URI("https://bitbucket.org/$path");
 
-    return $uri;
+  return $uri;
 }
 
 1;
index f50160e8999ed2a227a68e7827983d9551d1154e..c4748367763afd2d5cee52d9df85a4cc472442fd 100644 (file)
@@ -18,24 +18,24 @@ use parent qw(Bugzilla::BugUrl);
 ###############################
 
 sub should_handle {
-    my ($class, $uri) = @_;
+  my ($class, $uri) = @_;
 
-    # GetSatisfaction URLs only have one form:
-    #   http(s)://getsatisfaction.com/PROJECT_NAME/topics/TOPIC_NAME
-    return (lc($uri->authority) eq 'getsatisfaction.com'
-            and $uri->path =~ m|^/[^/]+/topics/[^/]+$|) ? 1 : 0;
+  # GetSatisfaction URLs only have one form:
+  #   http(s)://getsatisfaction.com/PROJECT_NAME/topics/TOPIC_NAME
+  return (lc($uri->authority) eq 'getsatisfaction.com'
+      and $uri->path =~ m|^/[^/]+/topics/[^/]+$|) ? 1 : 0;
 }
 
 sub _check_value {
-    my ($class, $uri) = @_;
+  my ($class, $uri) = @_;
 
-    $uri = $class->SUPER::_check_value($uri);
+  $uri = $class->SUPER::_check_value($uri);
 
-    # GetSatisfaction HTTP URLs redirect to HTTPS, so just use the HTTPS
-    # scheme.
-    $uri->scheme('https');
+  # GetSatisfaction HTTP URLs redirect to HTTPS, so just use the HTTPS
+  # scheme.
+  $uri->scheme('https');
 
-    return $uri;
+  return $uri;
 }
 
 1;
index 0cca7262f1503a28a7252617472ac5fa3b74f441..04f1b0bcb64a037aefed384f05a79fb94dbd7e25 100644 (file)
@@ -18,27 +18,27 @@ use parent qw(Bugzilla::BugUrl);
 ###############################
 
 sub should_handle {
-    my ($class, $uri) = @_;
+  my ($class, $uri) = @_;
 
-    # PHP Bug URLs have only one form:
-    #   https://bugs.php.net/bug.php?id=1234
-    return (lc($uri->authority) eq 'bugs.php.net'
-            and $uri->path =~ m|/bug\.php$|
-            and $uri->query_param('id') =~ /^\d+$/) ? 1 : 0;
+  # PHP Bug URLs have only one form:
+  #   https://bugs.php.net/bug.php?id=1234
+  return (lc($uri->authority) eq 'bugs.php.net'
+      and $uri->path =~ m|/bug\.php$|
+      and $uri->query_param('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(@_);
 
-    # PHP Bug URLs redirect to HTTPS, so just use the HTTPS scheme.
-    $uri->scheme('https');
+  # PHP Bug URLs redirect to HTTPS, so just use the HTTPS scheme.
+  $uri->scheme('https');
 
-    # And remove any # part if there is one.
-    $uri->fragment(undef);
+  # And remove any # part if there is one.
+  $uri->fragment(undef);
 
-    return $uri;
+  return $uri;
 }
 
 1;
index c282b1bda2de5c50cb68f24c70901a057a4aebd1..dbcae5aa1867760cfdf308a11dd843657add61d2 100644 (file)
@@ -18,24 +18,25 @@ use parent qw(Bugzilla::BugUrl);
 ###############################
 
 sub should_handle {
-    my ($class, $uri) = @_;
-    return ($uri->path =~ m|^/T\d+$|) ? 1 : 0;
+  my ($class, $uri) = @_;
+  return ($uri->path =~ m|^/T\d+$|) ? 1 : 0;
 }
 
 sub _check_value {
-    my $class = shift;
+  my $class = shift;
 
-    my $uri = $class->SUPER::_check_value(@_);
+  my $uri = $class->SUPER::_check_value(@_);
 
-    # Phabricator URLs have only one form:
-    #   http://example.com/T111
+  # Phabricator URLs have only one form:
+  #   http://example.com/T111
 
-    # 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;
index d25813e4b2cb7e6201ddf54069ff3ff4f6b0925d..514ded137c256bb99ea50c0c78e4283011710225 100644 (file)
@@ -18,24 +18,25 @@ use parent qw(Bugzilla::BugUrl);
 ###############################
 
 sub should_handle {
-    my ($class, $uri) = @_;
-
-    # RT URLs can look like various things:
-    #   http://example.com/rt/Ticket/Display.html?id=1234
-    #   https://example.com/Public/Bug/Display.html?id=1234
-    return ($uri->path =~ m|/Display\.html$|
-            and $uri->query_param('id') =~ /^\d+$/) ? 1 : 0;
+  my ($class, $uri) = @_;
+
+  # RT URLs can look like various things:
+  #   http://example.com/rt/Ticket/Display.html?id=1234
+  #   https://example.com/Public/Bug/Display.html?id=1234
+  return ($uri->path =~ m|/Display\.html$| and $uri->query_param('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(@_);
 
-    # And remove any # part if there is one.
-    $uri->fragment(undef);
+  # And remove any # part if there is one.
+  $uri->fragment(undef);
 
-    return $uri;
+  return $uri;
 }
 
 1;
index 5be0e25a0e93b8b1460ba4ac3ed7e386629e6224..600856f2216d2f05ff6e068cc319c41f94b296ae 100644 (file)
@@ -18,24 +18,25 @@ use parent qw(Bugzilla::BugUrl);
 ###############################
 
 sub should_handle {
-    my ($class, $uri) = @_;
-    return ($uri->path =~ m|/issues/\d+$|) ? 1 : 0;
+  my ($class, $uri) = @_;
+  return ($uri->path =~ m|/issues/\d+$|) ? 1 : 0;
 }
 
 sub _check_value {
-    my $class = shift;
+  my $class = shift;
 
-    my $uri = $class->SUPER::_check_value(@_);
+  my $uri = $class->SUPER::_check_value(@_);
 
-    # Redmine URLs have only one form:
-    #   http://demo.redmine.com/issues/111
+  # Redmine URLs have only one form:
+  #   http://demo.redmine.com/issues/111
 
-    # 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;
index d4691f7a0d269eb7a0feb4140167092ae3e89604..c50277a72bc5784becf4ed46ea20a9751b816da7 100644 (file)
@@ -18,29 +18,30 @@ use parent qw(Bugzilla::BugUrl);
 ###############################
 
 sub should_handle {
-    my ($class, $uri) = @_;
-    return ($uri->path =~ m|/r/\d+/?$|) ? 1 : 0;
+  my ($class, $uri) = @_;
+  return ($uri->path =~ m|/r/\d+/?$|) ? 1 : 0;
 }
 
 sub _check_value {
-    my $class = shift;
+  my $class = shift;
 
-    my $uri = $class->SUPER::_check_value(@_);
+  my $uri = $class->SUPER::_check_value(@_);
 
-    # Review Board URLs have only one form (the trailing slash is optional):
-    #   http://reviews.reviewboard.org/r/111/
+  # Review Board URLs have only one form (the trailing slash is optional):
+  #   http://reviews.reviewboard.org/r/111/
 
-    # 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);
 
-    # make sure the trailing slash is present
-    if ($uri->path !~ m|/$|) {
-        $uri->path($uri->path . '/');
-    }
+  # And remove any # part if there is one.
+  $uri->fragment(undef);
 
-    return $uri;
+  # make sure the trailing slash is present
+  if ($uri->path !~ m|/$|) {
+    $uri->path($uri->path . '/');
+  }
+
+  return $uri;
 }
 
 1;
index 4587c57b92c956a760aba14b9c1ff09364c4febd..d8cacea61dfc0a52d5d34e63e42a91b41d0c7235 100644 (file)
@@ -18,32 +18,34 @@ use parent qw(Bugzilla::BugUrl);
 ###############################
 
 sub should_handle {
-    my ($class, $uri) = @_;
-    return ($uri->authority =~ /\.appspot\.com$/i
-            and $uri->path =~ m#^/\d+(?:/|/show)?$#) ? 1 : 0;
+  my ($class, $uri) = @_;
+  return ($uri->authority =~ /\.appspot\.com$/i
+      and $uri->path =~ m#^/\d+(?:/|/show)?$#) ? 1 : 0;
 }
 
 sub _check_value {
-    my ($class, $uri) = @_;
-
-    $uri = $class->SUPER::_check_value($uri);
-
-    # Rietveld URLs have three forms:
-    #   http(s)://example.appspot.com/1234
-    #   http(s)://example.appspot.com/1234/
-    #   http(s)://example.appspot.com/1234/show
-    if ($uri->path =~ m#^/(\d+)(?:/|/show)$#) {
-        # This is the shortest standard URL form for Rietveld issues,
-        # and so we reduce all URLs to this.
-        $uri->path('/' . $1);
-    }
-
-    # Make sure there are no query parameters.
-    $uri->query(undef);
-    # And remove any # part if there is one.
-    $uri->fragment(undef);
-
-    return $uri;
+  my ($class, $uri) = @_;
+
+  $uri = $class->SUPER::_check_value($uri);
+
+  # Rietveld URLs have three forms:
+  #   http(s)://example.appspot.com/1234
+  #   http(s)://example.appspot.com/1234/
+  #   http(s)://example.appspot.com/1234/show
+  if ($uri->path =~ m#^/(\d+)(?:/|/show)$#) {
+
+    # This is the shortest standard URL form for Rietveld issues,
+    # and so we reduce all URLs to this.
+    $uri->path('/' . $1);
+  }
+
+  # Make sure there are no query parameters.
+  $uri->query(undef);
+
+  # And remove any # part if there is one.
+  $uri->fragment(undef);
+
+  return $uri;
 }
 
 1;
index 722d58bbc820e7a1798ef4132c5e28ee264a1944..8f7966fbc01441fde0a995091440da88fa5609ab 100644 (file)
@@ -18,24 +18,27 @@ use parent qw(Bugzilla::BugUrl);
 ###############################
 
 sub should_handle {
-    my ($class, $uri) = @_;
-    # Savane URLs look like the following (the index.php is optional):
-    #   https://savannah.gnu.org/bugs/index.php?107657
-    #   https://savannah.gnu.org/patch/index.php?107657
-    #   https://savannah.gnu.org/support/index.php?107657
-    #   https://savannah.gnu.org/task/index.php?107657
-    return ($uri->as_string =~ m|/(bugs\|patch\|support\|task)/(index\.php)?\?\d+$|) ? 1 : 0;
+  my ($class, $uri) = @_;
+
+  # Savane URLs look like the following (the index.php is optional):
+  #   https://savannah.gnu.org/bugs/index.php?107657
+  #   https://savannah.gnu.org/patch/index.php?107657
+  #   https://savannah.gnu.org/support/index.php?107657
+  #   https://savannah.gnu.org/task/index.php?107657
+  return ($uri->as_string =~ m|/(bugs\|patch\|support\|task)/(index\.php)?\?\d+$|)
+    ? 1
+    : 0;
 }
 
 sub _check_value {
-    my $class = shift;
+  my $class = shift;
 
-    my $uri = $class->SUPER::_check_value(@_);
+  my $uri = $class->SUPER::_check_value(@_);
 
-    # And remove any # part if there is one.
-    $uri->fragment(undef);
+  # And remove any # part if there is one.
+  $uri->fragment(undef);
 
-    return $uri;
+  return $uri;
 }
 
 1;
index 0fd8f37c304741e1af91df3827340ad0d68c2338..81625f85c6168ca7594954dcf525ee03169f535f 100644 (file)
@@ -13,10 +13,8 @@ use warnings;
 
 use constant NAME => 'Voting';
 
-use constant REQUIRED_MODULES => [
-];
+use constant REQUIRED_MODULES => [];
 
-use constant OPTIONAL_MODULES => [
-];
+use constant OPTIONAL_MODULES => [];
 
 __PACKAGE__->NAME;
index 3de790d836dea5f593b6c7e0dff618b90bd02ad7..194c8f0f46e503d19281d755cf5bbb913f37215d 100644 (file)
@@ -25,82 +25,84 @@ use Bugzilla::Token;
 
 use List::Util qw(min sum);
 
-use constant VERSION => BUGZILLA_VERSION;
+use constant VERSION               => BUGZILLA_VERSION;
 use constant DEFAULT_VOTES_PER_BUG => 1;
+
 # These came from Bugzilla itself, so they maintain the old numbers
 # they had before.
 use constant CMT_POPULAR_VOTES => 3;
-use constant REL_VOTER => 4;
+use constant REL_VOTER         => 4;
 
 ################
 # Installation #
 ################
 
 BEGIN {
-    *Bugzilla::Bug::votes = \&votes;
+  *Bugzilla::Bug::votes = \&votes;
 }
 
 sub votes {
-    my $self = shift;
-    my $dbh = Bugzilla->dbh;
+  my $self = shift;
+  my $dbh  = Bugzilla->dbh;
 
-    return $self->{votes} if exists $self->{votes};
+  return $self->{votes} if exists $self->{votes};
 
-    $self->{votes} = $dbh->selectrow_array('SELECT votes FROM bugs WHERE bug_id = ?',
-                                           undef, $self->id);
-    return $self->{votes};
+  $self->{votes}
+    = $dbh->selectrow_array('SELECT votes FROM bugs WHERE bug_id = ?',
+    undef, $self->id);
+  return $self->{votes};
 }
 
 sub db_schema_abstract_schema {
-    my ($self, $args) = @_;
-    $args->{'schema'}->{'votes'} = {
-        FIELDS => [
-            who        => {TYPE => 'INT3', NOTNULL => 1,
-                           REFERENCES => {TABLE  => 'profiles',
-                                          COLUMN => 'userid',
-                                          DELETE => 'CASCADE'}},
-            bug_id     => {TYPE => 'INT3', NOTNULL => 1,
-                           REFERENCES  => {TABLE  =>  'bugs',
-                                           COLUMN =>  'bug_id',
-                                           DELETE => 'CASCADE'}},
-            vote_count => {TYPE => 'INT2', NOTNULL => 1},
-        ],
-        INDEXES => [
-            votes_who_idx    => ['who'],
-            votes_bug_id_idx => ['bug_id'],
-        ],
-    };
+  my ($self, $args) = @_;
+  $args->{'schema'}->{'votes'} = {
+    FIELDS => [
+      who => {
+        TYPE       => 'INT3',
+        NOTNULL    => 1,
+        REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'}
+      },
+      bug_id => {
+        TYPE       => 'INT3',
+        NOTNULL    => 1,
+        REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'}
+      },
+      vote_count => {TYPE => 'INT2', NOTNULL => 1},
+    ],
+    INDEXES => [votes_who_idx => ['who'], votes_bug_id_idx => ['bug_id'],],
+  };
 }
 
 sub install_update_db {
-    my $dbh = Bugzilla->dbh;
-    # Note that before Bugzilla 4.0, voting was a built-in part of Bugzilla,
-    # so updates to the columns for old versions of Bugzilla happen in
-    # Bugzilla::Install::DB, and can't safely be moved to this extension.
-
-    my $field = new Bugzilla::Field({ name => 'votes' });
-    if (!$field) {
-        Bugzilla::Field->create(
-            { name => 'votes', description => 'Votes', buglist => 1 });
-    }
-
-    $dbh->bz_add_column('products', 'votesperuser',
-        {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0});
-    $dbh->bz_add_column('products', 'maxvotesperbug', 
-        {TYPE => 'INT2', NOTNULL => 1, DEFAULT => DEFAULT_VOTES_PER_BUG});
-    $dbh->bz_add_column('products', 'votestoconfirm',
-        {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0});
-
-    $dbh->bz_add_column('bugs', 'votes',
-        {TYPE => 'INT3', NOTNULL => 1, DEFAULT => 0});
-    $dbh->bz_add_index('bugs', 'bugs_votes_idx', ['votes']);
-
-    # maxvotesperbug used to default to 10,000, which isn't very sensible.
-    my $per_bug = $dbh->bz_column_info('products', 'maxvotesperbug');
-    if ($per_bug->{DEFAULT} != DEFAULT_VOTES_PER_BUG) {
-        $dbh->bz_alter_column('products', 'maxvotesperbug',
-            {TYPE => 'INT2', NOTNULL => 1, DEFAULT => DEFAULT_VOTES_PER_BUG});
-    }
+  my $dbh = Bugzilla->dbh;
+
+  # Note that before Bugzilla 4.0, voting was a built-in part of Bugzilla,
+  # so updates to the columns for old versions of Bugzilla happen in
+  # Bugzilla::Install::DB, and can't safely be moved to this extension.
+
+  my $field = new Bugzilla::Field({name => 'votes'});
+  if (!$field) {
+    Bugzilla::Field->create(
+      {name => 'votes', description => 'Votes', buglist => 1});
+  }
+
+  $dbh->bz_add_column('products', 'votesperuser',
+    {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0});
+  $dbh->bz_add_column('products', 'maxvotesperbug',
+    {TYPE => 'INT2', NOTNULL => 1, DEFAULT => DEFAULT_VOTES_PER_BUG});
+  $dbh->bz_add_column('products', 'votestoconfirm',
+    {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0});
+
+  $dbh->bz_add_column('bugs', 'votes',
+    {TYPE => 'INT3', NOTNULL => 1, DEFAULT => 0});
+  $dbh->bz_add_index('bugs', 'bugs_votes_idx', ['votes']);
+
+  # maxvotesperbug used to default to 10,000, which isn't very sensible.
+  my $per_bug = $dbh->bz_column_info('products', 'maxvotesperbug');
+  if ($per_bug->{DEFAULT} != DEFAULT_VOTES_PER_BUG) {
+    $dbh->bz_alter_column('products', 'maxvotesperbug',
+      {TYPE => 'INT2', NOTNULL => 1, DEFAULT => DEFAULT_VOTES_PER_BUG});
+  }
 }
 
 ###########
@@ -108,93 +110,98 @@ sub install_update_db {
 ###########
 
 sub object_columns {
-    my ($self, $args) = @_;
-    my ($class, $columns) = @$args{qw(class columns)};
-    if ($class->isa('Bugzilla::Bug')) {
-        push(@$columns, 'votes');
-    }
-    elsif ($class->isa('Bugzilla::Product')) {
-        push(@$columns, qw(votesperuser maxvotesperbug votestoconfirm));
-    }
+  my ($self,  $args)    = @_;
+  my ($class, $columns) = @$args{qw(class columns)};
+  if ($class->isa('Bugzilla::Bug')) {
+    push(@$columns, 'votes');
+  }
+  elsif ($class->isa('Bugzilla::Product')) {
+    push(@$columns, qw(votesperuser maxvotesperbug votestoconfirm));
+  }
 }
 
 sub bug_fields {
-    my ($self, $args) = @_;
-    my $fields = $args->{fields};
-    push(@$fields, 'votes');
+  my ($self, $args) = @_;
+  my $fields = $args->{fields};
+  push(@$fields, 'votes');
 }
 
 sub object_update_columns {
-    my ($self, $args) = @_;
-    my ($object, $columns) = @$args{qw(object columns)};
-    if ($object->isa('Bugzilla::Product')) {
-        push(@$columns, qw(votesperuser maxvotesperbug votestoconfirm));
-    }
+  my ($self,   $args)    = @_;
+  my ($object, $columns) = @$args{qw(object columns)};
+  if ($object->isa('Bugzilla::Product')) {
+    push(@$columns, qw(votesperuser maxvotesperbug votestoconfirm));
+  }
 }
 
 sub object_validators {
-    my ($self, $args) = @_;
-    my ($class, $validators) = @$args{qw(class validators)};
-    if ($class->isa('Bugzilla::Product')) {
-        $validators->{'votesperuser'}   = \&_check_votesperuser;
-        $validators->{'maxvotesperbug'} = \&_check_maxvotesperbug;
-        $validators->{'votestoconfirm'} = \&_check_votestoconfirm;
-    }
+  my ($self,  $args)       = @_;
+  my ($class, $validators) = @$args{qw(class validators)};
+  if ($class->isa('Bugzilla::Product')) {
+    $validators->{'votesperuser'}   = \&_check_votesperuser;
+    $validators->{'maxvotesperbug'} = \&_check_maxvotesperbug;
+    $validators->{'votestoconfirm'} = \&_check_votestoconfirm;
+  }
 }
 
 sub object_before_create {
-    my ($self, $args) = @_;
-    my ($class, $params) = @$args{qw(class params)};
-    if ($class->isa('Bugzilla::Bug')) {
-        # Don't ever allow people to directly specify "votes" into the bugs
-        # table.
-        delete $params->{votes};
-    }
-    elsif ($class->isa('Bugzilla::Product')) {
-        my $input = Bugzilla->input_params;
-        $params->{votesperuser}   = $input->{'votesperuser'};
-        $params->{maxvotesperbug} = $input->{'maxvotesperbug'};
-        $params->{votestoconfirm} = $input->{'votestoconfirm'};
-    }
+  my ($self,  $args)   = @_;
+  my ($class, $params) = @$args{qw(class params)};
+  if ($class->isa('Bugzilla::Bug')) {
+
+    # Don't ever allow people to directly specify "votes" into the bugs
+    # table.
+    delete $params->{votes};
+  }
+  elsif ($class->isa('Bugzilla::Product')) {
+    my $input = Bugzilla->input_params;
+    $params->{votesperuser}   = $input->{'votesperuser'};
+    $params->{maxvotesperbug} = $input->{'maxvotesperbug'};
+    $params->{votestoconfirm} = $input->{'votestoconfirm'};
+  }
 }
 
 sub object_end_of_set_all {
-    my ($self, $args) = @_;
-    my ($object) = $args->{object};
-    if ($object->isa('Bugzilla::Product')) {
-        my $input = Bugzilla->input_params;
-        $object->set('votesperuser',   $input->{'votesperuser'});
-        $object->set('maxvotesperbug', $input->{'maxvotesperbug'});
-        $object->set('votestoconfirm', $input->{'votestoconfirm'});
-    }
+  my ($self, $args) = @_;
+  my ($object) = $args->{object};
+  if ($object->isa('Bugzilla::Product')) {
+    my $input = Bugzilla->input_params;
+    $object->set('votesperuser',   $input->{'votesperuser'});
+    $object->set('maxvotesperbug', $input->{'maxvotesperbug'});
+    $object->set('votestoconfirm', $input->{'votestoconfirm'});
+  }
 }
 
 sub object_end_of_update {
-    my ($self, $args) = @_;
-    my ($object, $changes) = @$args{qw(object changes)};
-    if ( $object->isa('Bugzilla::Product')
-         and ($changes->{maxvotesperbug} or $changes->{votesperuser} 
-              or $changes->{votestoconfirm}) ) 
-    {
-        _modify_bug_votes($object, $changes);
-    }
+  my ($self,   $args)    = @_;
+  my ($object, $changes) = @$args{qw(object changes)};
+  if (
+    $object->isa('Bugzilla::Product')
+    and ($changes->{maxvotesperbug}
+      or $changes->{votesperuser}
+      or $changes->{votestoconfirm})
+    )
+  {
+    _modify_bug_votes($object, $changes);
+  }
 }
 
 sub bug_end_of_update {
-    my ($self, $args) = @_;
-    my ($bug, $changes) = @$args{qw(bug changes)};
-
-    if ($changes->{'product'}) {
-        my @msgs;
-        # If some votes have been removed, RemoveVotes() returns
-        # a list of messages to send to voters.
-        @msgs = _remove_votes($bug->id, 0, 'votes_bug_moved');
-        _confirm_if_vote_confirmed($bug);
-
-        foreach my $msg (@msgs) {
-            MessageToMTA($msg);
-        }
+  my ($self, $args)    = @_;
+  my ($bug,  $changes) = @$args{qw(bug changes)};
+
+  if ($changes->{'product'}) {
+    my @msgs;
+
+    # If some votes have been removed, RemoveVotes() returns
+    # a list of messages to send to voters.
+    @msgs = _remove_votes($bug->id, 0, 'votes_bug_moved');
+    _confirm_if_vote_confirmed($bug);
+
+    foreach my $msg (@msgs) {
+      MessageToMTA($msg);
     }
+  }
 }
 
 #############
@@ -202,27 +209,28 @@ sub bug_end_of_update {
 #############
 
 sub template_before_create {
-    my ($self, $args) = @_;
-    my $config = $args->{config};
-    my $constants = $config->{CONSTANTS};
-    $constants->{REL_VOTER} = REL_VOTER;
-    $constants->{CMT_POPULAR_VOTES} = CMT_POPULAR_VOTES;
-    $constants->{DEFAULT_VOTES_PER_BUG} = DEFAULT_VOTES_PER_BUG;
+  my ($self, $args) = @_;
+  my $config    = $args->{config};
+  my $constants = $config->{CONSTANTS};
+  $constants->{REL_VOTER}             = REL_VOTER;
+  $constants->{CMT_POPULAR_VOTES}     = CMT_POPULAR_VOTES;
+  $constants->{DEFAULT_VOTES_PER_BUG} = DEFAULT_VOTES_PER_BUG;
 }
 
 
 sub template_before_process {
-    my ($self, $args) = @_;
-    my ($vars, $file) = @$args{qw(vars file)};
-    if ($file eq 'admin/users/confirm-delete.html.tmpl') {
-        my $who = $vars->{otheruser};
-        my $votes = Bugzilla->dbh->selectrow_array(
-            'SELECT COUNT(*) FROM votes WHERE who = ?', undef, $who->id);
-        if ($votes) {
-            $vars->{other_safe} = 1;
-            $vars->{votes} = $votes;
-        }
-    }
+  my ($self, $args) = @_;
+  my ($vars, $file) = @$args{qw(vars file)};
+  if ($file eq 'admin/users/confirm-delete.html.tmpl') {
+    my $who = $vars->{otheruser};
+    my $votes
+      = Bugzilla->dbh->selectrow_array('SELECT COUNT(*) FROM votes WHERE who = ?',
+      undef, $who->id);
+    if ($votes) {
+      $vars->{other_safe} = 1;
+      $vars->{votes}      = $votes;
+    }
+  }
 }
 
 ###########
@@ -230,19 +238,19 @@ sub template_before_process {
 ###########
 
 sub bugmail_recipients {
-    my ($self, $args) = @_;
-    my ($bug, $recipients) = @$args{qw(bug recipients)};
-    my $dbh = Bugzilla->dbh;
+  my ($self, $args)       = @_;
+  my ($bug,  $recipients) = @$args{qw(bug recipients)};
+  my $dbh = Bugzilla->dbh;
 
-    my $voters = $dbh->selectcol_arrayref(
-        "SELECT who FROM votes WHERE bug_id = ?", undef, $bug->id);
-    $recipients->{$_}->{+REL_VOTER} = 1 foreach (@$voters);
+  my $voters = $dbh->selectcol_arrayref("SELECT who FROM votes WHERE bug_id = ?",
+    undef, $bug->id);
+  $recipients->{$_}->{+REL_VOTER} = 1 foreach (@$voters);
 }
 
 sub bugmail_relationships {
-    my ($self, $args) = @_;
-    my $relationships = $args->{relationships};
-    $relationships->{+REL_VOTER} = 'Voter';
+  my ($self, $args) = @_;
+  my $relationships = $args->{relationships};
+  $relationships->{+REL_VOTER} = 'Voter';
 }
 
 ###############
@@ -250,59 +258,59 @@ sub bugmail_relationships {
 ###############
 
 sub sanitycheck_check {
-    my ($self, $args) = @_;
-    my $status = $args->{status};
-
-    # Vote Cache
-    $status->('voting_count_start');
-    my $dbh = Bugzilla->dbh;
-    my %cached_counts = @{ $dbh->selectcol_arrayref(
-        'SELECT bug_id, votes FROM bugs', {Columns=>[1,2]}) };
-
-    my %real_counts = @{ $dbh->selectcol_arrayref(
-        'SELECT bug_id, SUM(vote_count) FROM votes '
-        . $dbh->sql_group_by('bug_id'), {Columns=>[1,2]}) };
-
-    my $needs_rebuild;
-    foreach my $id (keys %cached_counts) {
-        my $cached_count = $cached_counts{$id};
-        my $real_count = $real_counts{$id} || 0;
-        if ($cached_count < 0) {
-            $status->('voting_count_alert', { id => $id }, 'alert');
-        }
-        elsif ($cached_count != $real_count) {
-            $status->('voting_cache_alert', { id => $id }, 'alert');
-            $needs_rebuild = 1;
-        }
-    }
-
-    $status->('voting_cache_rebuild_fix') if $needs_rebuild;
+  my ($self, $args) = @_;
+  my $status = $args->{status};
+
+  # Vote Cache
+  $status->('voting_count_start');
+  my $dbh           = Bugzilla->dbh;
+  my %cached_counts = @{$dbh->selectcol_arrayref('SELECT bug_id, votes FROM bugs',
+      {Columns => [1, 2]})};
+
+  my %real_counts = @{
+    $dbh->selectcol_arrayref(
+      'SELECT bug_id, SUM(vote_count) FROM votes ' . $dbh->sql_group_by('bug_id'),
+      {Columns => [1, 2]})
+  };
+
+  my $needs_rebuild;
+  foreach my $id (keys %cached_counts) {
+    my $cached_count = $cached_counts{$id};
+    my $real_count = $real_counts{$id} || 0;
+    if ($cached_count < 0) {
+      $status->('voting_count_alert', {id => $id}, 'alert');
+    }
+    elsif ($cached_count != $real_count) {
+      $status->('voting_cache_alert', {id => $id}, 'alert');
+      $needs_rebuild = 1;
+    }
+  }
+
+  $status->('voting_cache_rebuild_fix') if $needs_rebuild;
 }
 
 sub sanitycheck_repair {
-    my ($self, $args) = @_;
-    my $status = $args->{status};
-    my $input = Bugzilla->input_params;
-    my $dbh = Bugzilla->dbh;
-
-    return if !$input->{rebuild_vote_cache};
-
-    $status->('voting_cache_rebuild_start');
-    $dbh->bz_start_transaction();
-    $dbh->do('UPDATE bugs SET votes = 0');
-
-    my $sth = $dbh->prepare(
-        'SELECT bug_id, SUM(vote_count) FROM votes '
-        . $dbh->sql_group_by('bug_id'));
-    $sth->execute();
-
-    my $sth_update = $dbh->prepare(
-        'UPDATE bugs SET votes = ? WHERE bug_id = ?');
-    while (my ($id, $count) = $sth->fetchrow_array) {
-        $sth_update->execute($count, $id);
-    }
-    $dbh->bz_commit_transaction();
-    $status->('voting_cache_rebuild_end');
+  my ($self, $args) = @_;
+  my $status = $args->{status};
+  my $input  = Bugzilla->input_params;
+  my $dbh    = Bugzilla->dbh;
+
+  return if !$input->{rebuild_vote_cache};
+
+  $status->('voting_cache_rebuild_start');
+  $dbh->bz_start_transaction();
+  $dbh->do('UPDATE bugs SET votes = 0');
+
+  my $sth = $dbh->prepare(
+    'SELECT bug_id, SUM(vote_count) FROM votes ' . $dbh->sql_group_by('bug_id'));
+  $sth->execute();
+
+  my $sth_update = $dbh->prepare('UPDATE bugs SET votes = ? WHERE bug_id = ?');
+  while (my ($id, $count) = $sth->fetchrow_array) {
+    $sth_update->execute($count, $id);
+  }
+  $dbh->bz_commit_transaction();
+  $status->('voting_cache_rebuild_end');
 }
 
 
@@ -311,35 +319,36 @@ sub sanitycheck_repair {
 ##############
 
 sub _check_votesperuser {
-    return _check_votes(0, @_);
+  return _check_votes(0, @_);
 }
 
 sub _check_maxvotesperbug {
-    return _check_votes(DEFAULT_VOTES_PER_BUG, @_);
+  return _check_votes(DEFAULT_VOTES_PER_BUG, @_);
 }
 
 sub _check_votestoconfirm {
-    return _check_votes(0, @_);
+  return _check_votes(0, @_);
 }
 
 # This subroutine is only used internally by other _check_votes_* validators.
 sub _check_votes {
-    my ($default, $invocant, $votes, $field) = @_;
-
-    detaint_natural($votes) if defined $votes;
-    # On product creation, if the number of votes is not a valid integer,
-    # we silently fall back to the given default value.
-    # If the product already exists and the change is illegal, we complain.
-    if (!defined $votes) {
-        if (ref $invocant) {
-            ThrowUserError('voting_product_illegal_votes',
-                           { field => $field, votes => $_[2] });
-        }
-        else {
-            $votes = $default;
-        }
+  my ($default, $invocant, $votes, $field) = @_;
+
+  detaint_natural($votes) if defined $votes;
+
+  # On product creation, if the number of votes is not a valid integer,
+  # we silently fall back to the given default value.
+  # If the product already exists and the change is illegal, we complain.
+  if (!defined $votes) {
+    if (ref $invocant) {
+      ThrowUserError('voting_product_illegal_votes',
+        {field => $field, votes => $_[2]});
+    }
+    else {
+      $votes = $default;
     }
-    return $votes;
+  }
+  return $votes;
 }
 
 #########
@@ -347,290 +356,321 @@ sub _check_votes {
 #########
 
 sub page_before_template {
-    my ($self, $args) = @_;
-    my $page = $args->{page_id};
-    my $vars = $args->{vars};
-
-    if ($page =~ m{^voting/bug\.}) {
-        _page_bug($vars);
-    }
-    elsif ($page =~ m{^voting/user\.}) {
-        _page_user($vars);
-    }
+  my ($self, $args) = @_;
+  my $page = $args->{page_id};
+  my $vars = $args->{vars};
+
+  if ($page =~ m{^voting/bug\.}) {
+    _page_bug($vars);
+  }
+  elsif ($page =~ m{^voting/user\.}) {
+    _page_user($vars);
+  }
 }
 
 sub _page_bug {
-    my ($vars) = @_;
-    my $dbh = Bugzilla->dbh;
-    my $input = Bugzilla->input_params;
+  my ($vars) = @_;
+  my $dbh    = Bugzilla->dbh;
+  my $input  = Bugzilla->input_params;
 
-    my $bug_id = $input->{bug_id};
-    my $bug = Bugzilla::Bug->check($bug_id);
+  my $bug_id = $input->{bug_id};
+  my $bug    = Bugzilla::Bug->check($bug_id);
 
-    $vars->{'bug'} = $bug;
-    $vars->{'users'} =
-        $dbh->selectall_arrayref('SELECT profiles.login_name,
+  $vars->{'bug'}   = $bug;
+  $vars->{'users'} = $dbh->selectall_arrayref(
+    'SELECT profiles.login_name,
                                          profiles.userid AS id,
                                          votes.vote_count
                                     FROM votes
                               INNER JOIN profiles
                                       ON profiles.userid = votes.who
-                                   WHERE votes.bug_id = ?',
-                                  {Slice=>{}}, $bug->id);
+                                   WHERE votes.bug_id = ?', {Slice => {}}, $bug->id
+  );
 }
 
 sub _page_user {
-    my ($vars) = @_;
-    my $dbh = Bugzilla->dbh;
-    my $user = Bugzilla->user;
-    my $input = Bugzilla->input_params;
+  my ($vars) = @_;
+  my $dbh    = Bugzilla->dbh;
+  my $user   = Bugzilla->user;
+  my $input  = Bugzilla->input_params;
 
-    my $action = $input->{action};
-    if ($action and $action eq 'vote') {
-        _update_votes($vars);
-    }
+  my $action = $input->{action};
+  if ($action and $action eq 'vote') {
+    _update_votes($vars);
+  }
 
-    # If a bug_id is given, and we're editing, we'll add it to the votes list.
-    
-    my $bug_id = $input->{bug_id};
-    my $bug = Bugzilla::Bug->check({ id => $bug_id, cache => 1 }) if $bug_id;
-    my $who_id = $input->{user_id} || $user->id;
+  # If a bug_id is given, and we're editing, we'll add it to the votes list.
 
-    # Logged-out users must specify a user_id.
-    Bugzilla->login(LOGIN_REQUIRED) if !$who_id;
+  my $bug_id = $input->{bug_id};
+  my $bug    = Bugzilla::Bug->check({id => $bug_id, cache => 1}) if $bug_id;
+  my $who_id = $input->{user_id} || $user->id;
 
-    my $who = Bugzilla::User->check({ id => $who_id });
+  # Logged-out users must specify a user_id.
+  Bugzilla->login(LOGIN_REQUIRED) if !$who_id;
 
-    my $canedit = $user->id == $who->id;
+  my $who = Bugzilla::User->check({id => $who_id});
 
-    $dbh->bz_start_transaction();
+  my $canedit = $user->id == $who->id;
 
-    if ($canedit && $bug) {
-        # Make sure there is an entry for this bug
-        # in the vote table, just so that things display right.
-        my $has_votes = $dbh->selectrow_array('SELECT vote_count FROM votes 
-                                               WHERE bug_id = ? AND who = ?',
-                                               undef, ($bug->id, $who->id));
-        if (!$has_votes) {
-            $dbh->do('INSERT INTO votes (who, bug_id, vote_count) 
-                      VALUES (?, ?, 0)', undef, ($who->id, $bug->id));
-        }
+  $dbh->bz_start_transaction();
+
+  if ($canedit && $bug) {
+
+    # Make sure there is an entry for this bug
+    # in the vote table, just so that things display right.
+    my $has_votes = $dbh->selectrow_array(
+      'SELECT vote_count FROM votes 
+                                               WHERE bug_id = ? AND who = ?', undef,
+      ($bug->id, $who->id)
+    );
+    if (!$has_votes) {
+      $dbh->do(
+        'INSERT INTO votes (who, bug_id, vote_count) 
+                      VALUES (?, ?, 0)', undef, ($who->id, $bug->id)
+      );
     }
+  }
 
-    my (@products, @all_bug_ids);
-    # Read the votes data for this user for each product.
-    foreach my $product (@{ $user->get_selectable_products }) {
-        next unless ($product->{votesperuser} > 0);
+  my (@products, @all_bug_ids);
 
-        my $vote_list =
-          $dbh->selectall_arrayref('SELECT votes.bug_id, votes.vote_count
+  # Read the votes data for this user for each product.
+  foreach my $product (@{$user->get_selectable_products}) {
+    next unless ($product->{votesperuser} > 0);
+
+    my $vote_list = $dbh->selectall_arrayref(
+      'SELECT votes.bug_id, votes.vote_count
                                       FROM votes
                                 INNER JOIN bugs
                                         ON votes.bug_id = bugs.bug_id
                                      WHERE votes.who = ?
-                                       AND bugs.product_id = ?',
-                                     undef, ($who->id, $product->id));
-
-        my %votes = map { $_->[0] => $_->[1] } @$vote_list;
-        my @bug_ids = sort keys %votes;
-        # Exclude bugs that the user can no longer see.
-        @bug_ids = @{ $user->visible_bugs(\@bug_ids) };
-        next unless scalar @bug_ids;
-
-        push(@all_bug_ids, @bug_ids);
-        my @bugs = @{ Bugzilla::Bug->new_from_list(\@bug_ids) };
-        $_->{count} = $votes{$_->id} foreach @bugs;
-        # We include votes from bugs that the user can no longer see.
-        my $total = sum(values %votes) || 0;
-
-        my $onevoteonly = 0;
-        $onevoteonly = 1 if (min($product->{votesperuser},
-                                 $product->{maxvotesperbug}) == 1);
-
-        push(@products, { name => $product->name,
-                          bugs => \@bugs,
-                          bug_ids => \@bug_ids,
-                          onevoteonly => $onevoteonly,
-                          total => $total,
-                          maxvotes => $product->{votesperuser},
-                          maxperbug => $product->{maxvotesperbug} });
-    }
-
-    if ($canedit && $bug) {
-        $dbh->do('DELETE FROM votes WHERE vote_count = 0 AND who = ?',
-                 undef, $who->id);
-    }
-    $dbh->bz_commit_transaction();
-
-    $vars->{'canedit'} = $canedit;
-    $vars->{'voting_user'} = { "login" => $who->name };
-    $vars->{'products'} = \@products;
-    $vars->{'this_bug'} = $bug;
-    $vars->{'all_bug_ids'} = \@all_bug_ids;
+                                       AND bugs.product_id = ?', undef,
+      ($who->id, $product->id)
+    );
+
+    my %votes = map { $_->[0] => $_->[1] } @$vote_list;
+    my @bug_ids = sort keys %votes;
+
+    # Exclude bugs that the user can no longer see.
+    @bug_ids = @{$user->visible_bugs(\@bug_ids)};
+    next unless scalar @bug_ids;
+
+    push(@all_bug_ids, @bug_ids);
+    my @bugs = @{Bugzilla::Bug->new_from_list(\@bug_ids)};
+    $_->{count} = $votes{$_->id} foreach @bugs;
+
+    # We include votes from bugs that the user can no longer see.
+    my $total = sum(values %votes) || 0;
+
+    my $onevoteonly = 0;
+    $onevoteonly = 1
+      if (min($product->{votesperuser}, $product->{maxvotesperbug}) == 1);
+
+    push(
+      @products,
+      {
+        name        => $product->name,
+        bugs        => \@bugs,
+        bug_ids     => \@bug_ids,
+        onevoteonly => $onevoteonly,
+        total       => $total,
+        maxvotes    => $product->{votesperuser},
+        maxperbug   => $product->{maxvotesperbug}
+      }
+    );
+  }
+
+  if ($canedit && $bug) {
+    $dbh->do('DELETE FROM votes WHERE vote_count = 0 AND who = ?', undef, $who->id);
+  }
+  $dbh->bz_commit_transaction();
+
+  $vars->{'canedit'}     = $canedit;
+  $vars->{'voting_user'} = {"login" => $who->name};
+  $vars->{'products'}    = \@products;
+  $vars->{'this_bug'}    = $bug;
+  $vars->{'all_bug_ids'} = \@all_bug_ids;
 }
 
 sub _update_votes {
-    my ($vars) = @_;
-
-    ############################################################################
-    # Begin Data/Security Validation
-    ############################################################################
-
-    my $cgi = Bugzilla->cgi;
-    my $dbh = Bugzilla->dbh;
-    my $template = Bugzilla->template;
-    my $user = Bugzilla->login(LOGIN_REQUIRED);
-    my $input = Bugzilla->input_params;
-
-    # Build a list of bug IDs for which votes have been submitted.  Votes
-    # are submitted in form fields in which the field names are the bug 
-    # IDs and the field values are the number of votes.
-
-    my @buglist = grep {/^\d+$/} keys %$input;
-    my (%bugs, %votes);
-
-    # If no bugs are in the buglist, let's make sure the user gets notified
-    # that their votes will get nuked if they continue.
-    if (scalar(@buglist) == 0) {
-        if (!defined $cgi->param('delete_all_votes')) {
-            print $cgi->header();
-            $template->process("voting/delete-all.html.tmpl", $vars)
-              || ThrowTemplateError($template->error());
-            exit;
-        }
-        elsif ($cgi->param('delete_all_votes') == 0) {
-            print $cgi->redirect("page.cgi?id=voting/user.html");
-            exit;
+  my ($vars) = @_;
+
+  ############################################################################
+  # Begin Data/Security Validation
+  ############################################################################
+
+  my $cgi      = Bugzilla->cgi;
+  my $dbh      = Bugzilla->dbh;
+  my $template = Bugzilla->template;
+  my $user     = Bugzilla->login(LOGIN_REQUIRED);
+  my $input    = Bugzilla->input_params;
+
+  # Build a list of bug IDs for which votes have been submitted.  Votes
+  # are submitted in form fields in which the field names are the bug
+  # IDs and the field values are the number of votes.
+
+  my @buglist = grep {/^\d+$/} keys %$input;
+  my (%bugs, %votes);
+
+  # If no bugs are in the buglist, let's make sure the user gets notified
+  # that their votes will get nuked if they continue.
+  if (scalar(@buglist) == 0) {
+    if (!defined $cgi->param('delete_all_votes')) {
+      print $cgi->header();
+      $template->process("voting/delete-all.html.tmpl", $vars)
+        || ThrowTemplateError($template->error());
+      exit;
+    }
+    elsif ($cgi->param('delete_all_votes') == 0) {
+      print $cgi->redirect("page.cgi?id=voting/user.html");
+      exit;
+    }
+  }
+  else {
+    $user->visible_bugs(\@buglist);
+    my $bugs_obj = Bugzilla::Bug->new_from_list(\@buglist);
+    $bugs{$_->id} = $_ foreach @$bugs_obj;
+  }
+
+  # Call check_is_visible() on each bug to make sure it is an existing bug
+  # that the user is authorized to access, and make sure the number of votes
+  # submitted is also an integer.
+  foreach my $id (@buglist) {
+    my $bug = $bugs{$id}
+      or ThrowUserError('bug_id_does_not_exist', {bug_id => $id});
+    $bug->check_is_visible;
+    $id = $bug->id;
+    $votes{$id} = $input->{$id};
+    detaint_natural($votes{$id}) || ThrowUserError("voting_must_be_nonnegative");
+  }
+
+  my $token = $cgi->param('token');
+  check_hash_token($token, ['vote']);
+
+  ############################################################################
+  # End Data/Security Validation
+  ############################################################################
+  my $who = $user->id;
+
+  # If the user is voting for bugs, make sure they aren't overstuffing
+  # the ballot box.
+  if (scalar @buglist) {
+    my (%prodcount, %products);
+    foreach my $bug_id (keys %bugs) {
+      my $bug  = $bugs{$bug_id};
+      my $prod = $bug->product;
+      $products{$prod}  ||= $bug->product_obj;
+      $prodcount{$prod} ||= 0;
+      $prodcount{$prod} += $votes{$bug_id};
+
+      # Make sure we haven't broken the votes-per-bug limit
+      ($votes{$bug_id} <= $products{$prod}->{maxvotesperbug}) || ThrowUserError(
+        "voting_too_many_votes_for_bug",
+        {
+          max     => $products{$prod}->{maxvotesperbug},
+          product => $prod,
+          votes   => $votes{$bug_id}
         }
-    }
-    else {
-        $user->visible_bugs(\@buglist);
-        my $bugs_obj = Bugzilla::Bug->new_from_list(\@buglist);
-        $bugs{$_->id} = $_ foreach @$bugs_obj;
+      );
     }
 
-    # Call check_is_visible() on each bug to make sure it is an existing bug
-    # that the user is authorized to access, and make sure the number of votes
-    # submitted is also an integer.
-    foreach my $id (@buglist) {
-        my $bug = $bugs{$id}
-          or ThrowUserError('bug_id_does_not_exist', { bug_id => $id });
-        $bug->check_is_visible;
-        $id = $bug->id;
-        $votes{$id} = $input->{$id};
-        detaint_natural($votes{$id})
-          || ThrowUserError("voting_must_be_nonnegative");
-    }
-
-    my $token = $cgi->param('token');
-    check_hash_token($token, ['vote']);
-
-    ############################################################################
-    # End Data/Security Validation
-    ############################################################################
-    my $who = $user->id;
-
-    # If the user is voting for bugs, make sure they aren't overstuffing
-    # the ballot box.
-    if (scalar @buglist) {
-        my (%prodcount, %products);
-        foreach my $bug_id (keys %bugs) {
-            my $bug = $bugs{$bug_id};
-            my $prod = $bug->product;
-            $products{$prod} ||= $bug->product_obj;
-            $prodcount{$prod} ||= 0;
-            $prodcount{$prod} += $votes{$bug_id};
-
-            # Make sure we haven't broken the votes-per-bug limit
-            ($votes{$bug_id} <= $products{$prod}->{maxvotesperbug})
-              || ThrowUserError("voting_too_many_votes_for_bug",
-                                {max => $products{$prod}->{maxvotesperbug},
-                                 product => $prod,
-                                 votes => $votes{$bug_id}});
-        }
-
-        # Make sure we haven't broken the votes-per-product limit
-        foreach my $prod (keys(%prodcount)) {
-            ($prodcount{$prod} <= $products{$prod}->{votesperuser})
-              || ThrowUserError("voting_too_many_votes_for_product",
-                                {max => $products{$prod}->{votesperuser},
-                                 product => $prod,
-                                 votes => $prodcount{$prod}});
+    # Make sure we haven't broken the votes-per-product limit
+    foreach my $prod (keys(%prodcount)) {
+      ($prodcount{$prod} <= $products{$prod}->{votesperuser}) || ThrowUserError(
+        "voting_too_many_votes_for_product",
+        {
+          max     => $products{$prod}->{votesperuser},
+          product => $prod,
+          votes   => $prodcount{$prod}
         }
+      );
     }
+  }
 
-    # Update the user's votes in the database.
-    $dbh->bz_start_transaction();
+  # Update the user's votes in the database.
+  $dbh->bz_start_transaction();
 
-    my $old_list = $dbh->selectall_arrayref('SELECT bug_id, vote_count FROM votes
-                                             WHERE who = ?', undef, $who);
+  my $old_list = $dbh->selectall_arrayref(
+    'SELECT bug_id, vote_count FROM votes
+                                             WHERE who = ?', undef, $who
+  );
 
-    my %old_votes = map { $_->[0] => $_->[1] } @$old_list;
+  my %old_votes = map { $_->[0] => $_->[1] } @$old_list;
 
-    my $sth_insertVotes = $dbh->prepare('INSERT INTO votes (who, bug_id, vote_count)
-                                         VALUES (?, ?, ?)');
-    my $sth_updateVotes = $dbh->prepare('UPDATE votes SET vote_count = ?
-                                         WHERE bug_id = ? AND who = ?');
+  my $sth_insertVotes = $dbh->prepare(
+    'INSERT INTO votes (who, bug_id, vote_count)
+                                         VALUES (?, ?, ?)'
+  );
+  my $sth_updateVotes = $dbh->prepare(
+    'UPDATE votes SET vote_count = ?
+                                         WHERE bug_id = ? AND who = ?'
+  );
 
-    my %affected = map { $_ => 1 } (@buglist, keys %old_votes);
-    my @deleted_votes;
-
-    foreach my $id (keys %affected) {
-        if (!$votes{$id}) {
-            push(@deleted_votes, $id);
-            next;
-        }
-        if ($votes{$id} == ($old_votes{$id} || 0)) {
-            delete $affected{$id};
-            next;
-        }
-        # We use 'defined' in case 0 was accidentally stored in the DB.
-        if (defined $old_votes{$id}) {
-            $sth_updateVotes->execute($votes{$id}, $id, $who);
-        }
-        else {
-            $sth_insertVotes->execute($who, $id, $votes{$id});
-        }
-    }
+  my %affected = map { $_ => 1 } (@buglist, keys %old_votes);
+  my @deleted_votes;
 
-    if (@deleted_votes) {
-        $dbh->do('DELETE FROM votes WHERE who = ? AND ' .
-                 $dbh->sql_in('bug_id', \@deleted_votes), undef, $who);
+  foreach my $id (keys %affected) {
+    if (!$votes{$id}) {
+      push(@deleted_votes, $id);
+      next;
     }
-
-    # Update the cached values in the bugs table
-    my @updated_bugs = ();
-
-    my $sth_getVotes = $dbh->prepare("SELECT SUM(vote_count) FROM votes
-                                      WHERE bug_id = ?");
-
-    $sth_updateVotes = $dbh->prepare('UPDATE bugs SET votes = ? WHERE bug_id = ?');
-
-    foreach my $id (keys %affected) {
-        $sth_getVotes->execute($id);
-        my $v = $sth_getVotes->fetchrow_array || 0;
-        $sth_updateVotes->execute($v, $id);
-        $bugs{$id}->{votes} = $v if $bugs{$id};
-        my $confirmed = _confirm_if_vote_confirmed($bugs{$id} || $id);
-        push (@updated_bugs, $id) if $confirmed;
+    if ($votes{$id} == ($old_votes{$id} || 0)) {
+      delete $affected{$id};
+      next;
     }
 
-    $dbh->bz_commit_transaction();
-
-    print $cgi->header() if scalar @updated_bugs;
-    $vars->{'type'} = "votes";
-    $vars->{'title_tag'} = 'change_votes';
-    foreach my $bug_id (@updated_bugs) {
-        $vars->{'id'} = $bug_id;
-        $vars->{'sent_bugmail'} = 
-            Bugzilla::BugMail::Send($bug_id, { 'changer' => $user });
-        
-        $template->process("bug/process/results.html.tmpl", $vars)
-          || ThrowTemplateError($template->error());
-        # Set header_done to 1 only after the first bug.
-        $vars->{'header_done'} = 1;
+    # We use 'defined' in case 0 was accidentally stored in the DB.
+    if (defined $old_votes{$id}) {
+      $sth_updateVotes->execute($votes{$id}, $id, $who);
     }
-    $vars->{'message'} = 'votes_recorded';
+    else {
+      $sth_insertVotes->execute($who, $id, $votes{$id});
+    }
+  }
+
+  if (@deleted_votes) {
+    $dbh->do(
+      'DELETE FROM votes WHERE who = ? AND '
+        . $dbh->sql_in('bug_id', \@deleted_votes),
+      undef, $who
+    );
+  }
+
+  # Update the cached values in the bugs table
+  my @updated_bugs = ();
+
+  my $sth_getVotes = $dbh->prepare(
+    "SELECT SUM(vote_count) FROM votes
+                                      WHERE bug_id = ?"
+  );
+
+  $sth_updateVotes = $dbh->prepare('UPDATE bugs SET votes = ? WHERE bug_id = ?');
+
+  foreach my $id (keys %affected) {
+    $sth_getVotes->execute($id);
+    my $v = $sth_getVotes->fetchrow_array || 0;
+    $sth_updateVotes->execute($v, $id);
+    $bugs{$id}->{votes} = $v if $bugs{$id};
+    my $confirmed = _confirm_if_vote_confirmed($bugs{$id} || $id);
+    push(@updated_bugs, $id) if $confirmed;
+  }
+
+  $dbh->bz_commit_transaction();
+
+  print $cgi->header() if scalar @updated_bugs;
+  $vars->{'type'}      = "votes";
+  $vars->{'title_tag'} = 'change_votes';
+  foreach my $bug_id (@updated_bugs) {
+    $vars->{'id'} = $bug_id;
+    $vars->{'sent_bugmail'}
+      = Bugzilla::BugMail::Send($bug_id, {'changer' => $user});
+
+    $template->process("bug/process/results.html.tmpl", $vars)
+      || ThrowTemplateError($template->error());
+
+    # Set header_done to 1 only after the first bug.
+    $vars->{'header_done'} = 1;
+  }
+  $vars->{'message'} = 'votes_recorded';
 }
 
 ######################
@@ -638,233 +678,245 @@ sub _update_votes {
 ######################
 
 sub _modify_bug_votes {
-    my ($product, $changes) = @_;
-    my $dbh = Bugzilla->dbh;
-    my @msgs;
+  my ($product, $changes) = @_;
+  my $dbh = Bugzilla->dbh;
+  my @msgs;
 
-    # 1. too many votes for a single user on a single bug.
-    my @toomanyvotes_list;
-    if ($product->{maxvotesperbug} < $product->{votesperuser}) {
-        my $votes = $dbh->selectall_arrayref(
-            'SELECT votes.who, votes.bug_id
+  # 1. too many votes for a single user on a single bug.
+  my @toomanyvotes_list;
+  if ($product->{maxvotesperbug} < $product->{votesperuser}) {
+    my $votes = $dbh->selectall_arrayref(
+      'SELECT votes.who, votes.bug_id
                FROM votes
                     INNER JOIN bugs ON bugs.bug_id = votes.bug_id
               WHERE bugs.product_id = ?
-                    AND votes.vote_count > ?',
-            undef, ($product->id, $product->{maxvotesperbug}));
-
-        foreach my $vote (@$votes) {
-            my ($who, $id) = (@$vote);
-            # If some votes are removed, _remove_votes() returns a list
-            # of messages to send to voters.
-            push(@msgs, _remove_votes($id, $who, 'votes_too_many_per_bug'));
-            my $name = Bugzilla::User->new($who)->login;
-            
-            push(@toomanyvotes_list, {id => $id, name => $name});
-        }
+                    AND votes.vote_count > ?', undef,
+      ($product->id, $product->{maxvotesperbug})
+    );
+
+    foreach my $vote (@$votes) {
+      my ($who, $id) = (@$vote);
+
+      # If some votes are removed, _remove_votes() returns a list
+      # of messages to send to voters.
+      push(@msgs, _remove_votes($id, $who, 'votes_too_many_per_bug'));
+      my $name = Bugzilla::User->new($who)->login;
+
+      push(@toomanyvotes_list, {id => $id, name => $name});
     }
+  }
 
-    $changes->{'_too_many_votes'} = \@toomanyvotes_list;
+  $changes->{'_too_many_votes'} = \@toomanyvotes_list;
 
-    # 2. too many total votes for a single user.
-    # This part doesn't work in the general case because _remove_votes
-    # doesn't enforce votesperuser (except per-bug when it's less
-    # than maxvotesperbug).  See _remove_votes().
+  # 2. too many total votes for a single user.
+  # This part doesn't work in the general case because _remove_votes
+  # doesn't enforce votesperuser (except per-bug when it's less
+  # than maxvotesperbug).  See _remove_votes().
 
-    my $votes = $dbh->selectall_arrayref(
-        'SELECT votes.who, votes.vote_count
+  my $votes = $dbh->selectall_arrayref(
+    'SELECT votes.who, votes.vote_count
            FROM votes
                 INNER JOIN bugs ON bugs.bug_id = votes.bug_id
-          WHERE bugs.product_id = ?',
-         undef, $product->id);
+          WHERE bugs.product_id = ?', undef, $product->id
+  );
 
-    my %counts;
-    foreach my $vote (@$votes) {
-        my ($who, $count) = @$vote;
-        if (!defined $counts{$who}) {
-            $counts{$who} = $count;
-        } else {
-            $counts{$who} += $count;
-        }
+  my %counts;
+  foreach my $vote (@$votes) {
+    my ($who, $count) = @$vote;
+    if (!defined $counts{$who}) {
+      $counts{$who} = $count;
+    }
+    else {
+      $counts{$who} += $count;
     }
+  }
 
-    my @toomanytotalvotes_list;
-    foreach my $who (keys(%counts)) {
-        if ($counts{$who} > $product->{votesperuser}) {
-            my $bug_ids = $dbh->selectcol_arrayref(
-                'SELECT votes.bug_id
+  my @toomanytotalvotes_list;
+  foreach my $who (keys(%counts)) {
+    if ($counts{$who} > $product->{votesperuser}) {
+      my $bug_ids = $dbh->selectcol_arrayref(
+        'SELECT votes.bug_id
                    FROM votes
                         INNER JOIN bugs ON bugs.bug_id = votes.bug_id
                   WHERE bugs.product_id = ?
-                        AND votes.who = ?',
-                undef, $product->id, $who);
-
-            my $name = Bugzilla::User->new($who)->login;
-            foreach my $bug_id (@$bug_ids) {
-                # _remove_votes returns a list of messages to send
-                # in case some voters had too many votes.
-                push(@msgs, _remove_votes($bug_id, $who, 
-                                          'votes_too_many_per_user'));
-
-                push(@toomanytotalvotes_list, {id => $bug_id, name => $name});
-            }
-        }
-    }
-
-    $changes->{'_too_many_total_votes'} = \@toomanytotalvotes_list;
-
-    # 3. enough votes to confirm
-    my $bug_list = $dbh->selectcol_arrayref(
-        'SELECT bug_id FROM bugs 
-          WHERE product_id = ? AND bug_status = ? AND votes >= ?',
-        undef, ($product->id, 'UNCONFIRMED', $product->{votestoconfirm}));
-
-    my @updated_bugs;
-    foreach my $bug_id (@$bug_list) {
-        my $confirmed = _confirm_if_vote_confirmed($bug_id);
-        push (@updated_bugs, $bug_id) if $confirmed;
-    }
-    $changes->{'_confirmed_bugs'} = \@updated_bugs;
-
-    # Now that changes are done, we can send emails to voters.
-    foreach my $msg (@msgs) {
-        MessageToMTA($msg);
-    }
-    # And send out emails about changed bugs
-    foreach my $bug_id (@updated_bugs) {
-        my $sent_bugmail = Bugzilla::BugMail::Send(
-            $bug_id, { changer => Bugzilla->user });
-        $changes->{'_confirmed_bugs_sent_bugmail'}->{$bug_id} = $sent_bugmail;
-    }
+                        AND votes.who = ?', undef, $product->id, $who
+      );
+
+      my $name = Bugzilla::User->new($who)->login;
+      foreach my $bug_id (@$bug_ids) {
+
+        # _remove_votes returns a list of messages to send
+        # in case some voters had too many votes.
+        push(@msgs, _remove_votes($bug_id, $who, 'votes_too_many_per_user'));
+
+        push(@toomanytotalvotes_list, {id => $bug_id, name => $name});
+      }
+    }
+  }
+
+  $changes->{'_too_many_total_votes'} = \@toomanytotalvotes_list;
+
+  # 3. enough votes to confirm
+  my $bug_list = $dbh->selectcol_arrayref(
+    'SELECT bug_id FROM bugs 
+          WHERE product_id = ? AND bug_status = ? AND votes >= ?', undef,
+    ($product->id, 'UNCONFIRMED', $product->{votestoconfirm})
+  );
+
+  my @updated_bugs;
+  foreach my $bug_id (@$bug_list) {
+    my $confirmed = _confirm_if_vote_confirmed($bug_id);
+    push(@updated_bugs, $bug_id) if $confirmed;
+  }
+  $changes->{'_confirmed_bugs'} = \@updated_bugs;
+
+  # Now that changes are done, we can send emails to voters.
+  foreach my $msg (@msgs) {
+    MessageToMTA($msg);
+  }
+
+  # And send out emails about changed bugs
+  foreach my $bug_id (@updated_bugs) {
+    my $sent_bugmail
+      = Bugzilla::BugMail::Send($bug_id, {changer => Bugzilla->user});
+    $changes->{'_confirmed_bugs_sent_bugmail'}->{$bug_id} = $sent_bugmail;
+  }
 }
 
 # If a bug is moved to a product which allows less votes per bug
 # compared to the previous product, extra votes need to be removed.
 sub _remove_votes {
-    my ($id, $who, $reason) = (@_);
-    my $dbh = Bugzilla->dbh;
-
-    my $whopart = ($who) ? " AND votes.who = $who" : "";
-
-    my $sth = $dbh->prepare("SELECT profiles.email, " .
-                            "profiles.userid, votes.vote_count, " .
-                            "products.votesperuser, products.maxvotesperbug " .
-                            "FROM profiles " .
-                            "LEFT JOIN votes ON profiles.userid = votes.who " .
-                            "LEFT JOIN bugs ON votes.bug_id = bugs.bug_id " .
-                            "LEFT JOIN products ON products.id = bugs.product_id " .
-                            "WHERE votes.bug_id = ? " . $whopart);
-    $sth->execute($id);
-    my @list;
-    while (my ($email, $userid, $oldvotes, $votesperuser, $maxvotesperbug) = $sth->fetchrow_array()) {
-        push(@list, [$email, $userid, $oldvotes, $votesperuser, $maxvotesperbug]);
-    }
-
-    # @messages stores all emails which have to be sent, if any.
-    # This array is passed to the caller which will send these emails itself.
-    my @messages = ();
-
-    if (scalar(@list)) {
-        foreach my $ref (@list) {
-            my ($email, $userid, $oldvotes, $votesperuser, $maxvotesperbug) = (@$ref);
-
-            $maxvotesperbug = min($votesperuser, $maxvotesperbug);
-
-            # If this product allows voting and the user's votes are in
-            # the acceptable range, then don't do anything.
-            next if $votesperuser && $oldvotes <= $maxvotesperbug;
-
-            # If the user has more votes on this bug than this product
-            # allows, then reduce the number of votes so it fits
-            my $newvotes = $maxvotesperbug;
-
-            my $removedvotes = $oldvotes - $newvotes;
-
-            if ($newvotes) {
-                $dbh->do("UPDATE votes SET vote_count = ? " .
-                         "WHERE bug_id = ? AND who = ?",
-                         undef, ($newvotes, $id, $userid));
-            } else {
-                $dbh->do("DELETE FROM votes WHERE bug_id = ? AND who = ?",
-                         undef, ($id, $userid));
-            }
-
-            # Notice that we did not make sure that the user fit within the $votesperuser
-            # range.  This is considered to be an acceptable alternative to losing votes
-            # during product moves.  Then next time the user attempts to change their votes,
-            # they will be forced to fit within the $votesperuser limit.
-
-            # Now lets send the e-mail to alert the user to the fact that their votes have
-            # been reduced or removed.
-            my $vars = {
-                'to' => $email,
-                'bugid' => $id,
-                'reason' => $reason,
-
-                'votesremoved' => $removedvotes,
-                'votesold' => $oldvotes,
-                'votesnew' => $newvotes,
-            };
-
-            my $voter = new Bugzilla::User($userid);
-            my $template = Bugzilla->template_inner($voter->setting('lang'));
-
-            my $msg;
-            $template->process("voting/votes-removed.txt.tmpl", $vars, \$msg);
-            push(@messages, $msg);
-        }
-
-        my $votes = $dbh->selectrow_array("SELECT SUM(vote_count) " .
-                                          "FROM votes WHERE bug_id = ?",
-                                          undef, $id) || 0;
-        $dbh->do("UPDATE bugs SET votes = ? WHERE bug_id = ?",
-                 undef, ($votes, $id));
-    }
-    # Now return the array containing emails to be sent.
-    return @messages;
+  my ($id, $who, $reason) = (@_);
+  my $dbh = Bugzilla->dbh;
+
+  my $whopart = ($who) ? " AND votes.who = $who" : "";
+
+  my $sth
+    = $dbh->prepare("SELECT profiles.email, "
+      . "profiles.userid, votes.vote_count, "
+      . "products.votesperuser, products.maxvotesperbug "
+      . "FROM profiles "
+      . "LEFT JOIN votes ON profiles.userid = votes.who "
+      . "LEFT JOIN bugs ON votes.bug_id = bugs.bug_id "
+      . "LEFT JOIN products ON products.id = bugs.product_id "
+      . "WHERE votes.bug_id = ? "
+      . $whopart);
+  $sth->execute($id);
+  my @list;
+  while (my ($email, $userid, $oldvotes, $votesperuser, $maxvotesperbug)
+    = $sth->fetchrow_array())
+  {
+    push(@list, [$email, $userid, $oldvotes, $votesperuser, $maxvotesperbug]);
+  }
+
+  # @messages stores all emails which have to be sent, if any.
+  # This array is passed to the caller which will send these emails itself.
+  my @messages = ();
+
+  if (scalar(@list)) {
+    foreach my $ref (@list) {
+      my ($email, $userid, $oldvotes, $votesperuser, $maxvotesperbug) = (@$ref);
+
+      $maxvotesperbug = min($votesperuser, $maxvotesperbug);
+
+      # If this product allows voting and the user's votes are in
+      # the acceptable range, then don't do anything.
+      next if $votesperuser && $oldvotes <= $maxvotesperbug;
+
+      # If the user has more votes on this bug than this product
+      # allows, then reduce the number of votes so it fits
+      my $newvotes = $maxvotesperbug;
+
+      my $removedvotes = $oldvotes - $newvotes;
+
+      if ($newvotes) {
+        $dbh->do("UPDATE votes SET vote_count = ? " . "WHERE bug_id = ? AND who = ?",
+          undef, ($newvotes, $id, $userid));
+      }
+      else {
+        $dbh->do("DELETE FROM votes WHERE bug_id = ? AND who = ?",
+          undef, ($id, $userid));
+      }
+
+      # Notice that we did not make sure that the user fit within the $votesperuser
+      # range.  This is considered to be an acceptable alternative to losing votes
+      # during product moves.  Then next time the user attempts to change their votes,
+      # they will be forced to fit within the $votesperuser limit.
+
+      # Now lets send the e-mail to alert the user to the fact that their votes have
+      # been reduced or removed.
+      my $vars = {
+        'to'     => $email,
+        'bugid'  => $id,
+        'reason' => $reason,
+
+        'votesremoved' => $removedvotes,
+        'votesold'     => $oldvotes,
+        'votesnew'     => $newvotes,
+      };
+
+      my $voter    = new Bugzilla::User($userid);
+      my $template = Bugzilla->template_inner($voter->setting('lang'));
+
+      my $msg;
+      $template->process("voting/votes-removed.txt.tmpl", $vars, \$msg);
+      push(@messages, $msg);
+    }
+
+    my $votes
+      = $dbh->selectrow_array(
+      "SELECT SUM(vote_count) " . "FROM votes WHERE bug_id = ?",
+      undef, $id)
+      || 0;
+    $dbh->do("UPDATE bugs SET votes = ? WHERE bug_id = ?", undef, ($votes, $id));
+  }
+
+  # Now return the array containing emails to be sent.
+  return @messages;
 }
 
 # If a user votes for a bug, or the number of votes required to
 # confirm a bug has been reduced, check if the bug is now confirmed.
 sub _confirm_if_vote_confirmed {
-    my $id = shift;
-    my $bug = ref $id ? $id : new Bugzilla::Bug({ id => $id, cache => 1 });
-
-    my $ret = 0;
-    if (!$bug->everconfirmed
-        and $bug->product_obj->{votestoconfirm}
-        and $bug->votes >= $bug->product_obj->{votestoconfirm})
-    {
-        $bug->add_comment('', { type => CMT_POPULAR_VOTES });
-
-        if ($bug->bug_status eq 'UNCONFIRMED') {
-            # Get a valid open state.
-            my $new_status;
-            foreach my $state (@{$bug->status->can_change_to}) {
-                if ($state->is_open && $state->name ne 'UNCONFIRMED') {
-                    $new_status = $state->name;
-                    last;
-                }
-            }
-            ThrowCodeError('voting_no_open_bug_status') unless $new_status;
-
-            # We cannot call $bug->set_bug_status() here, because a user without
-            # canconfirm privs should still be able to confirm a bug by
-            # popular vote. We already know the new status is valid, so it's safe.
-            $bug->{bug_status} = $new_status;
-            $bug->{everconfirmed} = 1;
-            delete $bug->{'status'}; # Contains the status object.
-        }
-        else {
-            # If the bug is in a closed state, only set everconfirmed to 1.
-            # Do not call $bug->_set_everconfirmed(), for the same reason as above.
-            $bug->{everconfirmed} = 1;
+  my $id = shift;
+  my $bug = ref $id ? $id : new Bugzilla::Bug({id => $id, cache => 1});
+
+  my $ret = 0;
+  if ( !$bug->everconfirmed
+    and $bug->product_obj->{votestoconfirm}
+    and $bug->votes >= $bug->product_obj->{votestoconfirm})
+  {
+    $bug->add_comment('', {type => CMT_POPULAR_VOTES});
+
+    if ($bug->bug_status eq 'UNCONFIRMED') {
+
+      # Get a valid open state.
+      my $new_status;
+      foreach my $state (@{$bug->status->can_change_to}) {
+        if ($state->is_open && $state->name ne 'UNCONFIRMED') {
+          $new_status = $state->name;
+          last;
         }
-        $bug->update();
+      }
+      ThrowCodeError('voting_no_open_bug_status') unless $new_status;
 
-        $ret = 1;
+      # We cannot call $bug->set_bug_status() here, because a user without
+      # canconfirm privs should still be able to confirm a bug by
+      # popular vote. We already know the new status is valid, so it's safe.
+      $bug->{bug_status}    = $new_status;
+      $bug->{everconfirmed} = 1;
+      delete $bug->{'status'};    # Contains the status object.
     }
-    return $ret;
+    else {
+      # If the bug is in a closed state, only set everconfirmed to 1.
+      # Do not call $bug->_set_everconfirmed(), for the same reason as above.
+      $bug->{everconfirmed} = 1;
+    }
+    $bug->update();
+
+    $ret = 1;
+  }
+  return $ret;
 }
 
 
index 2f6f4a0be6ca11e06a82d2a709a6df2a51d63ea8..7419c90e63bed4c763275f4dbb631ac2d1aa0162 100755 (executable)
@@ -27,42 +27,41 @@ my $base_dir = bz_locations()->{'extensionsdir'};
 my $name = $ARGV[0] or ThrowUserError('extension_create_no_name');
 $name = ucfirst($name);
 if ($name !~ /^[A-Z]/) {
-    ThrowUserError('extension_first_letter_caps', { name => $name });
+  ThrowUserError('extension_first_letter_caps', {name => $name});
 }
 
-my $extension_dir = "$base_dir/$name"; 
-mkpath($extension_dir) 
+my $extension_dir = "$base_dir/$name";
+mkpath($extension_dir)
   || die "$extension_dir already exists or cannot be created.\n";
 
 my $lcname = lc($name);
 foreach my $path (qw(lib docs/en/rst web template/en/default/hook),
-                  "template/en/default/$lcname")
+  "template/en/default/$lcname")
 {
-    mkpath("$extension_dir/$path") || die "$extension_dir/$path: $!";
+  mkpath("$extension_dir/$path") || die "$extension_dir/$path: $!";
 }
 
-my $template = Bugzilla->template;
-my $vars = { name => $name, path => $extension_dir };
+my $template     = Bugzilla->template;
+my $vars         = {name => $name, path => $extension_dir};
 my %create_files = (
-    'config.pm.tmpl'       => 'Config.pm',
-    'extension.pm.tmpl'    => 'Extension.pm',
-    'util.pm.tmpl'         => 'lib/Util.pm',
-    'web-readme.txt.tmpl'  => 'web/README',
-    'hook-readme.txt.tmpl' => 'template/en/default/hook/README',
-    'name-readme.txt.tmpl' => "template/en/default/$lcname/README",
-    'index-admin.rst.tmpl' => "docs/en/rst/index-admin.rst",
-    'index-user.rst.tmpl'  => "docs/en/rst/index-user.rst",
+  'config.pm.tmpl'       => 'Config.pm',
+  'extension.pm.tmpl'    => 'Extension.pm',
+  'util.pm.tmpl'         => 'lib/Util.pm',
+  'web-readme.txt.tmpl'  => 'web/README',
+  'hook-readme.txt.tmpl' => 'template/en/default/hook/README',
+  'name-readme.txt.tmpl' => "template/en/default/$lcname/README",
+  'index-admin.rst.tmpl' => "docs/en/rst/index-admin.rst",
+  'index-user.rst.tmpl'  => "docs/en/rst/index-user.rst",
 );
 
 foreach my $template_file (keys %create_files) {
-    my $target = $create_files{$template_file};
-    my $output;
-    $template->process("extensions/$template_file", $vars, \$output)
-      or ThrowTemplateError($template->error());
-    open(my $fh, '>', "$extension_dir/$target")
-      or die "extension_dir/$target: $!";
-    print $fh $output;
-    close($fh);
+  my $target = $create_files{$template_file};
+  my $output;
+  $template->process("extensions/$template_file", $vars, \$output)
+    or ThrowTemplateError($template->error());
+  open(my $fh, '>', "$extension_dir/$target") or die "extension_dir/$target: $!";
+  print $fh $output;
+  close($fh);
 }
 
 say get_text('extension_created', $vars);
index 51d717d7c87e7cf8fe59c5781e1740fe7e0ec61e..a57735b0c73eff2909934046020333c2bd3221b2 100755 (executable)
@@ -21,50 +21,50 @@ use lib qw(. lib local/lib/perl5);
 use Getopt::Long qw(:config gnu_getopt);
 
 if (-f "MYMETA.json") {
-    eval {
-        require CPAN::Meta;
-        require Module::CPANfile;
-
-        my (@with_feature, @without_feature);
-        my $with_all_features = 0;
-        GetOptions(
-            'with-all-features|A!' => \$with_all_features,
-            'with-feature|D=s@'    => \@with_feature,
-            'without-feature|U=s@' => \@without_feature
-        );
-
-
-        my $meta = CPAN::Meta->load_file("MYMETA.json");
-
-        my @phases = qw(configure build test develop runtime);
-        my @types  = qw(requires recommends suggests conflicts);
-
-        my %features;
-        if ($with_all_features) {
-            $features{$_->identifier} = 1 foreach ($meta->features);
-        }
-        $features{$_} = 1 foreach @with_feature;
-        $features{$_} = 0 foreach @without_feature;
-        my @features = grep { $features{$_} } keys %features;
-
-        my $prereqs = $meta->effective_prereqs(\@features)->as_string_hash;
-        my $filtered = {};
-
-        while (my($phase, $types) = each %$prereqs) {
-            while (my($type, $reqs) = each %$types) {
-                $filtered->{$phase}{$type} = $reqs;
-            }
-        }
-
-        my $cpanfile = Module::CPANfile->from_prereqs($filtered);
-        open my $cpanfile_fh, '>', 'cpanfile' or die "cannot write to cpanfile: $!";
-        print $cpanfile_fh $cpanfile->to_string();
-        close $cpanfile_fh;
-    };
-    die "Unable generate cpanfile: $@\n" if $@;
+  eval {
+    require CPAN::Meta;
+    require Module::CPANfile;
+
+    my (@with_feature, @without_feature);
+    my $with_all_features = 0;
+    GetOptions(
+      'with-all-features|A!' => \$with_all_features,
+      'with-feature|D=s@'    => \@with_feature,
+      'without-feature|U=s@' => \@without_feature
+    );
+
+
+    my $meta = CPAN::Meta->load_file("MYMETA.json");
+
+    my @phases = qw(configure build test develop runtime);
+    my @types  = qw(requires recommends suggests conflicts);
+
+    my %features;
+    if ($with_all_features) {
+      $features{$_->identifier} = 1 foreach ($meta->features);
+    }
+    $features{$_} = 1 foreach @with_feature;
+    $features{$_} = 0 foreach @without_feature;
+    my @features = grep { $features{$_} } keys %features;
+
+    my $prereqs  = $meta->effective_prereqs(\@features)->as_string_hash;
+    my $filtered = {};
+
+    while (my ($phase, $types) = each %$prereqs) {
+      while (my ($type, $reqs) = each %$types) {
+        $filtered->{$phase}{$type} = $reqs;
+      }
+    }
+
+    my $cpanfile = Module::CPANfile->from_prereqs($filtered);
+    open my $cpanfile_fh, '>', 'cpanfile' or die "cannot write to cpanfile: $!";
+    print $cpanfile_fh $cpanfile->to_string();
+    close $cpanfile_fh;
+  };
+  die "Unable generate cpanfile: $@\n" if $@;
 }
 else {
-    die "MYMETA.yml is missing, cannot generate cpanfile\n";
+  die "MYMETA.yml is missing, cannot generate cpanfile\n";
 }
 
 
index f4f9be956632b218bd9d1e45e9357df5c17956c8..d2c0c935cdfc77262a8ccd1483999313741d5e47 100755 (executable)
@@ -38,17 +38,21 @@ use warnings;
 #####################################################################
 
 use File::Basename qw(dirname);
+
 # MTAs may call this script from any directory, but it should always
 # run from this one so that it can find its modules.
 BEGIN {
-    require File::Basename;
-    my $dir = $0; $dir =~ /(.*)/; $dir = $1; # trick taint
-    chdir(File::Basename::dirname($dir));
+  require File::Basename;
+  my $dir = $0;
+  $dir =~ /(.*)/;
+  $dir = $1;    # trick taint
+  chdir(File::Basename::dirname($dir));
 }
 
 use lib qw(. lib local/lib/perl5);
-# Data dumber is used for debugging, I got tired of copying it back in 
-# and then removing it. 
+
+# Data dumber is used for debugging, I got tired of copying it back in
+# and then removing it.
 #use Data::Dumper;
 
 
@@ -76,22 +80,22 @@ use Getopt::Long;
 use Pod::Usage;
 use XML::Twig;
 
-my $debug = 0;
-my $mail  = '';
-my $attach_path = '';
-my $help  = 0;
-my $bug_page = 'show_bug.cgi?id=';
-my $default_product_name = '';
+my $debug                  = 0;
+my $mail                   = '';
+my $attach_path            = '';
+my $help                   = 0;
+my $bug_page               = 'show_bug.cgi?id=';
+my $default_product_name   = '';
 my $default_component_name = '';
 
 my $result = GetOptions(
-    "verbose|debug+" => \$debug,
-    "mail|sendmail!" => \$mail,
-    "attach_path=s"  => \$attach_path,
-    "help|?"         => \$help,
-    "bug_page=s"     => \$bug_page,
-    "product=s"      => \$default_product_name,
-    "component=s"    => \$default_component_name,
+  "verbose|debug+" => \$debug,
+  "mail|sendmail!" => \$mail,
+  "attach_path=s"  => \$attach_path,
+  "help|?"         => \$help,
+  "bug_page=s"     => \$bug_page,
+  "product=s"      => \$default_product_name,
+  "component=s"    => \$default_component_name,
 );
 
 pod2usage(0) if $help;
@@ -104,8 +108,8 @@ our @logs;
 our @attachments;
 our $bugtotal;
 my $xml;
-my $dbh = Bugzilla->dbh;
-my $params = Bugzilla->params;
+my $dbh         = Bugzilla->dbh;
+my $params      = Bugzilla->params;
 my ($timestamp) = $dbh->selectrow_array("SELECT NOW()");
 
 ###############################################################################
@@ -113,166 +117,168 @@ my ($timestamp) = $dbh->selectrow_array("SELECT NOW()");
 ###############################################################################
 
 sub MailMessage {
-    return unless ($mail);
-    my $subject    = shift;
-    my $message    = shift;
-    my @recipients = @_;
-    my $from   = $params->{"mailfrom"};
-    $from =~ s/@/\@/g;
-
-    foreach my $to (@recipients){
-        my $header = "To: $to\n";
-        $header .= "From: Bugzilla <$from>\n";
-        $header .= "Subject: $subject\n\n";
-        my $sendmessage = $header . $message . "\n";
-        MessageToMTA($sendmessage);
-    }
+  return unless ($mail);
+  my $subject    = shift;
+  my $message    = shift;
+  my @recipients = @_;
+  my $from       = $params->{"mailfrom"};
+  $from =~ s/@/\@/g;
+
+  foreach my $to (@recipients) {
+    my $header = "To: $to\n";
+    $header .= "From: Bugzilla <$from>\n";
+    $header .= "Subject: $subject\n\n";
+    my $sendmessage = $header . $message . "\n";
+    MessageToMTA($sendmessage);
+  }
 
 }
 
 sub Debug {
-    return unless ($debug);
-    my ( $message, $level ) = (@_);
-    print STDERR "OK: $message \n" if ( $level == OK_LEVEL );
-    print STDERR "ERR: $message \n" if ( $level == ERR_LEVEL );
-    print STDERR "$message\n"
-      if ( ( $debug == $level ) && ( $level == DEBUG_LEVEL ) );
+  return unless ($debug);
+  my ($message, $level) = (@_);
+  print STDERR "OK: $message \n"  if ($level == OK_LEVEL);
+  print STDERR "ERR: $message \n" if ($level == ERR_LEVEL);
+  print STDERR "$message\n" if (($debug == $level) && ($level == DEBUG_LEVEL));
 }
 
 sub Error {
-    my ( $reason, $errtype, $exporter ) = @_;
-    my $subject = "Bug import error: $reason";
-    my $message = "Cannot import these bugs because $reason ";
-    $message .= "\n\nPlease re-open the original bug.\n" if ($errtype);
-    $message .= "For more info, contact " . $params->{"maintainer"} . ".\n";
-    my @to = ( $params->{"maintainer"}, $exporter);
-    Debug( $message, ERR_LEVEL );
-    MailMessage( $subject, $message, @to );
-    exit;
+  my ($reason, $errtype, $exporter) = @_;
+  my $subject = "Bug import error: $reason";
+  my $message = "Cannot import these bugs because $reason ";
+  $message .= "\n\nPlease re-open the original bug.\n" if ($errtype);
+  $message .= "For more info, contact " . $params->{"maintainer"} . ".\n";
+  my @to = ($params->{"maintainer"}, $exporter);
+  Debug($message, ERR_LEVEL);
+  MailMessage($subject, $message, @to);
+  exit;
 }
 
 # This subroutine handles flags for process_bug. It is generic in that
 # it can handle both attachment flags and bug flags.
 sub flag_handler {
-    my (
-        $name,            $status,      $setter_login,
-        $requestee_login, $exporterid,  $bugid,
-        $componentid,     $productid,   $attachid
-      )
-      = @_;
-
-    my $type         = ($attachid) ? "attachment" : "bug";
-    my $err          = '';
-    my $setter       = new Bugzilla::User({ name => $setter_login });
-    my $requestee;
-    my $requestee_id;
-
-    unless ($setter) {
-        $err = "Invalid setter $setter_login on $type flag $name\n";
-        $err .= "   Dropping flag $name\n";
-        return $err;
-    }
-    if ( !$setter->can_see_bug($bugid) ) {
-        $err .= "Setter is not a member of bug group\n";
-        $err .= "   Dropping flag $name\n";
-        return $err;
-    }
-    my $setter_id = $setter->id;
-    if ( defined($requestee_login) ) {
-        $requestee = new Bugzilla::User({ name => $requestee_login });
-        if ( $requestee ) {
-            if ( !$requestee->can_see_bug($bugid) ) {
-                $err .= "Requestee is not a member of bug group\n";
-                $err .= "   Requesting from the wind\n";
-            }    
-            else{
-                $requestee_id = $requestee->id;
-            }
-        }
-        else {
-            $err = "Invalid requestee $requestee_login on $type flag $name\n";
-            $err .= "   Requesting from the wind.\n";
-        }
-        
-    }
-    my $flag_types;
-
-    # If this is an attachment flag we need to do some dirty work to look
-    # up the flagtype ID
-    if ($attachid) {
-        $flag_types = Bugzilla::FlagType::match(
-            {
-                'target_type'  => 'attachment',
-                'product_id'   => $productid,
-                'component_id' => $componentid
-            } );
+  my (
+    $name,            $status,     $setter_login,
+    $requestee_login, $exporterid, $bugid,
+    $componentid,     $productid,  $attachid
+  ) = @_;
+
+  my $type   = ($attachid) ? "attachment" : "bug";
+  my $err    = '';
+  my $setter = new Bugzilla::User({name => $setter_login});
+  my $requestee;
+  my $requestee_id;
+
+  unless ($setter) {
+    $err = "Invalid setter $setter_login on $type flag $name\n";
+    $err .= "   Dropping flag $name\n";
+    return $err;
+  }
+  if (!$setter->can_see_bug($bugid)) {
+    $err .= "Setter is not a member of bug group\n";
+    $err .= "   Dropping flag $name\n";
+    return $err;
+  }
+  my $setter_id = $setter->id;
+  if (defined($requestee_login)) {
+    $requestee = new Bugzilla::User({name => $requestee_login});
+    if ($requestee) {
+      if (!$requestee->can_see_bug($bugid)) {
+        $err .= "Requestee is not a member of bug group\n";
+        $err .= "   Requesting from the wind\n";
+      }
+      else {
+        $requestee_id = $requestee->id;
+      }
     }
     else {
-        my $bug = new Bugzilla::Bug($bugid);
-        $flag_types = $bug->flag_types;
-    }
-    unless ($flag_types){
-        $err  = "No flag types defined for this bug\n";
-        $err .= "   Dropping flag $name\n";
-        return $err;
+      $err = "Invalid requestee $requestee_login on $type flag $name\n";
+      $err .= "   Requesting from the wind.\n";
     }
 
-    # We need to see if the imported flag is in the list of known flags
-    # It is possible for two flags on the same bug have the same name
-    # If this is the case, we will only match the first one.
-    my $ftype;
-    foreach my $f ( @{$flag_types} ) {
-        if ( $f->name eq $name) {
-            $ftype = $f;
-            last;
-        }
+  }
+  my $flag_types;
+
+  # If this is an attachment flag we need to do some dirty work to look
+  # up the flagtype ID
+  if ($attachid) {
+    $flag_types = Bugzilla::FlagType::match({
+      'target_type'  => 'attachment',
+      'product_id'   => $productid,
+      'component_id' => $componentid
+    });
+  }
+  else {
+    my $bug = new Bugzilla::Bug($bugid);
+    $flag_types = $bug->flag_types;
+  }
+  unless ($flag_types) {
+    $err = "No flag types defined for this bug\n";
+    $err .= "   Dropping flag $name\n";
+    return $err;
+  }
+
+  # We need to see if the imported flag is in the list of known flags
+  # It is possible for two flags on the same bug have the same name
+  # If this is the case, we will only match the first one.
+  my $ftype;
+  foreach my $f (@{$flag_types}) {
+    if ($f->name eq $name) {
+      $ftype = $f;
+      last;
     }
+  }
 
-    if ($ftype) {    # We found the flag in the list
-        my $grant_group = $ftype->grant_group;
-        if (( $status eq '+' || $status eq '-' ) 
-            && $grant_group && !$setter->in_group_id($grant_group->id)) {
-            $err = "Setter $setter_login on $type flag $name ";
-            $err .= "is not in the Grant Group\n";
-            $err .= "   Dropping flag $name\n";
-            return $err;
-        }
-        my $request_group = $ftype->request_group;
-        if ($request_group
-            && $status eq '?' && !$setter->in_group_id($request_group->id)) {
-            $err = "Setter $setter_login on $type flag $name ";
-            $err .= "is not in the Request Group\n";
-            $err .= "   Dropping flag $name\n";
-            return $err;
-        }
+  if ($ftype) {    # We found the flag in the list
+    my $grant_group = $ftype->grant_group;
+    if ( ($status eq '+' || $status eq '-')
+      && $grant_group
+      && !$setter->in_group_id($grant_group->id))
+    {
+      $err = "Setter $setter_login on $type flag $name ";
+      $err .= "is not in the Grant Group\n";
+      $err .= "   Dropping flag $name\n";
+      return $err;
+    }
+    my $request_group = $ftype->request_group;
+    if ( $request_group
+      && $status eq '?'
+      && !$setter->in_group_id($request_group->id))
+    {
+      $err = "Setter $setter_login on $type flag $name ";
+      $err .= "is not in the Request Group\n";
+      $err .= "   Dropping flag $name\n";
+      return $err;
+    }
 
-        # Take the first flag_type that matches
-        unless ($ftype->is_active) {
-            $err = "Flag $name is not active in this database\n";
-            $err .= "   Dropping flag $name\n";
-            return $err;
-        }
+    # Take the first flag_type that matches
+    unless ($ftype->is_active) {
+      $err = "Flag $name is not active in this database\n";
+      $err .= "   Dropping flag $name\n";
+      return $err;
+    }
 
-        $dbh->do("INSERT INTO flags 
+    $dbh->do(
+      "INSERT INTO flags 
                  (type_id, status, bug_id, attach_id, creation_date, 
                   setter_id, requestee_id)
                   VALUES (?, ?, ?, ?, ?, ?, ?)", undef,
-            ($ftype->id, $status, $bugid, $attachid, $timestamp,
-            $setter_id, $requestee_id));
-    }
-    else {
-        $err = "Dropping unknown $type flag: $name\n";
-        return $err;
-    }
+      ($ftype->id, $status, $bugid, $attachid, $timestamp, $setter_id, $requestee_id)
+    );
+  }
+  else {
+    $err = "Dropping unknown $type flag: $name\n";
     return $err;
+  }
+  return $err;
 }
 
 # Converts and returns the input data as an array.
 sub _to_array {
-    my $value = shift;
+  my $value = shift;
 
-    $value = [$value] if !ref($value);
-    return @$value;
+  $value = [$value] if !ref($value);
+  return @$value;
 }
 
 ###############################################################################
@@ -290,27 +296,28 @@ sub _to_array {
 #                bugs are being moved from
 #
 sub init() {
-    my ( $twig, $bugzilla ) = @_;
-    my $root       = $twig->root;
-    my $maintainer = $root->{'att'}->{'maintainer'};
-    my $exporter   = $root->{'att'}->{'exporter'};
-    my $urlbase    = $root->{'att'}->{'urlbase'};
-    my $xmlversion = $root->{'att'}->{'version'};
-
-    if ($xmlversion ne BUGZILLA_VERSION) {
-            my $log = "Possible version conflict!\n";
-            $log .= "   XML was exported from Bugzilla version $xmlversion\n";
-            $log .= "   But this installation uses ";
-            $log .= BUGZILLA_VERSION . "\n";
-            Debug($log, OK_LEVEL);
-            push(@logs, $log);
-    }
-    Error( "no maintainer", "REOPEN", $exporter ) unless ($maintainer);
-    Error( "no exporter",   "REOPEN", $exporter ) unless ($exporter);
-    Error( "invalid exporter: $exporter", "REOPEN", $exporter ) if ( !login_to_id($exporter) );
-    Error( "no urlbase set", "REOPEN", $exporter ) unless ($urlbase);
+  my ($twig, $bugzilla) = @_;
+  my $root       = $twig->root;
+  my $maintainer = $root->{'att'}->{'maintainer'};
+  my $exporter   = $root->{'att'}->{'exporter'};
+  my $urlbase    = $root->{'att'}->{'urlbase'};
+  my $xmlversion = $root->{'att'}->{'version'};
+
+  if ($xmlversion ne BUGZILLA_VERSION) {
+    my $log = "Possible version conflict!\n";
+    $log .= "   XML was exported from Bugzilla version $xmlversion\n";
+    $log .= "   But this installation uses ";
+    $log .= BUGZILLA_VERSION . "\n";
+    Debug($log, OK_LEVEL);
+    push(@logs, $log);
+  }
+  Error("no maintainer", "REOPEN", $exporter) unless ($maintainer);
+  Error("no exporter",   "REOPEN", $exporter) unless ($exporter);
+  Error("invalid exporter: $exporter", "REOPEN", $exporter)
+    if (!login_to_id($exporter));
+  Error("no urlbase set", "REOPEN", $exporter) unless ($urlbase);
 }
-    
+
 
 # Parse attachments.
 #
@@ -327,69 +334,74 @@ sub init() {
 # The submitter_id gets filled in with $exporterid.
 
 sub process_attachment() {
-    my ( $twig, $attach ) = @_;
-    Debug( "Parsing attachments", DEBUG_LEVEL );
-    my %attachment;
-
-    $attachment{'date'} =
-        format_time( $attach->field('date'), "%Y-%m-%d %R" ) || $timestamp;
-    $attachment{'desc'}       = $attach->field('desc');
-    $attachment{'ctype'}      = $attach->field('type') || "unknown/unknown";
-    $attachment{'attachid'}   = $attach->field('attachid');
-    $attachment{'ispatch'}    = $attach->{'att'}->{'ispatch'} || 0;
-    $attachment{'isobsolete'} = $attach->{'att'}->{'isobsolete'} || 0;
-    $attachment{'isprivate'}  = $attach->{'att'}->{'isprivate'} || 0;
-    $attachment{'filename'}   = $attach->field('filename') || "file";
-    $attachment{'attacher'}   = $attach->field('attacher');
-    # Attachment data is not exported in versions 2.20 and older.
-    if (defined $attach->first_child('data') &&
-            defined $attach->first_child('data')->{'att'}->{'encoding'}) {
-        my $encoding = $attach->first_child('data')->{'att'}->{'encoding'};
-        if ($encoding =~ /base64/) {
-            # decode the base64
-            my $data   = $attach->field('data');
-            my $output = decode_base64($data);
-            $attachment{'data'} = $output;
-        }
-        elsif ($encoding =~ /filename/) {
-            # read the attachment file
-            Error("attach_path is required", undef) unless ($attach_path);
-            
-            my $filename = $attach->field('data');
-            # Remove any leading path data from the filename
-            $filename =~ s/(.*\/|.*\\)//gs;
-            
-            my $attach_filename = $attach_path . "/" . $filename;
-            open(ATTACH_FH, "<", $attach_filename) or
-                Error("cannot open $attach_filename", undef);
-            $attachment{'data'} = do { local $/; <ATTACH_FH> };
-            close ATTACH_FH;
-        }
-    }
-    else {
-        $attachment{'data'} = $attach->field('data');
+  my ($twig, $attach) = @_;
+  Debug("Parsing attachments", DEBUG_LEVEL);
+  my %attachment;
+
+  $attachment{'date'}
+    = format_time($attach->field('date'), "%Y-%m-%d %R") || $timestamp;
+  $attachment{'desc'}       = $attach->field('desc');
+  $attachment{'ctype'}      = $attach->field('type') || "unknown/unknown";
+  $attachment{'attachid'}   = $attach->field('attachid');
+  $attachment{'ispatch'}    = $attach->{'att'}->{'ispatch'} || 0;
+  $attachment{'isobsolete'} = $attach->{'att'}->{'isobsolete'} || 0;
+  $attachment{'isprivate'}  = $attach->{'att'}->{'isprivate'} || 0;
+  $attachment{'filename'}   = $attach->field('filename') || "file";
+  $attachment{'attacher'}   = $attach->field('attacher');
+
+  # Attachment data is not exported in versions 2.20 and older.
+  if ( defined $attach->first_child('data')
+    && defined $attach->first_child('data')->{'att'}->{'encoding'})
+  {
+    my $encoding = $attach->first_child('data')->{'att'}->{'encoding'};
+    if ($encoding =~ /base64/) {
+
+      # decode the base64
+      my $data   = $attach->field('data');
+      my $output = decode_base64($data);
+      $attachment{'data'} = $output;
     }
+    elsif ($encoding =~ /filename/) {
 
-    # attachment flags
-    my @aflags;
-    foreach my $aflag ( $attach->children('flag') ) {
-        my %aflag;
-        $aflag{'name'}      = $aflag->{'att'}->{'name'};
-        $aflag{'status'}    = $aflag->{'att'}->{'status'};
-        $aflag{'setter'}    = $aflag->{'att'}->{'setter'};
-        $aflag{'requestee'} = $aflag->{'att'}->{'requestee'};
-        push @aflags, \%aflag;
-    }
-    $attachment{'flags'} = \@aflags if (@aflags);
+      # read the attachment file
+      Error("attach_path is required", undef) unless ($attach_path);
 
-    # free up the memory for use by the rest of the script
-    $attach->delete;
-    if ($attachment{'attachid'}) {
-        push @attachments, \%attachment;
-    }
-    else {
-        push @attachments, "err";
+      my $filename = $attach->field('data');
+
+      # Remove any leading path data from the filename
+      $filename =~ s/(.*\/|.*\\)//gs;
+
+      my $attach_filename = $attach_path . "/" . $filename;
+      open(ATTACH_FH, "<", $attach_filename)
+        or Error("cannot open $attach_filename", undef);
+      $attachment{'data'} = do { local $/; <ATTACH_FH> };
+      close ATTACH_FH;
     }
+  }
+  else {
+    $attachment{'data'} = $attach->field('data');
+  }
+
+  # attachment flags
+  my @aflags;
+  foreach my $aflag ($attach->children('flag')) {
+    my %aflag;
+    $aflag{'name'}      = $aflag->{'att'}->{'name'};
+    $aflag{'status'}    = $aflag->{'att'}->{'status'};
+    $aflag{'setter'}    = $aflag->{'att'}->{'setter'};
+    $aflag{'requestee'} = $aflag->{'att'}->{'requestee'};
+    push @aflags, \%aflag;
+  }
+  $attachment{'flags'} = \@aflags if (@aflags);
+
+  # free up the memory for use by the rest of the script
+  $attach->delete;
+  if ($attachment{'attachid'}) {
+    push @attachments, \%attachment;
+  }
+  else {
+    push @attachments, "err";
+  }
 }
 
 # This subroutine will be called once for each <bug> in the xml file.
@@ -401,849 +413,871 @@ sub process_attachment() {
 # purged from memory to free it up for later bugs.
 
 sub process_bug {
-    my ( $twig, $bug ) = @_;
-    my $root             = $twig->root;
-    my $maintainer       = $root->{'att'}->{'maintainer'};
-    my $exporter_login   = $root->{'att'}->{'exporter'};
-    my $exporter         = new Bugzilla::User({ name => $exporter_login });
-    my $urlbase          = $root->{'att'}->{'urlbase'};
-    my $url              = $urlbase . $bug_page;
-    trick_taint($url);
-
-    # We will store output information in this variable.
-    my $log = "";
-    if ( defined $bug->{'att'}->{'error'} ) {
-        $log .= "\nError in bug " . $bug->field('bug_id') . "\@$urlbase: ";
-        $log .= $bug->{'att'}->{'error'} . "\n";
-        if ( $bug->{'att'}->{'error'} =~ /NotFound/ ) {
-            $log .= "$exporter_login tried to move bug " . $bug->field('bug_id');
-            $log .= " here, but $urlbase reports that this bug";
-            $log .= " does not exist.\n";
-        }
-        elsif ( $bug->{'att'}->{'error'} =~ /NotPermitted/ ) {
-            $log .= "$exporter_login tried to move bug " . $bug->field('bug_id');
-            $log .= " here, but $urlbase reports that $exporter_login does ";
-            $log .= " not have access to that bug.\n";
-        }
-        return;
+  my ($twig, $bug) = @_;
+  my $root           = $twig->root;
+  my $maintainer     = $root->{'att'}->{'maintainer'};
+  my $exporter_login = $root->{'att'}->{'exporter'};
+  my $exporter       = new Bugzilla::User({name => $exporter_login});
+  my $urlbase        = $root->{'att'}->{'urlbase'};
+  my $url            = $urlbase . $bug_page;
+  trick_taint($url);
+
+  # We will store output information in this variable.
+  my $log = "";
+  if (defined $bug->{'att'}->{'error'}) {
+    $log .= "\nError in bug " . $bug->field('bug_id') . "\@$urlbase: ";
+    $log .= $bug->{'att'}->{'error'} . "\n";
+    if ($bug->{'att'}->{'error'} =~ /NotFound/) {
+      $log .= "$exporter_login tried to move bug " . $bug->field('bug_id');
+      $log .= " here, but $urlbase reports that this bug";
+      $log .= " does not exist.\n";
     }
-    $bugtotal++;
-
-    # This list contains all other bug fields that we want to process.
-    # If it is not in this list it will not be included.
-    my %all_fields;
-    foreach my $field ( 
-        qw(long_desc attachment flag group), Bugzilla::Bug::fields() )
-    {
-        $all_fields{$field} = 1;
+    elsif ($bug->{'att'}->{'error'} =~ /NotPermitted/) {
+      $log .= "$exporter_login tried to move bug " . $bug->field('bug_id');
+      $log .= " here, but $urlbase reports that $exporter_login does ";
+      $log .= " not have access to that bug.\n";
     }
-    
-    my %bug_fields;
-    my $err = "";
-
-   # Loop through all the xml tags inside a <bug> and compare them to the
-   # lists of fields. If they match throw them into the hash. Otherwise
-   # append it to the log, which will go into the comments when we are done.
-    foreach my $bugchild ( $bug->children() ) {
-        Debug( "Parsing field: " . $bugchild->name, DEBUG_LEVEL );
-
-        # Skip the token if one is included. We don't want it included in
-        # the comments, and it is not used by the importer.
-        next if $bugchild->name eq 'token';
-
-        if ( defined $all_fields{ $bugchild->name } ) {
-            my @values = $bug->children_text($bugchild->name);
-            if (scalar @values > 1) {
-                $bug_fields{$bugchild->name} = \@values;
-            }
-            else {
-                $bug_fields{$bugchild->name} = $values[0];
-            }
-        }
-        else {
-            $err .= "Unknown bug field \"" . $bugchild->name . "\"";
-            $err .= " encountered while moving bug\n";
-            $err .= "   <" . $bugchild->name . ">";
-            if ( $bugchild->children_count > 1 ) {
-                $err .= "\n";
-                foreach my $subchild ( $bugchild->children() ) {
-                    $err .= "     <" . $subchild->name . ">";
-                    $err .= $subchild->field;
-                    $err .= "</" . $subchild->name . ">\n";
-                }
-            }
-            else {
-                $err .= $bugchild->field;
-            }
-            $err .= "</" . $bugchild->name . ">\n";
-        }
+    return;
+  }
+  $bugtotal++;
+
+  # This list contains all other bug fields that we want to process.
+  # If it is not in this list it will not be included.
+  my %all_fields;
+  foreach my $field (qw(long_desc attachment flag group), Bugzilla::Bug::fields())
+  {
+    $all_fields{$field} = 1;
+  }
+
+  my %bug_fields;
+  my $err = "";
+
+  # Loop through all the xml tags inside a <bug> and compare them to the
+  # lists of fields. If they match throw them into the hash. Otherwise
+  # append it to the log, which will go into the comments when we are done.
+  foreach my $bugchild ($bug->children()) {
+    Debug("Parsing field: " . $bugchild->name, DEBUG_LEVEL);
+
+    # Skip the token if one is included. We don't want it included in
+    # the comments, and it is not used by the importer.
+    next if $bugchild->name eq 'token';
+
+    if (defined $all_fields{$bugchild->name}) {
+      my @values = $bug->children_text($bugchild->name);
+      if (scalar @values > 1) {
+        $bug_fields{$bugchild->name} = \@values;
+      }
+      else {
+        $bug_fields{$bugchild->name} = $values[0];
+      }
     }
-
-    # Parse long descriptions
-    my @long_descs;
-    foreach my $comment ( $bug->children('long_desc') ) {
-        Debug( "Parsing Long Description", DEBUG_LEVEL );
-        my %long_desc = ( who       => $comment->field('who'),
-                          bug_when  => format_time($comment->field('bug_when'), '%Y-%m-%d %T'),
-                          isprivate => $comment->{'att'}->{'isprivate'} || 0 );
-
-        # If the exporter is not in the insidergroup, keep the comment public.
-        $long_desc{isprivate} = 0 unless $exporter->is_insider;
-
-        my $data = $comment->field('thetext');
-        if ( defined $comment->first_child('thetext')->{'att'}->{'encoding'}
-            && $comment->first_child('thetext')->{'att'}->{'encoding'} =~
-            /base64/ )
-        {
-            $data = decode_base64($data);
-        }
-
-        # For backwards-compatibility with Bugzillas before 3.6:
-        #
-        # If we leave the attachment ID in the comment it will be made a link
-        # to the wrong attachment. Since the new attachment ID is unknown yet
-        # let's strip it out for now. We will make a comment with the right ID
-        # later
-        $data =~ s/Created an attachment \(id=\d+\)/Created an attachment/g;
-
-        # Same goes for bug #'s Since we don't know if the referenced bug
-        # is also being moved, lets make sure they know it means a different
-        # bugzilla.
-        $data =~ s/([Bb]ugs?\s*\#?\s*(\d+))/$url$2/g;
-
-        # Keep the original commenter if possible, else we will fall back
-        # to the exporter account.
-        $long_desc{whoid} = login_to_id($long_desc{who});
-
-        if (!$long_desc{whoid}) {
-            $data = "The original author of this comment is $long_desc{who}.\n\n" . $data;
+    else {
+      $err .= "Unknown bug field \"" . $bugchild->name . "\"";
+      $err .= " encountered while moving bug\n";
+      $err .= "   <" . $bugchild->name . ">";
+      if ($bugchild->children_count > 1) {
+        $err .= "\n";
+        foreach my $subchild ($bugchild->children()) {
+          $err .= "     <" . $subchild->name . ">";
+          $err .= $subchild->field;
+          $err .= "</" . $subchild->name . ">\n";
         }
-
-        $long_desc{'thetext'} = $data;
-        push @long_descs, \%long_desc;
+      }
+      else {
+        $err .= $bugchild->field;
+      }
+      $err .= "</" . $bugchild->name . ">\n";
     }
-
-    my @sorted_descs = sort { $a->{'bug_when'} cmp $b->{'bug_when'} } @long_descs;
-
-    my $comments = "\n\n--- Bug imported by $exporter_login ";
-    $comments .= format_time(scalar localtime(time()), '%Y-%m-%d %R %Z') . " ";
-    $comments .= " ---\n\n";
-    $comments .= "This bug was previously known as _bug_ $bug_fields{'bug_id'} at ";
-    $comments .= $url . $bug_fields{'bug_id'} . "\n";
-    if ( defined $bug_fields{'dependson'} ) {
-        $comments .= "This bug depended on bug(s) " .
-                     join(' ', _to_array($bug_fields{'dependson'})) . ".\n";
+  }
+
+  # Parse long descriptions
+  my @long_descs;
+  foreach my $comment ($bug->children('long_desc')) {
+    Debug("Parsing Long Description", DEBUG_LEVEL);
+    my %long_desc = (
+      who       => $comment->field('who'),
+      bug_when  => format_time($comment->field('bug_when'), '%Y-%m-%d %T'),
+      isprivate => $comment->{'att'}->{'isprivate'} || 0
+    );
+
+    # If the exporter is not in the insidergroup, keep the comment public.
+    $long_desc{isprivate} = 0 unless $exporter->is_insider;
+
+    my $data = $comment->field('thetext');
+    if (defined $comment->first_child('thetext')->{'att'}->{'encoding'}
+      && $comment->first_child('thetext')->{'att'}->{'encoding'} =~ /base64/)
+    {
+      $data = decode_base64($data);
     }
-    if ( defined $bug_fields{'blocked'} ) {
-        $comments .= "This bug blocked bug(s) " .
-                     join(' ', _to_array($bug_fields{'blocked'})) . ".\n";
+
+    # For backwards-compatibility with Bugzillas before 3.6:
+    #
+    # If we leave the attachment ID in the comment it will be made a link
+    # to the wrong attachment. Since the new attachment ID is unknown yet
+    # let's strip it out for now. We will make a comment with the right ID
+    # later
+    $data =~ s/Created an attachment \(id=\d+\)/Created an attachment/g;
+
+    # Same goes for bug #'s Since we don't know if the referenced bug
+    # is also being moved, lets make sure they know it means a different
+    # bugzilla.
+    $data =~ s/([Bb]ugs?\s*\#?\s*(\d+))/$url$2/g;
+
+    # Keep the original commenter if possible, else we will fall back
+    # to the exporter account.
+    $long_desc{whoid} = login_to_id($long_desc{who});
+
+    if (!$long_desc{whoid}) {
+      $data = "The original author of this comment is $long_desc{who}.\n\n" . $data;
     }
 
-    # Now we process each of the fields in turn and make sure they contain
-    # valid data. We will create two parallel arrays, one for the query
-    # and one for the values. For every field we need to push an entry onto
-    # each array.
-    my @query  = ();
-    my @values = ();
-
-    # Each of these fields we will check for newlines and shove onto the array
-    foreach my $field (qw(status_whiteboard bug_file_loc short_desc)) {
-        if ($bug_fields{$field}) {
-            $bug_fields{$field} = clean_text( $bug_fields{$field} );
-            push( @query,  $field );
-            push( @values, $bug_fields{$field} );
-        }
+    $long_desc{'thetext'} = $data;
+    push @long_descs, \%long_desc;
+  }
+
+  my @sorted_descs = sort { $a->{'bug_when'} cmp $b->{'bug_when'} } @long_descs;
+
+  my $comments = "\n\n--- Bug imported by $exporter_login ";
+  $comments .= format_time(scalar localtime(time()), '%Y-%m-%d %R %Z') . " ";
+  $comments .= " ---\n\n";
+  $comments .= "This bug was previously known as _bug_ $bug_fields{'bug_id'} at ";
+  $comments .= $url . $bug_fields{'bug_id'} . "\n";
+  if (defined $bug_fields{'dependson'}) {
+    $comments .= "This bug depended on bug(s) "
+      . join(' ', _to_array($bug_fields{'dependson'})) . ".\n";
+  }
+  if (defined $bug_fields{'blocked'}) {
+    $comments .= "This bug blocked bug(s) "
+      . join(' ', _to_array($bug_fields{'blocked'})) . ".\n";
+  }
+
+  # Now we process each of the fields in turn and make sure they contain
+  # valid data. We will create two parallel arrays, one for the query
+  # and one for the values. For every field we need to push an entry onto
+  # each array.
+  my @query  = ();
+  my @values = ();
+
+  # Each of these fields we will check for newlines and shove onto the array
+  foreach my $field (qw(status_whiteboard bug_file_loc short_desc)) {
+    if ($bug_fields{$field}) {
+      $bug_fields{$field} = clean_text($bug_fields{$field});
+      push(@query,  $field);
+      push(@values, $bug_fields{$field});
     }
+  }
 
-    # Alias
-    if ( $bug_fields{'alias'} ) {
-        my ($alias) = $dbh->selectrow_array("SELECT COUNT(*) FROM bugs 
+  # Alias
+  if ($bug_fields{'alias'}) {
+    my ($alias) = $dbh->selectrow_array(
+      "SELECT COUNT(*) FROM bugs 
                                                 WHERE alias = ?", undef,
-                                                $bug_fields{'alias'} );
-        if ($alias) {
-            $err .= "Dropping conflicting bug alias ";
-            $err .= $bug_fields{'alias'} . "\n";
-        }
-        else {
-            $alias = $bug_fields{'alias'};
-            push @query,  'alias';
-            push @values, $alias;
-        }
+      $bug_fields{'alias'}
+    );
+    if ($alias) {
+      $err .= "Dropping conflicting bug alias ";
+      $err .= $bug_fields{'alias'} . "\n";
     }
-
-    # Timestamps
-    push( @query, "creation_ts" );
-    push( @values,
-        format_time( $bug_fields{'creation_ts'}, "%Y-%m-%d %T" )
-          || $timestamp );
-
-    push( @query, "delta_ts" );
-    push( @values,
-        format_time( $bug_fields{'delta_ts'}, "%Y-%m-%d %T" )
-          || $timestamp );
-
-    # Bug Access
-    push( @query,  "cclist_accessible" );
-    push( @values, $bug_fields{'cclist_accessible'} ? 1 : 0 );
-
-    push( @query,  "reporter_accessible" );
-    push( @values, $bug_fields{'reporter_accessible'} ? 1 : 0 );
-
-    my $product = new Bugzilla::Product(
-        { name => $bug_fields{'product'} || '' });
-    if (!$product) {
-        $err .= "Unknown Product " . $bug_fields{'product'} . "\n";
-        $err .= "   Using default product set at the command line.\n";
-        $product = new Bugzilla::Product({ name => $default_product_name })
-            or Error("an invalid default product was defined for the target"
-                     . " DB. " . $params->{"maintainer"} . " needs to specify "
-                     . "--product when calling importxml.pl", "REOPEN", 
-                     $exporter);
+    else {
+      $alias = $bug_fields{'alias'};
+      push @query,  'alias';
+      push @values, $alias;
     }
-    my $component = new Bugzilla::Component({
-        product => $product, name => $bug_fields{'component'} || '' });
+  }
+
+  # Timestamps
+  push(@query, "creation_ts");
+  push(@values,
+    format_time($bug_fields{'creation_ts'}, "%Y-%m-%d %T") || $timestamp);
+
+  push(@query, "delta_ts");
+  push(@values,
+    format_time($bug_fields{'delta_ts'}, "%Y-%m-%d %T") || $timestamp);
+
+  # Bug Access
+  push(@query, "cclist_accessible");
+  push(@values, $bug_fields{'cclist_accessible'} ? 1 : 0);
+
+  push(@query, "reporter_accessible");
+  push(@values, $bug_fields{'reporter_accessible'} ? 1 : 0);
+
+  my $product = new Bugzilla::Product({name => $bug_fields{'product'} || ''});
+  if (!$product) {
+    $err .= "Unknown Product " . $bug_fields{'product'} . "\n";
+    $err .= "   Using default product set at the command line.\n";
+    $product = new Bugzilla::Product({name => $default_product_name})
+      or Error(
+      "an invalid default product was defined for the target" . " DB. "
+        . $params->{"maintainer"}
+        . " needs to specify "
+        . "--product when calling importxml.pl",
+      "REOPEN", $exporter
+      );
+  }
+  my $component
+    = new Bugzilla::Component({
+    product => $product, name => $bug_fields{'component'} || ''
+    });
+  if (!$component) {
+    $err .= "Unknown Component " . $bug_fields{'component'} . "\n";
+    $err .= "   Using default product and component set ";
+    $err .= "at the command line.\n";
+
+    $product = new Bugzilla::Product({name => $default_product_name});
+    $component = new Bugzilla::Component(
+      {name => $default_component_name, product => $product});
     if (!$component) {
-        $err .= "Unknown Component " . $bug_fields{'component'} . "\n";
-        $err .= "   Using default product and component set ";
-        $err .= "at the command line.\n";
-
-        $product = new Bugzilla::Product({ name => $default_product_name });
-        $component = new Bugzilla::Component({
-           name => $default_component_name, product => $product });
-        if (!$component) {
-            Error("an invalid default component was defined for the target" 
-                  . " DB. ".  $params->{"maintainer"} . " needs to specify " 
-                  . "--component when calling importxml.pl", "REOPEN", 
-                  $exporter);
-        }
+      Error(
+        "an invalid default component was defined for the target" . " DB. "
+          . $params->{"maintainer"}
+          . " needs to specify "
+          . "--component when calling importxml.pl",
+        "REOPEN", $exporter
+      );
     }
+  }
+
+  my $prod_id = $product->id;
+  my $comp_id = $component->id;
+
+  push(@query,  "product_id");
+  push(@values, $prod_id);
+  push(@query,  "component_id");
+  push(@values, $comp_id);
+
+  # Since there is no default version for a product, we check that the one
+  # coming over is valid. If not we will use the first one in @versions
+  # and warn them.
+  my $version = new Bugzilla::Version(
+    {product => $product, name => $bug_fields{'version'}});
+
+  push(@query, "version");
+  if ($version) {
+    push(@values, $version->name);
+  }
+  else {
+    my @versions = @{$product->versions};
+    my $v        = $versions[0];
+    push(@values, $v->name);
+    $err .= "Unknown version \"";
+    $err .= (defined $bug_fields{'version'}) ? $bug_fields{'version'} : "unknown";
+    $err .= " in product " . $product->name . ". \n";
+    $err .= "   Setting version to \"" . $v->name . "\".\n";
+  }
+
+  # Milestone
+  if ($params->{"usetargetmilestone"}) {
+    my $milestone;
+    if (defined $bug_fields{'target_milestone'}
+      && $bug_fields{'target_milestone'} ne "")
+    {
 
-    my $prod_id = $product->id;
-    my $comp_id = $component->id;
-
-    push( @query,  "product_id" );
-    push( @values, $prod_id );
-    push( @query,  "component_id" );
-    push( @values, $comp_id );
-
-    # Since there is no default version for a product, we check that the one
-    # coming over is valid. If not we will use the first one in @versions
-    # and warn them.
-    my $version = new Bugzilla::Version(
-          { product => $product, name => $bug_fields{'version'} });
-
-    push( @query, "version" );
-    if ($version) {
-        push( @values, $version->name );
+      $milestone = new Bugzilla::Milestone(
+        {product => $product, name => $bug_fields{'target_milestone'}});
+    }
+    if ($milestone) {
+      push(@values, $milestone->name);
     }
     else {
-        my @versions = @{ $product->versions };
-        my $v        = $versions[0];
-        push( @values, $v->name );
-        $err .= "Unknown version \"";
-        $err .= ( defined $bug_fields{'version'} )
-            ? $bug_fields{'version'}
-            : "unknown";
-        $err .= " in product " . $product->name . ". \n";
-        $err .= "   Setting version to \"" . $v->name . "\".\n";
+      push(@values, $product->default_milestone);
+      $err .= "Unknown milestone \"";
+      $err
+        .= (defined $bug_fields{'target_milestone'})
+        ? $bug_fields{'target_milestone'}
+        : "unknown";
+      $err .= " in product " . $product->name . ". \n";
+      $err .= "   Setting to default milestone for this product, ";
+      $err .= "\"" . $product->default_milestone . "\".\n";
     }
-
-    # Milestone
-    if ( $params->{"usetargetmilestone"} ) {
-        my $milestone;
-        if (defined $bug_fields{'target_milestone'}
-            && $bug_fields{'target_milestone'} ne "") {
-
-            $milestone = new Bugzilla::Milestone(
-                { product => $product, name => $bug_fields{'target_milestone'} });
-        }
-        if ($milestone) {
-            push( @values, $milestone->name );
-        }
-        else {
-            push( @values, $product->default_milestone );
-            $err .= "Unknown milestone \"";
-            $err .= ( defined $bug_fields{'target_milestone'} )
-                ? $bug_fields{'target_milestone'}
-                : "unknown";
-            $err .= " in product " . $product->name . ". \n";
-            $err .= "   Setting to default milestone for this product, ";
-            $err .= "\"" . $product->default_milestone . "\".\n";
-        }
-        push( @query, "target_milestone" );
+    push(@query, "target_milestone");
+  }
+
+  # For priority, severity, opsys and platform we check that the one being
+  # imported is valid. If it is not we use the defaults set in the parameters.
+  if (
+    defined($bug_fields{'bug_severity'})
+    && check_field(
+      'bug_severity', scalar $bug_fields{'bug_severity'},
+      undef, ERR_LEVEL
+    )
+    )
+  {
+    push(@values, $bug_fields{'bug_severity'});
+  }
+  else {
+    push(@values, $params->{'defaultseverity'});
+    $err .= "Unknown severity ";
+    $err
+      .= (defined $bug_fields{'bug_severity'})
+      ? $bug_fields{'bug_severity'}
+      : "unknown";
+    $err .= ". Setting to default severity \"";
+    $err .= $params->{'defaultseverity'} . "\".\n";
+  }
+  push(@query, "bug_severity");
+
+  if (defined($bug_fields{'priority'})
+    && check_field('priority', scalar $bug_fields{'priority'}, undef, ERR_LEVEL))
+  {
+    push(@values, $bug_fields{'priority'});
+  }
+  else {
+    push(@values, $params->{'defaultpriority'});
+    $err .= "Unknown priority ";
+    $err .= (defined $bug_fields{'priority'}) ? $bug_fields{'priority'} : "unknown";
+    $err .= ". Setting to default priority \"";
+    $err .= $params->{'defaultpriority'} . "\".\n";
+  }
+  push(@query, "priority");
+
+  if (
+    defined($bug_fields{'rep_platform'})
+    && check_field(
+      'rep_platform', scalar $bug_fields{'rep_platform'},
+      undef, ERR_LEVEL
+    )
+    )
+  {
+    push(@values, $bug_fields{'rep_platform'});
+  }
+  else {
+    push(@values, $params->{'defaultplatform'});
+    $err .= "Unknown platform ";
+    $err
+      .= (defined $bug_fields{'rep_platform'})
+      ? $bug_fields{'rep_platform'}
+      : "unknown";
+    $err .= ". Setting to default platform \"";
+    $err .= $params->{'defaultplatform'} . "\".\n";
+  }
+  push(@query, "rep_platform");
+
+  if (defined($bug_fields{'op_sys'})
+    && check_field('op_sys', scalar $bug_fields{'op_sys'}, undef, ERR_LEVEL))
+  {
+    push(@values, $bug_fields{'op_sys'});
+  }
+  else {
+    push(@values, $params->{'defaultopsys'});
+    $err .= "Unknown operating system ";
+    $err .= (defined $bug_fields{'op_sys'}) ? $bug_fields{'op_sys'} : "unknown";
+    $err .= ". Setting to default OS \"" . $params->{'defaultopsys'} . "\".\n";
+  }
+  push(@query, "op_sys");
+
+  # Process time fields
+  if ($params->{"timetrackinggroup"}) {
+    my $date
+      = validate_date($bug_fields{'deadline'}) ? $bug_fields{'deadline'} : undef;
+    push(@values, $date);
+    push(@query,  "deadline");
+    if (defined $bug_fields{'estimated_time'}) {
+      eval { Bugzilla::Object::_validate_time($bug_fields{'estimated_time'}, "e"); };
+      if (!$@) {
+        push(@values, $bug_fields{'estimated_time'});
+        push(@query,  "estimated_time");
+      }
     }
-
-    # For priority, severity, opsys and platform we check that the one being
-    # imported is valid. If it is not we use the defaults set in the parameters.
-    if (defined( $bug_fields{'bug_severity'} )
-        && check_field('bug_severity', scalar $bug_fields{'bug_severity'},
-                       undef, ERR_LEVEL) )
-    {
-        push( @values, $bug_fields{'bug_severity'} );
+    if (defined $bug_fields{'remaining_time'}) {
+      eval { Bugzilla::Object::_validate_time($bug_fields{'remaining_time'}, "r"); };
+      if (!$@) {
+        push(@values, $bug_fields{'remaining_time'});
+        push(@query,  "remaining_time");
+      }
+    }
+    if (defined $bug_fields{'actual_time'}) {
+      eval { Bugzilla::Object::_validate_time($bug_fields{'actual_time'}, "a"); };
+      if ($@) {
+        $bug_fields{'actual_time'} = 0.0;
+        $err .= "Invalid Actual Time. Setting to 0.0\n";
+      }
     }
     else {
-        push( @values, $params->{'defaultseverity'} );
-        $err .= "Unknown severity ";
-        $err .= ( defined $bug_fields{'bug_severity'} )
-          ? $bug_fields{'bug_severity'}
-          : "unknown";
-        $err .= ". Setting to default severity \"";
-        $err .= $params->{'defaultseverity'} . "\".\n";
+      $bug_fields{'actual_time'} = 0.0;
+      $err .= "Actual time not defined. Setting to 0.0\n";
     }
-    push( @query, "bug_severity" );
-
-    if (defined( $bug_fields{'priority'} )
-        && check_field('priority', scalar $bug_fields{'priority'},
-                       undef, ERR_LEVEL ) )
-    {
-        push( @values, $bug_fields{'priority'} );
+  }
+
+  # Reporter Assignee QA Contact
+  my $exporterid = $exporter->id;
+  my $reporterid = login_to_id($bug_fields{'reporter'})
+    if $bug_fields{'reporter'};
+  push(@query, "reporter");
+  if (($bug_fields{'reporter'}) && ($reporterid)) {
+    push(@values, $reporterid);
+  }
+  else {
+    push(@values, $exporterid);
+    $err .= "The original reporter of this bug does not have\n";
+    $err .= "   an account here. Reassigning to the person who moved\n";
+    $err .= "   it here: $exporter_login.\n";
+    if ($bug_fields{'reporter'}) {
+      $err .= "   Previous reporter was $bug_fields{'reporter'}.\n";
     }
     else {
-        push( @values, $params->{'defaultpriority'} );
-        $err .= "Unknown priority ";
-        $err .= ( defined $bug_fields{'priority'} )
-          ? $bug_fields{'priority'}
-          : "unknown";
-        $err .= ". Setting to default priority \"";
-        $err .= $params->{'defaultpriority'} . "\".\n";
+      $err .= "   Previous reporter is unknown.\n";
     }
-    push( @query, "priority" );
-
-    if (defined( $bug_fields{'rep_platform'} )
-        && check_field('rep_platform', scalar $bug_fields{'rep_platform'},
-                       undef, ERR_LEVEL ) )
-    {
-        push( @values, $bug_fields{'rep_platform'} );
+  }
+
+  my $changed_owner = 0;
+  my $owner;
+  push(@query, "assigned_to");
+  if ( ($bug_fields{'assigned_to'})
+    && ($owner = login_to_id($bug_fields{'assigned_to'})))
+  {
+    push(@values, $owner);
+  }
+  else {
+    push(@values, $component->default_assignee->id);
+    $changed_owner = 1;
+    $err .= "The original assignee of this bug does not have\n";
+    $err .= "   an account here. Reassigning to the default assignee\n";
+    $err .= "   for the component, " . $component->default_assignee->login . ".\n";
+    if ($bug_fields{'assigned_to'}) {
+      $err .= "   Previous assignee was $bug_fields{'assigned_to'}.\n";
     }
     else {
-        push( @values, $params->{'defaultplatform'} );
-        $err .= "Unknown platform ";
-        $err .= ( defined $bug_fields{'rep_platform'} )
-          ? $bug_fields{'rep_platform'}
-          : "unknown";
-        $err .=". Setting to default platform \"";
-        $err .= $params->{'defaultplatform'} . "\".\n";
+      $err .= "   Previous assignee is unknown.\n";
     }
-    push( @query, "rep_platform" );
+  }
 
-    if (defined( $bug_fields{'op_sys'} )
-        && check_field('op_sys',  scalar $bug_fields{'op_sys'},
-                       undef, ERR_LEVEL ) )
+  if ($params->{"useqacontact"}) {
+    my $qa_contact;
+    push(@query, "qa_contact");
+    if ( (defined $bug_fields{'qa_contact'})
+      && ($qa_contact = login_to_id($bug_fields{'qa_contact'})))
     {
-        push( @values, $bug_fields{'op_sys'} );
+      push(@values, $qa_contact);
     }
     else {
-        push( @values, $params->{'defaultopsys'} );
-        $err .= "Unknown operating system ";
-        $err .= ( defined $bug_fields{'op_sys'} )
-          ? $bug_fields{'op_sys'}
-          : "unknown";
-        $err .= ". Setting to default OS \"" . $params->{'defaultopsys'} . "\".\n";
-    }
-    push( @query, "op_sys" );
-
-    # Process time fields
-    if ( $params->{"timetrackinggroup"} ) {
-        my $date = validate_date( $bug_fields{'deadline'} ) ? $bug_fields{'deadline'} : undef;
-        push( @values, $date );
-        push( @query,  "deadline" );
-        if ( defined $bug_fields{'estimated_time'} ) {
-            eval {
-                Bugzilla::Object::_validate_time($bug_fields{'estimated_time'}, "e");
-            };
-            if (!$@){
-                push( @values, $bug_fields{'estimated_time'} );
-                push( @query,  "estimated_time" );
-            }
-        }
-        if ( defined $bug_fields{'remaining_time'} ) {
-            eval {
-                Bugzilla::Object::_validate_time($bug_fields{'remaining_time'}, "r");
-            };
-            if (!$@){
-                push( @values, $bug_fields{'remaining_time'} );
-                push( @query,  "remaining_time" );
-            }
-        }
-        if ( defined $bug_fields{'actual_time'} ) {
-            eval {
-                Bugzilla::Object::_validate_time($bug_fields{'actual_time'}, "a");
-            };
-            if ($@){
-                $bug_fields{'actual_time'} = 0.0;
-                $err .= "Invalid Actual Time. Setting to 0.0\n";
-            }
-        }
-        else {
-            $bug_fields{'actual_time'} = 0.0;
-            $err .= "Actual time not defined. Setting to 0.0\n";
-        }
-    }
+      push(@values,
+        $component->default_qa_contact ? $component->default_qa_contact->id : undef);
 
-    # Reporter Assignee QA Contact
-    my $exporterid = $exporter->id;
-    my $reporterid = login_to_id( $bug_fields{'reporter'} )
-      if $bug_fields{'reporter'};
-    push( @query, "reporter" );
-    if ( ( $bug_fields{'reporter'} ) && ($reporterid) ) {
-        push( @values, $reporterid );
+      if ($component->default_qa_contact) {
+        $err .= "Setting qa contact to the default for this product.\n";
+        $err .= "   This bug either had no qa contact or an invalid one.\n";
+      }
+    }
+  }
+
+  # Status & Resolution
+  my $valid_res
+    = check_field('resolution', scalar $bug_fields{'resolution'}, undef,
+    ERR_LEVEL);
+  my $valid_status
+    = check_field('bug_status', scalar $bug_fields{'bug_status'}, undef,
+    ERR_LEVEL);
+  my $status     = $bug_fields{'bug_status'} || undef;
+  my $resolution = $bug_fields{'resolution'} || undef;
+
+  # Check everconfirmed
+  my $everconfirmed;
+  if ($product->allows_unconfirmed) {
+    $everconfirmed = $bug_fields{'everconfirmed'} || 0;
+  }
+  else {
+    $everconfirmed = 1;
+  }
+  push(@query,  "everconfirmed");
+  push(@values, $everconfirmed);
+
+  # Sanity check will complain about having bugs marked duplicate but no
+  # entry in the dup table. Since we can't tell the bug ID of bugs
+  # that might not yet be in the database we have no way of populating
+  # this table. Change the resolution instead.
+  if ($valid_res && ($bug_fields{'resolution'} eq "DUPLICATE")) {
+    $resolution = "INVALID";
+    $err .= "This bug was marked DUPLICATE in the database ";
+    $err .= "it was moved from.\n    Changing resolution to \"INVALID\"\n";
+  }
+
+  # If there is at least 1 initial bug status different from UNCO, use it,
+  # else use the open bug status with the lowest sortkey (different from UNCO).
+  my @bug_statuses = @{Bugzilla::Status->can_change_to()};
+  @bug_statuses = grep { $_->name ne 'UNCONFIRMED' } @bug_statuses;
+
+  my $initial_status;
+  if (scalar(@bug_statuses)) {
+    $initial_status = $bug_statuses[0]->name;
+  }
+  else {
+    @bug_statuses = Bugzilla::Status->get_all();
+
+    # Exclude UNCO and inactive bug statuses.
+    @bug_statuses
+      = grep { $_->is_active && $_->name ne 'UNCONFIRMED' } @bug_statuses;
+    my @open_statuses = grep { $_->is_open } @bug_statuses;
+    if (scalar(@open_statuses)) {
+      $initial_status = $open_statuses[0]->name;
     }
     else {
-        push( @values, $exporterid );
-        $err .= "The original reporter of this bug does not have\n";
-        $err .= "   an account here. Reassigning to the person who moved\n";
-        $err .= "   it here: $exporter_login.\n";
-        if ( $bug_fields{'reporter'} ) {
-            $err .= "   Previous reporter was $bug_fields{'reporter'}.\n";
+      # There is NO other open bug statuses outside UNCO???
+      Error("no open bug statuses available.");
+    }
+  }
+
+  if ($status) {
+    if ($valid_status) {
+      if (is_open_state($status)) {
+        if ($resolution) {
+          $err .= "Resolution set on an open status.\n";
+          $err .= "   Dropping resolution $resolution\n";
+          $resolution = undef;
         }
-        else {
-            $err .= "   Previous reporter is unknown.\n";
+        if ($changed_owner) {
+          if ($everconfirmed) {
+            $status = $initial_status;
+          }
+          else {
+            $status = "UNCONFIRMED";
+          }
+          if ($status ne $bug_fields{'bug_status'}) {
+            $err .= "Bug reassigned, setting status to \"$status\".\n";
+            $err .= "   Previous status was \"";
+            $err .= $bug_fields{'bug_status'} . "\".\n";
+          }
         }
-    }
-
-    my $changed_owner = 0;
-    my $owner;
-    push( @query, "assigned_to" );
-    if ( ( $bug_fields{'assigned_to'} )
-        && ( $owner = login_to_id( $bug_fields{'assigned_to'} )) ) {
-        push( @values, $owner );
-    }
-    else {
-        push( @values, $component->default_assignee->id );
-        $changed_owner = 1;
-        $err .= "The original assignee of this bug does not have\n";
-        $err .= "   an account here. Reassigning to the default assignee\n";
-        $err .= "   for the component, ". $component->default_assignee->login .".\n";
-        if ( $bug_fields{'assigned_to'} ) {
-            $err .= "   Previous assignee was $bug_fields{'assigned_to'}.\n";
+        if ($everconfirmed) {
+          if ($status eq "UNCONFIRMED") {
+            $err .= "Bug Status was UNCONFIRMED but everconfirmed was true\n";
+            $err .= "   Setting status to $initial_status\n";
+            $status = $initial_status;
+          }
         }
-        else {
-            $err .= "   Previous assignee is unknown.\n";
+        else {    # $everconfirmed is false
+          if ($status ne "UNCONFIRMED") {
+            $err .= "Bug Status was $status but everconfirmed was false\n";
+            $err .= "   Setting status to UNCONFIRMED\n";
+            $status = "UNCONFIRMED";
+          }
         }
-    }
-
-    if ( $params->{"useqacontact"} ) {
-        my $qa_contact;
-        push( @query, "qa_contact" );
-        if ( ( defined $bug_fields{'qa_contact'})
-            && ( $qa_contact = login_to_id( $bug_fields{'qa_contact'} ) ) ) {
-            push( @values, $qa_contact );
+      }
+      else {
+        if (!$resolution) {
+          $err .= "Missing Resolution. Setting status to ";
+          if ($everconfirmed) {
+            $status = $initial_status;
+            $err .= "$initial_status\n";
+          }
+          else {
+            $status = "UNCONFIRMED";
+            $err .= "UNCONFIRMED\n";
+          }
         }
-        else {
-            push(@values, $component->default_qa_contact ?
-                          $component->default_qa_contact->id : undef);
-
-            if ($component->default_qa_contact) {
-                $err .= "Setting qa contact to the default for this product.\n";
-                $err .= "   This bug either had no qa contact or an invalid one.\n";
-            }
+        elsif (!$valid_res) {
+          $err .= "Unknown resolution \"$resolution\".\n";
+          $err .= "   Setting resolution to INVALID\n";
+          $resolution = "INVALID";
         }
+      }
     }
-
-    # Status & Resolution
-    my $valid_res = check_field('resolution',  
-                                  scalar $bug_fields{'resolution'}, 
-                                  undef, ERR_LEVEL );
-    my $valid_status = check_field('bug_status',  
-                                  scalar $bug_fields{'bug_status'}, 
-                                  undef, ERR_LEVEL );
-    my $status = $bug_fields{'bug_status'} || undef;
-    my $resolution = $bug_fields{'resolution'} || undef;
-    
-    # Check everconfirmed 
-    my $everconfirmed;
-    if ($product->allows_unconfirmed) {
-        $everconfirmed = $bug_fields{'everconfirmed'} || 0;
+    else {    # $valid_status is false
+      if ($everconfirmed) {
+        $status = $initial_status;
+      }
+      else {
+        $status = "UNCONFIRMED";
+      }
+      $err .= "Bug has invalid status, setting status to \"$status\".\n";
+      $err .= "   Previous status was \"";
+      $err .= $bug_fields{'bug_status'} . "\".\n";
+      $resolution = undef;
+    }
+  }
+  else {
+    if ($everconfirmed) {
+      $status = $initial_status;
     }
     else {
-        $everconfirmed = 1;
+      $status = "UNCONFIRMED";
     }
-    push (@query,  "everconfirmed");
-    push (@values, $everconfirmed);
-
-    # Sanity check will complain about having bugs marked duplicate but no
-    # entry in the dup table. Since we can't tell the bug ID of bugs
-    # that might not yet be in the database we have no way of populating
-    # this table. Change the resolution instead.
-    if ( $valid_res  && ( $bug_fields{'resolution'} eq "DUPLICATE" ) ) {
-        $resolution = "INVALID";
-        $err .= "This bug was marked DUPLICATE in the database ";
-        $err .= "it was moved from.\n    Changing resolution to \"INVALID\"\n";
-    } 
-
-    # If there is at least 1 initial bug status different from UNCO, use it,
-    # else use the open bug status with the lowest sortkey (different from UNCO).
-    my @bug_statuses = @{Bugzilla::Status->can_change_to()};
-    @bug_statuses = grep { $_->name ne 'UNCONFIRMED' } @bug_statuses;
-
-    my $initial_status;
-    if (scalar(@bug_statuses)) {
-        $initial_status = $bug_statuses[0]->name;
+    $err .= "Bug has no status, setting status to \"$status\".\n";
+    $err .= "   Previous status was unknown\n";
+    $resolution = undef;
+  }
+
+  if ($resolution) {
+    push(@query,  "resolution");
+    push(@values, $resolution);
+  }
+
+  # Bug status
+  push(@query,  "bug_status");
+  push(@values, $status);
+
+  # Custom fields - Multi-select fields have their own table.
+  my %multi_select_fields;
+  foreach my $field (Bugzilla->active_custom_fields) {
+    my $custom_field = $field->name;
+    my $value        = $bug_fields{$custom_field};
+    next unless defined $value;
+    if ($field->type == FIELD_TYPE_FREETEXT) {
+      push(@query,  $custom_field);
+      push(@values, clean_text($value));
     }
-    else {
-        @bug_statuses = Bugzilla::Status->get_all();
-        # Exclude UNCO and inactive bug statuses.
-        @bug_statuses = grep { $_->is_active && $_->name ne 'UNCONFIRMED'} @bug_statuses;
-        my @open_statuses = grep { $_->is_open } @bug_statuses;
-        if (scalar(@open_statuses)) {
-            $initial_status = $open_statuses[0]->name;
+    elsif ($field->type == FIELD_TYPE_TEXTAREA) {
+      push(@query,  $custom_field);
+      push(@values, $value);
+    }
+    elsif ($field->type == FIELD_TYPE_SINGLE_SELECT) {
+      my $is_well_formed = check_field($custom_field, $value, undef, ERR_LEVEL);
+      if ($is_well_formed) {
+        push(@query,  $custom_field);
+        push(@values, $value);
+      }
+      else {
+        $err .= "Skipping illegal value \"$value\" in $custom_field.\n";
+      }
+    }
+    elsif ($field->type == FIELD_TYPE_MULTI_SELECT) {
+      my @legal_values;
+      foreach my $item (_to_array($value)) {
+        my $is_well_formed = check_field($custom_field, $item, undef, ERR_LEVEL);
+        if ($is_well_formed) {
+          push(@legal_values, $item);
         }
         else {
-            # There is NO other open bug statuses outside UNCO???
-            Error("no open bug statuses available.");
+          $err .= "Skipping illegal value \"$item\" in $custom_field.\n";
         }
+      }
+      if (scalar @legal_values) {
+        $multi_select_fields{$custom_field} = \@legal_values;
+      }
     }
-
-    if ($status) {
-        if($valid_status){
-            if (is_open_state($status)) {
-                if ($resolution) {
-                    $err .= "Resolution set on an open status.\n";
-                    $err .= "   Dropping resolution $resolution\n";
-                    $resolution = undef;
-                }
-                if($changed_owner){
-                    if($everconfirmed){  
-                        $status = $initial_status;
-                    }
-                    else{
-                        $status = "UNCONFIRMED";
-                    }
-                    if ($status ne $bug_fields{'bug_status'}){
-                        $err .= "Bug reassigned, setting status to \"$status\".\n";
-                        $err .= "   Previous status was \"";
-                        $err .=  $bug_fields{'bug_status'} . "\".\n";
-                    }
-                }
-                if($everconfirmed){
-                    if($status eq "UNCONFIRMED"){
-                        $err .= "Bug Status was UNCONFIRMED but everconfirmed was true\n";
-                        $err .= "   Setting status to $initial_status\n";
-                        $status = $initial_status;
-                    }
-                }
-                else{ # $everconfirmed is false
-                    if($status ne "UNCONFIRMED"){
-                        $err .= "Bug Status was $status but everconfirmed was false\n";
-                        $err .= "   Setting status to UNCONFIRMED\n";
-                        $status = "UNCONFIRMED";
-                    }
-                }
-            }
-            else {
-               if (!$resolution) {
-                   $err .= "Missing Resolution. Setting status to ";
-                   if($everconfirmed){
-                       $status = $initial_status;
-                       $err .= "$initial_status\n";
-                   }
-                   else{
-                       $status = "UNCONFIRMED";
-                       $err .= "UNCONFIRMED\n";
-                   }
-               }
-               elsif (!$valid_res) {
-                   $err .= "Unknown resolution \"$resolution\".\n";
-                   $err .= "   Setting resolution to INVALID\n";
-                   $resolution = "INVALID";
-               }
-            }   
-        }
-        else{ # $valid_status is false
-            if($everconfirmed){  
-                $status = $initial_status;
-            }
-            else{
-                $status = "UNCONFIRMED";
-            }        
-            $err .= "Bug has invalid status, setting status to \"$status\".\n";
-            $err .= "   Previous status was \"";
-            $err .=  $bug_fields{'bug_status'} . "\".\n";
-            $resolution = undef;
-        }
+    elsif ($field->type == FIELD_TYPE_DATETIME) {
+      eval { $value = Bugzilla::Bug->_check_datetime_field($value); };
+      if ($@) {
+        $err .= "Skipping illegal value \"$value\" in $custom_field.\n";
+      }
+      else {
+        push(@query,  $custom_field);
+        push(@values, $value);
+      }
     }
-    else {
-        if($everconfirmed){  
-            $status = $initial_status;
-        }
-        else{
-            $status = "UNCONFIRMED";
-        }        
-        $err .= "Bug has no status, setting status to \"$status\".\n";
-        $err .= "   Previous status was unknown\n";
-        $resolution = undef;
+    elsif ($field->type == FIELD_TYPE_DATE) {
+      eval { $value = Bugzilla::Bug->_check_date_field($value); };
+      if ($@) {
+        $err .= "Skipping illegal value \"$value\" in $custom_field.\n";
+      }
+      else {
+        push(@query,  $custom_field);
+        push(@values, $value);
+      }
     }
-
-    if ($resolution) {
-        push( @query,  "resolution" );
-        push( @values, $resolution );
+    else {
+      $err .= "Type of custom field $custom_field is an unhandled FIELD_TYPE: "
+        . $field->type . "\n";
     }
-    
-    # Bug status
-    push( @query,  "bug_status" );
-    push( @values, $status );
-
-    # Custom fields - Multi-select fields have their own table.
-    my %multi_select_fields;
-    foreach my $field (Bugzilla->active_custom_fields) {
-        my $custom_field = $field->name;
-        my $value = $bug_fields{$custom_field};
-        next unless defined $value;
-        if ($field->type == FIELD_TYPE_FREETEXT) {
-            push(@query, $custom_field);
-            push(@values, clean_text($value));
-        } elsif ($field->type == FIELD_TYPE_TEXTAREA) {
-            push(@query, $custom_field);
-            push(@values, $value);
-        } elsif ($field->type == FIELD_TYPE_SINGLE_SELECT) {
-            my $is_well_formed = check_field($custom_field, $value, undef, ERR_LEVEL);
-            if ($is_well_formed) {
-                push(@query, $custom_field);
-                push(@values, $value);
-            } else {
-                $err .= "Skipping illegal value \"$value\" in $custom_field.\n" ;
-            }
-        } elsif ($field->type == FIELD_TYPE_MULTI_SELECT) {
-            my @legal_values;
-            foreach my $item (_to_array($value)) {
-                my $is_well_formed = check_field($custom_field, $item, undef, ERR_LEVEL);
-                if ($is_well_formed) {
-                    push(@legal_values, $item);
-                } else {
-                    $err .= "Skipping illegal value \"$item\" in $custom_field.\n" ;
-                }
-            }
-            if (scalar @legal_values) {
-                $multi_select_fields{$custom_field} = \@legal_values;
-            }
-        } elsif ($field->type == FIELD_TYPE_DATETIME) {
-            eval { $value = Bugzilla::Bug->_check_datetime_field($value); };
-            if ($@) {
-                $err .= "Skipping illegal value \"$value\" in $custom_field.\n" ;
-            }
-            else {
-                push(@query, $custom_field);
-                push(@values, $value);
-            }
-        } elsif ($field->type == FIELD_TYPE_DATE) {
-            eval { $value = Bugzilla::Bug->_check_date_field($value); };
-            if ($@) {
-                $err .= "Skipping illegal value \"$value\" in $custom_field.\n" ;
-            }
-            else {
-                push(@query, $custom_field);
-                push(@values, $value);
-            }
-        } else {
-            $err .= "Type of custom field $custom_field is an unhandled FIELD_TYPE: " .
-                    $field->type . "\n";
+  }
+
+  # For the sake of sanitycheck.cgi we do this.
+  # Update lastdiffed if you do not want to have mail sent
+  unless ($mail) {
+    push @query,  "lastdiffed";
+    push @values, $timestamp;
+  }
+
+  # INSERT the bug
+  my $query = "INSERT INTO bugs (" . join(", ", @query) . ") VALUES (";
+  $query .= '?,' foreach (@values);
+  chop($query);    # Remove the last comma.
+  $query .= ")";
+
+  $dbh->do($query, undef, @values);
+  my $id = $dbh->bz_last_key('bugs', 'bug_id');
+  my $bug_obj = Bugzilla::Bug->new($id);
+
+  # We are almost certain to get some uninitialized warnings
+  # Since this is just for debugging the query, let's shut them up
+  eval {
+    no warnings 'uninitialized';
+    Debug(
+      "Bug Query: INSERT INTO bugs (\n"
+        . join(",\n", @query)
+        . "\n) VALUES (\n"
+        . join(",\n", @values),
+      DEBUG_LEVEL
+    );
+  };
+
+  # Handle CC's
+  if (defined $bug_fields{'cc'}) {
+    my %ccseen;
+    my $sth_cc = $dbh->prepare("INSERT INTO cc (bug_id, who) VALUES (?,?)");
+    foreach my $person (_to_array($bug_fields{'cc'})) {
+      next unless $person;
+      my $uid;
+      if ($uid = login_to_id($person)) {
+        if (!$ccseen{$uid}) {
+          $sth_cc->execute($id, $uid);
+          $ccseen{$uid} = 1;
         }
+      }
+      else {
+        $err .= "CC member $person does not have an account here\n";
+      }
     }
+  }
 
-    # For the sake of sanitycheck.cgi we do this.
-    # Update lastdiffed if you do not want to have mail sent
-    unless ($mail) {
-        push @query,  "lastdiffed";
-        push @values, $timestamp;
+  # Handle keywords
+  if (defined($bug_fields{'keywords'})) {
+    my %keywordseen;
+    my $key_sth = $dbh->prepare(
+      "INSERT INTO keywords 
+                      (bug_id, keywordid) VALUES (?,?)"
+    );
+    foreach my $keyword (split(/[\s,]+/, $bug_fields{'keywords'})) {
+      next unless $keyword;
+      my $keyword_obj = new Bugzilla::Keyword({name => $keyword});
+      if (!$keyword_obj) {
+        $err .= "Skipping unknown keyword: $keyword.\n";
+        next;
+      }
+      if (!$keywordseen{$keyword_obj->id}) {
+        $key_sth->execute($id, $keyword_obj->id);
+        $keywordseen{$keyword_obj->id} = 1;
+      }
     }
-
-    # INSERT the bug
-    my $query = "INSERT INTO bugs (" . join( ", ", @query ) . ") VALUES (";
-       $query .= '?,' foreach (@values);
-    chop($query);    # Remove the last comma.
-       $query .= ")";
-
-    $dbh->do( $query, undef, @values );
-    my $id = $dbh->bz_last_key( 'bugs', 'bug_id' );
-    my $bug_obj = Bugzilla::Bug->new($id);
-
-    # We are almost certain to get some uninitialized warnings
-    # Since this is just for debugging the query, let's shut them up
-    eval {
-        no warnings 'uninitialized';
-        Debug(
-            "Bug Query: INSERT INTO bugs (\n"
-              . join( ",\n", @query )
-              . "\n) VALUES (\n"
-              . join( ",\n", @values ),
-            DEBUG_LEVEL
-        );
-    };
-
-    # Handle CC's
-    if ( defined $bug_fields{'cc'} ) {
-        my %ccseen;
-        my $sth_cc = $dbh->prepare("INSERT INTO cc (bug_id, who) VALUES (?,?)");
-        foreach my $person (_to_array($bug_fields{'cc'})) {
-            next unless $person;
-            my $uid;
-            if ($uid = login_to_id($person)) {
-                if ( !$ccseen{$uid} ) {
-                    $sth_cc->execute( $id, $uid );
-                    $ccseen{$uid} = 1;
-                }
-            }
-            else {
-                $err .= "CC member $person does not have an account here\n";
-            }
-        }
+  }
+
+  # Insert values of custom multi-select fields. They have already
+  # been validated.
+  foreach my $custom_field (keys %multi_select_fields) {
+    my $sth = $dbh->prepare(
+      "INSERT INTO bug_$custom_field
+                                 (bug_id, value) VALUES (?, ?)"
+    );
+    foreach my $value (@{$multi_select_fields{$custom_field}}) {
+      $sth->execute($id, $value);
     }
-
-    # Handle keywords
-    if ( defined( $bug_fields{'keywords'} ) ) {
-        my %keywordseen;
-        my $key_sth = $dbh->prepare(
-            "INSERT INTO keywords 
-                      (bug_id, keywordid) VALUES (?,?)"
-        );
-        foreach my $keyword ( split( /[\s,]+/, $bug_fields{'keywords'} )) {
-            next unless $keyword;
-            my $keyword_obj = new Bugzilla::Keyword({name => $keyword});
-            if (!$keyword_obj) {
-                $err .= "Skipping unknown keyword: $keyword.\n";
-                next;
-            }
-            if (!$keywordseen{$keyword_obj->id}) {
-                $key_sth->execute($id, $keyword_obj->id);
-                $keywordseen{$keyword_obj->id} = 1;
-            }
-        }
+  }
+
+  # Parse bug flags
+  foreach my $bflag ($bug->children('flag')) {
+    next unless (defined($bflag));
+    $err .= flag_handler(
+      $bflag->{'att'}->{'name'},   $bflag->{'att'}->{'status'},
+      $bflag->{'att'}->{'setter'}, $bflag->{'att'}->{'requestee'},
+      $exporterid,                 $id,
+      $comp_id,                    $prod_id,
+      undef
+    );
+  }
+
+  # Insert Attachments for the bug
+  foreach my $att (@attachments) {
+    if ($att eq "err") {
+      $err .= "No attachment ID specified, dropping attachment\n";
+      next;
     }
 
-    # Insert values of custom multi-select fields. They have already
-    # been validated.
-    foreach my $custom_field (keys %multi_select_fields) {
-        my $sth = $dbh->prepare("INSERT INTO bug_$custom_field
-                                 (bug_id, value) VALUES (?, ?)");
-        foreach my $value (@{$multi_select_fields{$custom_field}}) {
-            $sth->execute($id, $value);
-        }
+    my $attacher;
+    if ($att->{'attacher'}) {
+      $attacher = Bugzilla::User->new({name => $att->{'attacher'}, cache => 1});
     }
+    my $new_attacher = $attacher || $exporter;
 
-    # Parse bug flags
-    foreach my $bflag ( $bug->children('flag')) {
-        next unless ( defined($bflag) );
-        $err .= flag_handler(
-            $bflag->{'att'}->{'name'},   $bflag->{'att'}->{'status'},
-            $bflag->{'att'}->{'setter'}, $bflag->{'att'}->{'requestee'},
-            $exporterid,                 $id,
-            $comp_id,                    $prod_id,
-            undef
-        );
+    if ($att->{'isprivate'} && !$new_attacher->is_insider) {
+      my $who = $new_attacher->login;
+      $err .= "$who not in insidergroup and attachment marked private.\n";
+      $err .= "   Marking attachment public\n";
+      $att->{'isprivate'} = 0;
     }
 
-    # Insert Attachments for the bug
-    foreach my $att (@attachments) {
-        if ($att eq "err"){
-            $err .= "No attachment ID specified, dropping attachment\n";
-            next;
-        }
-
-        my $attacher;
-        if ($att->{'attacher'}) {
-            $attacher = Bugzilla::User->new({name => $att->{'attacher'}, cache => 1});
-        }
-        my $new_attacher = $attacher || $exporter;
-
-        if ($att->{'isprivate'} && !$new_attacher->is_insider) {
-            my $who = $new_attacher->login;
-            $err .= "$who not in insidergroup and attachment marked private.\n";
-            $err .= "   Marking attachment public\n";
-            $att->{'isprivate'} = 0;
-        }
-
-        # We log in the user so that the attachment creator is set correctly.
-        Bugzilla->set_user($new_attacher);
-
-        my $attachment = Bugzilla::Attachment->create(
-            { bug           => $bug_obj,
-              creation_ts   => $att->{date},
-              data          => $att->{data},
-              description   => $att->{desc},
-              filename      => $att->{filename},
-              ispatch       => $att->{ispatch},
-              isprivate     => $att->{isprivate},
-              isobsolete    => $att->{isobsolete},
-              mimetype      => $att->{ctype},
-            });
-        my $att_id = $attachment->id;
-
-        # We log out the attacher as the remaining steps are not on his behalf.
-        Bugzilla->logout_request;
-
-        $comments .= "Imported an attachment (id=$att_id)\n";
-        if (!$attacher) {
-            if ($att->{'attacher'}) {
-                $err .= "The original submitter of attachment $att_id was\n   ";
-                $err .= $att->{'attacher'} . ", but they don't have an account here.\n";
-            }
-            else {
-                $err .= "The original submitter of attachment $att_id is unknown.\n";
-            }
-            $err .= "   Reassigning to the person who moved it here: $exporter_login.\n";
-        }
+    # We log in the user so that the attachment creator is set correctly.
+    Bugzilla->set_user($new_attacher);
+
+    my $attachment = Bugzilla::Attachment->create({
+      bug         => $bug_obj,
+      creation_ts => $att->{date},
+      data        => $att->{data},
+      description => $att->{desc},
+      filename    => $att->{filename},
+      ispatch     => $att->{ispatch},
+      isprivate   => $att->{isprivate},
+      isobsolete  => $att->{isobsolete},
+      mimetype    => $att->{ctype},
+    });
+    my $att_id = $attachment->id;
+
+    # We log out the attacher as the remaining steps are not on his behalf.
+    Bugzilla->logout_request;
+
+    $comments .= "Imported an attachment (id=$att_id)\n";
+    if (!$attacher) {
+      if ($att->{'attacher'}) {
+        $err .= "The original submitter of attachment $att_id was\n   ";
+        $err .= $att->{'attacher'} . ", but they don't have an account here.\n";
+      }
+      else {
+        $err .= "The original submitter of attachment $att_id is unknown.\n";
+      }
+      $err .= "   Reassigning to the person who moved it here: $exporter_login.\n";
+    }
 
-        # Process attachment flags
-        foreach my $aflag (@{ $att->{'flags'} }) {
-            next unless defined($aflag) ;
-            $err .= flag_handler(
-                $aflag->{'name'},   $aflag->{'status'},
-                $aflag->{'setter'}, $aflag->{'requestee'},
-                $exporterid,        $id,
-                $comp_id,           $prod_id,
-                $att_id
-            );
-        }
+    # Process attachment flags
+    foreach my $aflag (@{$att->{'flags'}}) {
+      next unless defined($aflag);
+      $err .= flag_handler(
+        $aflag->{'name'},      $aflag->{'status'}, $aflag->{'setter'},
+        $aflag->{'requestee'}, $exporterid,        $id,
+        $comp_id,              $prod_id,           $att_id
+      );
     }
+  }
 
-    # Clear the attachments array for the next bug
-    @attachments = ();
+  # Clear the attachments array for the next bug
+  @attachments = ();
 
-    # Insert comments and append any errors
-    my $worktime = $bug_fields{'actual_time'} || 0.0;
-    $worktime = 0.0 if (!$exporter->is_timetracker);
-    $comments .= "\n$err\n" if $err;
+  # Insert comments and append any errors
+  my $worktime = $bug_fields{'actual_time'} || 0.0;
+  $worktime = 0.0 if (!$exporter->is_timetracker);
+  $comments .= "\n$err\n" if $err;
 
-    my $sth_comment =
-      $dbh->prepare('INSERT INTO longdescs (bug_id, who, bug_when, isprivate,
+  my $sth_comment = $dbh->prepare(
+    'INSERT INTO longdescs (bug_id, who, bug_when, isprivate,
                                             thetext, work_time)
-                     VALUES (?, ?, ?, ?, ?, ?)');
-
-    foreach my $c (@sorted_descs) {
-        $sth_comment->execute($id, $c->{whoid} || $exporterid, $c->{bug_when},
-                              $c->{isprivate}, $c->{thetext}, 0);
-    }
-    $sth_comment->execute($id, $exporterid, $timestamp, 0, $comments, $worktime);
-    Bugzilla::Bug->new($id)->_sync_fulltext( new_bug => 1);
-
-    # Add this bug to each group of which its product is a member.
-    my $sth_group = $dbh->prepare("INSERT INTO bug_group_map (bug_id, group_id) 
-                         VALUES (?, ?)");
-    foreach my $group_id ( keys %{ $product->group_controls } ) {
-        if ($product->group_controls->{$group_id}->{'membercontrol'} != CONTROLMAPNA
-            && $product->group_controls->{$group_id}->{'othercontrol'} != CONTROLMAPNA){
-            $sth_group->execute( $id, $group_id );
-        }
-    }
-
-    $log .= "Bug ${url}$bug_fields{'bug_id'} ";
-    $log .= "imported as bug $id.\n";
-    $log .= $params->{"urlbase"} . "show_bug.cgi?id=$id\n\n";
-    if ($err) {
-        $log .= "The following problems were encountered while creating bug $id.\n";
-        $log .= $err;
-        $log .= "You may have to set certain fields in the new bug by hand.\n\n";
+                     VALUES (?, ?, ?, ?, ?, ?)'
+  );
+
+  foreach my $c (@sorted_descs) {
+    $sth_comment->execute($id, $c->{whoid} || $exporterid,
+      $c->{bug_when}, $c->{isprivate}, $c->{thetext}, 0);
+  }
+  $sth_comment->execute($id, $exporterid, $timestamp, 0, $comments, $worktime);
+  Bugzilla::Bug->new($id)->_sync_fulltext(new_bug => 1);
+
+  # Add this bug to each group of which its product is a member.
+  my $sth_group = $dbh->prepare(
+    "INSERT INTO bug_group_map (bug_id, group_id) 
+                         VALUES (?, ?)"
+  );
+  foreach my $group_id (keys %{$product->group_controls}) {
+    if ( $product->group_controls->{$group_id}->{'membercontrol'} != CONTROLMAPNA
+      && $product->group_controls->{$group_id}->{'othercontrol'} != CONTROLMAPNA)
+    {
+      $sth_group->execute($id, $group_id);
     }
-    Debug( $log, OK_LEVEL );
-    push(@logs, $log);
-    Bugzilla::BugMail::Send( $id, { 'changer' => $exporter } ) if ($mail);
-
-    # done with the xml data. Lets clear it from memory
-    $twig->purge;
+  }
+
+  $log .= "Bug ${url}$bug_fields{'bug_id'} ";
+  $log .= "imported as bug $id.\n";
+  $log .= $params->{"urlbase"} . "show_bug.cgi?id=$id\n\n";
+  if ($err) {
+    $log .= "The following problems were encountered while creating bug $id.\n";
+    $log .= $err;
+    $log .= "You may have to set certain fields in the new bug by hand.\n\n";
+  }
+  Debug($log, OK_LEVEL);
+  push(@logs, $log);
+  Bugzilla::BugMail::Send($id, {'changer' => $exporter}) if ($mail);
+
+  # done with the xml data. Lets clear it from memory
+  $twig->purge;
 
 }
 
-Debug( "Reading xml", DEBUG_LEVEL );
+Debug("Reading xml", DEBUG_LEVEL);
 
 # Read STDIN in slurp mode. VERY dangerous, but we live on the wild side ;-)
 local ($/);
@@ -1251,30 +1285,28 @@ $xml = <>;
 
 # If there's anything except whitespace before <?xml then we guess it's a mail
 # and MIME::Parser should parse it. Else don't.
-if ($xml =~ m/\S.*<\?xml/s ) {
+if ($xml =~ m/\S.*<\?xml/s) {
 
-    # If the email was encoded (Mailer::MessageToMTA() does it when using UTF-8),
-    # we have to decode it first, else the XML parsing will fail.
-    my $parser = MIME::Parser->new;
-    $parser->output_to_core(1);
-    $parser->tmp_to_core(1);
-    my $entity = $parser->parse_data($xml);
-    my $bodyhandle = $entity->bodyhandle;
-    $xml = $bodyhandle->as_string;
+  # If the email was encoded (Mailer::MessageToMTA() does it when using UTF-8),
+  # we have to decode it first, else the XML parsing will fail.
+  my $parser = MIME::Parser->new;
+  $parser->output_to_core(1);
+  $parser->tmp_to_core(1);
+  my $entity     = $parser->parse_data($xml);
+  my $bodyhandle = $entity->bodyhandle;
+  $xml = $bodyhandle->as_string;
 
 }
 
 # remove everything in file before xml header
 $xml =~ s/^.+(<\?xml version.+)$/$1/s;
 
-Debug( "Parsing tree", DEBUG_LEVEL );
+Debug("Parsing tree", DEBUG_LEVEL);
 my $twig = XML::Twig->new(
-    twig_handlers => {
-        bug        => \&process_bug,
-        attachment => \&process_attachment
-    },
-    start_tag_handlers => { bugzilla => \&init }
+  twig_handlers => {bug => \&process_bug, attachment => \&process_attachment},
+  start_tag_handlers => {bugzilla => \&init}
 );
+
 # Prevent DoS using the billion laughs attack.
 $twig->{NoExpand} = 1;
 
@@ -1286,11 +1318,11 @@ my $urlbase    = $root->{'att'}->{'urlbase'};
 
 # It is time to email the result of the import.
 my $log = join("\n\n", @logs);
-$log .=  "\n\nImported $bugtotal bug(s) from $urlbase,\n  sent by $exporter.\n";
-my $subject =  "$bugtotal Bug(s) successfully moved from $urlbase to " 
-   . $params->{"urlbase"};
+$log .= "\n\nImported $bugtotal bug(s) from $urlbase,\n  sent by $exporter.\n";
+my $subject = "$bugtotal Bug(s) successfully moved from $urlbase to "
+  . $params->{"urlbase"};
 my @to = ($exporter, $maintainer);
-MailMessage( $subject, $log, @to );
+MailMessage($subject, $log, @to);
 
 __END__
 
index dde4cc1fc38b4181fed2121ee1c693936dcdb1d2..26f6923ad932c1a23029c686d697745503a2bc7e 100755 (executable)
--- a/index.cgi
+++ b/index.cgi
@@ -18,46 +18,52 @@ use Bugzilla::Error;
 use Bugzilla::Update;
 
 # Check whether or not the user is logged in
-my $user = Bugzilla->login(LOGIN_OPTIONAL);
-my $cgi = Bugzilla->cgi;
+my $user     = Bugzilla->login(LOGIN_OPTIONAL);
+my $cgi      = Bugzilla->cgi;
 my $template = Bugzilla->template;
-my $vars = {};
+my $vars     = {};
 
 # And log out the user if requested. We do this first so that nothing
 # else accidentally relies on the current login.
 if ($cgi->param('logout')) {
-    Bugzilla->logout();
-    $user = Bugzilla->user;
-    $vars->{'message'} = "logged_out";
-    # Make sure that templates or other code doesn't get confused about this.
-    $cgi->delete('logout');
+  Bugzilla->logout();
+  $user = Bugzilla->user;
+  $vars->{'message'} = "logged_out";
+
+  # Make sure that templates or other code doesn't get confused about this.
+  $cgi->delete('logout');
 }
 
 # Return the appropriate HTTP response headers.
 print $cgi->header();
 
 if ($user->in_group('admin')) {
-    # If 'urlbase' is not set, display the Welcome page.
-    unless (Bugzilla->params->{'urlbase'}) {
-        $template->process('welcome-admin.html.tmpl')
-          || ThrowTemplateError($template->error());
-        exit;
-    }
-    # Inform the administrator about new releases, if any.
-    $vars->{'release'} = Bugzilla::Update::get_notifications();
+
+  # If 'urlbase' is not set, display the Welcome page.
+  unless (Bugzilla->params->{'urlbase'}) {
+    $template->process('welcome-admin.html.tmpl')
+      || ThrowTemplateError($template->error());
+    exit;
+  }
+
+  # Inform the administrator about new releases, if any.
+  $vars->{'release'} = Bugzilla::Update::get_notifications();
 }
 
 if ($user->id) {
-    my $dbh = Bugzilla->dbh;
-    $vars->{assignee_count} =
-      $dbh->selectrow_array("SELECT COUNT(*) FROM bugs WHERE assigned_to = ?
-                             AND resolution = ''", undef, $user->id);
-    $vars->{reporter_count} =
-      $dbh->selectrow_array("SELECT COUNT(*) FROM bugs WHERE reporter = ?
-                             AND resolution = ''", undef, $user->id);
-    $vars->{requestee_count} =
-      $dbh->selectrow_array('SELECT COUNT(DISTINCT bug_id) FROM flags
-                             WHERE requestee_id = ?', undef, $user->id);
+  my $dbh = Bugzilla->dbh;
+  $vars->{assignee_count} = $dbh->selectrow_array(
+    "SELECT COUNT(*) FROM bugs WHERE assigned_to = ?
+                             AND resolution = ''", undef, $user->id
+  );
+  $vars->{reporter_count} = $dbh->selectrow_array(
+    "SELECT COUNT(*) FROM bugs WHERE reporter = ?
+                             AND resolution = ''", undef, $user->id
+  );
+  $vars->{requestee_count} = $dbh->selectrow_array(
+    'SELECT COUNT(DISTINCT bug_id) FROM flags
+                             WHERE requestee_id = ?', undef, $user->id
+  );
 }
 
 # Generate and return the UI (HTML page) from the appropriate template.
index 25239772012775660eabcdd2b3ab7b53ed6cd15c..75fdf1f04307e0589ffa468d23ee2a1ecc9753fe 100755 (executable)
@@ -12,10 +12,11 @@ use warnings;
 
 use Cwd qw(abs_path);
 use File::Basename;
+
 BEGIN {
-    # Untaint the abs_path.
-    my ($a) = abs_path($0) =~ /^(.*)$/;
-    chdir dirname($a);
+  # Untaint the abs_path.
+  my ($a) = abs_path($0) =~ /^(.*)$/;
+  chdir dirname($a);
 }
 
 use lib qw(. lib local/lib/perl5);
index 7a91b4b107d27d4643561e91d653911e053ba318..3d5ac67a23afd68dc2c8ac1e96f0a48c15b95667 100755 (executable)
@@ -16,10 +16,11 @@ use Bugzilla;
 use Bugzilla::Constants;
 use Bugzilla::Error;
 use Bugzilla::WebService::Constants;
+
 BEGIN {
-    if (!Bugzilla->feature('jsonrpc')) {
-        ThrowUserError('feature_disabled', { feature => 'jsonrpc' });
-    }
+  if (!Bugzilla->feature('jsonrpc')) {
+    ThrowUserError('feature_disabled', {feature => 'jsonrpc'});
+  }
 }
 use Bugzilla::WebService::Server::JSONRPC;
 
index 0ce82f2a3fc4ed50ff9086a1cce45f02c22e3d41..a89409e157804f60e8b3d1155f4859cf6d149867 100755 (executable)
@@ -25,7 +25,7 @@ GetOptions(\%switch, 'help|h|?', 'from=s', 'verbose|v+', 'dry-run');
 # Print the help message if that switch was selected or if --from
 # wasn't specified.
 if (!$switch{'from'} or $switch{'help'}) {
-    pod2usage({-exitval => 1});
+  pod2usage({-exitval => 1});
 }
 
 my $migrator = Bugzilla::Migrate->load($switch{'from'});
@@ -37,13 +37,13 @@ $migrator->do_migration();
 # Even if there's an error, we want to be sure that the serial values
 # get reset properly.
 END {
-    if ($migrator and $migrator->dry_run) {
-        my $dbh = Bugzilla->dbh;
-        if ($dbh->bz_in_transaction) {
-            $dbh->bz_rollback_transaction();
-        }
-        $migrator->reset_serial_values();
+  if ($migrator and $migrator->dry_run) {
+    my $dbh = Bugzilla->dbh;
+    if ($dbh->bz_in_transaction) {
+      $dbh->bz_rollback_transaction();
     }
+    $migrator->reset_serial_values();
+  }
 }
 
 __END__
index 873cf26b6aff8a2715495e86e4ae888973f27fce..6f843028ea7d30634f5cb562cedad32c95611be3 100644 (file)
@@ -16,10 +16,15 @@ use warnings;
 # configuration.
 use File::Basename;
 use File::Spec;
+
 BEGIN {
-    require lib;
-    my $dir = dirname(__FILE__);
-    lib->import($dir, File::Spec->catdir($dir, "lib"), File::Spec->catdir($dir, qw(local lib perl5)));
+  require lib;
+  my $dir = dirname(__FILE__);
+  lib->import(
+    $dir,
+    File::Spec->catdir($dir, "lib"),
+    File::Spec->catdir($dir, qw(local lib perl5))
+  );
 }
 
 use Bugzilla::Constants ();
@@ -36,18 +41,19 @@ use Bugzilla::Constants ();
 use Apache2::Log ();
 use Apache2::ServerUtil;
 use ModPerl::RegistryLoader ();
-use File::Basename ();
-use DateTime ();
+use File::Basename          ();
+use DateTime                ();
 
 # This loads most of our modules.
 use Bugzilla ();
+
 # Loading Bugzilla.pm doesn't load this, though, and we want it preloaded.
-use Bugzilla::BugMail ();
-use Bugzilla::CGI ();
-use Bugzilla::Extension ();
+use Bugzilla::BugMail               ();
+use Bugzilla::CGI                   ();
+use Bugzilla::Extension             ();
 use Bugzilla::Install::Requirements ();
-use Bugzilla::Util ();
-use Bugzilla::RNG ();
+use Bugzilla::Util                  ();
+use Bugzilla::RNG                   ();
 
 # Make warnings go to the virtual host's log and not the main
 # server log.
@@ -57,16 +63,18 @@ BEGIN { *CORE::GLOBAL::warn = \&Apache2::ServerRec::warn; }
 Bugzilla::CGI->compile(qw(:cgi :push));
 
 use Apache2::SizeLimit;
+
 # This means that every httpd child will die after processing
 # a CGI if it is taking up more than 45MB of RAM all by itself,
 # not counting RAM it is sharing with the other httpd processes.
-Apache2::SizeLimit->set_max_unshared_size(Bugzilla->localconfig->{apache_size_limit});
+Apache2::SizeLimit->set_max_unshared_size(
+  Bugzilla->localconfig->{apache_size_limit});
 
 my $cgi_path = Bugzilla::Constants::bz_locations()->{'cgi_path'};
 
 # Set up the configuration for the web server
 my $server = Apache2::ServerUtil->server;
-my $conf = <<EOT;
+my $conf   = <<EOT;
 # Make sure each httpd child receives a different random seed (bug 476622).
 # Bugzilla::RNG has one srand that needs to be called for
 # every process, and Perl has another. (Various Perl modules still use
@@ -92,7 +100,8 @@ $Bugzilla::extension_packages = Bugzilla::Extension->load_all();
 
 # Have ModPerl::RegistryLoader pre-compile all CGI scripts.
 my $rl = new ModPerl::RegistryLoader();
-# If we try to do this in "new" it fails because it looks for a 
+
+# If we try to do this in "new" it fails because it looks for a
 # Bugzilla/ModPerl/ResponseHandler.pm
 $rl->{package} = 'Bugzilla::ModPerl::ResponseHandler';
 my $feature_files = Bugzilla::Install::Requirements::map_files_to_features();
@@ -101,16 +110,16 @@ my $feature_files = Bugzilla::Install::Requirements::map_files_to_features();
 # This is important to prevent the current directory from getting into
 # @INC and messing things up. (See bug 630750.)
 no warnings 'redefine';
-local *lib::import = sub {};
+local *lib::import = sub { };
 use warnings;
 
 foreach my $file (glob "$cgi_path/*.cgi") {
-    my $base_filename = File::Basename::basename($file);
-    if (my $feature = $feature_files->{$base_filename}) {
-        next if !Bugzilla->feature($feature);
-    }
-    Bugzilla::Util::trick_taint($file);
-    $rl->handler($file, $file);
+  my $base_filename = File::Basename::basename($file);
+  if (my $feature = $feature_files->{$base_filename}) {
+    next if !Bugzilla->feature($feature);
+  }
+  Bugzilla::Util::trick_taint($file);
+  $rl->handler($file, $file);
 }
 
 package Bugzilla::ModPerl::ResponseHandler;
@@ -124,29 +133,28 @@ use Bugzilla;
 use Bugzilla::Constants qw(USAGE_MODE_REST);
 
 sub handler : method {
-    my $class = shift;
-
-    # $0 is broken under mod_perl before 2.0.2, so we have to set it
-    # here explicitly or init_page's shutdownhtml code won't work right.
-    $0 = $ENV{'SCRIPT_FILENAME'};
-
-    # Prevent "use lib" from modifying @INC in the case where a .cgi file
-    # is being automatically recompiled by mod_perl when Apache is
-    # running. (This happens if a file changes while Apache is already
-    # running.)
-    no warnings 'redefine';
-    local *lib::import = sub {};
-    use warnings;
-
-    Bugzilla::init_page();
-    my $result = $class->SUPER::handler(@_);
-
-    # When returning data from the REST api we must only return 200 or 304,
-    # which tells Apache not to append its error html documents to the
-    # response.
-    return Bugzilla->usage_mode == USAGE_MODE_REST && $result != 304
-           ? Apache2::Const::OK
-           : $result;
+  my $class = shift;
+
+  # $0 is broken under mod_perl before 2.0.2, so we have to set it
+  # here explicitly or init_page's shutdownhtml code won't work right.
+  $0 = $ENV{'SCRIPT_FILENAME'};
+
+  # Prevent "use lib" from modifying @INC in the case where a .cgi file
+  # is being automatically recompiled by mod_perl when Apache is
+  # running. (This happens if a file changes while Apache is already
+  # running.)
+  no warnings 'redefine';
+  local *lib::import = sub { };
+  use warnings;
+
+  Bugzilla::init_page();
+  my $result = $class->SUPER::handler(@_);
+
+  # When returning data from the REST api we must only return 200 or 304,
+  # which tells Apache not to append its error html documents to the
+  # response.
+  return Bugzilla->usage_mode == USAGE_MODE_REST
+    && $result != 304 ? Apache2::Const::OK : $result;
 }
 
 
@@ -159,16 +167,17 @@ use warnings;
 use Apache2::Const -compile => qw(OK);
 
 sub handler {
-    my $r = shift;
+  my $r = shift;
+
+  Bugzilla::_cleanup();
 
-    Bugzilla::_cleanup();
-    # Sometimes mod_perl doesn't properly call DESTROY on all
-    # the objects in pnotes()
-    foreach my $key (keys %{$r->pnotes}) {
-        delete $r->pnotes->{$key};
-    }
+  # Sometimes mod_perl doesn't properly call DESTROY on all
+  # the objects in pnotes()
+  foreach my $key (keys %{$r->pnotes}) {
+    delete $r->pnotes->{$key};
+  }
 
-    return Apache2::Const::OK;
+  return Apache2::Const::OK;
 }
 
 1;
index 58679d57f6fea0530f0df7dc7eb911df31bec4ea..67b67f99dfe894eac17312aaa54018d0e6095571 100755 (executable)
--- a/page.cgi
+++ b/page.cgi
@@ -30,16 +30,17 @@ use Bugzilla::Search::Quicksearch;
 
 # For quicksearch.html.
 sub quicksearch_field_names {
-    my $fields = Bugzilla::Search::Quicksearch->FIELD_MAP;
-    my %fields_reverse;
-    # Put longer names before shorter names.
-    my @nicknames = sort { length($b) <=> length($a) } (keys %$fields);
-    foreach my $nickname (@nicknames) {
-        my $db_field = $fields->{$nickname};
-        $fields_reverse{$db_field} ||= [];
-        push(@{ $fields_reverse{$db_field} }, $nickname);
-    }
-    return \%fields_reverse;
+  my $fields = Bugzilla::Search::Quicksearch->FIELD_MAP;
+  my %fields_reverse;
+
+  # Put longer names before shorter names.
+  my @nicknames = sort { length($b) <=> length($a) } (keys %$fields);
+  foreach my $nickname (@nicknames) {
+    my $db_field = $fields->{$nickname};
+    $fields_reverse{$db_field} ||= [];
+    push(@{$fields_reverse{$db_field}}, $nickname);
+  }
+  return \%fields_reverse;
 }
 
 ###############
@@ -48,38 +49,40 @@ sub quicksearch_field_names {
 
 Bugzilla->login();
 
-my $cgi = Bugzilla->cgi;
+my $cgi      = Bugzilla->cgi;
 my $template = Bugzilla->template;
 
 my $id = $cgi->param('id');
 if ($id) {
-    # Be careful not to allow directory traversal.
-    if ($id =~ /\.\./) {
-        # two dots in a row is bad
-        ThrowUserError("bad_page_cgi_id", { "page_id" => $id });
-    }
-    # Split into name and ctype.
-    $id =~ /^([\w\-\/\.]+)\.(\w+)$/;
-    if (!$2) {
-        # if this regexp fails to match completely, something bad came in
-        ThrowUserError("bad_page_cgi_id", { "page_id" => $id });
-    }
-
-    my %vars = ( 
-      quicksearch_field_names => \&quicksearch_field_names,
-    );
-    Bugzilla::Hook::process('page_before_template', 
-                            { page_id => $id, vars => \%vars });
-
-    my $format = $template->get_format("pages/$1", undef, $2);
-    
-    $cgi->param('id', $id);
-
-    print $cgi->header($format->{'ctype'});
-
-    $template->process("$format->{'template'}", \%vars)
-      || ThrowTemplateError($template->error());
+
+  # Be careful not to allow directory traversal.
+  if ($id =~ /\.\./) {
+
+    # two dots in a row is bad
+    ThrowUserError("bad_page_cgi_id", {"page_id" => $id});
+  }
+
+  # Split into name and ctype.
+  $id =~ /^([\w\-\/\.]+)\.(\w+)$/;
+  if (!$2) {
+
+    # if this regexp fails to match completely, something bad came in
+    ThrowUserError("bad_page_cgi_id", {"page_id" => $id});
+  }
+
+  my %vars = (quicksearch_field_names => \&quicksearch_field_names,);
+  Bugzilla::Hook::process('page_before_template',
+    {page_id => $id, vars => \%vars});
+
+  my $format = $template->get_format("pages/$1", undef, $2);
+
+  $cgi->param('id', $id);
+
+  print $cgi->header($format->{'ctype'});
+
+  $template->process("$format->{'template'}", \%vars)
+    || ThrowTemplateError($template->error());
 }
 else {
-    ThrowUserError("no_page_specified");
+  ThrowUserError("no_page_specified");
 }
index 712d765d832680293dfe2d29fdb8c36aac0c9132..e2771f0edbdc847eab5fdaa23bf53324b0c50dcb 100755 (executable)
@@ -28,10 +28,10 @@ use List::MoreUtils qw(uniq);
 
 my $user = Bugzilla->login(LOGIN_REQUIRED);
 
-my $cgi = Bugzilla->cgi;
-my $dbh = Bugzilla->dbh;
+my $cgi      = Bugzilla->cgi;
+my $dbh      = Bugzilla->dbh;
 my $template = Bugzilla->template;
-my $vars = {};
+my $vars     = {};
 
 ######################################################################
 # Main Script
@@ -39,8 +39,8 @@ my $vars = {};
 
 # redirect to enter_bug if no field is passed.
 unless ($cgi->param()) {
-    print $cgi->redirect(correct_urlbase() . 'enter_bug.cgi');
-    exit;
+  print $cgi->redirect(correct_urlbase() . 'enter_bug.cgi');
+  exit;
 }
 
 # Detect if the user already used the same form to submit a bug
@@ -48,39 +48,42 @@ my $token = trim(scalar $cgi->param('token'));
 check_token_data($token, 'create_bug', 'index.cgi');
 
 # do a match on the fields if applicable
-Bugzilla::User::match_field ({
-    'cc'            => { 'type' => 'multi'  },
-    'assigned_to'   => { 'type' => 'single' },
-    'qa_contact'    => { 'type' => 'single' },
+Bugzilla::User::match_field({
+  'cc'          => {'type' => 'multi'},
+  'assigned_to' => {'type' => 'single'},
+  'qa_contact'  => {'type' => 'single'},
 });
 
 if (defined $cgi->param('maketemplate')) {
-    $vars->{'url'} = $cgi->canonicalise_query('token');
+  $vars->{'url'} = $cgi->canonicalise_query('token');
 
-    print $cgi->header();
-    $template->process("bug/create/make-template.html.tmpl", $vars)
-      || ThrowTemplateError($template->error());
-    exit;
+  print $cgi->header();
+  $template->process("bug/create/make-template.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
+  exit;
 }
 
 # The format of the initial comment can be structured by adding fields to the
 # enter_bug template and then referencing them in the comment template.
 my $comment;
-my $format = $template->get_format("bug/create/comment",
-                                   scalar($cgi->param('format')), "txt");
+my $format
+  = $template->get_format("bug/create/comment", scalar($cgi->param('format')),
+  "txt");
 $template->process($format->{'template'}, $vars, \$comment)
-    || ThrowTemplateError($template->error());
+  || ThrowTemplateError($template->error());
 
 # Include custom fields editable on bug creation.
-my @custom_bug_fields = grep {$_->type != FIELD_TYPE_MULTI_SELECT && $_->enter_bug}
-                             Bugzilla->active_custom_fields;
+my @custom_bug_fields
+  = grep { $_->type != FIELD_TYPE_MULTI_SELECT && $_->enter_bug }
+  Bugzilla->active_custom_fields;
 
 # Undefined custom fields are ignored to ensure they will get their default
 # value (e.g. "---" for custom single select fields).
 my @bug_fields = grep { defined $cgi->param($_->name) } @custom_bug_fields;
 @bug_fields = map { $_->name } @bug_fields;
 
-push(@bug_fields, qw(
+push(
+  @bug_fields, qw(
     product
     component
 
@@ -105,36 +108,39 @@ push(@bug_fields, qw(
     see_also
     estimated_time
     deadline
-));
+    )
+);
 my %bug_params;
 foreach my $field (@bug_fields) {
-    $bug_params{$field} = $cgi->param($field);
+  $bug_params{$field} = $cgi->param($field);
 }
 foreach my $field (qw(cc groups)) {
-    next if !$cgi->should_set($field);
-    $bug_params{$field} = [$cgi->multi_param($field)];
+  next if !$cgi->should_set($field);
+  $bug_params{$field} = [$cgi->multi_param($field)];
 }
-$bug_params{'comment'} = $comment;
+$bug_params{'comment'}     = $comment;
 $bug_params{'is_markdown'} = $cgi->param('use_markdown');
 
-my @multi_selects = grep {$_->type == FIELD_TYPE_MULTI_SELECT && $_->enter_bug}
-                         Bugzilla->active_custom_fields;
+my @multi_selects
+  = grep { $_->type == FIELD_TYPE_MULTI_SELECT && $_->enter_bug }
+  Bugzilla->active_custom_fields;
 
 foreach my $field (@multi_selects) {
-    next if !$cgi->should_set($field->name);
-    $bug_params{$field->name} = [$cgi->multi_param($field->name)];
+  next if !$cgi->should_set($field->name);
+  $bug_params{$field->name} = [$cgi->multi_param($field->name)];
 }
 
 
 my $product = Bugzilla::Product->check($bug_params{'product'});
-my $component_id = Bugzilla::Component->check({
-    product => $product,
-    name => $bug_params{'component'}})->id;
+my $component_id
+  = Bugzilla::Component->check({
+  product => $product, name => $bug_params{'component'}
+  })->id;
 
 # Set bug flags.
-my (undef, $flag_data) = Bugzilla::Flag->extract_flags_from_cgi($vars, SKIP_REQUESTEE_ON_ERROR,{
-                                                         product_id => $product->id,
-                                                         component_id => $component_id });
+my (undef, $flag_data)
+  = Bugzilla::Flag->extract_flags_from_cgi($vars, SKIP_REQUESTEE_ON_ERROR,
+  {product_id => $product->id, component_id => $component_id});
 $bug_params{flags} = $flag_data;
 
 my $bug = Bugzilla::Bug->create(\%bug_params);
@@ -145,15 +151,18 @@ delete_token($token);
 
 # We do this directly from the DB because $bug->creation_ts has the seconds
 # formatted out of it (which should be fixed some day).
-my $timestamp = $dbh->selectrow_array(
-    'SELECT creation_ts FROM bugs WHERE bug_id = ?', undef, $id);
+my $timestamp
+  = $dbh->selectrow_array('SELECT creation_ts FROM bugs WHERE bug_id = ?',
+  undef, $id);
 
 # Set Version cookie, but only if the user actually selected
 # a version on the page.
 if (defined $cgi->param('version')) {
-    $cgi->send_cookie(-name => "VERSION-" . $bug->product,
-                      -value => $bug->version,
-                      -expires => "Fri, 01-Jan-2038 00:00:00 GMT");
+  $cgi->send_cookie(
+    -name    => "VERSION-" . $bug->product,
+    -value   => $bug->version,
+    -expires => "Fri, 01-Jan-2038 00:00:00 GMT"
+  );
 }
 
 # We don't have to check if the user can see the bug, because a user filing
@@ -161,84 +170,84 @@ if (defined $cgi->param('version')) {
 # after the bug is filed.
 
 # Add an attachment if requested.
-my $data_fh = $cgi->upload('data');
+my $data_fh     = $cgi->upload('data');
 my $attach_text = $cgi->param('attach_text');
 
 if ($data_fh || $attach_text) {
-    $cgi->param('isprivate', scalar $cgi->param('comment_is_private'));
-
-    # Must be called before create() as it may alter $cgi->param('ispatch').
-    my $content_type = Bugzilla::Attachment::get_content_type();
-    my $attachment;
-
-    # If the attachment cannot be successfully added to the bug,
-    # we notify the user, but we don't interrupt the bug creation process.
-    my $error_mode_cache = Bugzilla->error_mode;
-    Bugzilla->error_mode(ERROR_MODE_DIE);
-    eval {
-        $attachment = Bugzilla::Attachment->create(
-            {bug           => $bug,
-             creation_ts   => $timestamp,
-             data          => $attach_text || $data_fh,
-             description   => scalar $cgi->param('description'),
-             filename      => $attach_text ? "file_$id.txt" : $data_fh,
-             ispatch       => scalar $cgi->param('ispatch'),
-             isprivate     => scalar $cgi->param('isprivate'),
-             mimetype      => $content_type,
-            });
-    };
-    Bugzilla->error_mode($error_mode_cache);
-
-    if ($attachment) {
-        # Set attachment flags.
-        my ($flags, $new_flags) = Bugzilla::Flag->extract_flags_from_cgi(
-                                       $vars, SKIP_REQUESTEE_ON_ERROR,
-                                       { bug => $bug, attachment => $attachment });
-        $attachment->set_flags($flags, $new_flags);
-        $attachment->update($timestamp);
-        my $comment = $bug->comments->[0];
-        $comment->set_all({ type => CMT_ATTACHMENT_CREATED, 
-                            extra_data => $attachment->id });
-        $comment->update();
-    }
-    else {
-        $vars->{'message'} = 'attachment_creation_failed';
-    }
+  $cgi->param('isprivate', scalar $cgi->param('comment_is_private'));
+
+  # Must be called before create() as it may alter $cgi->param('ispatch').
+  my $content_type = Bugzilla::Attachment::get_content_type();
+  my $attachment;
+
+  # If the attachment cannot be successfully added to the bug,
+  # we notify the user, but we don't interrupt the bug creation process.
+  my $error_mode_cache = Bugzilla->error_mode;
+  Bugzilla->error_mode(ERROR_MODE_DIE);
+  eval {
+    $attachment = Bugzilla::Attachment->create({
+      bug         => $bug,
+      creation_ts => $timestamp,
+      data        => $attach_text || $data_fh,
+      description => scalar $cgi->param('description'),
+      filename    => $attach_text ? "file_$id.txt" : $data_fh,
+      ispatch     => scalar $cgi->param('ispatch'),
+      isprivate   => scalar $cgi->param('isprivate'),
+      mimetype    => $content_type,
+    });
+  };
+  Bugzilla->error_mode($error_mode_cache);
+
+  if ($attachment) {
+
+    # Set attachment flags.
+    my ($flags, $new_flags)
+      = Bugzilla::Flag->extract_flags_from_cgi($vars, SKIP_REQUESTEE_ON_ERROR,
+      {bug => $bug, attachment => $attachment});
+    $attachment->set_flags($flags, $new_flags);
+    $attachment->update($timestamp);
+    my $comment = $bug->comments->[0];
+    $comment->set_all(
+      {type => CMT_ATTACHMENT_CREATED, extra_data => $attachment->id});
+    $comment->update();
+  }
+  else {
+    $vars->{'message'} = 'attachment_creation_failed';
+  }
 }
 
 
-$vars->{'id'} = $id;
+$vars->{'id'}  = $id;
 $vars->{'bug'} = $bug;
 
-Bugzilla::Hook::process('post_bug_after_creation', { vars => $vars });
+Bugzilla::Hook::process('post_bug_after_creation', {vars => $vars});
 
-my $recipients = { changer => $user };
+my $recipients = {changer => $user};
 my $bug_sent = Bugzilla::BugMail::Send($id, $recipients);
 $bug_sent->{type} = 'created';
 $bug_sent->{id}   = $id;
 my @all_mail_results = ($bug_sent);
 
 foreach my $dep (@{$bug->dependson || []}, @{$bug->blocked || []}) {
-    my $dep_sent = Bugzilla::BugMail::Send($dep, $recipients);
-    $dep_sent->{type} = 'dep';
-    $dep_sent->{id}   = $dep;
-    push(@all_mail_results, $dep_sent);
+  my $dep_sent = Bugzilla::BugMail::Send($dep, $recipients);
+  $dep_sent->{type} = 'dep';
+  $dep_sent->{id}   = $dep;
+  push(@all_mail_results, $dep_sent);
 }
 
 # Sending emails for any referenced bugs.
-foreach my $ref_bug_id (uniq @{ $bug->{see_also_changes} || [] }) {
-    my $ref_sent = Bugzilla::BugMail::Send($ref_bug_id, $recipients);
-    $ref_sent->{id} = $ref_bug_id;
-    push(@all_mail_results, $ref_sent);
+foreach my $ref_bug_id (uniq @{$bug->{see_also_changes} || []}) {
+  my $ref_sent = Bugzilla::BugMail::Send($ref_bug_id, $recipients);
+  $ref_sent->{id} = $ref_bug_id;
+  push(@all_mail_results, $ref_sent);
 }
 
 $vars->{sentmail} = \@all_mail_results;
 
 $format = $template->get_format("bug/create/created",
-                                 scalar($cgi->param('created-format')),
-                                 "html");
+  scalar($cgi->param('created-format')), "html");
 print $cgi->header();
 $template->process($format->{'template'}, $vars)
-    || ThrowTemplateError($template->error());
+  || ThrowTemplateError($template->error());
 
 1;
index 2b34d1b80ae15036a0cde82b00e5f31b14781d14..330ec2d79c7e4aa100a8eb524fef2a9dadef7e34 100755 (executable)
@@ -27,10 +27,10 @@ use Storable qw(dclone);
 
 my $user = Bugzilla->login(LOGIN_REQUIRED);
 
-my $cgi = Bugzilla->cgi;
-my $dbh = Bugzilla->dbh;
+my $cgi      = Bugzilla->cgi;
+my $dbh      = Bugzilla->dbh;
 my $template = Bugzilla->template;
-my $vars = {};
+my $vars     = {};
 
 ######################################################################
 # Subroutines
@@ -38,18 +38,19 @@ my $vars = {};
 
 # Tells us whether or not a field should be changed by process_bug.
 sub should_set {
-    # check_defined is used for fields where there's another field
-    # whose name starts with "defined_" and then the field name--it's used
-    # to know when we did things like empty a multi-select or deselect
-    # a checkbox.
-    my ($field, $check_defined) = @_;
-    my $cgi = Bugzilla->cgi;
-    if ( defined $cgi->param($field) 
-         || ($check_defined && defined $cgi->param("defined_$field")) )
-    {
-        return 1;
-    }
-    return 0;
+
+  # check_defined is used for fields where there's another field
+  # whose name starts with "defined_" and then the field name--it's used
+  # to know when we did things like empty a multi-select or deselect
+  # a checkbox.
+  my ($field, $check_defined) = @_;
+  my $cgi = Bugzilla->cgi;
+  if (defined $cgi->param($field)
+    || ($check_defined && defined $cgi->param("defined_$field")))
+  {
+    return 1;
+  }
+  return 0;
 }
 
 ######################################################################
@@ -62,39 +63,40 @@ if (defined $cgi->param('id')) {
   my $bug = Bugzilla::Bug->check_for_edit(scalar $cgi->param('id'));
   $cgi->param('id', $bug->id);
   push(@bug_objects, $bug);
-} else {
-    foreach my $i ($cgi->multi_param()) {
-        if ($i =~ /^id_([1-9][0-9]*)/) {
-            my $id = $1;
-            push(@bug_objects, Bugzilla::Bug->check_for_edit($id));
-        }
+}
+else {
+  foreach my $i ($cgi->multi_param()) {
+    if ($i =~ /^id_([1-9][0-9]*)/) {
+      my $id = $1;
+      push(@bug_objects, Bugzilla::Bug->check_for_edit($id));
     }
+  }
 }
 
 # Make sure there are bugs to process.
 scalar(@bug_objects) || ThrowUserError("no_bugs_chosen", {action => 'modify'});
 
-my $first_bug = $bug_objects[0]; # Used when we're only updating a single bug.
+my $first_bug = $bug_objects[0];   # Used when we're only updating a single bug.
 
 # Delete any parameter set to 'dontchange'.
 if (defined $cgi->param('dontchange')) {
-    foreach my $name ($cgi->multi_param()) {
-        next if $name eq 'dontchange'; # But don't delete dontchange itself!
-        # Skip ones we've already deleted (such as "defined_$name").
-        next if !defined $cgi->param($name);
-        if ($cgi->param($name) eq $cgi->param('dontchange')) {
-            $cgi->delete($name);
-            $cgi->delete("defined_$name");
-        }
+  foreach my $name ($cgi->multi_param()) {
+    next if $name eq 'dontchange';    # But don't delete dontchange itself!
+         # Skip ones we've already deleted (such as "defined_$name").
+    next if !defined $cgi->param($name);
+    if ($cgi->param($name) eq $cgi->param('dontchange')) {
+      $cgi->delete($name);
+      $cgi->delete("defined_$name");
     }
+  }
 }
 
 # do a match on the fields if applicable
 Bugzilla::User::match_field({
-    'qa_contact'                => { 'type' => 'single' },
-    'newcc'                     => { 'type' => 'multi'  },
-    'masscc'                    => { 'type' => 'multi'  },
-    'assigned_to'               => { 'type' => 'single' },
+  'qa_contact'  => {'type' => 'single'},
+  'newcc'       => {'type' => 'multi'},
+  'masscc'      => {'type' => 'multi'},
+  'assigned_to' => {'type' => 'single'},
 });
 
 print $cgi->header() unless Bugzilla->usage_mode == USAGE_MODE_EMAIL;
@@ -104,48 +106,49 @@ print $cgi->header() unless Bugzilla->usage_mode == USAGE_MODE_EMAIL;
 my $delta_ts = $cgi->param('delta_ts') || '';
 
 if ($delta_ts) {
-    my $delta_ts_z = datetime_from($delta_ts)
-      or ThrowCodeError('invalid_timestamp', { timestamp => $delta_ts });
-
-    my $first_delta_tz_z =  datetime_from($first_bug->delta_ts);
-
-    if ($first_delta_tz_z ne $delta_ts_z) {
-        ($vars->{'operations'}) = $first_bug->get_activity(undef, $delta_ts);
-
-        # Always sort midair collision comments oldest to newest,
-        # regardless of the user's personal preference.
-        my $comments = $first_bug->comments({ order => 'oldest_to_newest',
-                                              after => $delta_ts });
-
-        # Show midair if previous changes made other than CC
-        # and/or one or more comments were made
-        my $do_midair = scalar @$comments ? 1 : 0;
-
-        if (!$do_midair) {
-            foreach my $operation (@{ $vars->{'operations'} }) {
-                foreach my $change (@{ $operation->{'changes'} }) {
-                    if ($change->{'fieldname'} ne 'cc') {
-                        $do_midair = 1;
-                        last;
-                    }
-                }
-                last if $do_midair;
-            }
+  my $delta_ts_z = datetime_from($delta_ts)
+    or ThrowCodeError('invalid_timestamp', {timestamp => $delta_ts});
+
+  my $first_delta_tz_z = datetime_from($first_bug->delta_ts);
+
+  if ($first_delta_tz_z ne $delta_ts_z) {
+    ($vars->{'operations'}) = $first_bug->get_activity(undef, $delta_ts);
+
+    # Always sort midair collision comments oldest to newest,
+    # regardless of the user's personal preference.
+    my $comments
+      = $first_bug->comments({order => 'oldest_to_newest', after => $delta_ts});
+
+    # Show midair if previous changes made other than CC
+    # and/or one or more comments were made
+    my $do_midair = scalar @$comments ? 1 : 0;
+
+    if (!$do_midair) {
+      foreach my $operation (@{$vars->{'operations'}}) {
+        foreach my $change (@{$operation->{'changes'}}) {
+          if ($change->{'fieldname'} ne 'cc') {
+            $do_midair = 1;
+            last;
+          }
         }
+        last if $do_midair;
+      }
+    }
 
-        if ($do_midair) {
-            $vars->{'title_tag'} = "mid_air";
-            $vars->{'comments'} = $comments;
-            $vars->{'bug'} = $first_bug;
-            # The token contains the old delta_ts. We need a new one.
-            $cgi->param('token', issue_hash_token([$first_bug->id, $first_bug->delta_ts]));
-
-            # Warn the user about the mid-air collision and ask them what to do.
-            $template->process("bug/process/midair.html.tmpl", $vars)
-                || ThrowTemplateError($template->error());
-            exit;
-        }
+    if ($do_midair) {
+      $vars->{'title_tag'} = "mid_air";
+      $vars->{'comments'}  = $comments;
+      $vars->{'bug'}       = $first_bug;
+
+      # The token contains the old delta_ts. We need a new one.
+      $cgi->param('token', issue_hash_token([$first_bug->id, $first_bug->delta_ts]));
+
+      # Warn the user about the mid-air collision and ask them what to do.
+      $template->process("bug/process/midair.html.tmpl", $vars)
+        || ThrowTemplateError($template->error());
+      exit;
     }
+  }
 }
 
 # We couldn't do this check earlier as we first had to validate bug IDs
@@ -154,10 +157,10 @@ if ($delta_ts) {
 my $token = $cgi->param('token');
 
 if ($cgi->param('id')) {
-    check_hash_token($token, [$first_bug->id, $delta_ts || $first_bug->delta_ts]);
+  check_hash_token($token, [$first_bug->id, $delta_ts || $first_bug->delta_ts]);
 }
 else {
-    check_token_data($token, 'buglist_mass_change', 'query.cgi');
+  check_token_data($token, 'buglist_mass_change', 'query.cgi');
 }
 
 ######################################################################
@@ -168,207 +171,215 @@ $vars->{'title_tag'} = "bug_processed";
 
 my $action;
 if (defined $cgi->param('id')) {
-    $action = $user->setting('post_bug_submit_action');
-
-    if ($action eq 'next_bug') {
-        my $bug_list_obj = $user->recent_search_for($first_bug);
-        my @bug_list = $bug_list_obj ? @{$bug_list_obj->bug_list} : ();
-        my $cur = firstidx { $_ eq $cgi->param('id') } @bug_list;
-        if ($cur >= 0 && $cur < $#bug_list) {
-            my $next_bug_id = $bug_list[$cur + 1];
-            detaint_natural($next_bug_id);
-            if ($next_bug_id and $user->can_see_bug($next_bug_id)) {
-                # We create an object here so that $bug->send_changes can use it
-                # when displaying the header.
-                $vars->{'bug'} = new Bugzilla::Bug($next_bug_id);
-            }
-        }
-    }
-    # Include both action = 'same_bug' and 'nothing'.
-    else {
-        $vars->{'bug'} = $first_bug;
+  $action = $user->setting('post_bug_submit_action');
+
+  if ($action eq 'next_bug') {
+    my $bug_list_obj = $user->recent_search_for($first_bug);
+    my @bug_list     = $bug_list_obj ? @{$bug_list_obj->bug_list} : ();
+    my $cur          = firstidx { $_ eq $cgi->param('id') } @bug_list;
+    if ($cur >= 0 && $cur < $#bug_list) {
+      my $next_bug_id = $bug_list[$cur + 1];
+      detaint_natural($next_bug_id);
+      if ($next_bug_id and $user->can_see_bug($next_bug_id)) {
+
+        # We create an object here so that $bug->send_changes can use it
+        # when displaying the header.
+        $vars->{'bug'} = new Bugzilla::Bug($next_bug_id);
+      }
     }
+  }
+
+  # Include both action = 'same_bug' and 'nothing'.
+  else {
+    $vars->{'bug'} = $first_bug;
+  }
 }
 else {
-    # param('id') is not defined when changing multiple bugs at once.
-    $action = 'nothing';
+  # param('id') is not defined when changing multiple bugs at once.
+  $action = 'nothing';
 }
 
 # Component, target_milestone, and version are in here just in case
 # the 'product' field wasn't defined in the CGI. It doesn't hurt to set
 # them twice.
 my @set_fields = qw(op_sys rep_platform priority bug_severity
-                    component target_milestone version
-                    bug_file_loc status_whiteboard short_desc
-                    deadline remaining_time estimated_time
-                    work_time set_default_assignee set_default_qa_contact
-                    cclist_accessible reporter_accessible
-                    product confirm_product_change
-                    bug_status resolution dup_id bug_ignored);
+  component target_milestone version
+  bug_file_loc status_whiteboard short_desc
+  deadline remaining_time estimated_time
+  work_time set_default_assignee set_default_qa_contact
+  cclist_accessible reporter_accessible
+  product confirm_product_change
+  bug_status resolution dup_id bug_ignored);
 push(@set_fields, 'assigned_to') if !$cgi->param('set_default_assignee');
 push(@set_fields, 'qa_contact')  if !$cgi->param('set_default_qa_contact');
 my %field_translation = (
-    bug_severity => 'severity',
-    rep_platform => 'platform',
-    short_desc   => 'summary',
-    bug_file_loc => 'url',
-    set_default_assignee   => 'reset_assigned_to',
-    set_default_qa_contact => 'reset_qa_contact',
-    confirm_product_change => 'product_change_confirmed',
+  bug_severity           => 'severity',
+  rep_platform           => 'platform',
+  short_desc             => 'summary',
+  bug_file_loc           => 'url',
+  set_default_assignee   => 'reset_assigned_to',
+  set_default_qa_contact => 'reset_qa_contact',
+  confirm_product_change => 'product_change_confirmed',
 );
 
-my %set_all_fields = ( other_bugs => \@bug_objects );
+my %set_all_fields = (other_bugs => \@bug_objects);
 foreach my $field_name (@set_fields) {
-    if (should_set($field_name, 1)) {
-        my $param_name = $field_translation{$field_name} || $field_name;
-        $set_all_fields{$param_name} = $cgi->param($field_name);
-    }
+  if (should_set($field_name, 1)) {
+    my $param_name = $field_translation{$field_name} || $field_name;
+    $set_all_fields{$param_name} = $cgi->param($field_name);
+  }
 }
 
 if (should_set('keywords')) {
-    my $action = $cgi->param('keywordaction') || '';
-    # Backward-compatibility for Bugzilla 3.x and older.
-    $action = 'remove' if $action eq 'delete';
-    $action = 'set'    if $action eq 'makeexact';
-    $set_all_fields{keywords}->{$action} = $cgi->param('keywords');
+  my $action = $cgi->param('keywordaction') || '';
+
+  # Backward-compatibility for Bugzilla 3.x and older.
+  $action = 'remove' if $action eq 'delete';
+  $action = 'set'    if $action eq 'makeexact';
+  $set_all_fields{keywords}->{$action} = $cgi->param('keywords');
 }
 if (should_set('comment')) {
-    my $is_markdown = ($user->use_markdown
-                       && $cgi->param('use_markdown')) ? 1 : 0;
-
-    $set_all_fields{comment} = {
-        body        => scalar $cgi->param('comment'),
-        is_private  => scalar $cgi->param('comment_is_private'),
-        is_markdown => $is_markdown,
-    };
+  my $is_markdown = ($user->use_markdown && $cgi->param('use_markdown')) ? 1 : 0;
+
+  $set_all_fields{comment} = {
+    body        => scalar $cgi->param('comment'),
+    is_private  => scalar $cgi->param('comment_is_private'),
+    is_markdown => $is_markdown,
+  };
 }
 if (should_set('see_also')) {
-    $set_all_fields{'see_also'}->{add} = 
-        [split(/[\s]+/, $cgi->param('see_also'))];
+  $set_all_fields{'see_also'}->{add} = [split(/[\s]+/, $cgi->param('see_also'))];
 }
 if (should_set('remove_see_also')) {
-    $set_all_fields{'see_also'}->{remove} = [$cgi->multi_param('remove_see_also')];
+  $set_all_fields{'see_also'}->{remove} = [$cgi->multi_param('remove_see_also')];
 }
 foreach my $dep_field (qw(dependson blocked)) {
-    if (should_set($dep_field)) {
-        if (my $dep_action = $cgi->param("${dep_field}_action")) {
-            $set_all_fields{$dep_field}->{$dep_action} =
-                [split(/[\s,]+/, $cgi->param($dep_field))];
-        }
-        else {
-            $set_all_fields{$dep_field}->{set} = $cgi->param($dep_field);
-        }
+  if (should_set($dep_field)) {
+    if (my $dep_action = $cgi->param("${dep_field}_action")) {
+      $set_all_fields{$dep_field}->{$dep_action}
+        = [split(/[\s,]+/, $cgi->param($dep_field))];
+    }
+    else {
+      $set_all_fields{$dep_field}->{set} = $cgi->param($dep_field);
     }
+  }
 }
+
 # Formulate the CC data into two arrays of users involved in this CC change.
-if (defined $cgi->param('newcc')
-    or defined $cgi->param('addselfcc')
-    or defined $cgi->param('removecc')
-    or defined $cgi->param('masscc')) 
+if ( defined $cgi->param('newcc')
+  or defined $cgi->param('addselfcc')
+  or defined $cgi->param('removecc')
+  or defined $cgi->param('masscc'))
 {
-    my (@cc_add, @cc_remove);
-    # If masscc is defined, then we came from buglist and need to either add or
-    # remove cc's... otherwise, we came from show_bug and may need to do both.
-    if (defined $cgi->param('masscc')) {
-        if ($cgi->param('ccaction') eq 'add') {
-            @cc_add = $cgi->multi_param('masscc');
-        } elsif ($cgi->param('ccaction') eq 'remove') {
-            @cc_remove = $cgi->multi_param('masscc');
-        }
-    } else {
-        @cc_add = $cgi->multi_param('newcc');
-        push(@cc_add, $user) if $cgi->param('addselfcc');
-
-        # We came from show_bug which uses a select box to determine what cc's
-        # need to be removed...
-        if ($cgi->param('removecc') && $cgi->param('cc')) {
-            @cc_remove = $cgi->multi_param('cc');
-        }
+  my (@cc_add, @cc_remove);
+
+  # If masscc is defined, then we came from buglist and need to either add or
+  # remove cc's... otherwise, we came from show_bug and may need to do both.
+  if (defined $cgi->param('masscc')) {
+    if ($cgi->param('ccaction') eq 'add') {
+      @cc_add = $cgi->multi_param('masscc');
+    }
+    elsif ($cgi->param('ccaction') eq 'remove') {
+      @cc_remove = $cgi->multi_param('masscc');
     }
+  }
+  else {
+    @cc_add = $cgi->multi_param('newcc');
+    push(@cc_add, $user) if $cgi->param('addselfcc');
+
+    # We came from show_bug which uses a select box to determine what cc's
+    # need to be removed...
+    if ($cgi->param('removecc') && $cgi->param('cc')) {
+      @cc_remove = $cgi->multi_param('cc');
+    }
+  }
 
-    $set_all_fields{cc} = { add => \@cc_add, remove => \@cc_remove };
+  $set_all_fields{cc} = {add => \@cc_add, remove => \@cc_remove};
 }
 
 # Fields that can only be set on one bug at a time.
 if (defined $cgi->param('id')) {
-    # Since aliases are unique (like bug numbers), they can only be changed
-    # for one bug at a time.
-    if (defined $cgi->param('newalias') || defined $cgi->param('removealias')) {
-        my @alias_add = split /[, ]+/, $cgi->param('newalias');
-
-        # We came from bug_form which uses a select box to determine what
-        # aliases need to be removed...
-        my @alias_remove = ();
-        if ($cgi->param('removealias') && $cgi->param('alias')) {
-            @alias_remove = $cgi->multi_param('alias');
-        }
 
-        $set_all_fields{alias} = { add => \@alias_add, remove => \@alias_remove };
+  # Since aliases are unique (like bug numbers), they can only be changed
+  # for one bug at a time.
+  if (defined $cgi->param('newalias') || defined $cgi->param('removealias')) {
+    my @alias_add = split /[, ]+/, $cgi->param('newalias');
+
+    # We came from bug_form which uses a select box to determine what
+    # aliases need to be removed...
+    my @alias_remove = ();
+    if ($cgi->param('removealias') && $cgi->param('alias')) {
+      @alias_remove = $cgi->multi_param('alias');
     }
+
+    $set_all_fields{alias} = {add => \@alias_add, remove => \@alias_remove};
+  }
 }
 
 my %is_private;
 foreach my $field (grep(/^defined_isprivate/, $cgi->multi_param())) {
-    if ($field =~ /(\d+)$/a) {
-        my $comment_id = $1;
-        $is_private{$comment_id} = $cgi->param("isprivate_$comment_id");
-    }
+  if ($field =~ /(\d+)$/a) {
+    my $comment_id = $1;
+    $is_private{$comment_id} = $cgi->param("isprivate_$comment_id");
+  }
 }
 $set_all_fields{comment_is_private} = \%is_private;
 
 my @check_groups = $cgi->multi_param('defined_groups');
-my @set_groups = $cgi->multi_param('groups');
+my @set_groups   = $cgi->multi_param('groups');
 my ($removed_groups) = diff_arrays(\@check_groups, \@set_groups);
-$set_all_fields{groups} = { add => \@set_groups, remove => $removed_groups };
+$set_all_fields{groups} = {add => \@set_groups, remove => $removed_groups};
 
 my @custom_fields = Bugzilla->active_custom_fields;
 foreach my $field (@custom_fields) {
-    my $fname = $field->name;
-    if (should_set($fname, 1)) {
-        $set_all_fields{$fname} = [$cgi->multi_param($fname)];
-    }
+  my $fname = $field->name;
+  if (should_set($fname, 1)) {
+    $set_all_fields{$fname} = [$cgi->multi_param($fname)];
+  }
 }
 
 # We are going to alter the list of removed groups, so we keep a copy here.
 my @unchecked_groups = @$removed_groups;
 foreach my $b (@bug_objects) {
-    # Don't blindly ask to remove unchecked groups available in the UI.
-    # A group can be already unchecked, and the user didn't try to remove it.
-    # In this case, we don't want remove_group() to complain.
-    my @remove_groups;
-    foreach my $g (@{$b->groups_in}) {
-        push(@remove_groups, $g->name) if grep { $_ eq $g->name } @unchecked_groups;
-    }
-    local $set_all_fields{groups}->{remove} = \@remove_groups;
-    $b->set_all(\%set_all_fields);
+
+  # Don't blindly ask to remove unchecked groups available in the UI.
+  # A group can be already unchecked, and the user didn't try to remove it.
+  # In this case, we don't want remove_group() to complain.
+  my @remove_groups;
+  foreach my $g (@{$b->groups_in}) {
+    push(@remove_groups, $g->name) if grep { $_ eq $g->name } @unchecked_groups;
+  }
+  local $set_all_fields{groups}->{remove} = \@remove_groups;
+  $b->set_all(\%set_all_fields);
 }
 
 if (defined $cgi->param('id')) {
-    # Flags should be set AFTER the bug has been moved into another
-    # product/component. The structure of flags code doesn't currently
-    # allow them to be set using set_all.
-    my ($flags, $new_flags) = Bugzilla::Flag->extract_flags_from_cgi(
-        $vars, undef, { bug => $first_bug } );
-    $first_bug->set_flags($flags, $new_flags);
-
-    # Tags can only be set to one bug at once.
-    if (should_set('tag')) {
-        my @new_tags = grep { trim($_) } split(/,/, $cgi->param('tag'));
-        my ($tags_removed, $tags_added) = diff_arrays($first_bug->tags, \@new_tags);
-        $first_bug->remove_tag($_) foreach @$tags_removed;
-        $first_bug->add_tag($_) foreach @$tags_added;
-    }
+
+  # Flags should be set AFTER the bug has been moved into another
+  # product/component. The structure of flags code doesn't currently
+  # allow them to be set using set_all.
+  my ($flags, $new_flags)
+    = Bugzilla::Flag->extract_flags_from_cgi($vars, undef, {bug => $first_bug});
+  $first_bug->set_flags($flags, $new_flags);
+
+  # Tags can only be set to one bug at once.
+  if (should_set('tag')) {
+    my @new_tags = grep { trim($_) } split(/,/, $cgi->param('tag'));
+    my ($tags_removed, $tags_added) = diff_arrays($first_bug->tags, \@new_tags);
+    $first_bug->remove_tag($_) foreach @$tags_removed;
+    $first_bug->add_tag($_)    foreach @$tags_added;
+  }
 }
 else {
-    # Update flags on multiple bugs. The cgi params are slightly different
-    # than on a single bug, so we need to call a different sub. We also
-    # need to call this per bug, since we might be updating a flag in one
-    # bug, but adding it to a second bug
-    foreach my $b (@bug_objects) {
-        my ($flags, $new_flags)
-            = Bugzilla::Flag->multi_extract_flags_from_cgi($b, $vars);
-        $b->set_flags($flags, $new_flags);
-    }
+  # Update flags on multiple bugs. The cgi params are slightly different
+  # than on a single bug, so we need to call a different sub. We also
+  # need to call this per bug, since we might be updating a flag in one
+  # bug, but adding it to a second bug
+  foreach my $b (@bug_objects) {
+    my ($flags, $new_flags)
+      = Bugzilla::Flag->multi_extract_flags_from_cgi($b, $vars);
+    $b->set_flags($flags, $new_flags);
+  }
 }
 
 ##############################
@@ -376,61 +387,65 @@ else {
 ##############################
 my $req_minor_update = $cgi->param('minor_update') ? 1 : 0;
 foreach my $bug (@bug_objects) {
-    my $minor_update = $bug->has_unsent_changes ? 0 : $req_minor_update;
-    my $changes = $bug->update();
-
-    if ($changes->{'bug_status'}) {
-        my $new_status = $changes->{'bug_status'}->[1];
-        # We may have zeroed the remaining time, if we moved into a closed
-        # status, so we should inform the user about that.
-        if (!is_open_state($new_status) && $changes->{'remaining_time'}) {
-            $vars->{'message'} = "remaining_time_zeroed"
-              if $user->is_timetracker;
-        }
+  my $minor_update = $bug->has_unsent_changes ? 0 : $req_minor_update;
+  my $changes = $bug->update();
+
+  if ($changes->{'bug_status'}) {
+    my $new_status = $changes->{'bug_status'}->[1];
+
+    # We may have zeroed the remaining time, if we moved into a closed
+    # status, so we should inform the user about that.
+    if (!is_open_state($new_status) && $changes->{'remaining_time'}) {
+      $vars->{'message'} = "remaining_time_zeroed" if $user->is_timetracker;
     }
+  }
 
-    $bug->send_changes($changes, $vars, $minor_update);
+  $bug->send_changes($changes, $vars, $minor_update);
 }
 
 # Delete the session token used for the mass-change.
 delete_token($token) unless $cgi->param('id');
 
 if (Bugzilla->usage_mode == USAGE_MODE_EMAIL) {
-    # Do nothing.
+
+  # Do nothing.
 }
 elsif ($action eq 'next_bug' or $action eq 'same_bug') {
-    my $bug = $vars->{'bug'};
-    if ($bug and $user->can_see_bug($bug)) {
-        if ($action eq 'same_bug') {
-            # $bug->update() does not update the internal structure of
-            # the bug sufficiently to display the bug with the new values.
-            # (That is, if we just passed in the old Bug object, we'd get
-            # a lot of old values displayed.)
-            $bug = new Bugzilla::Bug($bug->id);
-            $vars->{'bug'} = $bug;
-        }
-        $vars->{'bugs'} = [$bug];
-        if ($action eq 'next_bug') {
-            $vars->{'nextbug'} = $bug->id;
-        }
-        # For performance reasons, preload visibility of dependencies
-        # and duplicates related to this bug.
-        Bugzilla::Bug->preload([$bug]);
-
-        $template->process("bug/show.html.tmpl", $vars)
-          || ThrowTemplateError($template->error());
-        exit;
+  my $bug = $vars->{'bug'};
+  if ($bug and $user->can_see_bug($bug)) {
+    if ($action eq 'same_bug') {
+
+      # $bug->update() does not update the internal structure of
+      # the bug sufficiently to display the bug with the new values.
+      # (That is, if we just passed in the old Bug object, we'd get
+      # a lot of old values displayed.)
+      $bug = new Bugzilla::Bug($bug->id);
+      $vars->{'bug'} = $bug;
     }
-} elsif ($action ne 'nothing') {
-    ThrowCodeError("invalid_post_bug_submit_action");
+    $vars->{'bugs'} = [$bug];
+    if ($action eq 'next_bug') {
+      $vars->{'nextbug'} = $bug->id;
+    }
+
+    # For performance reasons, preload visibility of dependencies
+    # and duplicates related to this bug.
+    Bugzilla::Bug->preload([$bug]);
+
+    $template->process("bug/show.html.tmpl", $vars)
+      || ThrowTemplateError($template->error());
+    exit;
+  }
+}
+elsif ($action ne 'nothing') {
+  ThrowCodeError("invalid_post_bug_submit_action");
 }
 
 # End the response page.
 unless (Bugzilla->usage_mode == USAGE_MODE_EMAIL) {
-    $template->process("bug/navigate.html.tmpl", $vars)
-        || ThrowTemplateError($template->error());
-    $template->process("global/footer.html.tmpl", $vars)
-        || ThrowTemplateError($template->error());
+  $template->process("bug/navigate.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
+  $template->process("global/footer.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
 }
 
 1;
index 88277bf3515e46d4e9c4373392f4b4a71dff5529..1ac50c242a849f88a24e2e11153a51cf7dd064ab 100755 (executable)
--- a/query.cgi
+++ b/query.cgi
@@ -31,61 +31,62 @@ use Bugzilla::Token;
 ###############
 
 sub get_product_values {
-    my ($products, $field, $vars) = @_;
-    my @all_values = map { @{ $_->$field } } @$products;
-
-    my (@unique, %duplicates, %duplicate_count, %seen);
-    foreach my $value (@all_values) {
-        my $lc_name = lc($value->name);
-        if ($seen{$lc_name}) {
-            $duplicate_count{$seen{$lc_name}->id}++;
-            $duplicates{$value->id} = $seen{$lc_name};
-            next;
-        }
-        push(@unique, $value);
-        $seen{$lc_name} = $value;
+  my ($products, $field, $vars) = @_;
+  my @all_values = map { @{$_->$field} } @$products;
+
+  my (@unique, %duplicates, %duplicate_count, %seen);
+  foreach my $value (@all_values) {
+    my $lc_name = lc($value->name);
+    if ($seen{$lc_name}) {
+      $duplicate_count{$seen{$lc_name}->id}++;
+      $duplicates{$value->id} = $seen{$lc_name};
+      next;
     }
-
-    $field =~ s/s$//;
-    if ($field eq 'version') {
-        @unique = sort { vers_cmp(lc($a->name), lc($b->name)) } @unique;
-    }
-    else {
-        @unique = sort { lc($a->name) cmp lc($b->name) } @unique;
-    }
-
-    $field = 'target_milestone' if $field eq 'milestone';
-    $vars->{duplicates}->{$field} = \%duplicates;
-    $vars->{duplicate_count}->{$field} = \%duplicate_count;
-    # "component" is a reserved word in Template Toolkit.
-    $field = 'component_' if $field eq 'component';
-    $vars->{$field} = \@unique;
+    push(@unique, $value);
+    $seen{$lc_name} = $value;
+  }
+
+  $field =~ s/s$//;
+  if ($field eq 'version') {
+    @unique = sort { vers_cmp(lc($a->name), lc($b->name)) } @unique;
+  }
+  else {
+    @unique = sort { lc($a->name) cmp lc($b->name) } @unique;
+  }
+
+  $field = 'target_milestone' if $field eq 'milestone';
+  $vars->{duplicates}->{$field}      = \%duplicates;
+  $vars->{duplicate_count}->{$field} = \%duplicate_count;
+
+  # "component" is a reserved word in Template Toolkit.
+  $field = 'component_' if $field eq 'component';
+  $vars->{$field} = \@unique;
 }
 
 ###############
 # Main Script #
 ###############
 
-my $cgi = Bugzilla->cgi;
-my $dbh = Bugzilla->dbh;
+my $cgi      = Bugzilla->cgi;
+my $dbh      = Bugzilla->dbh;
 my $template = Bugzilla->template;
-my $vars = {};
-my $buffer = $cgi->query_string();
+my $vars     = {};
+my $buffer   = $cgi->query_string();
 
-my $user = Bugzilla->login();
+my $user   = Bugzilla->login();
 my $userid = $user->id;
 
 if ($cgi->param('nukedefaultquery')) {
-    if ($userid) {
-        my $token = $cgi->param('token');
-        check_hash_token($token, ['nukedefaultquery']);
-        my $named_queries = Bugzilla::Search::Saved->match(
-            { userid => $userid, name => DEFAULT_QUERY_NAME });
-        if (@$named_queries) {
-            $named_queries->[0]->remove_from_db();
-        }
+  if ($userid) {
+    my $token = $cgi->param('token');
+    check_hash_token($token, ['nukedefaultquery']);
+    my $named_queries = Bugzilla::Search::Saved->match(
+      {userid => $userid, name => DEFAULT_QUERY_NAME});
+    if (@$named_queries) {
+      $named_queries->[0]->remove_from_db();
     }
-    $buffer = "";
+  }
+  $buffer = "";
 }
 
 # We are done with changes committed to the DB.
@@ -93,10 +94,10 @@ $dbh = Bugzilla->switch_to_shadow_db;
 
 my $userdefaultquery;
 if ($userid) {
-    $userdefaultquery = $dbh->selectrow_array(
-        "SELECT query FROM namedqueries " .
-         "WHERE userid = ? AND name = ?",
-         undef, ($userid, DEFAULT_QUERY_NAME));
+  $userdefaultquery
+    = $dbh->selectrow_array(
+    "SELECT query FROM namedqueries " . "WHERE userid = ? AND name = ?",
+    undef, ($userid, DEFAULT_QUERY_NAME));
 }
 
 local our %default;
@@ -105,74 +106,77 @@ local our %default;
 # Items which are single-valued, the template should only reference [0]
 # and ignore any multiple values.
 sub PrefillForm {
-    my ($buf) = @_;
-    my $cgi = Bugzilla->cgi;
-    $buf = new Bugzilla::CGI($buf);
-    my $foundone = 0;
-
-    # If there are old-style boolean charts in the URL (from an old saved
-    # search or from an old link on the web somewhere) then convert them
-    # to the new "custom search" format so that the form is populated
-    # properly.
-    my $any_boolean_charts = grep { /^field-?\d+/ } $buf->multi_param();
-    if ($any_boolean_charts) {
-        my $search = new Bugzilla::Search(params => scalar $buf->Vars);
-        $search->boolean_charts_to_custom_search($buf);
+  my ($buf) = @_;
+  my $cgi = Bugzilla->cgi;
+  $buf = new Bugzilla::CGI($buf);
+  my $foundone = 0;
+
+  # If there are old-style boolean charts in the URL (from an old saved
+  # search or from an old link on the web somewhere) then convert them
+  # to the new "custom search" format so that the form is populated
+  # properly.
+  my $any_boolean_charts = grep {/^field-?\d+/} $buf->multi_param();
+  if ($any_boolean_charts) {
+    my $search = new Bugzilla::Search(params => scalar $buf->Vars);
+    $search->boolean_charts_to_custom_search($buf);
+  }
+
+  # Query parameters that don't represent form fields on this page.
+  my @skip = qw(format query_format list_id columnlist);
+
+  # Iterate over the URL parameters
+  foreach my $name ($buf->multi_param()) {
+    next if grep { $_ eq $name } @skip;
+    $foundone = 1;
+    my @values = $buf->multi_param($name);
+
+    # If the name is a single letter followed by numbers, it's part
+    # of Custom Search. We store these as an array of hashes.
+    if ($name =~ /^([[:lower:]])(\d+)$/) {
+      $default{'custom_search'}->[$2]->{$1} = $values[0];
     }
 
-    # Query parameters that don't represent form fields on this page.
-    my @skip = qw(format query_format list_id columnlist);
-
-    # Iterate over the URL parameters
-    foreach my $name ($buf->multi_param()) {
-        next if grep { $_ eq $name } @skip;
-        $foundone = 1;
-        my @values = $buf->multi_param($name);
-
-        # If the name is a single letter followed by numbers, it's part
-        # of Custom Search. We store these as an array of hashes.
-        if ($name =~ /^([[:lower:]])(\d+)$/) {
-            $default{'custom_search'}->[$2]->{$1} = $values[0];
-        }
-        # If the name ends in a number (which it does for the fields which
-        # are part of the email searching), we use the array
-        # positions to show the defaults for that number field.
-        elsif ($name =~ /^(\w+)(\d)$/) {
-            $default{$1}->[$2] = $values[0];
-        }
-        else {
-            push (@{ $default{$name} }, @values);
-        }
+    # If the name ends in a number (which it does for the fields which
+    # are part of the email searching), we use the array
+    # positions to show the defaults for that number field.
+    elsif ($name =~ /^(\w+)(\d)$/) {
+      $default{$1}->[$2] = $values[0];
+    }
+    else {
+      push(@{$default{$name}}, @values);
     }
+  }
 
-    return $foundone;
+  return $foundone;
 }
 
 if (!PrefillForm($buffer)) {
-    # Ah-hah, there was no form stuff specified.  Do it again with the
-    # default query.
-    if ($userdefaultquery) {
-        PrefillForm($userdefaultquery);
-    } else {
-        PrefillForm(Bugzilla->params->{"defaultquery"});
-    }
+
+  # Ah-hah, there was no form stuff specified.  Do it again with the
+  # default query.
+  if ($userdefaultquery) {
+    PrefillForm($userdefaultquery);
+  }
+  else {
+    PrefillForm(Bugzilla->params->{"defaultquery"});
+  }
 }
 
 # if using groups for entry, then we don't want people to see products they
 # don't have access to. Remove them from the list.
-my @selectable_products = sort {lc($a->name) cmp lc($b->name)}
-                               @{$user->get_selectable_products};
+my @selectable_products
+  = sort { lc($a->name) cmp lc($b->name) } @{$user->get_selectable_products};
 Bugzilla::Product::preload(\@selectable_products);
 $vars->{'product'} = \@selectable_products;
 
 # Create the component, version and milestone lists.
 foreach my $field (qw(components versions milestones)) {
-    get_product_values(\@selectable_products, $field, $vars);
+  get_product_values(\@selectable_products, $field, $vars);
 }
 
 # Create data structures representing each classification
 if (Bugzilla->params->{'useclassification'}) {
-    $vars->{'classification'} = $user->get_selectable_classifications;
+  $vars->{'classification'} = $user->get_selectable_classifications;
 }
 
 my @chfields;
@@ -182,76 +186,89 @@ push @chfields, "[Bug creation]";
 # This is what happens when you have variables whose definition depends
 # on the DB schema, and then the underlying schema changes...
 foreach my $val (editable_bug_fields()) {
-    if ($val eq 'classification_id') {
-        $val = 'classification';
-    } elsif ($val eq 'product_id') {
-        $val = 'product';
-    } elsif ($val eq 'component_id') {
-        $val = 'component';
-    }
-    push @chfields, $val;
+  if ($val eq 'classification_id') {
+    $val = 'classification';
+  }
+  elsif ($val eq 'product_id') {
+    $val = 'product';
+  }
+  elsif ($val eq 'component_id') {
+    $val = 'component';
+  }
+  push @chfields, $val;
 }
 
 if ($user->is_timetracker) {
-    push @chfields, "work_time";
-} else {
-    foreach my $tt_field (TIMETRACKING_FIELDS) {
-        @chfields = grep($_ ne $tt_field, @chfields);
-    }
+  push @chfields, "work_time";
+}
+else {
+  foreach my $tt_field (TIMETRACKING_FIELDS) {
+    @chfields = grep($_ ne $tt_field, @chfields);
+  }
 }
 @chfields = (sort(@chfields));
 $vars->{'chfield'} = \@chfields;
-$vars->{'bug_status'} = Bugzilla::Field->new({name => 'bug_status'})->legal_values;
-$vars->{'rep_platform'} = Bugzilla::Field->new({name => 'rep_platform'})->legal_values;
-$vars->{'op_sys'} = Bugzilla::Field->new({name => 'op_sys'})->legal_values;
+$vars->{'bug_status'}
+  = Bugzilla::Field->new({name => 'bug_status'})->legal_values;
+$vars->{'rep_platform'}
+  = Bugzilla::Field->new({name => 'rep_platform'})->legal_values;
+$vars->{'op_sys'}   = Bugzilla::Field->new({name => 'op_sys'})->legal_values;
 $vars->{'priority'} = Bugzilla::Field->new({name => 'priority'})->legal_values;
-$vars->{'bug_severity'} = Bugzilla::Field->new({name => 'bug_severity'})->legal_values;
-$vars->{'resolution'} = Bugzilla::Field->new({name => 'resolution'})->legal_values;
+$vars->{'bug_severity'}
+  = Bugzilla::Field->new({name => 'bug_severity'})->legal_values;
+$vars->{'resolution'}
+  = Bugzilla::Field->new({name => 'resolution'})->legal_values;
 
 # grab custom fields
 my @custom_fields = Bugzilla->active_custom_fields;
 $vars->{'custom_fields'} = \@custom_fields;
 foreach my $cf (@custom_fields) {
-    if ($cf->type == FIELD_TYPE_SINGLE_SELECT || $cf->type == FIELD_TYPE_MULTI_SELECT) {
-        $vars->{$cf->name} = $cf->legal_values;
-    }
+  if ( $cf->type == FIELD_TYPE_SINGLE_SELECT
+    || $cf->type == FIELD_TYPE_MULTI_SELECT)
+  {
+    $vars->{$cf->name} = $cf->legal_values;
+  }
 }
 
 # Boolean charts
-my @fields = @{ Bugzilla->fields({ obsolete => 0 }) };
+my @fields = @{Bugzilla->fields({obsolete => 0})};
 
 my %exclude_fields = ();
 
 # If we're not in the time-tracking group, exclude time-tracking fields.
 if (!$user->is_timetracker) {
-    foreach my $tt_field (TIMETRACKING_FIELDS) {
-        $exclude_fields{$tt_field} = 1;
-    }
+  foreach my $tt_field (TIMETRACKING_FIELDS) {
+    $exclude_fields{$tt_field} = 1;
+  }
 }
 
 # Exclude fields turned off by params
-my %param_controlled_fields = ('useqacontact'        => 'qa_contact',
-                               'usetargetmilestone'  => 'target_milestone',
-                               'useclassification'   => 'classification',
-                               'usestatuswhiteboard' => 'status_whiteboard');
+my %param_controlled_fields = (
+  'useqacontact'        => 'qa_contact',
+  'usetargetmilestone'  => 'target_milestone',
+  'useclassification'   => 'classification',
+  'usestatuswhiteboard' => 'status_whiteboard'
+);
 
 while (my ($param, $field) = each %param_controlled_fields) {
-    $exclude_fields{$field} = 1 unless Bugzilla->params->{$param};
+  $exclude_fields{$field} = 1 unless Bugzilla->params->{$param};
 }
 
 @fields = grep(!$exclude_fields{$_->name}, @fields);
 
-@fields = sort {lc($a->description) cmp lc($b->description)} @fields;
-unshift(@fields, { name => "noop", description => "---" });
+@fields = sort { lc($a->description) cmp lc($b->description) } @fields;
+unshift(@fields, {name => "noop", description => "---"});
 $vars->{'fields'} = \@fields;
 
 # Named queries
 if ($userid) {
-     $vars->{'namedqueries'} = $dbh->selectcol_arrayref(
-           "SELECT name FROM namedqueries " .
-            "WHERE userid = ? AND name != ? " .
-         "ORDER BY name",
-         undef, ($userid, DEFAULT_QUERY_NAME));
+  $vars->{'namedqueries'} = $dbh->selectcol_arrayref(
+    "SELECT name FROM namedqueries "
+      . "WHERE userid = ? AND name != ? "
+      . "ORDER BY name",
+    undef,
+    ($userid, DEFAULT_QUERY_NAME)
+  );
 }
 
 # Sort order
@@ -259,20 +276,21 @@ my $deforder;
 my @orders = ('Bug Number', 'Importance', 'Assignee', 'Last Changed');
 
 if ($cgi->cookie('LASTORDER')) {
-    $deforder = "Reuse same sort as last time";
-    unshift(@orders, $deforder);
+  $deforder = "Reuse same sort as last time";
+  unshift(@orders, $deforder);
 }
 
 if ($cgi->param('order')) { $deforder = $cgi->param('order') }
 
 $vars->{'userdefaultquery'} = $userdefaultquery;
-$vars->{'orders'} = \@orders;
+$vars->{'orders'}           = \@orders;
 $default{'order'} = [$deforder || 'Importance'];
 
-if (($cgi->param('query_format') || $cgi->param('format') || "")
-    eq "create-series") {
-    require Bugzilla::Chart;
-    $vars->{'category'} = Bugzilla::Chart::getVisibleSeries();
+if (
+  ($cgi->param('query_format') || $cgi->param('format') || "") eq "create-series")
+{
+  require Bugzilla::Chart;
+  $vars->{'category'} = Bugzilla::Chart::getVisibleSeries();
 }
 
 $vars->{'known_name'} = $cgi->param('known_name');
@@ -282,33 +300,38 @@ $vars->{'columnlist'} = $cgi->param('columnlist');
 # Add in the defaults.
 $vars->{'default'} = \%default;
 
-$vars->{'format'} = $cgi->param('format');
+$vars->{'format'}       = $cgi->param('format');
 $vars->{'query_format'} = $cgi->param('query_format');
 
 # Set default page to "specific" if none provided
 if (!($cgi->param('query_format') || $cgi->param('format'))) {
-    if (defined $cgi->cookie('DEFAULTFORMAT')) {
-        $vars->{'format'} = $cgi->cookie('DEFAULTFORMAT');
-    } else {
-        $vars->{'format'} = 'specific';
-    }
+  if (defined $cgi->cookie('DEFAULTFORMAT')) {
+    $vars->{'format'} = $cgi->cookie('DEFAULTFORMAT');
+  }
+  else {
+    $vars->{'format'} = 'specific';
+  }
 }
 
 # Set cookie to current format as default, but only if the format
 # one that we should remember.
 if (defined($vars->{'format'}) && IsValidQueryType($vars->{'format'})) {
-    $cgi->send_cookie(-name => 'DEFAULTFORMAT',
-                      -value => $vars->{'format'},
-                      -expires => "Fri, 01-Jan-2038 00:00:00 GMT");
+  $cgi->send_cookie(
+    -name    => 'DEFAULTFORMAT',
+    -value   => $vars->{'format'},
+    -expires => "Fri, 01-Jan-2038 00:00:00 GMT"
+  );
 }
 
 # Generate and return the UI (HTML page) from the appropriate template.
 # If we submit back to ourselves (for e.g. boolean charts), we need to
 # preserve format information; hence query_format taking priority over
 # format.
-my $format = $template->get_format("search/search",
-                                   $vars->{'query_format'} || $vars->{'format'},
-                                   scalar $cgi->param('ctype'));
+my $format = $template->get_format(
+  "search/search",
+  $vars->{'query_format'} || $vars->{'format'},
+  scalar $cgi->param('ctype')
+);
 
 Bugzilla::Hook::process("query_format", {'vars' => $vars, 'format' => $format});
 
index cddaabbf440b8209d192ea9d911688b39e615ae1..9300c074037168fcde891f48ada7fd6458ee5eb8 100755 (executable)
--- a/quips.cgi
+++ b/quips.cgi
@@ -21,120 +21,127 @@ use Bugzilla::Token;
 
 my $user = Bugzilla->login(LOGIN_REQUIRED);
 
-my $cgi = Bugzilla->cgi;
-my $dbh = Bugzilla->dbh;
+my $cgi      = Bugzilla->cgi;
+my $dbh      = Bugzilla->dbh;
 my $template = Bugzilla->template;
-my $vars = {};
+my $vars     = {};
 
 my $action = $cgi->param('action') || "";
 my $token = $cgi->param('token');
 
 if ($action eq "show") {
-    # Read in the entire quip list
-    my $quipsref = $dbh->selectall_arrayref(
-                       "SELECT quipid, userid, quip, approved FROM quips ORDER BY quipid");
-
-    my $quips;
-    my @quipids;
-    foreach my $quipref (@$quipsref) {
-        my ($quipid, $userid, $quip, $approved) = @$quipref;
-        $quips->{$quipid} = {'userid' => $userid, 'quip' => $quip, 
-                             'approved' => $approved};
-        push(@quipids, $quipid);
-    }
 
-    my $users;
-    my $sth = $dbh->prepare("SELECT login_name FROM profiles WHERE userid = ?");
-    foreach my $quipid (@quipids) {
-        my $userid = $quips->{$quipid}{'userid'};
-        if ($userid && not defined $users->{$userid}) {
-            ($users->{$userid}) = $dbh->selectrow_array($sth, undef, $userid);
-        }
+  # Read in the entire quip list
+  my $quipsref = $dbh->selectall_arrayref(
+    "SELECT quipid, userid, quip, approved FROM quips ORDER BY quipid");
+
+  my $quips;
+  my @quipids;
+  foreach my $quipref (@$quipsref) {
+    my ($quipid, $userid, $quip, $approved) = @$quipref;
+    $quips->{$quipid}
+      = {'userid' => $userid, 'quip' => $quip, 'approved' => $approved};
+    push(@quipids, $quipid);
+  }
+
+  my $users;
+  my $sth = $dbh->prepare("SELECT login_name FROM profiles WHERE userid = ?");
+  foreach my $quipid (@quipids) {
+    my $userid = $quips->{$quipid}{'userid'};
+    if ($userid && not defined $users->{$userid}) {
+      ($users->{$userid}) = $dbh->selectrow_array($sth, undef, $userid);
     }
-    $vars->{'quipids'} = \@quipids;
-    $vars->{'quips'} = $quips;
-    $vars->{'users'} = $users;
-    $vars->{'show_quips'} = 1;
+  }
+  $vars->{'quipids'}    = \@quipids;
+  $vars->{'quips'}      = $quips;
+  $vars->{'users'}      = $users;
+  $vars->{'show_quips'} = 1;
 }
 
 if ($action eq "add") {
-    (Bugzilla->params->{'quip_list_entry_control'} eq "closed") &&
-      ThrowUserError("no_new_quips");
-
-    check_hash_token($token, ['create-quips']);
-    # Add the quip 
-    my $approved = (Bugzilla->params->{'quip_list_entry_control'} eq "open")
-                   || $user->in_group('bz_quip_moderators') || 0;
-    my $comment = $cgi->param("quip");
-    $comment || ThrowUserError("need_quip");
-    
-    ThrowUserError("quip_too_long", { length => length($comment) }) 
-        if length($comment) > MAX_QUIP_LENGTH;
-
-    trick_taint($comment); # Used in a placeholder below
-
-    $dbh->do("INSERT INTO quips (userid, quip, approved) VALUES (?, ?, ?)",
-             undef, ($user->id, $comment, $approved));
-
-    $vars->{'added_quip'} = $comment;
-    $vars->{'message'} = 'quips_added';
+  (Bugzilla->params->{'quip_list_entry_control'} eq "closed")
+    && ThrowUserError("no_new_quips");
+
+  check_hash_token($token, ['create-quips']);
+
+  # Add the quip
+  my $approved
+    = (Bugzilla->params->{'quip_list_entry_control'} eq "open")
+    || $user->in_group('bz_quip_moderators')
+    || 0;
+  my $comment = $cgi->param("quip");
+  $comment || ThrowUserError("need_quip");
+
+  ThrowUserError("quip_too_long", {length => length($comment)})
+    if length($comment) > MAX_QUIP_LENGTH;
+
+  trick_taint($comment);    # Used in a placeholder below
+
+  $dbh->do("INSERT INTO quips (userid, quip, approved) VALUES (?, ?, ?)",
+    undef, ($user->id, $comment, $approved));
+
+  $vars->{'added_quip'} = $comment;
+  $vars->{'message'}    = 'quips_added';
 }
 
 if ($action eq 'approve') {
-    $user->in_group('bz_quip_moderators')
-      || ThrowUserError("auth_failure", {group  => "bz_quip_moderators",
-                                         action => "approve",
-                                         object => "quips"});
-
-    check_hash_token($token, ['approve-quips']);
-    # Read in the entire quip list
-    my $quipsref = $dbh->selectall_arrayref("SELECT quipid, approved FROM quips");
-    
-    my %quips;
-    foreach my $quipref (@$quipsref) {
-        my ($quipid, $approved) = @$quipref;
-        $quips{$quipid} = $approved;
-    }
-
-    my @approved;
-    my @unapproved;
-    foreach my $quipid (keys %quips) {
-        # Must check for each quipid being defined for concurrency and
-        # automated usage where only one quipid might be defined.
-        my $quip = $cgi->param("quipid_$quipid") ? 1 : 0;
-        if(defined($cgi->param("defined_quipid_$quipid"))) {
-            if($quips{$quipid} != $quip) {
-                if($quip) { 
-                    push(@approved, $quipid); 
-                } else { 
-                    push(@unapproved, $quipid); 
-                }
-            }
+  $user->in_group('bz_quip_moderators')
+    || ThrowUserError("auth_failure",
+    {group => "bz_quip_moderators", action => "approve", object => "quips"});
+
+  check_hash_token($token, ['approve-quips']);
+
+  # Read in the entire quip list
+  my $quipsref = $dbh->selectall_arrayref("SELECT quipid, approved FROM quips");
+
+  my %quips;
+  foreach my $quipref (@$quipsref) {
+    my ($quipid, $approved) = @$quipref;
+    $quips{$quipid} = $approved;
+  }
+
+  my @approved;
+  my @unapproved;
+  foreach my $quipid (keys %quips) {
+
+    # Must check for each quipid being defined for concurrency and
+    # automated usage where only one quipid might be defined.
+    my $quip = $cgi->param("quipid_$quipid") ? 1 : 0;
+    if (defined($cgi->param("defined_quipid_$quipid"))) {
+      if ($quips{$quipid} != $quip) {
+        if ($quip) {
+          push(@approved, $quipid);
+        }
+        else {
+          push(@unapproved, $quipid);
         }
+      }
     }
-    $dbh->do("UPDATE quips SET approved = 1 WHERE quipid IN (" .
-            join(",", @approved) . ")") if($#approved > -1);
-    $dbh->do("UPDATE quips SET approved = 0 WHERE quipid IN (" .
-            join(",", @unapproved) . ")") if($#unapproved > -1);
-    $vars->{ 'approved' }   = \@approved;
-    $vars->{ 'unapproved' } = \@unapproved;
-    $vars->{'message'} = 'quips_approved_unapproved';
+  }
+  $dbh->do(
+    "UPDATE quips SET approved = 1 WHERE quipid IN (" . join(",", @approved) . ")")
+    if ($#approved > -1);
+  $dbh->do("UPDATE quips SET approved = 0 WHERE quipid IN ("
+      . join(",", @unapproved) . ")")
+    if ($#unapproved > -1);
+  $vars->{'approved'}   = \@approved;
+  $vars->{'unapproved'} = \@unapproved;
+  $vars->{'message'}    = 'quips_approved_unapproved';
 }
 
 if ($action eq "delete") {
-    $user->in_group('bz_quip_moderators')
-      || ThrowUserError("auth_failure", {group  => "bz_quip_moderators",
-                                         action => "delete",
-                                         object => "quips"});
-    my $quipid = $cgi->param("quipid");
-    detaint_natural($quipid) || ThrowUserError("need_quipid");
-    check_hash_token($token, ['quips', $quipid]);
-
-    ($vars->{'deleted_quip'}) = $dbh->selectrow_array(
-                                    "SELECT quip FROM quips WHERE quipid = ?",
-                                    undef, $quipid);
-    $dbh->do("DELETE FROM quips WHERE quipid = ?", undef, $quipid);
-    $vars->{'message'} = 'quips_deleted';
+  $user->in_group('bz_quip_moderators')
+    || ThrowUserError("auth_failure",
+    {group => "bz_quip_moderators", action => "delete", object => "quips"});
+  my $quipid = $cgi->param("quipid");
+  detaint_natural($quipid) || ThrowUserError("need_quipid");
+  check_hash_token($token, ['quips', $quipid]);
+
+  ($vars->{'deleted_quip'})
+    = $dbh->selectrow_array("SELECT quip FROM quips WHERE quipid = ?",
+    undef, $quipid);
+  $dbh->do("DELETE FROM quips WHERE quipid = ?", undef, $quipid);
+  $vars->{'message'} = 'quips_deleted';
 }
 
 print $cgi->header();
index 798bba00d50fd65a7274f75b0ceea79e6184004d..b0a91bd26510a3780cb33f4ecc423b38f2643d18 100755 (executable)
@@ -22,7 +22,7 @@ use Bugzilla::Util;
 use Date::Format;
 
 my $template = Bugzilla->template;
-my $cgi = Bugzilla->cgi;
+my $cgi      = Bugzilla->cgi;
 
 my $action = $cgi->param('action') || '';
 
@@ -30,152 +30,158 @@ my $vars = {};
 my $target;
 
 if (!$action) {
-    # redirect to index.cgi if no action is defined.
-    print $cgi->redirect(correct_urlbase() . 'index.cgi');
-    exit;
+
+  # redirect to index.cgi if no action is defined.
+  print $cgi->redirect(correct_urlbase() . 'index.cgi');
+  exit;
 }
+
 # prepare-sudo: Display the sudo information & login page
 elsif ($action eq 'prepare-sudo') {
-    # We must have a logged-in user to do this
-    # That user must be in the 'bz_sudoers' group
-    my $user = Bugzilla->login(LOGIN_REQUIRED);
-    unless ($user->in_group('bz_sudoers')) {
-        ThrowUserError('auth_failure', {  group => 'bz_sudoers',
-                                         action => 'begin',
-                                         object => 'sudo_session' }
-        );
-    }
-    
-    # Do not try to start a new session if one is already in progress!
-    if (defined(Bugzilla->sudoer)) {
-        ThrowUserError('sudo_in_progress', { target => $user->login });
-    }
-
-    # Keep a temporary record of the user visiting this page
-    $vars->{'token'} = issue_session_token('sudo_prepared');
-
-    # Show the sudo page
-    $vars->{'target_login_default'} = $cgi->param('target_login');
-    $vars->{'reason_default'} = $cgi->param('reason');
-    $target = 'admin/sudo.html.tmpl';
+
+  # We must have a logged-in user to do this
+  # That user must be in the 'bz_sudoers' group
+  my $user = Bugzilla->login(LOGIN_REQUIRED);
+  unless ($user->in_group('bz_sudoers')) {
+    ThrowUserError('auth_failure',
+      {group => 'bz_sudoers', action => 'begin', object => 'sudo_session'});
+  }
+
+  # Do not try to start a new session if one is already in progress!
+  if (defined(Bugzilla->sudoer)) {
+    ThrowUserError('sudo_in_progress', {target => $user->login});
+  }
+
+  # Keep a temporary record of the user visiting this page
+  $vars->{'token'} = issue_session_token('sudo_prepared');
+
+  # Show the sudo page
+  $vars->{'target_login_default'} = $cgi->param('target_login');
+  $vars->{'reason_default'}       = $cgi->param('reason');
+  $target                         = 'admin/sudo.html.tmpl';
 }
+
 # begin-sudo: Confirm login and start sudo session
 elsif ($action eq 'begin-sudo') {
-    my $user = Bugzilla->login(LOGIN_REQUIRED);
-
-    my $target_login = $cgi->param('target_login');
-    my $reason = $cgi->param('reason') || '';
-
-    if ($user->authorizer->can_login) {
-        my $password = $cgi->param('password')
-          or ThrowUserError('sudo_password_required',
-                            { target_login => $target_login, reason => $reason });
-        $user->check_current_password($password);
-    }
-
-    # The user must be in the 'bz_sudoers' group
-    unless ($user->in_group('bz_sudoers')) {
-        ThrowUserError('auth_failure', {  group => 'bz_sudoers',
-                                         action => 'begin',
-                                         object => 'sudo_session' }
-        );
-    }
-
-    # Do not try to start a new session if one is already in progress!
-    if (defined(Bugzilla->sudoer)) {
-        ThrowUserError('sudo_in_progress', { target => $user->login });
-    }
-
-    # Get & verify the target user (the user who we will be impersonating)
-    my $target_user = new Bugzilla::User({ name => $target_login });
-    unless (defined($target_user)
-            && $target_user->id
-            && $user->can_see_user($target_user))
-    {
-        ThrowUserError('user_match_failed', { name => $target_login });
-    }
-
-    if ($target_user->in_group('bz_sudo_protect')) {
-        ThrowUserError('sudo_protected', { login => $target_user->login });
-    }
-
-    # Did the user actually go trough the 'sudo-prepare' action?  Do some 
-    # checks on the token the action should have left.
-    my $token = $cgi->param('token');
-    my ($token_user, $token_timestamp, $token_data) =
-        Bugzilla::Token::GetTokenData($token);
-    unless (defined($token_user)
-            && defined($token_data)
-            && ($token_user == $user->id)
-            && ($token_data eq 'sudo_prepared'))
-    {
-        ThrowUserError('sudo_preparation_required', 
-                       { target_login => $target_login, reason => $reason });
-    }
-    delete_token($token);
-
-    # Calculate the session expiry time (T + 6 hours)
-    my $time_string = time2str('%a, %d-%b-%Y %T %Z', time + MAX_SUDO_TOKEN_AGE, 'GMT');
-
-    # For future sessions, store the unique ID of the target user
-    $token = Bugzilla::Token::_create_token($user->id, 'sudo', $target_user->id);
-
-    my %args;
-    if (Bugzilla->params->{ssl_redirect}) {
-        $args{'-secure'} = 1;
-    }
-
-    $cgi->send_cookie('-name'    => 'sudo',
-                      '-expires' => $time_string,
-                      '-value'   => $token,
-                      '-httponly' => 1,
-                      %args);
-
-    # For the present, change the values of Bugzilla::user & Bugzilla::sudoer
-    Bugzilla->sudo_request($target_user, $user);
-
-    # NOTE: If you want to log the start of an sudo session, do it here.
-
-    # If we have a reason passed in, keep it under 200 characters
-    $reason = substr($reason, 0, 200);
-
-    # Go ahead and send out the message now
-    my $message;
-    my $mail_template = Bugzilla->template_inner($target_user->setting('lang'));
-    $mail_template->process('email/sudo.txt.tmpl', { reason => $reason }, \$message);
-    MessageToMTA($message);
-
-    $vars->{'message'} = 'sudo_started';
-    $vars->{'target'} = $target_user->login;
-    $target = 'global/message.html.tmpl';
+  my $user = Bugzilla->login(LOGIN_REQUIRED);
+
+  my $target_login = $cgi->param('target_login');
+  my $reason = $cgi->param('reason') || '';
+
+  if ($user->authorizer->can_login) {
+    my $password = $cgi->param('password')
+      or ThrowUserError('sudo_password_required',
+      {target_login => $target_login, reason => $reason});
+    $user->check_current_password($password);
+  }
+
+  # The user must be in the 'bz_sudoers' group
+  unless ($user->in_group('bz_sudoers')) {
+    ThrowUserError('auth_failure',
+      {group => 'bz_sudoers', action => 'begin', object => 'sudo_session'});
+  }
+
+  # Do not try to start a new session if one is already in progress!
+  if (defined(Bugzilla->sudoer)) {
+    ThrowUserError('sudo_in_progress', {target => $user->login});
+  }
+
+  # Get & verify the target user (the user who we will be impersonating)
+  my $target_user = new Bugzilla::User({name => $target_login});
+  unless (defined($target_user)
+    && $target_user->id
+    && $user->can_see_user($target_user))
+  {
+    ThrowUserError('user_match_failed', {name => $target_login});
+  }
+
+  if ($target_user->in_group('bz_sudo_protect')) {
+    ThrowUserError('sudo_protected', {login => $target_user->login});
+  }
+
+  # Did the user actually go trough the 'sudo-prepare' action?  Do some
+  # checks on the token the action should have left.
+  my $token = $cgi->param('token');
+  my ($token_user, $token_timestamp, $token_data)
+    = Bugzilla::Token::GetTokenData($token);
+  unless (defined($token_user)
+    && defined($token_data)
+    && ($token_user == $user->id)
+    && ($token_data eq 'sudo_prepared'))
+  {
+    ThrowUserError('sudo_preparation_required',
+      {target_login => $target_login, reason => $reason});
+  }
+  delete_token($token);
+
+  # Calculate the session expiry time (T + 6 hours)
+  my $time_string
+    = time2str('%a, %d-%b-%Y %T %Z', time + MAX_SUDO_TOKEN_AGE, 'GMT');
+
+  # For future sessions, store the unique ID of the target user
+  $token = Bugzilla::Token::_create_token($user->id, 'sudo', $target_user->id);
+
+  my %args;
+  if (Bugzilla->params->{ssl_redirect}) {
+    $args{'-secure'} = 1;
+  }
+
+  $cgi->send_cookie(
+    '-name'     => 'sudo',
+    '-expires'  => $time_string,
+    '-value'    => $token,
+    '-httponly' => 1,
+    %args
+  );
+
+  # For the present, change the values of Bugzilla::user & Bugzilla::sudoer
+  Bugzilla->sudo_request($target_user, $user);
+
+  # NOTE: If you want to log the start of an sudo session, do it here.
+
+  # If we have a reason passed in, keep it under 200 characters
+  $reason = substr($reason, 0, 200);
+
+  # Go ahead and send out the message now
+  my $message;
+  my $mail_template = Bugzilla->template_inner($target_user->setting('lang'));
+  $mail_template->process('email/sudo.txt.tmpl', {reason => $reason}, \$message);
+  MessageToMTA($message);
+
+  $vars->{'message'} = 'sudo_started';
+  $vars->{'target'}  = $target_user->login;
+  $target            = 'global/message.html.tmpl';
 }
+
 # end-sudo: End the current sudo session (if one is in progress)
 elsif ($action eq 'end-sudo') {
-    # Regardless of our state, delete the sudo cookie if it exists
-    my $token = $cgi->cookie('sudo');
-    $cgi->remove_cookie('sudo');
-
-    # Are we in an sudo session?
-    Bugzilla->login(LOGIN_OPTIONAL);
-    my $sudoer = Bugzilla->sudoer;
-    if (defined($sudoer)) {
-        Bugzilla->sudo_request($sudoer, undef);
-    }
-    # Now that the session is over, remove the token from the DB.
-    delete_token($token);
-
-    # NOTE: If you want to log the end of an sudo session, so it here.
-    
-    $vars->{'message'} = 'sudo_ended';
-    $target = 'global/message.html.tmpl';
+
+  # Regardless of our state, delete the sudo cookie if it exists
+  my $token = $cgi->cookie('sudo');
+  $cgi->remove_cookie('sudo');
+
+  # Are we in an sudo session?
+  Bugzilla->login(LOGIN_OPTIONAL);
+  my $sudoer = Bugzilla->sudoer;
+  if (defined($sudoer)) {
+    Bugzilla->sudo_request($sudoer, undef);
+  }
+
+  # Now that the session is over, remove the token from the DB.
+  delete_token($token);
+
+  # NOTE: If you want to log the end of an sudo session, so it here.
+
+  $vars->{'message'} = 'sudo_ended';
+  $target = 'global/message.html.tmpl';
 }
+
 # No valid action found
 else {
-    Bugzilla->login(LOGIN_OPTIONAL);
-    ThrowUserError('unknown_action', {action => $action});
+  Bugzilla->login(LOGIN_OPTIONAL);
+  ThrowUserError('unknown_action', {action => $action});
 }
 
 # Display the template
 print $cgi->header();
-$template->process($target, $vars)
-      || ThrowTemplateError($template->error());
+$template->process($target, $vars) || ThrowTemplateError($template->error());
index 5c57246f711d65745096ceebbbe30549be549185..cad4ca5301f28aec6d820533d14abef9108eddc8 100755 (executable)
@@ -23,76 +23,80 @@ use Bugzilla::Token;
 
 use List::MoreUtils qw(uniq);
 
-my $cgi = Bugzilla->cgi;
+my $cgi      = Bugzilla->cgi;
 my $template = Bugzilla->template;
-my $vars = {};
+my $vars     = {};
 
 # Go straight back to query.cgi if we are adding a boolean chart.
 if (grep(/^cmd-/, $cgi->multi_param())) {
-    my $params = $cgi->canonicalise_query("format", "ctype");
-    my $location = "query.cgi?format=" . $cgi->param('query_format') . 
-      ($params ? "&$params" : "");
-
-    print $cgi->redirect($location);
-    exit;
+  my $params = $cgi->canonicalise_query("format", "ctype");
+  my $location
+    = "query.cgi?format="
+    . $cgi->param('query_format')
+    . ($params ? "&$params" : "");
+
+  print $cgi->redirect($location);
+  exit;
 }
 
 Bugzilla->login();
 my $action = $cgi->param('action') || 'menu';
-my $token  = $cgi->param('token');
+my $token = $cgi->param('token');
 
 if ($action eq "menu") {
-    # No need to do any searching in this case, so bail out early.
-    print $cgi->header();
-    $template->process("reports/menu.html.tmpl", $vars)
-      || ThrowTemplateError($template->error());
-    exit;
+
+  # No need to do any searching in this case, so bail out early.
+  print $cgi->header();
+  $template->process("reports/menu.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
+  exit;
 
 }
 elsif ($action eq 'add') {
-    my $user = Bugzilla->login(LOGIN_REQUIRED);
-    check_hash_token($token, ['save_report']);
-
-    my $name = clean_text(scalar $cgi->param('name'));
-    my $query = $cgi->param('query');
-
-    if (my ($report) = grep{ lc($_->name) eq lc($name) } @{$user->reports}) {
-        $report->set_query($query);
-        $report->update;
-        $vars->{'message'} = "report_updated";
-    } else {
-        my $report = Bugzilla::Report->create({name => $name, query => $query});
-        $vars->{'message'} = "report_created";
-    }
+  my $user = Bugzilla->login(LOGIN_REQUIRED);
+  check_hash_token($token, ['save_report']);
+
+  my $name  = clean_text(scalar $cgi->param('name'));
+  my $query = $cgi->param('query');
+
+  if (my ($report) = grep { lc($_->name) eq lc($name) } @{$user->reports}) {
+    $report->set_query($query);
+    $report->update;
+    $vars->{'message'} = "report_updated";
+  }
+  else {
+    my $report = Bugzilla::Report->create({name => $name, query => $query});
+    $vars->{'message'} = "report_created";
+  }
 
-    $user->flush_reports_cache;
+  $user->flush_reports_cache;
 
-    print $cgi->header();
+  print $cgi->header();
 
-    $vars->{'reportname'} = $name;
+  $vars->{'reportname'} = $name;
 
-    $template->process("global/message.html.tmpl", $vars)
-      || ThrowTemplateError($template->error());
-    exit;
+  $template->process("global/message.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
+  exit;
 }
 elsif ($action eq 'del') {
-    my $user = Bugzilla->login(LOGIN_REQUIRED);
-    my $report_id = $cgi->param('saved_report_id');
-    check_hash_token($token, ['delete_report', $report_id]);
+  my $user      = Bugzilla->login(LOGIN_REQUIRED);
+  my $report_id = $cgi->param('saved_report_id');
+  check_hash_token($token, ['delete_report', $report_id]);
 
-    my $report = Bugzilla::Report->check({id => $report_id});
-    $report->remove_from_db();
+  my $report = Bugzilla::Report->check({id => $report_id});
+  $report->remove_from_db();
 
-    $user->flush_reports_cache;
+  $user->flush_reports_cache;
 
-    print $cgi->header();
+  print $cgi->header();
 
-    $vars->{'message'} = 'report_deleted';
-    $vars->{'reportname'} = $report->name;
+  $vars->{'message'}    = 'report_deleted';
+  $vars->{'reportname'} = $report->name;
 
-    $template->process("global/message.html.tmpl", $vars)
-      || ThrowTemplateError($template->error());
-    exit;
+  $template->process("global/message.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
+  exit;
 }
 
 # Sanitize the URL, to make URLs shorter.
@@ -103,16 +107,15 @@ my $row_field = $cgi->param('y_axis_field') || '';
 my $tbl_field = $cgi->param('z_axis_field') || '';
 
 if (!($col_field || $row_field || $tbl_field)) {
-    ThrowUserError("no_axes_defined");
+  ThrowUserError("no_axes_defined");
 }
 
 # There is no UI for these parameters anymore,
 # but they are still here just in case.
-my $width = $cgi->param('width') || 1024;
+my $width  = $cgi->param('width')  || 1024;
 my $height = $cgi->param('height') || 600;
 
-(detaint_natural($width) && $width > 0)
-  || ThrowUserError("invalid_dimensions");
+(detaint_natural($width) && $width > 0) || ThrowUserError("invalid_dimensions");
 $width <= 2000 || ThrowUserError("chart_too_large");
 
 (detaint_natural($height) && $height > 0)
@@ -121,50 +124,52 @@ $height <= 2000 || ThrowUserError("chart_too_large");
 
 my $formatparam = $cgi->param('format') || '';
 
-# These shenanigans are necessary to make sure that both vertical and 
+# These shenanigans are necessary to make sure that both vertical and
 # horizontal 1D tables convert to the correct dimension when you ask to
 # display them as some sort of chart.
 if ($formatparam eq "table") {
-    if ($col_field && !$row_field) {    
-        # 1D *tables* should be displayed vertically (with a row_field only)
-        $row_field = $col_field;
-        $col_field = '';
-    }
+  if ($col_field && !$row_field) {
+
+    # 1D *tables* should be displayed vertically (with a row_field only)
+    $row_field = $col_field;
+    $col_field = '';
+  }
 }
 else {
-    if (!Bugzilla->feature('graphical_reports')) {
-        ThrowUserError('feature_disabled', { feature => 'graphical_reports' });
-    }
+  if (!Bugzilla->feature('graphical_reports')) {
+    ThrowUserError('feature_disabled', {feature => 'graphical_reports'});
+  }
 
-    if ($row_field && !$col_field) {
-        # 1D *charts* should be displayed horizontally (with an col_field only)
-        $col_field = $row_field;
-        $row_field = '';
-    }
+  if ($row_field && !$col_field) {
+
+    # 1D *charts* should be displayed horizontally (with an col_field only)
+    $col_field = $row_field;
+    $row_field = '';
+  }
 }
 
 # Valid bug fields that can be reported on.
 my $valid_columns = Bugzilla::Search::REPORT_COLUMNS;
 
 # Validate the values in the axis fields or throw an error.
-!$row_field 
+!$row_field
   || ($valid_columns->{$row_field} && trick_taint($row_field))
   || ThrowUserError("report_axis_invalid", {fld => "x", val => $row_field});
-!$col_field 
+!$col_field
   || ($valid_columns->{$col_field} && trick_taint($col_field))
   || ThrowUserError("report_axis_invalid", {fld => "y", val => $col_field});
-!$tbl_field 
+!$tbl_field
   || ($valid_columns->{$tbl_field} && trick_taint($tbl_field))
   || ThrowUserError("report_axis_invalid", {fld => "z", val => $tbl_field});
 
-my @axis_fields = grep { $_ } ($row_field, $col_field, $tbl_field);
+my @axis_fields = grep {$_} ($row_field, $col_field, $tbl_field);
 
 # Clone the params, so that Bugzilla::Search can modify them
 my $params = new Bugzilla::CGI($cgi);
 my $search = new Bugzilla::Search(
-    fields => \@axis_fields, 
-    params => scalar $params->Vars,
-    allow_unlimited => 1,
+  fields          => \@axis_fields,
+  params          => scalar $params->Vars,
+  allow_unlimited => 1,
 );
 
 $::SIG{TERM} = 'DEFAULT';
@@ -173,7 +178,7 @@ $::SIG{PIPE} = 'DEFAULT';
 Bugzilla->switch_to_shadow_db();
 my ($results, $extra_data) = $search->data;
 
-# We have a hash of hashes for the data itself, and a hash to hold the 
+# We have a hash of hashes for the data itself, and a hash to hold the
 # row/col/table names.
 my %data;
 my %names;
@@ -188,63 +193,63 @@ my $row_isnumeric = 1;
 my $tbl_isnumeric = 1;
 
 # define which fields are multiselect
-my @multi_selects = map { $_->name } @{Bugzilla->fields(
-    {
-        obsolete => 0,
-        type => [FIELD_TYPE_MULTI_SELECT, FIELD_TYPE_KEYWORDS]
-    }
-)};
-my $col_ismultiselect = scalar grep {$col_field eq $_} @multi_selects;
-my $row_ismultiselect = scalar grep {$row_field eq $_} @multi_selects;
-my $tbl_ismultiselect = scalar grep {$tbl_field eq $_} @multi_selects;
+my @multi_selects
+  = map { $_->name }
+  @{Bugzilla->fields({
+    obsolete => 0, type => [FIELD_TYPE_MULTI_SELECT, FIELD_TYPE_KEYWORDS]
+  })};
+my $col_ismultiselect = scalar grep { $col_field eq $_ } @multi_selects;
+my $row_ismultiselect = scalar grep { $row_field eq $_ } @multi_selects;
+my $tbl_ismultiselect = scalar grep { $tbl_field eq $_ } @multi_selects;
 
 
 foreach my $result (@$results) {
-    # handle empty dimension member names
-    
-    my @rows = check_value($row_field, $result, $row_ismultiselect);
-    my @cols = check_value($col_field, $result, $col_ismultiselect);
-    my @tbls = check_value($tbl_field, $result, $tbl_ismultiselect);
-
-    my %in_total_row;
-    my %in_total_col;
-    for my $tbl (@tbls) {
-        my %in_row_total;
-        for my $col (@cols) {
-            for my $row (@rows) {
-                $data{$tbl}{$col}{$row}++;
-                $names{"row"}{$row}++;
-                $row_isnumeric &&= ($row =~ /^-?\d+(\.\d+)?$/o);
-                if ($formatparam eq "table") {
-                    if (!$in_row_total{$row}) {
-                        $data{$tbl}{'-total-'}{$row}++;
-                        $in_row_total{$row} = 1;
-                    }
-                    if (!$in_total_row{$row}) {
-                        $data{'-total-'}{'-total-'}{$row}++;
-                        $in_total_row{$row} = 1;
-                    }
-                }
-            }
-            if ($formatparam eq "table") {
-                $data{$tbl}{$col}{'-total-'}++;
-                if (!$in_total_col{$col}) {
-                    $data{'-total-'}{$col}{'-total-'}++;
-                    $in_total_col{$col} = 1;
-                }
-            }
-            $names{"col"}{$col}++;
-            $col_isnumeric &&= ($col =~ /^-?\d+(\.\d+)?$/o);
-        }
-        $names{"tbl"}{$tbl}++;
-        $tbl_isnumeric &&= ($tbl =~ /^-?\d+(\.\d+)?$/o);
+
+  # handle empty dimension member names
+
+  my @rows = check_value($row_field, $result, $row_ismultiselect);
+  my @cols = check_value($col_field, $result, $col_ismultiselect);
+  my @tbls = check_value($tbl_field, $result, $tbl_ismultiselect);
+
+  my %in_total_row;
+  my %in_total_col;
+  for my $tbl (@tbls) {
+    my %in_row_total;
+    for my $col (@cols) {
+      for my $row (@rows) {
+        $data{$tbl}{$col}{$row}++;
+        $names{"row"}{$row}++;
+        $row_isnumeric &&= ($row =~ /^-?\d+(\.\d+)?$/o);
         if ($formatparam eq "table") {
-            $data{$tbl}{'-total-'}{'-total-'}++;
+          if (!$in_row_total{$row}) {
+            $data{$tbl}{'-total-'}{$row}++;
+            $in_row_total{$row} = 1;
+          }
+          if (!$in_total_row{$row}) {
+            $data{'-total-'}{'-total-'}{$row}++;
+            $in_total_row{$row} = 1;
+          }
+        }
+      }
+      if ($formatparam eq "table") {
+        $data{$tbl}{$col}{'-total-'}++;
+        if (!$in_total_col{$col}) {
+          $data{'-total-'}{$col}{'-total-'}++;
+          $in_total_col{$col} = 1;
         }
+      }
+      $names{"col"}{$col}++;
+      $col_isnumeric &&= ($col =~ /^-?\d+(\.\d+)?$/o);
     }
+    $names{"tbl"}{$tbl}++;
+    $tbl_isnumeric &&= ($tbl =~ /^-?\d+(\.\d+)?$/o);
     if ($formatparam eq "table") {
-        $data{'-total-'}{'-total-'}{'-total-'}++;
+      $data{$tbl}{'-total-'}{'-total-'}++;
     }
+  }
+  if ($formatparam eq "table") {
+    $data{'-total-'}{'-total-'}{'-total-'}++;
+  }
 }
 
 my @col_names = get_names($names{"col"}, $col_isnumeric, $col_field);
@@ -255,106 +260,112 @@ my @tbl_names = get_names($names{"tbl"}, $tbl_isnumeric, $tbl_field);
 # gathered everything into the hashes and made sure we know the size of the
 # data, we reformat it into an array of arrays of arrays of data.
 push(@tbl_names, "-total-") if (scalar(@tbl_names) > 1);
-    
+
 my @image_data;
 foreach my $tbl (@tbl_names) {
-    my @tbl_data;
-    push(@tbl_data, \@col_names);
-    foreach my $row (@row_names) {
-        my @col_data;
-        foreach my $col (@col_names) {
-            $data{$tbl}{$col}{$row} = $data{$tbl}{$col}{$row} || 0;
-            push(@col_data, $data{$tbl}{$col}{$row});
-            if ($tbl ne "-total-") {
-                # This is a bit sneaky. We spend every loop except the last
-                # building up the -total- data, and then last time round,
-                # we process it as another tbl, and push() the total values 
-                # into the image_data array.
-                $data{"-total-"}{$col}{$row} += $data{$tbl}{$col}{$row};
-            }
-        }
-
-        push(@tbl_data, \@col_data);
+  my @tbl_data;
+  push(@tbl_data, \@col_names);
+  foreach my $row (@row_names) {
+    my @col_data;
+    foreach my $col (@col_names) {
+      $data{$tbl}{$col}{$row} = $data{$tbl}{$col}{$row} || 0;
+      push(@col_data, $data{$tbl}{$col}{$row});
+      if ($tbl ne "-total-") {
+
+        # This is a bit sneaky. We spend every loop except the last
+        # building up the -total- data, and then last time round,
+        # we process it as another tbl, and push() the total values
+        # into the image_data array.
+        $data{"-total-"}{$col}{$row} += $data{$tbl}{$col}{$row};
+      }
     }
-    
-    unshift(@image_data, \@tbl_data);
+
+    push(@tbl_data, \@col_data);
+  }
+
+  unshift(@image_data, \@tbl_data);
 }
 
 $vars->{'col_field'} = $col_field;
 $vars->{'row_field'} = $row_field;
 $vars->{'tbl_field'} = $tbl_field;
-$vars->{'time'} = localtime(time());
+$vars->{'time'}      = localtime(time());
 
-$vars->{'col_names'} = \@col_names;
-$vars->{'row_names'} = \@row_names;
-$vars->{'tbl_names'} = \@tbl_names;
+$vars->{'col_names'}         = \@col_names;
+$vars->{'row_names'}         = \@row_names;
+$vars->{'tbl_names'}         = \@tbl_names;
 $vars->{'note_multi_select'} = $row_ismultiselect || $col_ismultiselect;
 
 # Below a certain width, we don't see any bars, so there needs to be a minimum.
 if ($formatparam eq "bar") {
-    my $min_width = (scalar(@col_names) || 1) * 20;
+  my $min_width = (scalar(@col_names) || 1) * 20;
 
-    if (!$cgi->param('cumulate')) {
-        $min_width *= (scalar(@row_names) || 1);
-    }
+  if (!$cgi->param('cumulate')) {
+    $min_width *= (scalar(@row_names) || 1);
+  }
 
-    $vars->{'min_width'} = $min_width;
+  $vars->{'min_width'} = $min_width;
 }
 
-$vars->{'width'} = $width;
-$vars->{'height'} = $height;
-$vars->{'queries'} = $extra_data;
+$vars->{'width'}           = $width;
+$vars->{'height'}          = $height;
+$vars->{'queries'}         = $extra_data;
 $vars->{'saved_report_id'} = $cgi->param('saved_report_id');
 
-if ($cgi->param('debug')
-    && Bugzilla->params->{debug_group}
-    && Bugzilla->user->in_group(Bugzilla->params->{debug_group})
-{
-    $vars->{'debug'} = 1;
+if ( $cgi->param('debug')
+  && Bugzilla->params->{debug_group}
+  && Bugzilla->user->in_group(Bugzilla->params->{debug_group}))
+{
+  $vars->{'debug'} = 1;
 }
 
 if ($action eq "wrap") {
-    # So which template are we using? If action is "wrap", we will be using
-    # no format (it gets passed through to be the format of the actual data),
-    # and either report.csv.tmpl (CSV), or report.html.tmpl (everything else).
-    # report.html.tmpl produces an HTML framework for either tables of HTML
-    # data, or images generated by calling report.cgi again with action as
-    # "plot".
-    $formatparam =~ s/[^a-zA-Z\-]//g;
-    $vars->{'format'} = $formatparam;
-    $formatparam = '';
-
-    # We need to keep track of the defined restrictions on each of the 
-    # axes, because buglistbase, below, throws them away. Without this, we
-    # get buglistlinks wrong if there is a restriction on an axis field.
-    $vars->{'col_vals'} = get_field_restrictions($col_field);
-    $vars->{'row_vals'} = get_field_restrictions($row_field);
-    $vars->{'tbl_vals'} = get_field_restrictions($tbl_field);
-
-    # We need a number of different variants of the base URL for different
-    # URLs in the HTML.
-    $vars->{'buglistbase'} = $cgi->canonicalise_query(
-                                 "x_axis_field", "y_axis_field", "z_axis_field",
-                               "ctype", "format", "query_format", @axis_fields);
-    $vars->{'imagebase'}   = $cgi->canonicalise_query( 
-                    $tbl_field, "action", "ctype", "format", "width", "height");
-    $vars->{'switchbase'}  = $cgi->canonicalise_query( 
-                "query_format", "action", "ctype", "format", "width", "height");
-    $vars->{'data'} = \%data;
+
+  # So which template are we using? If action is "wrap", we will be using
+  # no format (it gets passed through to be the format of the actual data),
+  # and either report.csv.tmpl (CSV), or report.html.tmpl (everything else).
+  # report.html.tmpl produces an HTML framework for either tables of HTML
+  # data, or images generated by calling report.cgi again with action as
+  # "plot".
+  $formatparam =~ s/[^a-zA-Z\-]//g;
+  $vars->{'format'} = $formatparam;
+  $formatparam = '';
+
+  # We need to keep track of the defined restrictions on each of the
+  # axes, because buglistbase, below, throws them away. Without this, we
+  # get buglistlinks wrong if there is a restriction on an axis field.
+  $vars->{'col_vals'} = get_field_restrictions($col_field);
+  $vars->{'row_vals'} = get_field_restrictions($row_field);
+  $vars->{'tbl_vals'} = get_field_restrictions($tbl_field);
+
+  # We need a number of different variants of the base URL for different
+  # URLs in the HTML.
+  $vars->{'buglistbase'} = $cgi->canonicalise_query(
+    "x_axis_field", "y_axis_field", "z_axis_field", "ctype",
+    "format",       "query_format", @axis_fields
+  );
+  $vars->{'imagebase'}
+    = $cgi->canonicalise_query($tbl_field, "action", "ctype", "format", "width",
+    "height");
+  $vars->{'switchbase'}
+    = $cgi->canonicalise_query("query_format", "action", "ctype", "format",
+    "width", "height");
+  $vars->{'data'} = \%data;
 }
 elsif ($action eq "plot") {
-    # If action is "plot", we will be using a format as normal (pie, bar etc.)
-    # and a ctype as normal (currently only png.)
-    $vars->{'cumulate'} = $cgi->param('cumulate') ? 1 : 0;
-    $vars->{'x_labels_vertical'} = $cgi->param('x_labels_vertical') ? 1 : 0;
-    $vars->{'data'} = \@image_data;
+
+  # If action is "plot", we will be using a format as normal (pie, bar etc.)
+  # and a ctype as normal (currently only png.)
+  $vars->{'cumulate'}          = $cgi->param('cumulate')          ? 1 : 0;
+  $vars->{'x_labels_vertical'} = $cgi->param('x_labels_vertical') ? 1 : 0;
+  $vars->{'data'}              = \@image_data;
 }
 else {
-    ThrowUserError('unknown_action', {action => $action});
+  ThrowUserError('unknown_action', {action => $action});
 }
 
 my $format = $template->get_format("reports/report", $formatparam,
-                                   scalar($cgi->param('ctype')));
+  scalar($cgi->param('ctype')));
 
 # If we get a template or CGI error, it comes out as HTML, which isn't valid
 # PNG data, and the browser just displays a "corrupt PNG" message. So, you can
@@ -367,11 +378,11 @@ print $cgi->header($format->{'ctype'});
 # Problems with this CGI are often due to malformed data. Setting debug=1
 # prints out both data structures.
 if ($cgi->param('debug')) {
-    require Data::Dumper;
-    say "<pre>data hash:";
-    say html_quote(Data::Dumper::Dumper(%data));
-    say "\ndata array:";
-    say html_quote(Data::Dumper::Dumper(@image_data)) . "\n\n</pre>";
+  require Data::Dumper;
+  say "<pre>data hash:";
+  say html_quote(Data::Dumper::Dumper(%data));
+  say "\ndata array:";
+  say html_quote(Data::Dumper::Dumper(@image_data)) . "\n\n</pre>";
 }
 
 # All formats point to the same section of the documentation.
@@ -384,65 +395,69 @@ $template->process("$format->{'template'}", $vars)
 
 
 sub get_names {
-    my ($names, $isnumeric, $field_name) = @_;
-    my ($field, @sorted);
-    # XXX - This is a hack to handle the actual_time/work_time field,
-    # because it's named 'actual_time' in Search.pm but 'work_time' in Field.pm.
-    $_[2] = $field_name = 'work_time' if $field_name eq 'actual_time';
-
-    # _realname fields aren't real Bugzilla::Field objects, but they are a
-    # valid axis, so we don't vailidate them as Bugzilla::Field objects.
-    $field = Bugzilla::Field->check($field_name) 
-        if ($field_name && $field_name !~ /_realname$/);
-    
-    if ($field && $field->is_select) {
-        foreach my $value (@{$field->legal_values}) {
-            push(@sorted, $value->name) if $names->{$value->name};
-        }
-        unshift(@sorted, '---') if ($field_name eq 'resolution'
-                                    || $field->type == FIELD_TYPE_MULTI_SELECT);
-        @sorted = uniq @sorted;
-    }  
-    elsif ($isnumeric) {
-        # It's not a field we are preserving the order of, so sort it 
-        # numerically...
-        @sorted = sort { $a <=> $b } keys %$names;
-    }
-    else {
-        # ...or alphabetically, as appropriate.
-        @sorted = sort keys %$names;
+  my ($names, $isnumeric, $field_name) = @_;
+  my ($field, @sorted);
+
+  # XXX - This is a hack to handle the actual_time/work_time field,
+  # because it's named 'actual_time' in Search.pm but 'work_time' in Field.pm.
+  $_[2] = $field_name = 'work_time' if $field_name eq 'actual_time';
+
+  # _realname fields aren't real Bugzilla::Field objects, but they are a
+  # valid axis, so we don't vailidate them as Bugzilla::Field objects.
+  $field = Bugzilla::Field->check($field_name)
+    if ($field_name && $field_name !~ /_realname$/);
+
+  if ($field && $field->is_select) {
+    foreach my $value (@{$field->legal_values}) {
+      push(@sorted, $value->name) if $names->{$value->name};
     }
-    
-    return @sorted;
+    unshift(@sorted, '---')
+      if ($field_name eq 'resolution' || $field->type == FIELD_TYPE_MULTI_SELECT);
+    @sorted = uniq @sorted;
+  }
+  elsif ($isnumeric) {
+
+    # It's not a field we are preserving the order of, so sort it
+    # numerically...
+    @sorted = sort { $a <=> $b } keys %$names;
+  }
+  else {
+    # ...or alphabetically, as appropriate.
+    @sorted = sort keys %$names;
+  }
+
+  return @sorted;
 }
 
 sub check_value {
-    my ($field, $result, $ismultiselect) = @_;
-
-    my $value;
-    if (!defined $field) {
-        $value = '';
-    }
-    elsif ($field eq '') {
-        $value = ' ';
-    }
-    else {
-        $value = shift @$result;
-        $value = ' ' if (!defined $value || $value eq '');
-        $value = '---' if (($field eq 'resolution' || $ismultiselect ) &&
-                           $value eq ' ');
-    }
-    if ($ismultiselect) {
-        # Some DB servers have a space after the comma, some others don't.
-        return split(/, ?/, $value);
-    } else {
-        return ($value);
-    }
+  my ($field, $result, $ismultiselect) = @_;
+
+  my $value;
+  if (!defined $field) {
+    $value = '';
+  }
+  elsif ($field eq '') {
+    $value = ' ';
+  }
+  else {
+    $value = shift @$result;
+    $value = ' ' if (!defined $value || $value eq '');
+    $value = '---' if (($field eq 'resolution' || $ismultiselect) && $value eq ' ');
+  }
+  if ($ismultiselect) {
+
+    # Some DB servers have a space after the comma, some others don't.
+    return split(/, ?/, $value);
+  }
+  else {
+    return ($value);
+  }
 }
 
 sub get_field_restrictions {
-    my $field = shift;
-    my $cgi = Bugzilla->cgi;
+  my $field = shift;
+  my $cgi   = Bugzilla->cgi;
 
-    return join('&amp;', map {url_quote($field) . '=' . url_quote($_)} $cgi->multi_param($field));
+  return join('&amp;',
+    map { url_quote($field) . '=' . url_quote($_) } $cgi->multi_param($field));
 }
index e40c2238447387ff8e3d0321ffa2f3170916870d..be9f7a9aba4beb900e6fefc92139cc84a7a3a51c 100755 (executable)
@@ -23,91 +23,93 @@ use Digest::SHA qw(hmac_sha256_base64);
 
 # If we're using bug groups for products, we should apply those restrictions
 # to viewing reports, as well.  Time to check the login in that case.
-my $user = Bugzilla->login();
-my $cgi = Bugzilla->cgi;
+my $user     = Bugzilla->login();
+my $cgi      = Bugzilla->cgi;
 my $template = Bugzilla->template;
-my $vars = {};
+my $vars     = {};
 
 # We use a dummy product instance with ID 0, representing all products
 my $product_all = {id => 0};
 bless($product_all, 'Bugzilla::Product');
 
 if (!Bugzilla->feature('old_charts')) {
-    ThrowUserError('feature_disabled', { feature => 'old_charts' });
+  ThrowUserError('feature_disabled', {feature => 'old_charts'});
 }
 
-my $dir       = bz_locations()->{'datadir'} . "/mining";
-my $graph_dir = bz_locations()->{'graphsdir'};
-my $graph_url = basename($graph_dir);
+my $dir        = bz_locations()->{'datadir'} . "/mining";
+my $graph_dir  = bz_locations()->{'graphsdir'};
+my $graph_url  = basename($graph_dir);
 my $product_id = $cgi->param('product_id');
 
 Bugzilla->switch_to_shadow_db();
 
-if (! defined($product_id)) {
-    # Can we do bug charts?
-    (-d $dir && -d $graph_dir) 
-      || ThrowCodeError('chart_dir_nonexistent',
-                        {dir => $dir, graph_dir => $graph_dir});
+if (!defined($product_id)) {
 
-    my %default_sel = map { $_ => 1 } BUG_STATE_OPEN;
+  # Can we do bug charts?
+  (-d $dir && -d $graph_dir)
+    || ThrowCodeError('chart_dir_nonexistent',
+    {dir => $dir, graph_dir => $graph_dir});
 
-    my @datasets;
-    my @data = get_data($dir);
+  my %default_sel = map { $_ => 1 } BUG_STATE_OPEN;
 
-    foreach my $dataset (@data) {
-        my $datasets = {};
-        $datasets->{'value'} = $dataset;
-        $datasets->{'selected'} = $default_sel{$dataset} ? 1 : 0;
-        push(@datasets, $datasets);
-    }
+  my @datasets;
+  my @data = get_data($dir);
+
+  foreach my $dataset (@data) {
+    my $datasets = {};
+    $datasets->{'value'} = $dataset;
+    $datasets->{'selected'} = $default_sel{$dataset} ? 1 : 0;
+    push(@datasets, $datasets);
+  }
 
-    $vars->{'datasets'} = \@datasets;
+  $vars->{'datasets'} = \@datasets;
 
-    print $cgi->header();
+  print $cgi->header();
 }
 else {
-    my $product;
-    # For security and correctness, validate the value of the "product_id" form
-    # variable. Valid values are IDs of those products for which the user has
-    # permissions which appear in the "product_id" drop-down menu on the report
-    # generation form. The product_id 0 is a special case, meaning "All
-    # Products".
-    if ($product_id) {
-        $product = Bugzilla::Product->new($product_id);
-        $product && $user->can_see_product($product->name)
-            || ThrowUserError('product_access_denied',
-                              {id => $product_id});
-    }
-    else {
-        $product = $product_all;
-    }
-
-    # Make sure there is something to plot.
-    my @datasets = $cgi->multi_param('datasets');
-    scalar(@datasets) || ThrowUserError('missing_datasets');
-
-    if (grep { $_ !~ /^[A-Za-z0-9:_-]+$/ } @datasets) {
-        ThrowUserError('invalid_datasets', {'datasets' => \@datasets});
-    }
-
-    # Filenames must not be guessable as they can point to products
-    # you are not allowed to see. Also, different projects can have
-    # the same product IDs.
-    my $project = bz_locations()->{'project'} || '';
-    my $image_file =  join(':', ($project, $product->id, @datasets));
-    my $key = Bugzilla->localconfig->{'site_wide_secret'};
-    $image_file = hmac_sha256_base64($image_file, $key) . '.png';
-    $image_file =~ s/\+/-/g;
-    $image_file =~ s/\//_/g;
-    trick_taint($image_file);
-
-    if (! -e "$graph_dir/$image_file") {
-        generate_chart($dir, "$graph_dir/$image_file", $product, \@datasets);
-    }
-
-    $vars->{'url_image'} = "$graph_url/$image_file";
-
-    print $cgi->header(-Content_Disposition=>'inline; filename=bugzilla_report.html');
+  my $product;
+
+  # For security and correctness, validate the value of the "product_id" form
+  # variable. Valid values are IDs of those products for which the user has
+  # permissions which appear in the "product_id" drop-down menu on the report
+  # generation form. The product_id 0 is a special case, meaning "All
+  # Products".
+  if ($product_id) {
+    $product = Bugzilla::Product->new($product_id);
+    $product && $user->can_see_product($product->name)
+      || ThrowUserError('product_access_denied', {id => $product_id});
+  }
+  else {
+    $product = $product_all;
+  }
+
+  # Make sure there is something to plot.
+  my @datasets = $cgi->multi_param('datasets');
+  scalar(@datasets) || ThrowUserError('missing_datasets');
+
+  if (grep { $_ !~ /^[A-Za-z0-9:_-]+$/ } @datasets) {
+    ThrowUserError('invalid_datasets', {'datasets' => \@datasets});
+  }
+
+  # Filenames must not be guessable as they can point to products
+  # you are not allowed to see. Also, different projects can have
+  # the same product IDs.
+  my $project = bz_locations()->{'project'} || '';
+  my $image_file = join(':', ($project, $product->id, @datasets));
+  my $key = Bugzilla->localconfig->{'site_wide_secret'};
+  $image_file = hmac_sha256_base64($image_file, $key) . '.png';
+  $image_file =~ s/\+/-/g;
+  $image_file =~ s/\//_/g;
+  trick_taint($image_file);
+
+  if (!-e "$graph_dir/$image_file") {
+    generate_chart($dir, "$graph_dir/$image_file", $product, \@datasets);
+  }
+
+  $vars->{'url_image'} = "$graph_url/$image_file";
+
+  print $cgi->header(
+    -Content_Disposition => 'inline; filename=bugzilla_report.html');
 }
 
 $template->process('reports/old-charts.html.tmpl', $vars)
@@ -118,106 +120,107 @@ $template->process('reports/old-charts.html.tmpl', $vars)
 #####################
 
 sub get_data {
-    my $dir = shift;
+  my $dir = shift;
 
-    my @datasets;
-    open(DATA, '<', "$dir/0")
-      || ThrowCodeError('chart_file_open_fail', {filename => "$dir/0"});
+  my @datasets;
+  open(DATA, '<', "$dir/0")
+    || ThrowCodeError('chart_file_open_fail', {filename => "$dir/0"});
 
-    while (<DATA>) {
-        if (/^# fields?: (.+)\s*$/) {
-            @datasets = grep ! /date/i, (split /\|/, $1);
-            last;
-        }
+  while (<DATA>) {
+    if (/^# fields?: (.+)\s*$/) {
+      @datasets = grep !/date/i, (split /\|/, $1);
+      last;
     }
-    close(DATA);
-    return @datasets;
+  }
+  close(DATA);
+  return @datasets;
 }
 
 sub generate_chart {
-    my ($dir, $image_file, $product, $datasets) = @_;
-    my $data_file = $dir . '/' . $product->id;
-
-    if (!open(FILE, '<', $data_file)) {
-        ThrowCodeError('chart_data_not_generated', {'product' => $product});
-    }
-
-    my $product_in_title = $product->id ? $product->name : 'All Products';
-    my @fields;
-    my @labels = qw(DATE);
-    my %datasets = map { $_ => 1 } @$datasets;
-
-    my %data = ();
-    while (<FILE>) {
-        chomp;
-        next unless $_;
-        if (/^#/) {
-            if (/^# fields?: (.*)\s*$/) {
-                @fields = split /\||\r/, $1;
-                $data{$_} ||= [] foreach @fields;
-                unless ($fields[0] =~ /date/i) {
-                    ThrowCodeError('chart_datafile_corrupt', {'file' => $data_file});
-                }
-                push @labels, grep($datasets{$_}, @fields);
-            }
-            next;
-        }
-
-        unless (@fields) {
-            ThrowCodeError('chart_datafile_corrupt', {'file' => $data_file});
-        }
-
-        my @line = split /\|/;
-        my $date = $line[0];
-        my ($yy, $mm, $dd) = $date =~ /^\d{2}(\d{2})(\d{2})(\d{2})$/;
-        push @{$data{DATE}}, "$mm/$dd/$yy";
-        
-        for my $i (1 .. $#fields) {
-            my $field = $fields[$i];
-            if (! defined $line[$i] or $line[$i] eq '') {
-                # no data point given, don't plot (this will probably
-                # generate loads of Chart::Base warnings, but that's not
-                # our fault.)
-                push @{$data{$field}}, undef;
-            }
-            else {
-                push @{$data{$field}}, $line[$i];
-            }
+  my ($dir, $image_file, $product, $datasets) = @_;
+  my $data_file = $dir . '/' . $product->id;
+
+  if (!open(FILE, '<', $data_file)) {
+    ThrowCodeError('chart_data_not_generated', {'product' => $product});
+  }
+
+  my $product_in_title = $product->id ? $product->name : 'All Products';
+  my @fields;
+  my @labels = qw(DATE);
+  my %datasets = map { $_ => 1 } @$datasets;
+
+  my %data = ();
+  while (<FILE>) {
+    chomp;
+    next unless $_;
+    if (/^#/) {
+      if (/^# fields?: (.*)\s*$/) {
+        @fields = split /\||\r/, $1;
+        $data{$_} ||= [] foreach @fields;
+        unless ($fields[0] =~ /date/i) {
+          ThrowCodeError('chart_datafile_corrupt', {'file' => $data_file});
         }
+        push @labels, grep($datasets{$_}, @fields);
+      }
+      next;
     }
-    
-    shift @labels;
-
-    close FILE;
 
-    if (! @{$data{DATE}}) {
-        ThrowUserError('insufficient_data_points');
+    unless (@fields) {
+      ThrowCodeError('chart_datafile_corrupt', {'file' => $data_file});
     }
 
-    my $img = Chart::Lines->new (800, 600);
-    my $i = 0;
-
-    my $MAXTICKS = 20;      # Try not to show any more x ticks than this.
-    my $skip = 1;
-    if (@{$data{DATE}} > $MAXTICKS) {
-        $skip = int((@{$data{DATE}} + $MAXTICKS - 1) / $MAXTICKS);
+    my @line = split /\|/;
+    my $date = $line[0];
+    my ($yy, $mm, $dd) = $date =~ /^\d{2}(\d{2})(\d{2})(\d{2})$/;
+    push @{$data{DATE}}, "$mm/$dd/$yy";
+
+    for my $i (1 .. $#fields) {
+      my $field = $fields[$i];
+      if (!defined $line[$i] or $line[$i] eq '') {
+
+        # no data point given, don't plot (this will probably
+        # generate loads of Chart::Base warnings, but that's not
+        # our fault.)
+        push @{$data{$field}}, undef;
+      }
+      else {
+        push @{$data{$field}}, $line[$i];
+      }
     }
-
-    my %settings =
-        (
-         "title" => "Status Counts for $product_in_title",
-         "x_label" => "Dates",
-         "y_label" => "Bug Counts",
-         "legend_labels" => \@labels,
-         "skip_x_ticks" => $skip,
-         "y_grid_lines" => "true",
-         "grey_background" => "false",
-         "colors" => {
-                      # default dataset colours are too alike
-                      dataset4 => [0, 0, 0], # black
-                     },
-        );
-    
-    $img->set (%settings);
-    $img->png($image_file, [ @data{('DATE', @labels)} ]);
+  }
+
+  shift @labels;
+
+  close FILE;
+
+  if (!@{$data{DATE}}) {
+    ThrowUserError('insufficient_data_points');
+  }
+
+  my $img = Chart::Lines->new(800, 600);
+  my $i = 0;
+
+  my $MAXTICKS = 20;    # Try not to show any more x ticks than this.
+  my $skip     = 1;
+  if (@{$data{DATE}} > $MAXTICKS) {
+    $skip = int((@{$data{DATE}} + $MAXTICKS - 1) / $MAXTICKS);
+  }
+
+  my %settings = (
+    "title"           => "Status Counts for $product_in_title",
+    "x_label"         => "Dates",
+    "y_label"         => "Bug Counts",
+    "legend_labels"   => \@labels,
+    "skip_x_ticks"    => $skip,
+    "y_grid_lines"    => "true",
+    "grey_background" => "false",
+    "colors"          => {
+
+      # default dataset colours are too alike
+      dataset4 => [0, 0, 0],    # black
+    },
+  );
+
+  $img->set(%settings);
+  $img->png($image_file, [@data{('DATE', @labels)}]);
 }
index 54b568a84179e510e21aaf8d3c588f9e9d2d523d..e0c9e52fe8e7a9cd6c7c3f06888dc8fb9cc1184c 100755 (executable)
@@ -23,51 +23,53 @@ use Bugzilla::Component;
 
 # Make sure the user is logged in.
 my $user = Bugzilla->login();
-my $cgi = Bugzilla->cgi;
+my $cgi  = Bugzilla->cgi;
+
 # Force the script to run against the shadow DB. We already validated credentials.
 Bugzilla->switch_to_shadow_db;
 my $template = Bugzilla->template;
-my $action = $cgi->param('action') || '';
-my $format = $template->get_format('request/queue', 
-                                   scalar($cgi->param('format')),
-                                   scalar($cgi->param('ctype')));
+my $action   = $cgi->param('action') || '';
+my $format   = $template->get_format(
+  'request/queue',
+  scalar($cgi->param('format')),
+  scalar($cgi->param('ctype'))
+);
 
 $cgi->set_dated_content_disp("inline", "requests", $format->{extension});
 print $cgi->header($format->{'ctype'});
 
 my $fields;
 $fields->{'requester'}->{'type'} = 'single';
+
 # If the user doesn't restrict their search to requests from the wind
 # (requestee ne '-'), include the requestee for completion.
-unless (defined $cgi->param('requestee')
-        && $cgi->param('requestee') eq '-')
-{
-    $fields->{'requestee'}->{'type'} = 'single';
+unless (defined $cgi->param('requestee') && $cgi->param('requestee') eq '-') {
+  $fields->{'requestee'}->{'type'} = 'single';
 }
 
 Bugzilla::User::match_field($fields);
 
 if ($action eq 'queue') {
-    queue($format);
+  queue($format);
 }
 else {
-    my $flagtypes = get_flag_types();
-    my @types = ('all', @$flagtypes);
-
-    my $vars = {};
-    $vars->{'types'} = \@types;
-    $vars->{'requests'} = {};
-
-    my %components;
-    foreach my $prod (@{$user->get_selectable_products}) {
-        foreach my $comp (@{$prod->components}) {
-            $components{$comp->name} = 1;
-        }
+  my $flagtypes = get_flag_types();
+  my @types = ('all', @$flagtypes);
+
+  my $vars = {};
+  $vars->{'types'}    = \@types;
+  $vars->{'requests'} = {};
+
+  my %components;
+  foreach my $prod (@{$user->get_selectable_products}) {
+    foreach my $comp (@{$prod->components}) {
+      $components{$comp->name} = 1;
     }
-    $vars->{'components'} = [ sort { $a cmp $b } keys %components ];
+  }
+  $vars->{'components'} = [sort { $a cmp $b } keys %components];
 
-    $template->process($format->{'template'}, $vars)
-      || ThrowTemplateError($template->error());
+  $template->process($format->{'template'}, $vars)
+    || ThrowTemplateError($template->error());
 }
 exit;
 
@@ -76,18 +78,19 @@ exit;
 ################################################################################
 
 sub queue {
-    my $format = shift;
-    my $cgi = Bugzilla->cgi;
-    my $dbh = Bugzilla->dbh;
-    my $template = Bugzilla->template;
-    my $user = Bugzilla->user;
-    my $userid = $user->id;
-    my $vars = {};
-
-    my $status = validateStatus(scalar $cgi->param('status'));
-    my $form_group = validateGroup(scalar $cgi->param('group'));
-
-    my $query = 
+  my $format   = shift;
+  my $cgi      = Bugzilla->cgi;
+  my $dbh      = Bugzilla->dbh;
+  my $template = Bugzilla->template;
+  my $user     = Bugzilla->user;
+  my $userid   = $user->id;
+  my $vars     = {};
+
+  my $status     = validateStatus(scalar $cgi->param('status'));
+  my $form_group = validateGroup(scalar $cgi->param('group'));
+
+  my $query =
+
     # Select columns describing each flag, the bug/attachment on which
     # it has been set, who set it, and of whom they are requesting it.
     " SELECT    flags.id, flagtypes.name,
@@ -98,6 +101,7 @@ sub queue {
                 requesters.realname, requesters.login_name,
                 requestees.realname, requestees.login_name, COUNT(privs.group_id),
     " . $dbh->sql_date_format('flags.modification_date', '%Y.%m.%d %H:%i') .
+
     # Use the flags and flagtypes tables for information about the flags,
     # the bugs and attachments tables for target info, the profiles tables
     # for setter and requestee info, the products/components tables
@@ -129,204 +133,209 @@ sub queue {
                   ON bgmap.bug_id = bugs.bug_id
     ";
 
-    if (Bugzilla->params->{or_groups}) {
-        $query .= " AND bgmap.group_id IN (" . $user->groups_as_string . ")";
-        $query .= " WHERE     (privs.group_id IS NULL OR bgmap.group_id IS NOT NULL OR";
-    }
-    else {
-        $query .= " AND bgmap.group_id NOT IN (" . $user->groups_as_string . ")";
-        $query .= " WHERE     (bgmap.group_id IS NULL OR";
-    }
-
-    # Weed out bug the user does not have access to
-    $query .=
-    "            (ccmap.who IS NOT NULL AND cclist_accessible = 1) OR
+  if (Bugzilla->params->{or_groups}) {
+    $query .= " AND bgmap.group_id IN (" . $user->groups_as_string . ")";
+    $query .= " WHERE     (privs.group_id IS NULL OR bgmap.group_id IS NOT NULL OR";
+  }
+  else {
+    $query .= " AND bgmap.group_id NOT IN (" . $user->groups_as_string . ")";
+    $query .= " WHERE     (bgmap.group_id IS NULL OR";
+  }
+
+  # Weed out bug the user does not have access to
+  $query .= "            (ccmap.who IS NOT NULL AND cclist_accessible = 1) OR
                  (bugs.reporter = $userid AND bugs.reporter_accessible = 1) OR
-                 (bugs.assigned_to = $userid) " .
-                 (Bugzilla->params->{'useqacontact'} ? "OR
-                 (bugs.qa_contact = $userid))" : ")");
-
-    unless ($user->is_insider) {
-        $query .= " AND (attachments.attach_id IS NULL
+                 (bugs.assigned_to = $userid) " . (
+    Bugzilla->params->{'useqacontact'}
+    ? "OR
+                 (bugs.qa_contact = $userid))"
+    : ")"
+  );
+
+  unless ($user->is_insider) {
+    $query .= " AND (attachments.attach_id IS NULL
                          OR attachments.isprivate = 0
                          OR attachments.submitter_id = $userid)";
+  }
+
+  # Limit query to pending requests.
+  $query .= " AND flags.status = '?' " unless $status;
+
+  # The set of criteria by which we filter records to display in the queue.
+  my @criteria = ();
+
+  # A list of columns to exclude from the report because the report conditions
+  # limit the data being displayed to exact matches for those columns.
+  # In other words, if we are only displaying "pending" , we don't
+  # need to display a "status" column in the report because the value for that
+  # column will always be the same.
+  my @excluded_columns = ();
+  my $do_union         = $cgi->param('do_union');
+
+  # Filter results by exact email address of requester or requestee.
+  if (my $requester = $cgi->param('requester')) {
+    $requester = $dbh->quote($requester);
+    trick_taint($requester);    # Quoted above
+    push(@criteria, $dbh->sql_istrcmp('requesters.login_name', $requester));
+    push(@excluded_columns, 'requester') unless $do_union;
+  }
+  if (my $requestee = $cgi->param('requestee')) {
+    if ($requestee ne '-') {
+      $requestee = $dbh->quote($requestee);
+      trick_taint($requestee);    # Quoted above
+      push(@criteria, $dbh->sql_istrcmp('requestees.login_name', $requestee));
     }
-
-    # Limit query to pending requests.
-    $query .= " AND flags.status = '?' " unless $status;
-
-    # The set of criteria by which we filter records to display in the queue.
-    my @criteria = ();
-
-    # A list of columns to exclude from the report because the report conditions
-    # limit the data being displayed to exact matches for those columns.
-    # In other words, if we are only displaying "pending" , we don't
-    # need to display a "status" column in the report because the value for that
-    # column will always be the same.
-    my @excluded_columns = ();
-    my $do_union = $cgi->param('do_union');
-
-    # Filter results by exact email address of requester or requestee.
-    if (my $requester = $cgi->param('requester')) {
-        $requester = $dbh->quote($requester);
-        trick_taint($requester); # Quoted above
-        push(@criteria, $dbh->sql_istrcmp('requesters.login_name', $requester));
-        push(@excluded_columns, 'requester') unless $do_union;
+    else {
+      push(@criteria, "flags.requestee_id IS NULL");
     }
-    if (my $requestee = $cgi->param('requestee')) {
-        if ($requestee ne '-') {
-            $requestee = $dbh->quote($requestee);
-            trick_taint($requestee); # Quoted above
-            push(@criteria, $dbh->sql_istrcmp('requestees.login_name', $requestee));
-        }
-        else {
-            push(@criteria, "flags.requestee_id IS NULL");
-        }
-        push(@excluded_columns, 'requestee') unless $do_union;
+    push(@excluded_columns, 'requestee') unless $do_union;
+  }
+
+  # If the user wants requester = foo OR requestee = bar, we have to join
+  # these criteria separately as all other criteria use AND.
+  if (@criteria == 2 && $do_union) {
+    my $union = join(' OR ', @criteria);
+    @criteria = ("($union)");
+  }
+
+  # Filter requests by status: "pending", "granted", "denied", "all"
+  # (which means any), or "fulfilled" (which means "granted" or "denied").
+  if ($status) {
+    if ($status eq "+-") {
+      push(@criteria,         "flags.status IN ('+', '-')");
+      push(@excluded_columns, 'status');
     }
-
-    # If the user wants requester = foo OR requestee = bar, we have to join
-    # these criteria separately as all other criteria use AND.
-    if (@criteria == 2 && $do_union) {
-        my $union = join(' OR ', @criteria);
-        @criteria = ("($union)");
+    elsif ($status ne "all") {
+      push(@criteria,         "flags.status = '$status'");
+      push(@excluded_columns, 'status');
     }
-
-    # Filter requests by status: "pending", "granted", "denied", "all"
-    # (which means any), or "fulfilled" (which means "granted" or "denied").
-    if ($status) {
-        if ($status eq "+-") {
-            push(@criteria, "flags.status IN ('+', '-')");
-            push(@excluded_columns, 'status');
-        }
-        elsif ($status ne "all") {
-            push(@criteria, "flags.status = '$status'");
-            push(@excluded_columns, 'status');
-        }
+  }
+
+  # Filter results by exact product or component.
+  if (defined $cgi->param('product') && $cgi->param('product') ne "") {
+    my $product = Bugzilla::Product->check(scalar $cgi->param('product'));
+    push(@criteria,         "bugs.product_id = " . $product->id);
+    push(@excluded_columns, 'product');
+    if (defined $cgi->param('component') && $cgi->param('component') ne "") {
+      my $component
+        = Bugzilla::Component->check({
+        product => $product, name => scalar $cgi->param('component')
+        });
+      push(@criteria,         "bugs.component_id = " . $component->id);
+      push(@excluded_columns, 'component');
     }
+  }
 
-    # Filter results by exact product or component.
-    if (defined $cgi->param('product') && $cgi->param('product') ne "") {
-        my $product = Bugzilla::Product->check(scalar $cgi->param('product'));
-        push(@criteria, "bugs.product_id = " . $product->id);
-        push(@excluded_columns, 'product');
-        if (defined $cgi->param('component') && $cgi->param('component') ne "") {
-            my $component = Bugzilla::Component->check({ product => $product,
-                                                         name => scalar $cgi->param('component') });
-            push(@criteria, "bugs.component_id = " . $component->id);
-            push(@excluded_columns, 'component');
-        }
-    }
+  # Filter results by flag types.
+  my $form_type = $cgi->param('type');
+  if (defined $form_type && !grep($form_type eq $_, ("", "all"))) {
 
-    # Filter results by flag types.
-    my $form_type = $cgi->param('type');
-    if (defined $form_type && !grep($form_type eq $_, ("", "all"))) {
-        # Check if any matching types are for attachments.  If not, don't show
-        # the attachment column in the report.
-        my $has_attachment_type =
-            Bugzilla::FlagType::count({ 'name' => $form_type,
-                                        'target_type' => 'attachment' });
-
-        if (!$has_attachment_type) { push(@excluded_columns, 'attachment') }
-
-        my $quoted_form_type = $dbh->quote($form_type);
-        trick_taint($quoted_form_type); # Already SQL quoted
-        push(@criteria, "flagtypes.name = " . $quoted_form_type);
-        push(@excluded_columns, 'type');
-    }
+    # Check if any matching types are for attachments.  If not, don't show
+    # the attachment column in the report.
+    my $has_attachment_type = Bugzilla::FlagType::count(
+      {'name' => $form_type, 'target_type' => 'attachment'});
+
+    if (!$has_attachment_type) { push(@excluded_columns, 'attachment') }
+
+    my $quoted_form_type = $dbh->quote($form_type);
+    trick_taint($quoted_form_type);    # Already SQL quoted
+    push(@criteria,         "flagtypes.name = " . $quoted_form_type);
+    push(@excluded_columns, 'type');
+  }
 
-    $query .= ' AND ' . join(' AND ', @criteria) if scalar(@criteria);
+  $query .= ' AND ' . join(' AND ', @criteria) if scalar(@criteria);
 
-    # Group the records by flag ID so we don't get multiple rows of data
-    # for each flag.  This is only necessary because of the code that
-    # removes flags on bugs the user is unauthorized to access.
-    $query .= ' ' . $dbh->sql_group_by('flags.id',
-               'flagtypes.name, flags.status, flags.bug_id, bugs.short_desc,
+  # Group the records by flag ID so we don't get multiple rows of data
+  # for each flag.  This is only necessary because of the code that
+  # removes flags on bugs the user is unauthorized to access.
+  $query .= ' ' . $dbh->sql_group_by(
+    'flags.id', 'flagtypes.name, flags.status, flags.bug_id, bugs.short_desc,
                 products.name, components.name, flags.attach_id,
                 attachments.description, requesters.realname,
                 requesters.login_name, requestees.realname,
                 requestees.login_name, flags.modification_date,
                 cclist_accessible, bugs.reporter, bugs.reporter_accessible,
-                bugs.assigned_to');
-
-    # Group the records, in other words order them by the group column
-    # so the loop in the display template can break them up into separate
-    # tables every time the value in the group column changes.
-
-    $form_group ||= "requestee";
-    if ($form_group eq "requester") {
-        $query .= " ORDER BY requesters.realname, requesters.login_name";
-    }
-    elsif ($form_group eq "requestee") {
-        $query .= " ORDER BY requestees.realname, requestees.login_name";
-    }
-    elsif ($form_group eq "category") {
-        $query .= " ORDER BY products.name, components.name";
-    }
-    elsif ($form_group eq "type") {
-        $query .= " ORDER BY flagtypes.name";
-    }
-
-    # Order the records (within each group).
-    $query .= " , flags.modification_date";
-
-    # Pass the query to the template for use when debugging this script.
-    $vars->{'query'} = $query;
-    $vars->{'debug'} = $cgi->param('debug') ? 1 : 0;
-
-    my $results = $dbh->selectall_arrayref($query);
-    my @requests = ();
-    foreach my $result (@$results) {
-        my @data = @$result;
-        my $request = {
-          'id'              => $data[0] , 
-          'type'            => $data[1] , 
-          'status'          => $data[2] , 
-          'bug_id'          => $data[3] , 
-          'bug_summary'     => $data[4] , 
-          'category'        => "$data[5]: $data[6]" , 
-          'attach_id'       => $data[7] , 
-          'attach_summary'  => $data[8] ,
-          'requester'       => ($data[9] ? "$data[9] <$data[10]>" : $data[10]) , 
-          'requestee'       => ($data[11] ? "$data[11] <$data[12]>" : $data[12]) , 
-          'restricted'      => $data[13] ? 1 : 0,
-          'created'         => $data[14]
-        };
-        push(@requests, $request);
-    }
-
-    # Get a list of request type names to use in the filter form.
-    my @types = ("all");
-    my $flagtypes = get_flag_types();
-    push(@types, @$flagtypes);
-
-    $vars->{'excluded_columns'} = \@excluded_columns;
-    $vars->{'group_field'} = $form_group;
-    $vars->{'requests'} = \@requests;
-    $vars->{'types'} = \@types;
-
-    # This code is needed to populate the Product and Component select fields.
-    my ($products, %components);
-    if (Bugzilla->params->{useclassification}) {
-        foreach my $class (@{$user->get_selectable_classifications}) {
-            push @$products, @{$user->get_selectable_products($class->id)};
-        }
-    }
-    else {
-        $products = $user->get_selectable_products;
+                bugs.assigned_to'
+  );
+
+  # Group the records, in other words order them by the group column
+  # so the loop in the display template can break them up into separate
+  # tables every time the value in the group column changes.
+
+  $form_group ||= "requestee";
+  if ($form_group eq "requester") {
+    $query .= " ORDER BY requesters.realname, requesters.login_name";
+  }
+  elsif ($form_group eq "requestee") {
+    $query .= " ORDER BY requestees.realname, requestees.login_name";
+  }
+  elsif ($form_group eq "category") {
+    $query .= " ORDER BY products.name, components.name";
+  }
+  elsif ($form_group eq "type") {
+    $query .= " ORDER BY flagtypes.name";
+  }
+
+  # Order the records (within each group).
+  $query .= " , flags.modification_date";
+
+  # Pass the query to the template for use when debugging this script.
+  $vars->{'query'} = $query;
+  $vars->{'debug'} = $cgi->param('debug') ? 1 : 0;
+
+  my $results  = $dbh->selectall_arrayref($query);
+  my @requests = ();
+  foreach my $result (@$results) {
+    my @data    = @$result;
+    my $request = {
+      'id'             => $data[0],
+      'type'           => $data[1],
+      'status'         => $data[2],
+      'bug_id'         => $data[3],
+      'bug_summary'    => $data[4],
+      'category'       => "$data[5]: $data[6]",
+      'attach_id'      => $data[7],
+      'attach_summary' => $data[8],
+      'requester'      => ($data[9] ? "$data[9] <$data[10]>" : $data[10]),
+      'requestee'      => ($data[11] ? "$data[11] <$data[12]>" : $data[12]),
+      'restricted'     => $data[13] ? 1 : 0,
+      'created'        => $data[14]
+    };
+    push(@requests, $request);
+  }
+
+  # Get a list of request type names to use in the filter form.
+  my @types     = ("all");
+  my $flagtypes = get_flag_types();
+  push(@types, @$flagtypes);
+
+  $vars->{'excluded_columns'} = \@excluded_columns;
+  $vars->{'group_field'}      = $form_group;
+  $vars->{'requests'}         = \@requests;
+  $vars->{'types'}            = \@types;
+
+  # This code is needed to populate the Product and Component select fields.
+  my ($products, %components);
+  if (Bugzilla->params->{useclassification}) {
+    foreach my $class (@{$user->get_selectable_classifications}) {
+      push @$products, @{$user->get_selectable_products($class->id)};
     }
-
-    foreach my $product (@$products) {
-        $components{$_->name} = 1 foreach @{$product->components};
-    }
-    $vars->{'products'} = $products;
-    $vars->{'components'} = [ sort keys %components ];
-
-    $vars->{'urlquerypart'} = $cgi->canonicalise_query('ctype');
-
-    # Generate and return the UI (HTML page) from the appropriate template.
-    $template->process($format->{'template'}, $vars)
-      || ThrowTemplateError($template->error());
+  }
+  else {
+    $products = $user->get_selectable_products;
+  }
+
+  foreach my $product (@$products) {
+    $components{$_->name} = 1 foreach @{$product->components};
+  }
+  $vars->{'products'}   = $products;
+  $vars->{'components'} = [sort keys %components];
+
+  $vars->{'urlquerypart'} = $cgi->canonicalise_query('ctype');
+
+  # Generate and return the UI (HTML page) from the appropriate template.
+  $template->process($format->{'template'}, $vars)
+    || ThrowTemplateError($template->error());
 }
 
 ################################################################################
@@ -334,33 +343,35 @@ sub queue {
 ################################################################################
 
 sub validateStatus {
-    my $status = shift;
-    return if !defined $status;
+  my $status = shift;
+  return if !defined $status;
 
-    grep($status eq $_, qw(? +- + - all))
-      || ThrowUserError("flag_status_invalid", { status => $status });
-    trick_taint($status);
-    return $status;
+  grep($status eq $_, qw(? +- + - all))
+    || ThrowUserError("flag_status_invalid", {status => $status});
+  trick_taint($status);
+  return $status;
 }
 
 sub validateGroup {
-    my $group = shift;
-    return if !defined $group;
+  my $group = shift;
+  return if !defined $group;
 
-    grep($group eq $_, qw(requester requestee category type))
-      || ThrowUserError("request_queue_group_invalid", { group => $group });
-    trick_taint($group);
-    return $group;
+  grep($group eq $_, qw(requester requestee category type))
+    || ThrowUserError("request_queue_group_invalid", {group => $group});
+  trick_taint($group);
+  return $group;
 }
 
 # Returns all flag types which have at least one flag of this type.
 # If a flag type is inactive but still has flags, we want it.
 sub get_flag_types {
-    my $dbh = Bugzilla->dbh;
-    my $flag_types = $dbh->selectcol_arrayref('SELECT DISTINCT name
+  my $dbh        = Bugzilla->dbh;
+  my $flag_types = $dbh->selectcol_arrayref(
+    'SELECT DISTINCT name
                                                  FROM flagtypes
                                                 WHERE flagtypes.id IN
                                                       (SELECT DISTINCT type_id FROM flags)
-                                             ORDER BY name');
-    return $flag_types;
+                                             ORDER BY name'
+  );
+  return $flag_types;
 }
index 4c29f90fe9eb5a286b66aaba4738feb70c0990d3..26ed6e34b1cea781f6bdee8ec004f94e41ae04e0 100755 (executable)
@@ -26,26 +26,29 @@ use Bugzilla::Token;
 ###########################################################################
 
 sub get_string {
-    my ($san_tag, $vars) = @_;
-    $vars->{'san_tag'} = $san_tag;
-    return get_text('sanitycheck', $vars);
+  my ($san_tag, $vars) = @_;
+  $vars->{'san_tag'} = $san_tag;
+  return get_text('sanitycheck', $vars);
 }
 
 sub Status {
-    my ($san_tag, $vars, $alert) = @_;
-    my $cgi = Bugzilla->cgi;
-    return if (!$alert && Bugzilla->usage_mode == USAGE_MODE_CMDLINE && !$cgi->param('verbose'));
-
-    if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) {
-        my $output = $cgi->param('output') || '';
-        my $linebreak = $alert ? "\nALERT: " : "\n";
-        $cgi->param('error_found', 1) if $alert;
-        $cgi->param('output', $output . $linebreak . get_string($san_tag, $vars));
-    }
-    else {
-        my $start_tag = $alert ? '<p class="alert">' : '<p>';
-        say $start_tag . get_string($san_tag, $vars) . "</p>";
-    }
+  my ($san_tag, $vars, $alert) = @_;
+  my $cgi = Bugzilla->cgi;
+  return
+    if (!$alert
+    && Bugzilla->usage_mode == USAGE_MODE_CMDLINE
+    && !$cgi->param('verbose'));
+
+  if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) {
+    my $output = $cgi->param('output') || '';
+    my $linebreak = $alert ? "\nALERT: " : "\n";
+    $cgi->param('error_found', 1) if $alert;
+    $cgi->param('output', $output . $linebreak . get_string($san_tag, $vars));
+  }
+  else {
+    my $start_tag = $alert ? '<p class="alert">' : '<p>';
+    say $start_tag . get_string($san_tag, $vars) . "</p>";
+  }
 }
 
 ###########################################################################
@@ -56,25 +59,26 @@ my $user = Bugzilla->login(LOGIN_REQUIRED);
 
 my $cgi = Bugzilla->cgi;
 my $dbh = Bugzilla->dbh;
+
 # If the result of the sanity check is sent per email, then we have to
 # take the user prefs into account rather than querying the web browser.
 my $template;
 if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) {
-    $template = Bugzilla->template_inner($user->setting('lang'));
+  $template = Bugzilla->template_inner($user->setting('lang'));
 }
 else {
-    $template = Bugzilla->template;
-
-    # Only check the token if we are running this script from the
-    # web browser and a parameter is passed to the script.
-    # XXX - Maybe these two parameters should be deleted once logged in?
-    $cgi->delete('GoAheadAndLogIn', 'Bugzilla_restrictlogin');
-    if (scalar $cgi->multi_param()) {
-        my $token = $cgi->param('token');
-        check_hash_token($token, ['sanitycheck']);
-    }
+  $template = Bugzilla->template;
+
+  # Only check the token if we are running this script from the
+  # web browser and a parameter is passed to the script.
+  # XXX - Maybe these two parameters should be deleted once logged in?
+  $cgi->delete('GoAheadAndLogIn', 'Bugzilla_restrictlogin');
+  if (scalar $cgi->multi_param()) {
+    my $token = $cgi->param('token');
+    check_hash_token($token, ['sanitycheck']);
+  }
 }
-my $vars = {};
+my $vars            = {};
 my $clear_memcached = 0;
 
 print $cgi->header() unless Bugzilla->usage_mode == USAGE_MODE_CMDLINE;
@@ -83,13 +87,12 @@ print $cgi->header() unless Bugzilla->usage_mode == USAGE_MODE_CMDLINE;
 # As this script can now alter the group_control_map table, we no longer
 # let users with editbugs privs run it anymore.
 $user->in_group("editcomponents")
-  || ThrowUserError("auth_failure", {group  => "editcomponents",
-                                     action => "run",
-                                     object => "sanity_check"});
+  || ThrowUserError("auth_failure",
+  {group => "editcomponents", action => "run", object => "sanity_check"});
 
 unless (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) {
-    $template->process('admin/sanitycheck/list.html.tmpl', $vars)
-      || ThrowTemplateError($template->error());
+  $template->process('admin/sanitycheck/list.html.tmpl', $vars)
+    || ThrowTemplateError($template->error());
 }
 
 ###########################################################################
@@ -97,25 +100,28 @@ unless (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) {
 ###########################################################################
 
 if ($cgi->param('createmissinggroupcontrolmapentries')) {
-    Status('group_control_map_entries_creation');
+  Status('group_control_map_entries_creation');
 
-    my $na    = CONTROLMAPNA;
-    my $shown = CONTROLMAPSHOWN;
-    my $insertsth = $dbh->prepare(
-        qq{INSERT INTO group_control_map
+  my $na        = CONTROLMAPNA;
+  my $shown     = CONTROLMAPSHOWN;
+  my $insertsth = $dbh->prepare(
+    qq{INSERT INTO group_control_map
                        (group_id, product_id, membercontrol, othercontrol)
-                VALUES (?, ?, $shown, $na)});
+                VALUES (?, ?, $shown, $na)}
+  );
 
-    my $updatesth = $dbh->prepare(qq{UPDATE group_control_map
+  my $updatesth = $dbh->prepare(
+    qq{UPDATE group_control_map
                                         SET membercontrol = $shown
                                       WHERE group_id   = ?
-                                        AND product_id = ?});
-    my $counter = 0;
-
-    # Find all group/product combinations used for bugs but not set up
-    # correctly in group_control_map
-    my $invalid_combinations = $dbh->selectall_arrayref(
-        qq{    SELECT bugs.product_id,
+                                        AND product_id = ?}
+  );
+  my $counter = 0;
+
+  # Find all group/product combinations used for bugs but not set up
+  # correctly in group_control_map
+  my $invalid_combinations = $dbh->selectall_arrayref(
+    qq{    SELECT bugs.product_id,
                       bgm.group_id,
                       gcm.membercontrol,
                       groups.name,
@@ -131,28 +137,32 @@ if ($cgi->param('createmissinggroupcontrolmapentries')) {
                    ON bugs.product_id = gcm.product_id
                   AND    bgm.group_id = gcm.group_id
                 WHERE COALESCE(gcm.membercontrol, $na) = $na
-          } . $dbh->sql_group_by('bugs.product_id, bgm.group_id',
-                                 'gcm.membercontrol, groups.name, products.name'));
-
-    foreach (@$invalid_combinations) {
-        my ($product_id, $group_id, $currentmembercontrol,
-            $group_name, $product_name) = @$_;
-
-        $counter++;
-        if (defined($currentmembercontrol)) {
-            Status('group_control_map_entries_update',
-                   {group_name => $group_name, product_name => $product_name});
-            $updatesth->execute($group_id, $product_id);
-        }
-        else {
-            Status('group_control_map_entries_generation',
-                   {group_name => $group_name, product_name => $product_name});
-            $insertsth->execute($group_id, $product_id);
-        }
+          }
+      . $dbh->sql_group_by(
+      'bugs.product_id, bgm.group_id',
+      'gcm.membercontrol, groups.name, products.name'
+      )
+  );
+
+  foreach (@$invalid_combinations) {
+    my ($product_id, $group_id, $currentmembercontrol, $group_name, $product_name)
+      = @$_;
+
+    $counter++;
+    if (defined($currentmembercontrol)) {
+      Status('group_control_map_entries_update',
+        {group_name => $group_name, product_name => $product_name});
+      $updatesth->execute($group_id, $product_id);
     }
+    else {
+      Status('group_control_map_entries_generation',
+        {group_name => $group_name, product_name => $product_name});
+      $insertsth->execute($group_id, $product_id);
+    }
+  }
 
-    Status('group_control_map_entries_repaired', {counter => $counter});
-    $clear_memcached = 1 if $counter;
+  Status('group_control_map_entries_repaired', {counter => $counter});
+  $clear_memcached = 1 if $counter;
 }
 
 ###########################################################################
@@ -160,26 +170,32 @@ if ($cgi->param('createmissinggroupcontrolmapentries')) {
 ###########################################################################
 
 if ($cgi->param('repair_creation_date')) {
-    Status('bug_creation_date_start');
-
-    my $bug_ids = $dbh->selectcol_arrayref('SELECT bug_id FROM bugs
-                                            WHERE creation_ts IS NULL');
-
-    my $sth_UpdateDate = $dbh->prepare('UPDATE bugs SET creation_ts = ?
-                                        WHERE bug_id = ?');
-
-    # All bugs have an entry in the 'longdescs' table when they are created,
-    # even if no comment is required.
-    my $sth_getDate = $dbh->prepare('SELECT MIN(bug_when) FROM longdescs
-                                     WHERE bug_id = ?');
-
-    foreach my $bugid (@$bug_ids) {
-        $sth_getDate->execute($bugid);
-        my $date = $sth_getDate->fetchrow_array;
-        $sth_UpdateDate->execute($date, $bugid);
-    }
-    Status('bug_creation_date_fixed', {bug_count => scalar(@$bug_ids)});
-    $clear_memcached = 1 if @$bug_ids;
+  Status('bug_creation_date_start');
+
+  my $bug_ids = $dbh->selectcol_arrayref(
+    'SELECT bug_id FROM bugs
+                                            WHERE creation_ts IS NULL'
+  );
+
+  my $sth_UpdateDate = $dbh->prepare(
+    'UPDATE bugs SET creation_ts = ?
+                                        WHERE bug_id = ?'
+  );
+
+  # All bugs have an entry in the 'longdescs' table when they are created,
+  # even if no comment is required.
+  my $sth_getDate = $dbh->prepare(
+    'SELECT MIN(bug_when) FROM longdescs
+                                     WHERE bug_id = ?'
+  );
+
+  foreach my $bugid (@$bug_ids) {
+    $sth_getDate->execute($bugid);
+    my $date = $sth_getDate->fetchrow_array;
+    $sth_UpdateDate->execute($date, $bugid);
+  }
+  Status('bug_creation_date_fixed', {bug_count => scalar(@$bug_ids)});
+  $clear_memcached = 1 if @$bug_ids;
 }
 
 ###########################################################################
@@ -187,16 +203,19 @@ if ($cgi->param('repair_creation_date')) {
 ###########################################################################
 
 if ($cgi->param('repair_everconfirmed')) {
-    Status('everconfirmed_start');
+  Status('everconfirmed_start');
 
-    my @confirmed_open_states = grep {$_ ne 'UNCONFIRMED'} BUG_STATE_OPEN;
-    my $confirmed_open_states = join(', ', map {$dbh->quote($_)} @confirmed_open_states);
+  my @confirmed_open_states = grep { $_ ne 'UNCONFIRMED' } BUG_STATE_OPEN;
+  my $confirmed_open_states
+    = join(', ', map { $dbh->quote($_) } @confirmed_open_states);
 
-    $dbh->do("UPDATE bugs SET everconfirmed = 0 WHERE bug_status = 'UNCONFIRMED'");
-    $dbh->do("UPDATE bugs SET everconfirmed = 1 WHERE bug_status IN ($confirmed_open_states)");
+  $dbh->do("UPDATE bugs SET everconfirmed = 0 WHERE bug_status = 'UNCONFIRMED'");
+  $dbh->do(
+    "UPDATE bugs SET everconfirmed = 1 WHERE bug_status IN ($confirmed_open_states)"
+  );
 
-    Status('everconfirmed_end');
-    $clear_memcached = 1;
+  Status('everconfirmed_end');
+  $clear_memcached = 1;
 }
 
 ###########################################################################
@@ -204,20 +223,22 @@ if ($cgi->param('repair_everconfirmed')) {
 ###########################################################################
 
 if ($cgi->param('repair_bugs_fulltext')) {
-    Status('bugs_fulltext_start');
+  Status('bugs_fulltext_start');
 
-    my $bug_ids = $dbh->selectcol_arrayref('SELECT bugs.bug_id
+  my $bug_ids = $dbh->selectcol_arrayref(
+    'SELECT bugs.bug_id
                                             FROM bugs
                                             LEFT JOIN bugs_fulltext
                                             ON bugs_fulltext.bug_id = bugs.bug_id
-                                            WHERE bugs_fulltext.bug_id IS NULL');
+                                            WHERE bugs_fulltext.bug_id IS NULL'
+  );
 
-    foreach my $bugid (@$bug_ids) {
-        Bugzilla::Bug->new($bugid)->_sync_fulltext( new_bug => 1 );
-    }
+  foreach my $bugid (@$bug_ids) {
+    Bugzilla::Bug->new($bugid)->_sync_fulltext(new_bug => 1);
+  }
 
-    Status('bugs_fulltext_fixed', {bug_count => scalar(@$bug_ids)});
-    $clear_memcached = 1 if @$bug_ids;
+  Status('bugs_fulltext_fixed', {bug_count => scalar(@$bug_ids)});
+  $clear_memcached = 1 if @$bug_ids;
 }
 
 ###########################################################################
@@ -225,44 +246,46 @@ if ($cgi->param('repair_bugs_fulltext')) {
 ###########################################################################
 
 if ($cgi->param('rescanallBugMail')) {
-    require Bugzilla::BugMail;
+  require Bugzilla::BugMail;
 
-    Status('send_bugmail_start');
-    my $time = $dbh->sql_date_math('NOW()', '-', 30, 'MINUTE');
+  Status('send_bugmail_start');
+  my $time = $dbh->sql_date_math('NOW()', '-', 30, 'MINUTE');
 
-    my $list = $dbh->selectcol_arrayref(qq{
+  my $list = $dbh->selectcol_arrayref(
+    qq{
                                         SELECT bug_id
                                           FROM bugs 
                                          WHERE (lastdiffed IS NULL
                                                 OR lastdiffed < delta_ts)
                                            AND delta_ts < $time
-                                      ORDER BY bug_id});
-
-    Status('send_bugmail_status', {bug_count => scalar(@$list)});
-
-    # We cannot simply look at the bugs_activity table to find who did the
-    # last change in a given bug, as e.g. adding a comment doesn't add any
-    # entry to this table. And some other changes may be private
-    # (such as time-related changes or private attachments or comments)
-    # and so choosing this user as being the last one having done a change
-    # for the bug may be problematic. So the best we can do at this point
-    # is to choose the currently logged in user for email notification.
-    $vars->{'changer'} = $user;
-
-    foreach my $bugid (@$list) {
-        Bugzilla::BugMail::Send($bugid, $vars);
-    }
-
-    if (@$list) {
-        Status('send_bugmail_end');
-        Bugzilla->memcached->clear_all();
-    }
-
-    unless (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) {
-        $template->process('global/footer.html.tmpl', $vars)
-          || ThrowTemplateError($template->error());
-    }
-    exit;
+                                      ORDER BY bug_id}
+  );
+
+  Status('send_bugmail_status', {bug_count => scalar(@$list)});
+
+  # We cannot simply look at the bugs_activity table to find who did the
+  # last change in a given bug, as e.g. adding a comment doesn't add any
+  # entry to this table. And some other changes may be private
+  # (such as time-related changes or private attachments or comments)
+  # and so choosing this user as being the last one having done a change
+  # for the bug may be problematic. So the best we can do at this point
+  # is to choose the currently logged in user for email notification.
+  $vars->{'changer'} = $user;
+
+  foreach my $bugid (@$list) {
+    Bugzilla::BugMail::Send($bugid, $vars);
+  }
+
+  if (@$list) {
+    Status('send_bugmail_end');
+    Bugzilla->memcached->clear_all();
+  }
+
+  unless (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) {
+    $template->process('global/footer.html.tmpl', $vars)
+      || ThrowTemplateError($template->error());
+  }
+  exit;
 }
 
 ###########################################################################
@@ -270,32 +293,37 @@ if ($cgi->param('rescanallBugMail')) {
 ###########################################################################
 
 if ($cgi->param('remove_invalid_bug_references')) {
-    Status('bug_reference_deletion_start');
+  Status('bug_reference_deletion_start');
 
-    $dbh->bz_start_transaction();
+  $dbh->bz_start_transaction();
 
-    foreach my $pair ('attachments/', 'bug_group_map/', 'bugs_activity/',
-                      'bugs_fulltext/', 'cc/',
-                      'dependencies/blocked', 'dependencies/dependson',
-                      'duplicates/dupe', 'duplicates/dupe_of',
-                      'flags/', 'keywords/', 'longdescs/') {
+  foreach my $pair (
+    'attachments/',           'bug_group_map/',
+    'bugs_activity/',         'bugs_fulltext/',
+    'cc/',                    'dependencies/blocked',
+    'dependencies/dependson', 'duplicates/dupe',
+    'duplicates/dupe_of',     'flags/',
+    'keywords/',              'longdescs/'
+    )
+  {
 
-        my ($table, $field) = split('/', $pair);
-        $field ||= "bug_id";
+    my ($table, $field) = split('/', $pair);
+    $field ||= "bug_id";
 
-        my $bug_ids =
-          $dbh->selectcol_arrayref("SELECT $table.$field FROM $table
+    my $bug_ids = $dbh->selectcol_arrayref(
+      "SELECT $table.$field FROM $table
                                     LEFT JOIN bugs ON $table.$field = bugs.bug_id
-                                    WHERE bugs.bug_id IS NULL");
+                                    WHERE bugs.bug_id IS NULL"
+    );
 
-        if (scalar(@$bug_ids)) {
-            $dbh->do("DELETE FROM $table WHERE $field IN (" . join(',', @$bug_ids) . ")");
-            $clear_memcached = 1;
-        }
+    if (scalar(@$bug_ids)) {
+      $dbh->do("DELETE FROM $table WHERE $field IN (" . join(',', @$bug_ids) . ")");
+      $clear_memcached = 1;
     }
+  }
 
-    $dbh->bz_commit_transaction();
-    Status('bug_reference_deletion_end');
+  $dbh->bz_commit_transaction();
+  Status('bug_reference_deletion_end');
 }
 
 ###########################################################################
@@ -303,25 +331,26 @@ if ($cgi->param('remove_invalid_bug_references')) {
 ###########################################################################
 
 if ($cgi->param('remove_invalid_attach_references')) {
-    Status('attachment_reference_deletion_start');
+  Status('attachment_reference_deletion_start');
 
-    $dbh->bz_start_transaction();
+  $dbh->bz_start_transaction();
 
-    my $attach_ids =
-        $dbh->selectcol_arrayref('SELECT attach_data.id
+  my $attach_ids = $dbh->selectcol_arrayref(
+    'SELECT attach_data.id
                                     FROM attach_data
                                LEFT JOIN attachments
                                       ON attachments.attach_id = attach_data.id
-                                   WHERE attachments.attach_id IS NULL');
+                                   WHERE attachments.attach_id IS NULL'
+  );
 
-    if (scalar(@$attach_ids)) {
-        $dbh->do('DELETE FROM attach_data WHERE id IN (' .
-                 join(',', @$attach_ids) . ')');
-    }
+  if (scalar(@$attach_ids)) {
+    $dbh->do(
+      'DELETE FROM attach_data WHERE id IN (' . join(',', @$attach_ids) . ')');
+  }
 
-    $dbh->bz_commit_transaction();
-    Status('attachment_reference_deletion_end');
-    $clear_memcached = 1 if @$attach_ids;
+  $dbh->bz_commit_transaction();
+  Status('attachment_reference_deletion_end');
+  $clear_memcached = 1 if @$attach_ids;
 }
 
 ###########################################################################
@@ -329,30 +358,33 @@ if ($cgi->param('remove_invalid_attach_references')) {
 ###########################################################################
 
 if ($cgi->param('remove_old_whine_targets')) {
-    Status('whines_obsolete_target_deletion_start');
+  Status('whines_obsolete_target_deletion_start');
 
-    $dbh->bz_start_transaction();
+  $dbh->bz_start_transaction();
 
-    foreach my $target (['groups', 'id', MAILTO_GROUP],
-                        ['profiles', 'userid', MAILTO_USER])
-    {
-        my ($table, $col, $type) = @$target;
-        my $old_ids =
-          $dbh->selectcol_arrayref("SELECT DISTINCT mailto
+  foreach my $target (['groups', 'id', MAILTO_GROUP],
+    ['profiles', 'userid', MAILTO_USER])
+  {
+    my ($table, $col, $type) = @$target;
+    my $old_ids = $dbh->selectcol_arrayref(
+      "SELECT DISTINCT mailto
                                       FROM whine_schedules
                                  LEFT JOIN $table
                                         ON $table.$col = whine_schedules.mailto
-                                     WHERE mailto_type = $type AND $table.$col IS NULL");
-
-        if (scalar(@$old_ids)) {
-            $dbh->do("DELETE FROM whine_schedules
-                       WHERE mailto_type = $type AND mailto IN (" .
-                       join(',', @$old_ids) . ")");
-            $clear_memcached = 1;
-        }
+                                     WHERE mailto_type = $type AND $table.$col IS NULL"
+    );
+
+    if (scalar(@$old_ids)) {
+      $dbh->do(
+        "DELETE FROM whine_schedules
+                       WHERE mailto_type = $type AND mailto IN ("
+          . join(',', @$old_ids) . ")"
+      );
+      $clear_memcached = 1;
     }
-    $dbh->bz_commit_transaction();
-    Status('whines_obsolete_target_deletion_end');
+  }
+  $dbh->bz_commit_transaction();
+  Status('whines_obsolete_target_deletion_end');
 }
 
 # If any repairs were attempted or made, we need to clear memcached to ensure
@@ -363,7 +395,7 @@ Bugzilla->memcached->clear_all() if $clear_memcached;
 # Repair hook
 ###########################################################################
 
-Bugzilla::Hook::process('sanitycheck_repair', { status => \&Status });
+Bugzilla::Hook::process('sanitycheck_repair', {status => \&Status});
 
 ###########################################################################
 # Checks
@@ -393,182 +425,211 @@ Status('checks_start');
 #        The same goes for series; no bug for that yet.
 
 sub CrossCheck {
-    my $table = shift @_;
-    my $field = shift @_;
-    my $dbh = Bugzilla->dbh;
+  my $table = shift @_;
+  my $field = shift @_;
+  my $dbh   = Bugzilla->dbh;
 
-    Status('cross_check_to', {table => $table, field => $field});
+  Status('cross_check_to', {table => $table, field => $field});
 
-    while (@_) {
-        my $ref = shift @_;
-        my ($refertable, $referfield, $keyname, $exceptions) = @$ref;
+  while (@_) {
+    my $ref = shift @_;
+    my ($refertable, $referfield, $keyname, $exceptions) = @$ref;
 
-        $exceptions ||= [];
-        my %exceptions = map { $_ => 1 } @$exceptions;
+    $exceptions ||= [];
+    my %exceptions = map { $_ => 1 } @$exceptions;
 
-        Status('cross_check_from', {table => $refertable, field => $referfield});
+    Status('cross_check_from', {table => $refertable, field => $referfield});
 
-        my $query = qq{SELECT DISTINCT $refertable.$referfield} .
-            ($keyname ? qq{, $refertable.$keyname } : q{}) .
-                     qq{ FROM $refertable
+    my $query
+      = qq{SELECT DISTINCT $refertable.$referfield}
+      . ($keyname ? qq{, $refertable.$keyname } : q{})
+      . qq{ FROM $refertable
                     LEFT JOIN $table
                            ON $refertable.$referfield = $table.$field
                         WHERE $table.$field IS NULL
                           AND $refertable.$referfield IS NOT NULL};
 
-        my $sth = $dbh->prepare($query);
-        $sth->execute;
-
-        my $has_bad_references = 0;
-
-        while (my ($value, $key) = $sth->fetchrow_array) {
-            next if $exceptions{$value};
-            Status('cross_check_alert', {value => $value, table => $refertable,
-                                         field => $referfield, keyname => $keyname,
-                                         key => $key}, 'alert');
-            $has_bad_references = 1;
-        }
-        # References to non existent bugs can be safely removed, bug 288461
-        if ($table eq 'bugs' && $has_bad_references) {
-            Status('cross_check_bug_has_references');
-        }
-        # References to non existent attachments can be safely removed.
-        if ($table eq 'attachments' && $has_bad_references) {
-            Status('cross_check_attachment_has_references');
-        }
+    my $sth = $dbh->prepare($query);
+    $sth->execute;
+
+    my $has_bad_references = 0;
+
+    while (my ($value, $key) = $sth->fetchrow_array) {
+      next if $exceptions{$value};
+      Status(
+        'cross_check_alert',
+        {
+          value   => $value,
+          table   => $refertable,
+          field   => $referfield,
+          keyname => $keyname,
+          key     => $key
+        },
+        'alert'
+      );
+      $has_bad_references = 1;
+    }
+
+    # References to non existent bugs can be safely removed, bug 288461
+    if ($table eq 'bugs' && $has_bad_references) {
+      Status('cross_check_bug_has_references');
+    }
+
+    # References to non existent attachments can be safely removed.
+    if ($table eq 'attachments' && $has_bad_references) {
+      Status('cross_check_attachment_has_references');
     }
+  }
 }
 
-CrossCheck('classifications', 'id',
-           ['products', 'classification_id']);
-
-CrossCheck("keyworddefs", "id",
-           ["keywords", "keywordid"]);
-
-CrossCheck("fielddefs", "id",
-           ["bugs_activity", "fieldid"],
-           ['profiles_activity', 'fieldid']);
-
-CrossCheck("flagtypes", "id",
-           ["flags", "type_id"],
-           ["flagexclusions", "type_id"],
-           ["flaginclusions", "type_id"]);
-
-CrossCheck("bugs", "bug_id",
-           ["bugs_activity", "bug_id"],
-           ["bug_group_map", "bug_id"],
-           ["bugs_fulltext", "bug_id"],
-           ["attachments", "bug_id"],
-           ["cc", "bug_id"],
-           ["longdescs", "bug_id"],
-           ["dependencies", "blocked"],
-           ["dependencies", "dependson"],
-           ['flags', 'bug_id'],
-           ["keywords", "bug_id"],
-           ["duplicates", "dupe_of", "dupe"],
-           ["duplicates", "dupe", "dupe_of"]);
-
-CrossCheck("groups", "id",
-           ["bug_group_map", "group_id"],
-           ['category_group_map', 'group_id'],
-           ["group_group_map", "grantor_id"],
-           ["group_group_map", "member_id"],
-           ["group_control_map", "group_id"],
-           ["namedquery_group_map", "group_id"],
-           ["user_group_map", "group_id"],
-           ["flagtypes", "grant_group_id"],
-           ["flagtypes", "request_group_id"]);
-
-CrossCheck("namedqueries", "id",
-           ["namedqueries_link_in_footer", "namedquery_id"],
-           ["namedquery_group_map", "namedquery_id"],
-          );
-
-CrossCheck("profiles", "userid",
-           ['profiles_activity', 'userid'],
-           ['profiles_activity', 'who'],
-           ['email_setting', 'user_id'],
-           ['profile_setting', 'user_id'],
-           ["bugs", "reporter", "bug_id"],
-           ["bugs", "assigned_to", "bug_id"],
-           ["bugs", "qa_contact", "bug_id"],
-           ["attachments", "submitter_id", "bug_id"],
-           ['flags', 'setter_id', 'bug_id'],
-           ['flags', 'requestee_id', 'bug_id'],
-           ["bugs_activity", "who", "bug_id"],
-           ["cc", "who", "bug_id"],
-           ['quips', 'userid'],
-           ["longdescs", "who", "bug_id"],
-           ["logincookies", "userid"],
-           ["namedqueries", "userid"],
-           ["namedqueries_link_in_footer", "user_id"],
-           ['series', 'creator', 'series_id'],
-           ["watch", "watcher"],
-           ["watch", "watched"],
-           ['whine_events', 'owner_userid'],
-           ["tokens", "userid"],
-           ["user_group_map", "user_id"],
-           ["components", "initialowner", "name"],
-           ["components", "initialqacontact", "name"],
-           ["component_cc", "user_id"]);
-
-CrossCheck("products", "id",
-           ["bugs", "product_id", "bug_id"],
-           ["components", "product_id", "name"],
-           ["milestones", "product_id", "value"],
-           ["versions", "product_id", "value"],
-           ["group_control_map", "product_id"],
-           ["flaginclusions", "product_id", "type_id"],
-           ["flagexclusions", "product_id", "type_id"]);
-
-CrossCheck("components", "id",
-           ["component_cc", "component_id"],
-           ["flagexclusions", "component_id", "type_id"],
-           ["flaginclusions", "component_id", "type_id"]);
+CrossCheck('classifications', 'id', ['products', 'classification_id']);
+
+CrossCheck("keyworddefs", "id", ["keywords", "keywordid"]);
+
+CrossCheck(
+  "fielddefs", "id",
+  ["bugs_activity",     "fieldid"],
+  ['profiles_activity', 'fieldid']
+);
+
+CrossCheck(
+  "flagtypes", "id",
+  ["flags",          "type_id"],
+  ["flagexclusions", "type_id"],
+  ["flaginclusions", "type_id"]
+);
+
+CrossCheck(
+  "bugs",
+  "bug_id",
+  ["bugs_activity", "bug_id"],
+  ["bug_group_map", "bug_id"],
+  ["bugs_fulltext", "bug_id"],
+  ["attachments",   "bug_id"],
+  ["cc",            "bug_id"],
+  ["longdescs",     "bug_id"],
+  ["dependencies",  "blocked"],
+  ["dependencies",  "dependson"],
+  ['flags',         'bug_id'],
+  ["keywords",      "bug_id"],
+  ["duplicates", "dupe_of", "dupe"],
+  ["duplicates", "dupe",    "dupe_of"]
+);
+
+CrossCheck(
+  "groups",
+  "id",
+  ["bug_group_map",        "group_id"],
+  ['category_group_map',   'group_id'],
+  ["group_group_map",      "grantor_id"],
+  ["group_group_map",      "member_id"],
+  ["group_control_map",    "group_id"],
+  ["namedquery_group_map", "group_id"],
+  ["user_group_map",       "group_id"],
+  ["flagtypes",            "grant_group_id"],
+  ["flagtypes",            "request_group_id"]
+);
+
+CrossCheck(
+  "namedqueries", "id",
+  ["namedqueries_link_in_footer", "namedquery_id"],
+  ["namedquery_group_map",        "namedquery_id"],
+);
+
+CrossCheck(
+  "profiles",
+  "userid",
+  ['profiles_activity', 'userid'],
+  ['profiles_activity', 'who'],
+  ['email_setting',     'user_id'],
+  ['profile_setting',   'user_id'],
+  ["bugs",                        "reporter",         "bug_id"],
+  ["bugs",                        "assigned_to",      "bug_id"],
+  ["bugs",                        "qa_contact",       "bug_id"],
+  ["attachments",                 "submitter_id",     "bug_id"],
+  ['flags',                       'setter_id',        'bug_id'],
+  ['flags',                       'requestee_id',     'bug_id'],
+  ["bugs_activity",               "who",              "bug_id"],
+  ["cc",                          "who",              "bug_id"],
+  ['quips',                       'userid'],
+  ["longdescs",                   "who",              "bug_id"],
+  ["logincookies",                "userid"],
+  ["namedqueries",                "userid"],
+  ["namedqueries_link_in_footer", "user_id"],
+  ['series',                      'creator',          'series_id'],
+  ["watch",                       "watcher"],
+  ["watch",                       "watched"],
+  ['whine_events',                'owner_userid'],
+  ["tokens",                      "userid"],
+  ["user_group_map",              "user_id"],
+  ["components",                  "initialowner",     "name"],
+  ["components",                  "initialqacontact", "name"],
+  ["component_cc",                "user_id"]
+);
+
+CrossCheck(
+  "products",
+  "id",
+  ["bugs",              "product_id", "bug_id"],
+  ["components",        "product_id", "name"],
+  ["milestones",        "product_id", "value"],
+  ["versions",          "product_id", "value"],
+  ["group_control_map", "product_id"],
+  ["flaginclusions",    "product_id", "type_id"],
+  ["flagexclusions",    "product_id", "type_id"]
+);
+
+CrossCheck(
+  "components", "id",
+  ["component_cc", "component_id"],
+  ["flagexclusions", "component_id", "type_id"],
+  ["flaginclusions", "component_id", "type_id"]
+);
 
 # Check the former enum types -mkanat@bugzilla.org
-CrossCheck("bug_status", "value",
-            ["bugs", "bug_status", "bug_id"]);
+CrossCheck("bug_status", "value", ["bugs", "bug_status", "bug_id"]);
 
-CrossCheck("resolution", "value",
-            ["bugs", "resolution", "bug_id"]);
+CrossCheck("resolution", "value", ["bugs", "resolution", "bug_id"]);
 
-CrossCheck("bug_severity", "value",
-            ["bugs", "bug_severity", "bug_id"]);
+CrossCheck("bug_severity", "value", ["bugs", "bug_severity", "bug_id"]);
 
-CrossCheck("op_sys", "value",
-            ["bugs", "op_sys", "bug_id"]);
+CrossCheck("op_sys", "value", ["bugs", "op_sys", "bug_id"]);
 
-CrossCheck("priority", "value",
-            ["bugs", "priority", "bug_id"]);
+CrossCheck("priority", "value", ["bugs", "priority", "bug_id"]);
 
-CrossCheck("rep_platform", "value",
-            ["bugs", "rep_platform", "bug_id"]);
+CrossCheck("rep_platform", "value", ["bugs", "rep_platform", "bug_id"]);
 
-CrossCheck('series', 'series_id',
-           ['series_data', 'series_id']);
+CrossCheck('series', 'series_id', ['series_data', 'series_id']);
 
-CrossCheck('series_categories', 'id',
-           ['series', 'category'],
-           ["category_group_map", "category_id"],
-           ["series", "subcategory"]);
+CrossCheck(
+  'series_categories', 'id',
+  ['series',             'category'],
+  ["category_group_map", "category_id"],
+  ["series",             "subcategory"]
+);
 
-CrossCheck('whine_events', 'id',
-           ['whine_queries', 'eventid'],
-           ['whine_schedules', 'eventid']);
+CrossCheck(
+  'whine_events', 'id',
+  ['whine_queries',   'eventid'],
+  ['whine_schedules', 'eventid']
+);
 
-CrossCheck('attachments', 'attach_id',
-           ['attach_data', 'id'],
-           ['bugs_activity', 'attach_id']);
+CrossCheck(
+  'attachments', 'attach_id',
+  ['attach_data',   'id'],
+  ['bugs_activity', 'attach_id']
+);
 
-CrossCheck('bug_status', 'id',
-           ['status_workflow', 'old_status'],
-           ['status_workflow', 'new_status']);
+CrossCheck(
+  'bug_status', 'id',
+  ['status_workflow', 'old_status'],
+  ['status_workflow', 'new_status']
+);
 
 ###########################################################################
 # Perform double field referential (cross) checks
 ###########################################################################
+
 # This checks that a compound two-field foreign key has a valid primary key
 # value.  NULL references are acceptable and cause no problem.
 #
@@ -584,60 +645,77 @@ CrossCheck('bug_status', 'id',
 #     table to display when the check fails
 
 sub DoubleCrossCheck {
-    my $table = shift @_;
-    my $field1 = shift @_;
-    my $field2 = shift @_;
-    my $dbh = Bugzilla->dbh;
+  my $table  = shift @_;
+  my $field1 = shift @_;
+  my $field2 = shift @_;
+  my $dbh    = Bugzilla->dbh;
 
-    Status('double_cross_check_to',
-           {table => $table, field1 => $field1, field2 => $field2});
+  Status('double_cross_check_to',
+    {table => $table, field1 => $field1, field2 => $field2});
 
-    while (@_) {
-        my $ref = shift @_;
-        my ($refertable, $referfield1, $referfield2, $keyname) = @$ref;
+  while (@_) {
+    my $ref = shift @_;
+    my ($refertable, $referfield1, $referfield2, $keyname) = @$ref;
 
-        Status('double_cross_check_from',
-               {table => $refertable, field1 => $referfield1, field2 =>$referfield2});
+    Status('double_cross_check_from',
+      {table => $refertable, field1 => $referfield1, field2 => $referfield2});
 
-        my $d_cross_check = $dbh->selectall_arrayref(qq{
+    my $d_cross_check = $dbh->selectall_arrayref(
+      qq{
                         SELECT DISTINCT $refertable.$referfield1, 
-                                        $refertable.$referfield2 } .
-                       ($keyname ? qq{, $refertable.$keyname } : q{}) .
-                      qq{ FROM $refertable
+                                        $refertable.$referfield2 }
+        . ($keyname ? qq{, $refertable.$keyname } : q{}) . qq{ FROM $refertable
                      LEFT JOIN $table
                             ON $refertable.$referfield1 = $table.$field1
                            AND $refertable.$referfield2 = $table.$field2 
                          WHERE $table.$field1 IS NULL 
                            AND $table.$field2 IS NULL 
                            AND $refertable.$referfield1 IS NOT NULL 
-                           AND $refertable.$referfield2 IS NOT NULL});
-
-        foreach my $check (@$d_cross_check) {
-            my ($value1, $value2, $key) = @$check;
-            Status('double_cross_check_alert',
-                   {value1 => $value1, value2 => $value2,
-                    table => $refertable,
-                    field1 => $referfield1, field2 => $referfield2,
-                    keyname => $keyname, key => $key}, 'alert');
-        }
+                           AND $refertable.$referfield2 IS NOT NULL}
+    );
+
+    foreach my $check (@$d_cross_check) {
+      my ($value1, $value2, $key) = @$check;
+      Status(
+        'double_cross_check_alert',
+        {
+          value1  => $value1,
+          value2  => $value2,
+          table   => $refertable,
+          field1  => $referfield1,
+          field2  => $referfield2,
+          keyname => $keyname,
+          key     => $key
+        },
+        'alert'
+      );
     }
+  }
 }
 
-DoubleCrossCheck('attachments', 'bug_id', 'attach_id',
-                 ['flags', 'bug_id', 'attach_id'],
-                 ['bugs_activity', 'bug_id', 'attach_id']);
-
-DoubleCrossCheck("components", "product_id", "id",
-                 ["bugs", "product_id", "component_id", "bug_id"],
-                 ['flagexclusions', 'product_id', 'component_id'],
-                 ['flaginclusions', 'product_id', 'component_id']);
+DoubleCrossCheck(
+  'attachments', 'bug_id', 'attach_id',
+  ['flags',         'bug_id', 'attach_id'],
+  ['bugs_activity', 'bug_id', 'attach_id']
+);
+
+DoubleCrossCheck(
+  "components",
+  "product_id",
+  "id",
+  ["bugs",           "product_id", "component_id", "bug_id"],
+  ['flagexclusions', 'product_id', 'component_id'],
+  ['flaginclusions', 'product_id', 'component_id']
+);
 
 DoubleCrossCheck("versions", "product_id", "value",
-                 ["bugs", "product_id", "version", "bug_id"]);
-DoubleCrossCheck("milestones", "product_id", "value",
-                 ["bugs", "product_id", "target_milestone", "bug_id"],
-                 ["products", "id", "defaultmilestone", "name"]);
+  ["bugs", "product_id", "version", "bug_id"]);
+
+DoubleCrossCheck(
+  "milestones", "product_id", "value",
+  ["bugs",     "product_id", "target_milestone", "bug_id"],
+  ["products", "id",         "defaultmilestone", "name"]
+);
 
 ###########################################################################
 # Perform login checks
@@ -649,8 +727,8 @@ my $sth = $dbh->prepare(q{SELECT userid, email FROM profiles});
 $sth->execute;
 
 while (my ($id, $email) = $sth->fetchrow_array) {
-    validate_email_syntax($email)
-      || Status('profile_login_alert', {id => $id, email => $email}, 'alert');
+  validate_email_syntax($email)
+    || Status('profile_login_alert', {id => $id, email => $email}, 'alert');
 }
 
 ###########################################################################
@@ -658,42 +736,46 @@ while (my ($id, $email) = $sth->fetchrow_array) {
 ###########################################################################
 
 sub check_keywords {
-    my $dbh = Bugzilla->dbh;
-    my $cgi = Bugzilla->cgi;
-
-    Status('keyword_check_start');
-
-    my %keywordids;
-    my $keywords = $dbh->selectall_arrayref(q{SELECT id, name
-                                                FROM keyworddefs});
-
-    foreach (@$keywords) {
-        my ($id, $name) = @$_;
-        if ($keywordids{$id}) {
-            Status('keyword_check_alert', {id => $id}, 'alert');
-        }
-        $keywordids{$id} = 1;
-        if ($name =~ /[\s,]/) {
-            Status('keyword_check_invalid_name', {id => $id}, 'alert');
-        }
+  my $dbh = Bugzilla->dbh;
+  my $cgi = Bugzilla->cgi;
+
+  Status('keyword_check_start');
+
+  my %keywordids;
+  my $keywords = $dbh->selectall_arrayref(
+    q{SELECT id, name
+                                                FROM keyworddefs}
+  );
+
+  foreach (@$keywords) {
+    my ($id, $name) = @$_;
+    if ($keywordids{$id}) {
+      Status('keyword_check_alert', {id => $id}, 'alert');
     }
+    $keywordids{$id} = 1;
+    if ($name =~ /[\s,]/) {
+      Status('keyword_check_invalid_name', {id => $id}, 'alert');
+    }
+  }
 
-    my $sth = $dbh->prepare(q{SELECT bug_id, keywordid
+  my $sth = $dbh->prepare(
+    q{SELECT bug_id, keywordid
                                 FROM keywords
-                            ORDER BY bug_id, keywordid});
-    $sth->execute;
-    my $lastid;
-    my $lastk;
-    while (my ($id, $k) = $sth->fetchrow_array) {
-        if (!$keywordids{$k}) {
-            Status('keyword_check_invalid_id', {id => $k}, 'alert');
-        }
-        if (defined $lastid && $id eq $lastid && $k eq $lastk) {
-            Status('keyword_check_duplicated_ids', {id => $id}, 'alert');
-        }
-        $lastid = $id;
-        $lastk = $k;
+                            ORDER BY bug_id, keywordid}
+  );
+  $sth->execute;
+  my $lastid;
+  my $lastk;
+  while (my ($id, $k) = $sth->fetchrow_array) {
+    if (!$keywordids{$k}) {
+      Status('keyword_check_invalid_id', {id => $k}, 'alert');
     }
+    if (defined $lastid && $id eq $lastid && $k eq $lastk) {
+      Status('keyword_check_duplicated_ids', {id => $id}, 'alert');
+    }
+    $lastid = $id;
+    $lastk  = $k;
+  }
 }
 
 ###########################################################################
@@ -703,7 +785,7 @@ sub check_keywords {
 Status('flag_check_start');
 
 my $invalid_flags = $dbh->selectall_arrayref(
-       'SELECT DISTINCT flags.id, flags.bug_id, flags.attach_id
+  'SELECT DISTINCT flags.id, flags.bug_id, flags.attach_id
           FROM flags
     INNER JOIN bugs
             ON flags.bug_id = bugs.bug_id
@@ -711,40 +793,42 @@ my $invalid_flags = $dbh->selectall_arrayref(
             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 i.type_id IS NULL');
+         WHERE i.type_id IS NULL'
+);
 
 my @invalid_flags = @$invalid_flags;
 
 $invalid_flags = $dbh->selectall_arrayref(
-       'SELECT DISTINCT flags.id, flags.bug_id, flags.attach_id
+  'SELECT DISTINCT flags.id, flags.bug_id, flags.attach_id
           FROM flags
     INNER JOIN bugs
             ON flags.bug_id = bugs.bug_id
     INNER JOIN flagexclusions AS e
             ON flags.type_id = e.type_id
          WHERE (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)');
+           AND (bugs.component_id = e.component_id OR e.component_id IS NULL)'
+);
 
 push(@invalid_flags, @$invalid_flags);
 
 if (scalar(@invalid_flags)) {
-    if ($cgi->param('remove_invalid_flags')) {
-        Status('flag_deletion_start');
-        my @flag_ids = map {$_->[0]} @invalid_flags;
-        # Silently delete these flags, with no notification to requesters/setters.
-        $dbh->do('DELETE FROM flags WHERE id IN (' . join(',', @flag_ids) .')');
-        Status('flag_deletion_end');
-        Bugzilla->memcached->clear_all();
-    }
-    else {
-        foreach my $flag (@$invalid_flags) {
-            my ($flag_id, $bug_id, $attach_id) = @$flag;
-            Status('flag_alert',
-                   {flag_id => $flag_id, attach_id => $attach_id, bug_id => $bug_id},
-                   'alert');
-        }
-        Status('flag_fix');
+  if ($cgi->param('remove_invalid_flags')) {
+    Status('flag_deletion_start');
+    my @flag_ids = map { $_->[0] } @invalid_flags;
+
+    # Silently delete these flags, with no notification to requesters/setters.
+    $dbh->do('DELETE FROM flags WHERE id IN (' . join(',', @flag_ids) . ')');
+    Status('flag_deletion_end');
+    Bugzilla->memcached->clear_all();
+  }
+  else {
+    foreach my $flag (@$invalid_flags) {
+      my ($flag_id, $bug_id, $attach_id) = @$flag;
+      Status('flag_alert',
+        {flag_id => $flag_id, attach_id => $attach_id, bug_id => $bug_id}, 'alert');
     }
+    Status('flag_fix');
+  }
 }
 
 ###########################################################################
@@ -754,17 +838,18 @@ if (scalar(@invalid_flags)) {
 Status('product_check_start');
 
 my $products_missing_data = $dbh->selectcol_arrayref(
-      'SELECT DISTINCT products.name
+  'SELECT DISTINCT products.name
          FROM products
     LEFT JOIN components
            ON components.product_id = products.id
     LEFT JOIN versions
            ON versions.product_id = products.id
         WHERE components.id IS NULL
-           OR versions.id IS NULL');
+           OR versions.id IS NULL'
+);
 
 if (scalar(@$products_missing_data)) {
-    Status('product_alert', { name => $_ }, 'alert') foreach @$products_missing_data;
+  Status('product_alert', {name => $_}, 'alert') foreach @$products_missing_data;
 }
 
 ###########################################################################
@@ -772,45 +857,58 @@ if (scalar(@$products_missing_data)) {
 ###########################################################################
 
 sub BugCheck {
-    my ($middlesql, $errortext, $repairparam, $repairtext) = @_;
-    my $dbh = Bugzilla->dbh;
-    my $badbugs = $dbh->selectcol_arrayref(qq{SELECT DISTINCT bugs.bug_id
+  my ($middlesql, $errortext, $repairparam, $repairtext) = @_;
+  my $dbh = Bugzilla->dbh;
+
+  my $badbugs = $dbh->selectcol_arrayref(
+    qq{SELECT DISTINCT bugs.bug_id
                                                 FROM $middlesql 
-                                            ORDER BY bugs.bug_id});
-
-    if (scalar(@$badbugs)) {
-        Status('bug_check_alert',
-               {errortext => get_string($errortext), badbugs => $badbugs},
-               'alert');
-
-        if ($repairparam) {
-            $repairtext ||= 'repair_bugs';
-            Status('bug_check_repair',
-                   {param => $repairparam, text => get_string($repairtext)});
-        }
+                                            ORDER BY bugs.bug_id}
+  );
+
+  if (scalar(@$badbugs)) {
+    Status('bug_check_alert',
+      {errortext => get_string($errortext), badbugs => $badbugs}, 'alert');
+
+    if ($repairparam) {
+      $repairtext ||= 'repair_bugs';
+      Status('bug_check_repair',
+        {param => $repairparam, text => get_string($repairtext)});
     }
+  }
 }
 
 Status('bug_check_creation_date');
 
-BugCheck("bugs WHERE creation_ts IS NULL", 'bug_check_creation_date_error_text',
-         'repair_creation_date', 'bug_check_creation_date_repair_text');
+BugCheck(
+  "bugs WHERE creation_ts IS NULL", 'bug_check_creation_date_error_text',
+  'repair_creation_date',           'bug_check_creation_date_repair_text'
+);
 
 Status('bug_check_bugs_fulltext');
 
-BugCheck("bugs LEFT JOIN bugs_fulltext ON bugs_fulltext.bug_id = bugs.bug_id " .
-         "WHERE bugs_fulltext.bug_id IS NULL", 'bug_check_bugs_fulltext_error_text',
-         'repair_bugs_fulltext', 'bug_check_bugs_fulltext_repair_text');
+BugCheck(
+  "bugs LEFT JOIN bugs_fulltext ON bugs_fulltext.bug_id = bugs.bug_id "
+    . "WHERE bugs_fulltext.bug_id IS NULL",
+  'bug_check_bugs_fulltext_error_text',
+  'repair_bugs_fulltext',
+  'bug_check_bugs_fulltext_repair_text'
+);
 
 Status('bug_check_res_dupl');
 
-BugCheck("bugs INNER JOIN duplicates ON bugs.bug_id = duplicates.dupe " .
-         "WHERE bugs.resolution != 'DUPLICATE'", 'bug_check_res_dupl_error_text');
+BugCheck(
+  "bugs INNER JOIN duplicates ON bugs.bug_id = duplicates.dupe "
+    . "WHERE bugs.resolution != 'DUPLICATE'",
+  'bug_check_res_dupl_error_text'
+);
 
-BugCheck("bugs LEFT JOIN duplicates ON bugs.bug_id = duplicates.dupe WHERE " .
-         "bugs.resolution = 'DUPLICATE' AND " .
-         "duplicates.dupe IS NULL", 'bug_check_res_dupl_error_text2');
+BugCheck(
+  "bugs LEFT JOIN duplicates ON bugs.bug_id = duplicates.dupe WHERE "
+    . "bugs.resolution = 'DUPLICATE' AND "
+    . "duplicates.dupe IS NULL",
+  'bug_check_res_dupl_error_text2'
+);
 
 Status('bug_check_status_res');
 
@@ -818,20 +916,27 @@ my @open_states = map($dbh->quote($_), BUG_STATE_OPEN);
 my $open_states = join(', ', @open_states);
 
 BugCheck("bugs WHERE bug_status IN ($open_states) AND resolution != ''",
-         'bug_check_status_res_error_text');
+  'bug_check_status_res_error_text');
 BugCheck("bugs WHERE bug_status NOT IN ($open_states) AND resolution = ''",
-         'bug_check_status_res_error_text2');
+  'bug_check_status_res_error_text2');
 
 Status('bug_check_status_everconfirmed');
 
-BugCheck("bugs WHERE bug_status = 'UNCONFIRMED' AND everconfirmed = 1",
-         'bug_check_status_everconfirmed_error_text', 'repair_everconfirmed');
+BugCheck(
+  "bugs WHERE bug_status = 'UNCONFIRMED' AND everconfirmed = 1",
+  'bug_check_status_everconfirmed_error_text',
+  'repair_everconfirmed'
+);
 
-my @confirmed_open_states = grep {$_ ne 'UNCONFIRMED'} BUG_STATE_OPEN;
-my $confirmed_open_states = join(', ', map {$dbh->quote($_)} @confirmed_open_states);
+my @confirmed_open_states = grep { $_ ne 'UNCONFIRMED' } BUG_STATE_OPEN;
+my $confirmed_open_states
+  = join(', ', map { $dbh->quote($_) } @confirmed_open_states);
 
-BugCheck("bugs WHERE bug_status IN ($confirmed_open_states) AND everconfirmed = 0",
-         'bug_check_status_everconfirmed_error_text2', 'repair_everconfirmed');
+BugCheck(
+  "bugs WHERE bug_status IN ($confirmed_open_states) AND everconfirmed = 0",
+  'bug_check_status_everconfirmed_error_text2',
+  'repair_everconfirmed'
+);
 
 ###########################################################################
 # Control Values
@@ -840,8 +945,8 @@ BugCheck("bugs WHERE bug_status IN ($confirmed_open_states) AND everconfirmed =
 # Checks for values that are invalid OR
 # not among the 9 valid combinations
 Status('bug_check_control_values');
-my $groups = join(", ", (CONTROLMAPNA, CONTROLMAPSHOWN, CONTROLMAPDEFAULT,
-CONTROLMAPMANDATORY));
+my $groups = join(", ",
+  (CONTROLMAPNA, CONTROLMAPSHOWN, CONTROLMAPDEFAULT, CONTROLMAPMANDATORY));
 my $query = qq{
      SELECT COUNT(product_id) 
        FROM group_control_map 
@@ -853,10 +958,12 @@ my $query = qq{
                   OR (othercontrol = } . CONTROLMAPSHOWN . q{)))};
 
 my $entries = $dbh->selectrow_array($query);
-Status('bug_check_control_values_alert', {entries => $entries}, 'alert') if $entries;
+Status('bug_check_control_values_alert', {entries => $entries}, 'alert')
+  if $entries;
 
 Status('bug_check_control_values_violation');
-BugCheck("bugs
+BugCheck(
+  "bugs
          INNER JOIN bug_group_map
             ON bugs.bug_id = bug_group_map.bug_id
           LEFT JOIN group_control_map
@@ -864,11 +971,12 @@ BugCheck("bugs
            AND bug_group_map.group_id = group_control_map.group_id
          WHERE ((group_control_map.membercontrol = " . CONTROLMAPNA . ")
          OR (group_control_map.membercontrol IS NULL))",
-         'bug_check_control_values_error_text',
-         'createmissinggroupcontrolmapentries',
-         'bug_check_control_values_repair_text');
+  'bug_check_control_values_error_text', 'createmissinggroupcontrolmapentries',
+  'bug_check_control_values_repair_text'
+);
 
-BugCheck("bugs
+BugCheck(
+  "bugs
          INNER JOIN group_control_map
             ON bugs.product_id = group_control_map.product_id
          INNER JOIN groups
@@ -878,8 +986,8 @@ BugCheck("bugs
            AND group_control_map.group_id = bug_group_map.group_id
          WHERE group_control_map.membercontrol = " . CONTROLMAPMANDATORY . "
            AND bug_group_map.group_id IS NULL
-           AND groups.isactive != 0",
-         'bug_check_control_values_error_text2');
+           AND groups.isactive != 0", 'bug_check_control_values_error_text2'
+);
 
 ###########################################################################
 # Unsent mail
@@ -888,17 +996,19 @@ BugCheck("bugs
 Status('unsent_bugmail_check');
 
 my $time = $dbh->sql_date_math('NOW()', '-', 30, 'MINUTE');
-my $badbugs = $dbh->selectcol_arrayref(qq{
+my $badbugs = $dbh->selectcol_arrayref(
+  qq{
                     SELECT bug_id 
                       FROM bugs 
                      WHERE (lastdiffed IS NULL OR lastdiffed < delta_ts)
                        AND delta_ts < $time
-                  ORDER BY bug_id});
+                  ORDER BY bug_id}
+);
 
 
 if (scalar(@$badbugs > 0)) {
-    Status('unsent_bugmail_alert', {badbugs => $badbugs}, 'alert');
-    Status('unsent_bugmail_fix');
+  Status('unsent_bugmail_alert', {badbugs => $badbugs}, 'alert');
+  Status('unsent_bugmail_fix');
 }
 
 ###########################################################################
@@ -909,19 +1019,22 @@ Status('whines_obsolete_target_start');
 
 my $display_repair_whines_link = 0;
 foreach my $target (['groups', 'id', MAILTO_GROUP],
-                    ['profiles', 'userid', MAILTO_USER])
+  ['profiles', 'userid', MAILTO_USER])
 {
-    my ($table, $col, $type) = @$target;
-    my $old = $dbh->selectall_arrayref("SELECT whine_schedules.id, mailto
+  my ($table, $col, $type) = @$target;
+  my $old = $dbh->selectall_arrayref(
+    "SELECT whine_schedules.id, mailto
                                           FROM whine_schedules
                                      LEFT JOIN $table
                                             ON $table.$col = whine_schedules.mailto
-                                         WHERE mailto_type = $type AND $table.$col IS NULL");
-
-    if (scalar(@$old)) {
-        Status('whines_obsolete_target_alert', {schedules => $old, type => $type}, 'alert');
-        $display_repair_whines_link = 1;
-    }
+                                         WHERE mailto_type = $type AND $table.$col IS NULL"
+  );
+
+  if (scalar(@$old)) {
+    Status('whines_obsolete_target_alert',
+      {schedules => $old, type => $type}, 'alert');
+    $display_repair_whines_link = 1;
+  }
 }
 Status('whines_obsolete_target_fix') if $display_repair_whines_link;
 
@@ -929,7 +1042,7 @@ Status('whines_obsolete_target_fix') if $display_repair_whines_link;
 # Check hook
 ###########################################################################
 
-Bugzilla::Hook::process('sanitycheck_check', { status => \&Status });
+Bugzilla::Hook::process('sanitycheck_check', {status => \&Status});
 
 ###########################################################################
 # End
@@ -938,6 +1051,6 @@ Bugzilla::Hook::process('sanitycheck_check', { status => \&Status });
 Status('checks_completed');
 
 unless (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) {
-    $template->process('global/footer.html.tmpl', $vars)
-      || ThrowTemplateError($template->error());
+  $template->process('global/footer.html.tmpl', $vars)
+    || ThrowTemplateError($template->error());
 }
index 420cd393a098b352201ed98c2d858ffe5792a3d9..5e098ccac8befd1a3eafdb7b1be6796d2bf6c252 100755 (executable)
@@ -21,23 +21,25 @@ use Bugzilla::Mailer;
 use Getopt::Long;
 use Pod::Usage;
 
-my $verbose = 0; # Return all comments if true, else errors only.
+my $verbose = 0;    # Return all comments if true, else errors only.
 my $login = '';  # Login name of the user which is used to call sanitycheck.cgi.
-my $help = 0;    # Has user asked for help on this script?
+my $help  = 0;   # Has user asked for help on this script?
 
-my $result = GetOptions('verbose'  => \$verbose,
-                        'login=s'  => \$login,
-                        'help|h|?' => \$help);
+my $result = GetOptions(
+  'verbose'  => \$verbose,
+  'login=s'  => \$login,
+  'help|h|?' => \$help
+);
 
 pod2usage({-verbose => 1, -exitval => 1}) if $help;
 
 # Be sure a login name if given.
 $login || ThrowUserError('invalid_username');
 
-my $user = new Bugzilla::User({ name => $login })
-  || ThrowUserError('invalid_username', { name => $login });
+my $user = new Bugzilla::User({name => $login})
+  || ThrowUserError('invalid_username', {name => $login});
 
-my $cgi = Bugzilla->cgi;
+my $cgi      = Bugzilla->cgi;
 my $template = Bugzilla->template;
 
 # Authenticate using this user account.
@@ -50,16 +52,16 @@ require 'sanitycheck.cgi';
 
 # Now it's time to send an email to the user if there is something to notify.
 if ($cgi->param('output')) {
-    my $message;
-    my $vars = {};
-    $vars->{'addressee'} = $user->email;
-    $vars->{'output'} = $cgi->param('output');
-    $vars->{'error_found'} = $cgi->param('error_found') ? 1 : 0;
+  my $message;
+  my $vars = {};
+  $vars->{'addressee'}   = $user->email;
+  $vars->{'output'}      = $cgi->param('output');
+  $vars->{'error_found'} = $cgi->param('error_found') ? 1 : 0;
 
-    $template->process('email/sanitycheck.txt.tmpl', $vars, \$message)
-      || ThrowTemplateError($template->error());
+  $template->process('email/sanitycheck.txt.tmpl', $vars, \$message)
+    || ThrowTemplateError($template->error());
 
-    MessageToMTA($message);
+  MessageToMTA($message);
 }
 
 
index 0af1342014356f3c562ebc82e7a2d75ab8b5529e..9ee87b86d38829127700ed7c809eb4f270cd5446 100755 (executable)
@@ -18,9 +18,9 @@ use Bugzilla::Constants;
 
 Bugzilla->login();
 
-my $cgi = Bugzilla->cgi;
+my $cgi      = Bugzilla->cgi;
 my $template = Bugzilla->template;
-my $vars = {};
+my $vars     = {};
 
 # Return the appropriate HTTP response headers.
 print $cgi->header('application/xml');
@@ -28,10 +28,10 @@ print $cgi->header('application/xml');
 # Get the contents of favicon.ico
 my $filename = bz_locations()->{'libpath'} . "/images/favicon.ico";
 if (open(IN, '<', $filename)) {
-    local $/;
-    binmode IN;
-    $vars->{'favicon'} = <IN>;
-    close IN;
+  local $/;
+  binmode IN;
+  $vars->{'favicon'} = <IN>;
+  close IN;
 }
 $template->process("search/search-plugin.xml.tmpl", $vars)
   || ThrowTemplateError($template->error());
index fa231ed512039c8756580f24bafd9b9f98da840f..b7aaf453ed6f46d1caee48056e08b711e1f047e0 100755 (executable)
@@ -16,20 +16,20 @@ use Bugzilla;
 use Bugzilla::Error;
 use Bugzilla::Bug;
 
-my $cgi = Bugzilla->cgi;
+my $cgi      = Bugzilla->cgi;
 my $template = Bugzilla->template;
-my $vars = {};
+my $vars     = {};
 
 ###############################################################################
 # Begin Data/Security Validation
 ###############################################################################
 
-# Check whether or not the user is currently logged in. 
+# Check whether or not the user is currently logged in.
 Bugzilla->login();
 
 # Make sure the bug ID is a positive integer representing an existing
 # bug that the user is authorized to access.
-my $id = $cgi->param('id');
+my $id  = $cgi->param('id');
 my $bug = Bugzilla::Bug->check($id);
 
 ###############################################################################
@@ -40,7 +40,8 @@ my $bug = Bugzilla::Bug->check($id);
 # visible immediately due to replication lag.
 Bugzilla->switch_to_shadow_db;
 
-($vars->{'operations'}, $vars->{'incomplete_data'}) = $bug->get_activity(undef, undef, 1);
+($vars->{'operations'}, $vars->{'incomplete_data'})
+  = $bug->get_activity(undef, undef, 1);
 
 $vars->{'bug'} = $bug;
 
index 41aa006825a6d5a80c9fdef0a8421b4ffe5334ae..80c517fdeb4fd3fe82d51fbbbdcc187376046a1f 100755 (executable)
@@ -18,24 +18,27 @@ use Bugzilla::Error;
 use Bugzilla::Util;
 use Bugzilla::Bug;
 
-my $cgi = Bugzilla->cgi;
+my $cgi      = Bugzilla->cgi;
 my $template = Bugzilla->template;
-my $vars = {};
+my $vars     = {};
 
 my $user = Bugzilla->login();
 
-my $format = $template->get_format("bug/show", scalar $cgi->param('format'),
-                                   scalar $cgi->param('ctype'));
+my $format = $template->get_format(
+  "bug/show",
+  scalar $cgi->param('format'),
+  scalar $cgi->param('ctype')
+);
 
 # Editable, 'single' HTML bugs are treated slightly specially in a few places
 my $single = !$format->{format} && $format->{extension} eq 'html';
 
 # If we don't have an ID, _AND_ we're only doing a single bug, then prompt
 if (!$cgi->param('id') && $single) {
-    print $cgi->header();
-    $template->process("bug/choose.html.tmpl", $vars) ||
-      ThrowTemplateError($template->error());
-    exit;
+  print $cgi->header();
+  $template->process("bug/choose.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
+  exit;
 }
 
 my (@bugs, @illegal_bugs);
@@ -47,48 +50,51 @@ my %marks;
 Bugzilla->switch_to_shadow_db unless $user->id;
 
 if ($single) {
-    my $id = $cgi->param('id');
-    push @bugs, Bugzilla::Bug->check({ id => $id, cache => 1 });
-    if (defined $cgi->param('mark')) {
-        foreach my $range (split ',', $cgi->param('mark')) {
-            if ($range =~ /^(\d+)-(\d+)$/a) {
-               foreach my $i ($1..$2) {
-                   $marks{$i} = 1;
-               }
-            } elsif ($range =~ /^(\d+)$/a) {
-               $marks{$1} = 1;
-            }
+  my $id = $cgi->param('id');
+  push @bugs, Bugzilla::Bug->check({id => $id, cache => 1});
+  if (defined $cgi->param('mark')) {
+    foreach my $range (split ',', $cgi->param('mark')) {
+      if ($range =~ /^(\d+)-(\d+)$/a) {
+        foreach my $i ($1 .. $2) {
+          $marks{$i} = 1;
         }
+      }
+      elsif ($range =~ /^(\d+)$/a) {
+        $marks{$1} = 1;
+      }
+    }
+  }
+}
+else {
+  foreach my $id ($cgi->multi_param('id')) {
+
+    # Be kind enough and accept URLs of the form: id=1,2,3.
+    my @ids = split(/,/, $id);
+    my @check_bugs;
+
+    foreach my $bug_id (@ids) {
+      next unless $bug_id;
+      my $bug = new Bugzilla::Bug({id => $bug_id, cache => 1});
+      if (!$bug->{error}) {
+        push(@check_bugs, $bug);
+      }
+      else {
+        push(@illegal_bugs, {bug_id => trim($bug_id), error => $bug->{error}});
+      }
     }
-} else {
-    foreach my $id ($cgi->multi_param('id')) {
-        # Be kind enough and accept URLs of the form: id=1,2,3.
-        my @ids = split(/,/, $id);
-        my @check_bugs;
-
-        foreach my $bug_id (@ids) {
-            next unless $bug_id;
-            my $bug = new Bugzilla::Bug({ id => $bug_id, cache => 1 });
-            if (!$bug->{error}) {
-                push(@check_bugs, $bug);
-            }
-            else {
-                push(@illegal_bugs, { bug_id => trim($bug_id), error => $bug->{error} });
-            }
-        }
 
-        $user->visible_bugs(\@check_bugs);
+    $user->visible_bugs(\@check_bugs);
 
-        foreach my $bug (@check_bugs) {
-            if ($user->can_see_bug($bug->id)) {
-                push(@bugs, $bug);
-            }
-            else {
-                my $error = 'NotPermitted'; # Trick to make 012throwables.t happy.
-                push(@illegal_bugs, { bug_id => $bug->id, error => $error });
-            }
-        }
+    foreach my $bug (@check_bugs) {
+      if ($user->can_see_bug($bug->id)) {
+        push(@bugs, $bug);
+      }
+      else {
+        my $error = 'NotPermitted';    # Trick to make 012throwables.t happy.
+        push(@illegal_bugs, {bug_id => $bug->id, error => $error});
+      }
     }
+  }
 }
 
 Bugzilla::Bug->preload(\@bugs);
@@ -96,31 +102,35 @@ Bugzilla::Bug->preload(\@bugs);
 $vars->{'bugs'} = [@bugs, @illegal_bugs];
 $vars->{'marks'} = \%marks;
 
-my @bugids = map {$_->bug_id} grep {!$_->error} @bugs;
+my @bugids = map { $_->bug_id } grep { !$_->error } @bugs;
 $vars->{'bugids'} = join(", ", @bugids);
 
 # Work out which fields we are displaying (currently XML only.)
 # If no explicit list is defined, we show all fields. We then exclude any
-# on the exclusion list. This is so you can say e.g. "Everything except 
+# on the exclusion list. This is so you can say e.g. "Everything except
 # attachments" without listing almost all the fields.
-my @fieldlist = (Bugzilla::Bug->fields, 'flag', 'group', 'long_desc',
-                 'attachment', 'attachmentdata', 'token');
+my @fieldlist = (
+  Bugzilla::Bug->fields, 'flag',
+  'group',               'long_desc',
+  'attachment',          'attachmentdata',
+  'token'
+);
 my %displayfields;
 
 if ($cgi->param("field")) {
-    @fieldlist = $cgi->multi_param("field");
+  @fieldlist = $cgi->multi_param("field");
 }
 
 unless ($user->is_timetracker) {
-    @fieldlist = grep($_ !~ /_time$/, @fieldlist);
+  @fieldlist = grep($_ !~ /_time$/, @fieldlist);
 }
 
 foreach (@fieldlist) {
-    $displayfields{$_} = 1;
+  $displayfields{$_} = 1;
 }
 
 foreach ($cgi->multi_param("excludefield")) {
-    $displayfields{$_} = undef;
+  $displayfields{$_} = undef;
 }
 
 $vars->{'displayfields'} = \%displayfields;
index c45bd06f36271f08465c5a5526ba68dd9617d7a1..9f1cab10e09efdc5fef40bd6a822aae0aa5256db 100755 (executable)
@@ -24,9 +24,10 @@ use Bugzilla::Status;
 
 my $user = Bugzilla->login();
 
-my $cgi = Bugzilla->cgi;
+my $cgi      = Bugzilla->cgi;
 my $template = Bugzilla->template;
-my $vars = {};
+my $vars     = {};
+
 # Connect to the shadow database if this installation is using one to improve
 # performance.
 my $dbh = Bugzilla->switch_to_shadow_db();
@@ -34,7 +35,7 @@ my $dbh = Bugzilla->switch_to_shadow_db();
 our (%seen, %edgesdone, %bugtitles);
 our $bug_count = 0;
 
-# CreateImagemap: This sub grabs a local filename as a parameter, reads the 
+# CreateImagemap: This sub grabs a local filename as a parameter, reads the
 # dot-generated image map datafile residing in that file and turns it into
 # an HTML map element. THIS SUB IS ONLY USED FOR LOCAL DOT INSTALLATIONS.
 # The map datafile won't necessarily contain the bug summaries, so we'll
@@ -45,50 +46,53 @@ our $bug_count = 0;
 # rectangle (LEFTX,TOPY) (RIGHTX,BOTTOMY) URLBASE/show_bug.cgi?id=BUGNUM BUGNUM[\nSUMMARY]
 
 sub CreateImagemap {
-    my $mapfilename = shift;
-    my $map = "<map name=\"imagemap\">\n";
-    my $default = "";
-
-    open MAP, "<", $mapfilename;
-    while(my $line = <MAP>) {
-        if($line =~ /^default ([^ ]*)(.*)$/) {
-            $default = qq{<area alt="" shape="default" href="$1">\n};
-        }
-
-        if ($line =~ /^rectangle \((\d+),(\d+)\) \((\d+),(\d+)\) (http[^ ]*) (\d+)(?:\\n.*)?$/) {
-            my ($leftx, $rightx, $topy, $bottomy, $url, $bugid) = ($1, $3, $2, $4, $5, $6);
-
-            # Pick up bugid from the mapdata label field. Getting the title from
-            # bugtitle hash instead of mapdata allows us to get the summary even
-            # when showsummary is off, and also gives us status and resolution.
-            # This text is safe; it has already been escaped.
-            my $bugtitle = $bugtitles{$bugid};
-
-            # The URL is supposed to be safe, because it's built manually.
-            # But in case someone manages to inject code, it's safer to escape it.
-            $url = html_quote($url);
-
-            $map .= qq{<area alt="bug $bugid" name="bug$bugid" shape="rect" } .
-                    qq{title="$bugtitle" href="$url" } .
-                    qq{coords="$leftx,$topy,$rightx,$bottomy">\n};
-        }
+  my $mapfilename = shift;
+  my $map         = "<map name=\"imagemap\">\n";
+  my $default     = "";
+
+  open MAP, "<", $mapfilename;
+  while (my $line = <MAP>) {
+    if ($line =~ /^default ([^ ]*)(.*)$/) {
+      $default = qq{<area alt="" shape="default" href="$1">\n};
+    }
+
+    if ($line
+      =~ /^rectangle \((\d+),(\d+)\) \((\d+),(\d+)\) (http[^ ]*) (\d+)(?:\\n.*)?$/)
+    {
+      my ($leftx, $rightx, $topy, $bottomy, $url, $bugid) = ($1, $3, $2, $4, $5, $6);
+
+      # Pick up bugid from the mapdata label field. Getting the title from
+      # bugtitle hash instead of mapdata allows us to get the summary even
+      # when showsummary is off, and also gives us status and resolution.
+      # This text is safe; it has already been escaped.
+      my $bugtitle = $bugtitles{$bugid};
+
+      # The URL is supposed to be safe, because it's built manually.
+      # But in case someone manages to inject code, it's safer to escape it.
+      $url = html_quote($url);
+
+      $map
+        .= qq{<area alt="bug $bugid" name="bug$bugid" shape="rect" }
+        . qq{title="$bugtitle" href="$url" }
+        . qq{coords="$leftx,$topy,$rightx,$bottomy">\n};
     }
-    close MAP;
+  }
+  close MAP;
 
-    $map .= "$default</map>";
-    return $map;
+  $map .= "$default</map>";
+  return $map;
 }
 
 sub AddLink {
-    my ($blocked, $dependson, $fh) = (@_);
-    my $key = "$blocked,$dependson";
-    if (!exists $edgesdone{$key}) {
-        $edgesdone{$key} = 1;
-        print $fh "$dependson -> $blocked\n";
-        $bug_count++;
-        $seen{$blocked} = 1;
-        $seen{$dependson} = 1;
-    }
+  my ($blocked, $dependson, $fh) = (@_);
+  my $key = "$blocked,$dependson";
+  if (!exists $edgesdone{$key}) {
+    $edgesdone{$key} = 1;
+    print $fh "$dependson -> $blocked\n";
+    $bug_count++;
+    $seen{$blocked}   = 1;
+    $seen{$dependson} = 1;
+  }
 }
 
 ThrowUserError("missing_bug_id") unless $cgi->param('id');
@@ -98,22 +102,24 @@ ThrowUserError("missing_bug_id") unless $cgi->param('id');
 my @valid_rankdirs = ('LR', 'RL', 'TB', 'BT');
 
 my $rankdir = $cgi->param('rankdir') || 'TB';
+
 # Make sure the submitted 'rankdir' value is valid.
 if (!grep { $_ eq $rankdir } @valid_rankdirs) {
-    $rankdir = 'TB';
+  $rankdir = 'TB';
 }
 
 my $display = $cgi->param('display') || 'tree';
 my $webdotdir = bz_locations()->{'webdotdir'};
 
-my ($fh, $filename) = File::Temp::tempfile("XXXXXXXXXX",
-                                           SUFFIX => '.dot',
-                                           DIR => $webdotdir,
-                                           UNLINK => 1);
+my ($fh, $filename) = File::Temp::tempfile(
+  "XXXXXXXXXX",
+  SUFFIX => '.dot',
+  DIR    => $webdotdir,
+  UNLINK => 1
+);
 
 chmod Bugzilla::Install::Filesystem::CGI_WRITE, $filename
-    or warn install_string('chmod_failed', { path => $filename,
-                                             error => $! });
+  or warn install_string('chmod_failed', {path => $filename, error => $!});
 
 my $urlbase = correct_urlbase();
 
@@ -126,117 +132,127 @@ node [URL="${urlbase}show_bug.cgi?id=\\N", style=filled, color=lightgrey]
 my %baselist;
 
 foreach my $i (split('[\s,]+', $cgi->param('id'))) {
-    my $bug = Bugzilla::Bug->check($i);
-    $baselist{$bug->id} = 1;
+  my $bug = Bugzilla::Bug->check($i);
+  $baselist{$bug->id} = 1;
 }
 
 my @stack = keys(%baselist);
 
 if ($display eq 'web') {
-    my $sth = $dbh->prepare(q{SELECT blocked, dependson
+  my $sth = $dbh->prepare(
+    q{SELECT blocked, dependson
                                 FROM dependencies
-                               WHERE blocked = ? OR dependson = ?});
-
-    foreach my $id (@stack) {
-        my $dependencies = $dbh->selectall_arrayref($sth, undef, ($id, $id));
-        foreach my $dependency (@$dependencies) {
-            my ($blocked, $dependson) = @$dependency;
-            if ($blocked != $id && !exists $seen{$blocked}) {
-                push @stack, $blocked;
-            }
-            if ($dependson != $id && !exists $seen{$dependson}) {
-                push @stack, $dependson;
-            }
-            AddLink($blocked, $dependson, $fh);
-        }
+                               WHERE blocked = ? OR dependson = ?}
+  );
+
+  foreach my $id (@stack) {
+    my $dependencies = $dbh->selectall_arrayref($sth, undef, ($id, $id));
+    foreach my $dependency (@$dependencies) {
+      my ($blocked, $dependson) = @$dependency;
+      if ($blocked != $id && !exists $seen{$blocked}) {
+        push @stack, $blocked;
+      }
+      if ($dependson != $id && !exists $seen{$dependson}) {
+        push @stack, $dependson;
+      }
+      AddLink($blocked, $dependson, $fh);
     }
+  }
 }
+
 # This is the default: a tree instead of a spider web.
 else {
-    my @blocker_stack = @stack;
-    my $hide_resolved = $cgi->param('hide_resolved');
-
-    foreach my $id (@blocker_stack) {
-        my $blocker_ids = Bugzilla::Bug::EmitDependList('blocked', 'dependson', $id, $hide_resolved);
-        foreach my $blocker_id (@$blocker_ids) {
-            push(@blocker_stack, $blocker_id) unless $seen{$blocker_id};
-            AddLink($id, $blocker_id, $fh);
-        }
+  my @blocker_stack = @stack;
+  my $hide_resolved = $cgi->param('hide_resolved');
+
+  foreach my $id (@blocker_stack) {
+    my $blocker_ids
+      = Bugzilla::Bug::EmitDependList('blocked', 'dependson', $id, $hide_resolved);
+    foreach my $blocker_id (@$blocker_ids) {
+      push(@blocker_stack, $blocker_id) unless $seen{$blocker_id};
+      AddLink($id, $blocker_id, $fh);
     }
-    my @dependent_stack = @stack;
-    foreach my $id (@dependent_stack) {
-        my $dep_bug_ids = Bugzilla::Bug::EmitDependList('dependson', 'blocked', $id, $hide_resolved);
-        foreach my $dep_bug_id (@$dep_bug_ids) {
-            push(@dependent_stack, $dep_bug_id) unless $seen{$dep_bug_id};
-            AddLink($dep_bug_id, $id, $fh);
-        }
+  }
+  my @dependent_stack = @stack;
+  foreach my $id (@dependent_stack) {
+    my $dep_bug_ids
+      = Bugzilla::Bug::EmitDependList('dependson', 'blocked', $id, $hide_resolved);
+    foreach my $dep_bug_id (@$dep_bug_ids) {
+      push(@dependent_stack, $dep_bug_id) unless $seen{$dep_bug_id};
+      AddLink($dep_bug_id, $id, $fh);
     }
+  }
 }
 
 foreach my $k (keys(%baselist)) {
-    $seen{$k} = 1;
+  $seen{$k} = 1;
 }
 
 my $sth = $dbh->prepare(
-              q{SELECT bug_status, resolution, short_desc
+  q{SELECT bug_status, resolution, short_desc
                   FROM bugs
-                 WHERE bugs.bug_id = ?});
+                 WHERE bugs.bug_id = ?}
+);
 
 my @bug_ids = keys %seen;
 $user->visible_bugs(\@bug_ids);
 foreach my $k (@bug_ids) {
-    # Retrieve bug information from the database
-    my ($stat, $resolution, $summary) = $dbh->selectrow_array($sth, undef, $k);
-
-    $vars->{'short_desc'} = $summary if ($k eq $cgi->param('id'));
-
-    # The bug summary is shown only if the user can see the bug.
-    if ($user->can_see_bug($k)) {
-        $summary = html_quote(clean_text($summary));
-    }
-    else {
-        $summary = '';
-    }
-
-    my @params;
-
-    if ($summary ne "" && $cgi->param('showsummary')) {
-        # Wide characters cause GraphViz to die.
-        utf8::encode($summary) if utf8::is_utf8($summary);
-        $summary = wrap_comment($summary);
-        $summary =~ s/([\\\"])/\\$1/g;
-        # Newlines must be escaped too, to not break the .map file
-        # and to prevent code injection.
-        $summary =~ s/\n/\\n/g;
-        push(@params, qq{label="$k\\n$summary"});
-    }
-
-    if (exists $baselist{$k}) {
-        push(@params, "shape=box");
-    }
-
-    if (is_open_state($stat)) {
-        push(@params, "color=green");
-    }
-
-    if (@params) {
-        print $fh "$k [" . join(',', @params) . "]\n";
-    } else {
-        print $fh "$k\n";
-    }
 
-    # Push the bug tooltip texts into a global hash so that 
-    # CreateImagemap sub (used with local dot installations) can
-    # use them later on.
-    my $stat_display       = display_value('bug_status', $stat);
-    my $resolution_display = display_value('resolution', $resolution);
-    $bugtitles{$k} = trim("$stat_display $resolution_display");
-
-    # Show the bug summary in tooltips only if not shown on 
-    # the graph and it is non-empty (the user can see the bug)
-    if (!$cgi->param('showsummary') && $summary ne "") {
-        $bugtitles{$k} .= " - $summary";
-    }
+  # Retrieve bug information from the database
+  my ($stat, $resolution, $summary) = $dbh->selectrow_array($sth, undef, $k);
+
+  $vars->{'short_desc'} = $summary if ($k eq $cgi->param('id'));
+
+  # The bug summary is shown only if the user can see the bug.
+  if ($user->can_see_bug($k)) {
+    $summary = html_quote(clean_text($summary));
+  }
+  else {
+    $summary = '';
+  }
+
+  my @params;
+
+  if ($summary ne "" && $cgi->param('showsummary')) {
+
+    # Wide characters cause GraphViz to die.
+    utf8::encode($summary) if utf8::is_utf8($summary);
+    $summary = wrap_comment($summary);
+    $summary =~ s/([\\\"])/\\$1/g;
+
+    # Newlines must be escaped too, to not break the .map file
+    # and to prevent code injection.
+    $summary =~ s/\n/\\n/g;
+    push(@params, qq{label="$k\\n$summary"});
+  }
+
+  if (exists $baselist{$k}) {
+    push(@params, "shape=box");
+  }
+
+  if (is_open_state($stat)) {
+    push(@params, "color=green");
+  }
+
+  if (@params) {
+    print $fh "$k [" . join(',', @params) . "]\n";
+  }
+  else {
+    print $fh "$k\n";
+  }
+
+  # Push the bug tooltip texts into a global hash so that
+  # CreateImagemap sub (used with local dot installations) can
+  # use them later on.
+  my $stat_display       = display_value('bug_status', $stat);
+  my $resolution_display = display_value('resolution', $resolution);
+  $bugtitles{$k} = trim("$stat_display $resolution_display");
+
+  # Show the bug summary in tooltips only if not shown on
+  # the graph and it is non-empty (the user can see the bug)
+  if (!$cgi->param('showsummary') && $summary ne "") {
+    $bugtitles{$k} .= " - $summary";
+  }
 }
 
 
@@ -244,98 +260,97 @@ print $fh "}\n";
 close $fh;
 
 if ($bug_count > MAX_WEBDOT_BUGS) {
-    unlink($filename);
-    ThrowUserError("webdot_too_large");
+  unlink($filename);
+  ThrowUserError("webdot_too_large");
 }
 
 my $webdotbase = Bugzilla->localconfig->{'webdotbase'};
 
 if ($webdotbase =~ /^https?:/) {
-     # Remote dot server. We don't hardcode 'urlbase' here in case
-     # 'sslbase' is in use.
-     $webdotbase =~ s/%([a-z]*)%/Bugzilla->params->{$1}/eg;
-     my $url = $webdotbase . $filename;
-     $vars->{'image_url'} = $url . ".gif";
-     $vars->{'map_url'} = $url . ".map";
-} else {
-    # Local dot installation
-
-    # First, generate the png image file from the .dot source
-
-    my ($pngfh, $pngfilename) = File::Temp::tempfile("XXXXXXXXXX",
-                                                     SUFFIX => '.png',
-                                                     DIR => $webdotdir);
-
-    chmod Bugzilla::Install::Filesystem::WS_SERVE, $pngfilename
-        or warn install_string('chmod_failed', { path => $pngfilename,
-                                                 error => $! });
-
-    binmode $pngfh;
-    open(DOT, '-|', "\"$webdotbase\" -Tpng $filename");
-    binmode DOT;
-    print $pngfh $_ while <DOT>;
-    close DOT;
-    close $pngfh;
-
-    # On Windows $pngfilename will contain \ instead of /
-    $pngfilename =~ s|\\|/|g if ON_WINDOWS;
-
-    # Under mod_perl, pngfilename will have an absolute path, and we
-    # need to make that into a relative path.
-    my $cgi_root = bz_locations()->{cgi_path};
-    $pngfilename =~ s#^\Q$cgi_root\E/?##;
-    
-    $vars->{'image_url'} = $pngfilename;
-
-    # Then, generate a imagemap datafile that contains the corner data
-    # for drawn bug objects. Pass it on to CreateImagemap that
-    # turns this monster into html.
-
-    my ($mapfh, $mapfilename) = File::Temp::tempfile("XXXXXXXXXX",
-                                                     SUFFIX => '.map',
-                                                     DIR => $webdotdir);
-
-    chmod Bugzilla::Install::Filesystem::WS_SERVE, $mapfilename
-        or warn install_string('chmod_failed', { path => $mapfilename,
-                                                 error => $! });
-
-    binmode $mapfh;
-    open(DOT, '-|', "\"$webdotbase\" -Tismap $filename");
-    binmode DOT;
-    print $mapfh $_ while <DOT>;
-    close DOT;
-    close $mapfh;
-
-    $vars->{'image_map'} = CreateImagemap($mapfilename);
+
+  # Remote dot server. We don't hardcode 'urlbase' here in case
+  # 'sslbase' is in use.
+  $webdotbase =~ s/%([a-z]*)%/Bugzilla->params->{$1}/eg;
+  my $url = $webdotbase . $filename;
+  $vars->{'image_url'} = $url . ".gif";
+  $vars->{'map_url'}   = $url . ".map";
+}
+else {
+  # Local dot installation
+
+  # First, generate the png image file from the .dot source
+
+  my ($pngfh, $pngfilename)
+    = File::Temp::tempfile("XXXXXXXXXX", SUFFIX => '.png', DIR => $webdotdir);
+
+  chmod Bugzilla::Install::Filesystem::WS_SERVE, $pngfilename
+    or warn install_string('chmod_failed', {path => $pngfilename, error => $!});
+
+  binmode $pngfh;
+  open(DOT, '-|', "\"$webdotbase\" -Tpng $filename");
+  binmode DOT;
+  print $pngfh $_ while <DOT>;
+  close DOT;
+  close $pngfh;
+
+  # On Windows $pngfilename will contain \ instead of /
+  $pngfilename =~ s|\\|/|g if ON_WINDOWS;
+
+  # Under mod_perl, pngfilename will have an absolute path, and we
+  # need to make that into a relative path.
+  my $cgi_root = bz_locations()->{cgi_path};
+  $pngfilename =~ s#^\Q$cgi_root\E/?##;
+
+  $vars->{'image_url'} = $pngfilename;
+
+  # Then, generate a imagemap datafile that contains the corner data
+  # for drawn bug objects. Pass it on to CreateImagemap that
+  # turns this monster into html.
+
+  my ($mapfh, $mapfilename)
+    = File::Temp::tempfile("XXXXXXXXXX", SUFFIX => '.map', DIR => $webdotdir);
+
+  chmod Bugzilla::Install::Filesystem::WS_SERVE, $mapfilename
+    or warn install_string('chmod_failed', {path => $mapfilename, error => $!});
+
+  binmode $mapfh;
+  open(DOT, '-|', "\"$webdotbase\" -Tismap $filename");
+  binmode DOT;
+  print $mapfh $_ while <DOT>;
+  close DOT;
+  close $mapfh;
+
+  $vars->{'image_map'} = CreateImagemap($mapfilename);
 }
 
 # Cleanup any old .dot files created from previous runs.
 my $since = time() - 24 * 60 * 60;
+
 # Can't use glob, since even calling that fails taint checks for perl < 5.6
 opendir(DIR, $webdotdir);
 my @files = grep { /\.dot$|\.png$|\.map$/ && -f "$webdotdir/$_" } readdir(DIR);
 closedir DIR;
-foreach my $f (@files)
-{
-    $f = "$webdotdir/$f";
-    # Here we are deleting all old files. All entries are from the
-    # $webdot directory. Since we're deleting the file (not following
-    # symlinks), this can't escape to delete anything it shouldn't
-    # (unless someone moves the location of $webdotdir, of course)
-    trick_taint($f);
-    my $mtime = (stat($f))[9];
-    if ($mtime && $mtime < $since) {
-        unlink $f;
-    }
+foreach my $f (@files) {
+  $f = "$webdotdir/$f";
+
+  # Here we are deleting all old files. All entries are from the
+  # $webdot directory. Since we're deleting the file (not following
+  # symlinks), this can't escape to delete anything it shouldn't
+  # (unless someone moves the location of $webdotdir, of course)
+  trick_taint($f);
+  my $mtime = (stat($f))[9];
+  if ($mtime && $mtime < $since) {
+    unlink $f;
+  }
 }
 
 # Make sure we only include valid integers (protects us from XSS attacks).
 my @bugs = grep(detaint_natural($_), split(/[\s,]+/, $cgi->param('id')));
-$vars->{'bug_id'} = join(', ', @bugs);
+$vars->{'bug_id'}        = join(', ', @bugs);
 $vars->{'multiple_bugs'} = ($cgi->param('id') =~ /[ ,]/);
-$vars->{'display'} = $display;
-$vars->{'rankdir'} = $rankdir;
-$vars->{'showsummary'} = $cgi->param('showsummary');
+$vars->{'display'}       = $display;
+$vars->{'rankdir'}       = $rankdir;
+$vars->{'showsummary'}   = $cgi->param('showsummary');
 $vars->{'hide_resolved'} = $cgi->param('hide_resolved');
 
 # Generate and return the UI (HTML page) from the appropriate template.
index cbfcd59cd6ed56d3b6f534a68342fd1f2dfdebe1..7e5569f470fa7d9261576c19d2fd14509610a62a 100755 (executable)
@@ -20,9 +20,10 @@ use List::Util qw(max);
 
 my $user = Bugzilla->login();
 
-my $cgi = Bugzilla->cgi;
+my $cgi      = Bugzilla->cgi;
 my $template = Bugzilla->template;
-my $vars = {};
+my $vars     = {};
+
 # Connect to the shadow database if this installation is using one to improve
 # performance.
 my $dbh = Bugzilla->switch_to_shadow_db();
@@ -34,12 +35,12 @@ my $dbh = Bugzilla->switch_to_shadow_db();
 # Make sure the bug ID is a positive integer representing an existing
 # bug that the user is authorized to access.
 my $bug = Bugzilla::Bug->check(scalar $cgi->param('id'));
-my $id = $bug->id;
+my $id  = $bug->id;
 
 local our $hide_resolved = $cgi->param('hide_resolved') ? 1 : 0;
 local our $maxdepth = $cgi->param('maxdepth') || 0;
 if ($maxdepth !~ /^\d+$/) {
-    $maxdepth = 0;
+  $maxdepth = 0;
 }
 
 ################################################################################
@@ -51,7 +52,7 @@ local our $realdepth = 0;
 
 # Generate the tree of bugs that this bug depends on and a list of IDs
 # appearing in the tree.
-my $dependson_tree = { $id => $bug };
+my $dependson_tree = {$id => $bug};
 my $dependson_ids = {};
 GenerateTree($id, "dependson", 1, $dependson_tree, $dependson_ids);
 $vars->{'dependson_tree'} = $dependson_tree;
@@ -59,7 +60,7 @@ $vars->{'dependson_ids'}  = [keys(%$dependson_ids)];
 
 # Generate the tree of bugs that this bug blocks and a list of IDs
 # appearing in the tree.
-my $blocked_tree = { $id => $bug };
+my $blocked_tree = {$id => $bug};
 my $blocked_ids = {};
 GenerateTree($id, "blocked", 1, $blocked_tree, $blocked_ids);
 $vars->{'blocked_tree'} = $blocked_tree;
@@ -77,70 +78,73 @@ $template->process("bug/dependency-tree.html.tmpl", $vars)
 # Tree Generation Functions
 
 sub GenerateTree {
-    my ($bug_id, $relationship, $depth, $bugs, $ids) = @_;
-
-    # determine just the list of bug ids
-    _generate_bug_ids($bug_id, $relationship, $depth, $ids);
-    my $bug_ids = [ keys %$ids ];
-    return unless @$bug_ids;
-
-    # load all the bugs at once
-    foreach my $bug (@{ Bugzilla::Bug->new_from_list($bug_ids) }) {
-        if (!$bug->{error}) {
-            $bugs->{$bug->id} = $bug;
-        }
+  my ($bug_id, $relationship, $depth, $bugs, $ids) = @_;
+
+  # determine just the list of bug ids
+  _generate_bug_ids($bug_id, $relationship, $depth, $ids);
+  my $bug_ids = [keys %$ids];
+  return unless @$bug_ids;
+
+  # load all the bugs at once
+  foreach my $bug (@{Bugzilla::Bug->new_from_list($bug_ids)}) {
+    if (!$bug->{error}) {
+      $bugs->{$bug->id} = $bug;
     }
+  }
 
-    # preload bug visibility
-    Bugzilla->user->visible_bugs($bug_ids);
+  # preload bug visibility
+  Bugzilla->user->visible_bugs($bug_ids);
 
-    # and generate the tree
-    _generate_tree($bug_id, $relationship, $depth, $bugs, $ids);
+  # and generate the tree
+  _generate_tree($bug_id, $relationship, $depth, $bugs, $ids);
 }
 
 sub _generate_bug_ids {
-    my ($bug_id, $relationship, $depth, $ids) = @_;
-
-    # Record this depth in the global $realdepth variable if it's farther
-    # than we've gone before.
-    $realdepth = max($realdepth, $depth);
-
-    my $dependencies = _get_dependencies($bug_id, $relationship);
-    foreach my $dep_id (@$dependencies) {
-        if (!$maxdepth || $depth <= $maxdepth) {
-            $ids->{$dep_id} = 1;
-            _generate_bug_ids($dep_id, $relationship, $depth + 1, $ids);
-        }
+  my ($bug_id, $relationship, $depth, $ids) = @_;
+
+  # Record this depth in the global $realdepth variable if it's farther
+  # than we've gone before.
+  $realdepth = max($realdepth, $depth);
+
+  my $dependencies = _get_dependencies($bug_id, $relationship);
+  foreach my $dep_id (@$dependencies) {
+    if (!$maxdepth || $depth <= $maxdepth) {
+      $ids->{$dep_id} = 1;
+      _generate_bug_ids($dep_id, $relationship, $depth + 1, $ids);
     }
+  }
 }
 
 sub _generate_tree {
-    my ($bug_id, $relationship, $depth, $bugs, $ids) = @_;
-
-    my $dependencies = _get_dependencies($bug_id, $relationship);
-
-    foreach my $dep_id (@$dependencies) {
-        # recurse
-        if (!$maxdepth || $depth < $maxdepth) {
-            _generate_tree($dep_id, $relationship, $depth + 1, $bugs, $ids);
-        }
-
-        # remove bugs according to visiblity
-        if (!Bugzilla->user->can_see_bug($dep_id)) {
-            delete $ids->{$dep_id};
-        }
-        elsif (!grep { $_ == $dep_id } @{ $bugs->{dependencies}->{$bug_id} }) {
-            push @{ $bugs->{dependencies}->{$bug_id} }, $dep_id;
-        }
+  my ($bug_id, $relationship, $depth, $bugs, $ids) = @_;
+
+  my $dependencies = _get_dependencies($bug_id, $relationship);
+
+  foreach my $dep_id (@$dependencies) {
+
+    # recurse
+    if (!$maxdepth || $depth < $maxdepth) {
+      _generate_tree($dep_id, $relationship, $depth + 1, $bugs, $ids);
+    }
+
+    # remove bugs according to visiblity
+    if (!Bugzilla->user->can_see_bug($dep_id)) {
+      delete $ids->{$dep_id};
+    }
+    elsif (!grep { $_ == $dep_id } @{$bugs->{dependencies}->{$bug_id}}) {
+      push @{$bugs->{dependencies}->{$bug_id}}, $dep_id;
     }
+  }
 }
 
 sub _get_dependencies {
-    my ($bug_id, $relationship) = @_;
-    my $cache = Bugzilla->request_cache->{dependency_cache} ||= {};
-    return $cache->{$bug_id}->{$relationship} ||=
-        $relationship eq 'dependson'
-        ? Bugzilla::Bug::EmitDependList('blocked',   'dependson', $bug_id, $hide_resolved)
-        : Bugzilla::Bug::EmitDependList('dependson', 'blocked',   $bug_id, $hide_resolved);
+  my ($bug_id, $relationship) = @_;
+  my $cache = Bugzilla->request_cache->{dependency_cache} ||= {};
+  return $cache->{$bug_id}->{$relationship}
+    ||= $relationship eq 'dependson'
+    ? Bugzilla::Bug::EmitDependList('blocked', 'dependson', $bug_id,
+    $hide_resolved)
+    : Bugzilla::Bug::EmitDependList('dependson', 'blocked', $bug_id,
+    $hide_resolved);
 }
 
index dd1e9e08e22f76febc962555ce17dcb7e5cf1239..0854c9f62de0e97ca4e5691de485376fac06aad4 100755 (executable)
@@ -12,12 +12,12 @@ use warnings;
 
 use lib qw(. lib local/lib/perl5);
 
-use Date::Parse;         # strptime
+use Date::Parse;    # strptime
 
 use Bugzilla;
-use Bugzilla::Constants; # LOGIN_*
-use Bugzilla::Bug;       # EmitDependList
-use Bugzilla::Util;      # trim
+use Bugzilla::Constants;    # LOGIN_*
+use Bugzilla::Bug;          # EmitDependList
+use Bugzilla::Util;         # trim
 use Bugzilla::Error;
 
 #
@@ -25,152 +25,159 @@ use Bugzilla::Error;
 #
 
 sub date_adjust_down {
-   
-    my ($year, $month, $day) = @_;
-
-    if ($day == 0) {
-        $month -= 1;
-        $day = 31;
-        # Proper day adjustment is done later.
-
-        if ($month == 0) {
-            $year -= 1;
-            $month = 12;
-        }
-    }
 
-    if (($month == 2) && ($day > 28)) {
-        if ($year % 4 == 0 && $year % 100 != 0) {
-            $day = 29;
-        } else {
-            $day = 28;
-        }
+  my ($year, $month, $day) = @_;
+
+  if ($day == 0) {
+    $month -= 1;
+    $day = 31;
+
+    # Proper day adjustment is done later.
+
+    if ($month == 0) {
+      $year -= 1;
+      $month = 12;
     }
+  }
 
-    if (($month == 4 || $month == 6 || $month == 9 || $month == 11) &&
-        ($day == 31) ) 
-    {
-        $day = 30;
+  if (($month == 2) && ($day > 28)) {
+    if ($year % 4 == 0 && $year % 100 != 0) {
+      $day = 29;
+    }
+    else {
+      $day = 28;
     }
-    return ($year, $month, $day);
+  }
+
+  if (($month == 4 || $month == 6 || $month == 9 || $month == 11) && ($day == 31))
+  {
+    $day = 30;
+  }
+  return ($year, $month, $day);
 }
 
 sub date_adjust_up {
-    my ($year, $month, $day) = @_;
+  my ($year, $month, $day) = @_;
 
-    if ($day > 31) {
-        $month += 1;
-        $day    = 1;
+  if ($day > 31) {
+    $month += 1;
+    $day = 1;
 
-        if ($month == 13) {
-            $month = 1;
-            $year += 1;
-        }
+    if ($month == 13) {
+      $month = 1;
+      $year += 1;
     }
+  }
 
-    if ($month == 2 && $day > 28) {
-        if ($year % 4 != 0 || $year % 100 == 0 || $day > 29) {
-            $month = 3;
-            $day = 1;
-        }
+  if ($month == 2 && $day > 28) {
+    if ($year % 4 != 0 || $year % 100 == 0 || $day > 29) {
+      $month = 3;
+      $day   = 1;
     }
+  }
 
-    if (($month == 4 || $month == 6 || $month == 9 || $month == 11) &&
-        ($day == 31) )
-    {
-        $month += 1; 
-        $day    = 1;
-    }
+  if (($month == 4 || $month == 6 || $month == 9 || $month == 11) && ($day == 31))
+  {
+    $month += 1;
+    $day = 1;
+  }
 
-    return ($year, $month, $day);
+  return ($year, $month, $day);
 }
 
 sub split_by_month {
-    # Takes start and end dates and splits them into a list of
-    # monthly-spaced 2-lists of dates.
-    my ($start_date, $end_date) = @_;
 
-    # We assume at this point that the dates are provided and sane
-    my (undef, undef, undef, $sd, $sm, $sy, undef) = strptime($start_date);
-    my (undef, undef, undef, $ed, $em, $ey, undef) = strptime($end_date);
+  # Takes start and end dates and splits them into a list of
+  # monthly-spaced 2-lists of dates.
+  my ($start_date, $end_date) = @_;
 
-    # Find out how many months fit between the two dates so we know
-    # how many times we loop.
-    my $yd = $ey - $sy;
-    my $md = 12 * $yd + $em - $sm;
-    # If the end day is smaller than the start day, last interval is not a whole month.
-    if ($sd > $ed) {
-        $md -= 1;
-    }
+  # We assume at this point that the dates are provided and sane
+  my (undef, undef, undef, $sd, $sm, $sy, undef) = strptime($start_date);
+  my (undef, undef, undef, $ed, $em, $ey, undef) = strptime($end_date);
 
-    my (@months, $sub_start, $sub_end);
-    # This +1 and +1900 are a result of strptime's bizarre semantics
-    my $year = $sy + 1900;
-    my $month = $sm + 1;
-
-    # Keep the original date, when the date will be changed in the adjust_date.
-    my $sd_tmp = $sd;
-    my $month_tmp = $month;
-    my $year_tmp = $year;
-
-    # This section handles only the whole months.
-    for (my $i=0; $i < $md; $i++) {
-        # Start of interval is adjusted up: 31.2. -> 1.3.
-        ($year_tmp, $month_tmp, $sd_tmp) = date_adjust_up($year, $month, $sd);
-        $sub_start = sprintf("%04d-%02d-%02d", $year_tmp, $month_tmp, $sd_tmp); 
-        $month += 1;
-        if ($month == 13) {
-            $month = 1;
-            $year += 1;
-        }
-        # End of interval is adjusted down: 31.2 -> 28.2.
-        ($year_tmp, $month_tmp, $sd_tmp) = date_adjust_down($year, $month, $sd - 1);
-        $sub_end = sprintf("%04d-%02d-%02d", $year_tmp, $month_tmp, $sd_tmp);
-        push @months, [$sub_start, $sub_end];
-    }
-    
-    # This section handles the last (unfinished) month. 
-    $sub_end = sprintf("%04d-%02d-%02d", $ey + 1900, $em + 1, $ed);
+  # Find out how many months fit between the two dates so we know
+  # how many times we loop.
+  my $yd = $ey - $sy;
+  my $md = 12 * $yd + $em - $sm;
+
+# If the end day is smaller than the start day, last interval is not a whole month.
+  if ($sd > $ed) {
+    $md -= 1;
+  }
+
+  my (@months, $sub_start, $sub_end);
+
+  # This +1 and +1900 are a result of strptime's bizarre semantics
+  my $year  = $sy + 1900;
+  my $month = $sm + 1;
+
+  # Keep the original date, when the date will be changed in the adjust_date.
+  my $sd_tmp    = $sd;
+  my $month_tmp = $month;
+  my $year_tmp  = $year;
+
+  # This section handles only the whole months.
+  for (my $i = 0; $i < $md; $i++) {
+
+    # Start of interval is adjusted up: 31.2. -> 1.3.
     ($year_tmp, $month_tmp, $sd_tmp) = date_adjust_up($year, $month, $sd);
     $sub_start = sprintf("%04d-%02d-%02d", $year_tmp, $month_tmp, $sd_tmp);
+    $month += 1;
+    if ($month == 13) {
+      $month = 1;
+      $year += 1;
+    }
+
+    # End of interval is adjusted down: 31.2 -> 28.2.
+    ($year_tmp, $month_tmp, $sd_tmp) = date_adjust_down($year, $month, $sd - 1);
+    $sub_end = sprintf("%04d-%02d-%02d", $year_tmp, $month_tmp, $sd_tmp);
     push @months, [$sub_start, $sub_end];
+  }
+
+  # This section handles the last (unfinished) month.
+  $sub_end = sprintf("%04d-%02d-%02d", $ey + 1900, $em + 1, $ed);
+  ($year_tmp, $month_tmp, $sd_tmp) = date_adjust_up($year, $month, $sd);
+  $sub_start = sprintf("%04d-%02d-%02d", $year_tmp, $month_tmp, $sd_tmp);
+  push @months, [$sub_start, $sub_end];
 
-    return @months;
+  return @months;
 }
 
 sub sqlize_dates {
-    my ($start_date, $end_date) = @_;
-    my $date_bits = "";
-    my @date_values;
-    if ($start_date) {
-        # we've checked, trick_taint is fine
-        trick_taint($start_date);
-        $date_bits = " AND longdescs.bug_when > ?";
-        push @date_values, $start_date;
-    } 
-    if ($end_date) {
-        # we need to add one day to end_date to catch stuff done today
-        # do not forget to adjust date if it was the last day of month
-        my (undef, undef, undef, $ed, $em, $ey, undef) = strptime($end_date);
-        ($ey, $em, $ed) = date_adjust_up($ey+1900, $em+1, $ed+1);
-        $end_date = sprintf("%04d-%02d-%02d", $ey, $em, $ed);
-
-        $date_bits .= " AND longdescs.bug_when < ?"; 
-        push @date_values, $end_date;
-    }
-    return ($date_bits, \@date_values);
+  my ($start_date, $end_date) = @_;
+  my $date_bits = "";
+  my @date_values;
+  if ($start_date) {
+
+    # we've checked, trick_taint is fine
+    trick_taint($start_date);
+    $date_bits = " AND longdescs.bug_when > ?";
+    push @date_values, $start_date;
+  }
+  if ($end_date) {
+
+    # we need to add one day to end_date to catch stuff done today
+    # do not forget to adjust date if it was the last day of month
+    my (undef, undef, undef, $ed, $em, $ey, undef) = strptime($end_date);
+    ($ey, $em, $ed) = date_adjust_up($ey + 1900, $em + 1, $ed + 1);
+    $end_date = sprintf("%04d-%02d-%02d", $ey, $em, $ed);
+
+    $date_bits .= " AND longdescs.bug_when < ?";
+    push @date_values, $end_date;
+  }
+  return ($date_bits, \@date_values);
 }
 
 # Return all blockers of the current bug, recursively.
 sub get_blocker_ids {
-    my ($bug_id, $unique) = @_;
-    $unique ||= {$bug_id => 1};
-    my $deps = Bugzilla::Bug::EmitDependList("blocked", "dependson", $bug_id);
-    my @unseen = grep { !$unique->{$_}++ } @$deps;
-    foreach $bug_id (@unseen) {
-        get_blocker_ids($bug_id, $unique);
-    }
-    return keys %$unique;
+  my ($bug_id, $unique) = @_;
+  $unique ||= {$bug_id => 1};
+  my $deps = Bugzilla::Bug::EmitDependList("blocked", "dependson", $bug_id);
+  my @unseen = grep { !$unique->{$_}++ } @$deps;
+  foreach $bug_id (@unseen) {
+    get_blocker_ids($bug_id, $unique);
+  }
+  return keys %$unique;
 }
 
 # Return a hashref whose key is chosen by the user (bug ID or commenter)
@@ -178,63 +185,66 @@ sub get_blocker_ids {
 # So you can either view it as the time spent by commenters on each bug
 # or the time spent in bugs by each commenter.
 sub get_list {
-    my ($bugids, $start_date, $end_date, $keyname) = @_;
-    my $dbh = Bugzilla->dbh;
+  my ($bugids, $start_date, $end_date, $keyname) = @_;
+  my $dbh = Bugzilla->dbh;
 
-    my ($date_bits, $date_values) = sqlize_dates($start_date, $end_date);
-    my $buglist = join(", ", @$bugids);
+  my ($date_bits, $date_values) = sqlize_dates($start_date, $end_date);
+  my $buglist = join(", ", @$bugids);
 
-    # Returns the total time worked on each bug *per developer*.
-    my $data = $dbh->selectall_arrayref(
-            qq{SELECT SUM(work_time) AS total_time, login_name, longdescs.bug_id
+  # Returns the total time worked on each bug *per developer*.
+  my $data = $dbh->selectall_arrayref(
+    qq{SELECT SUM(work_time) AS total_time, login_name, longdescs.bug_id
                  FROM longdescs
            INNER JOIN profiles
                    ON longdescs.who = profiles.userid
            INNER JOIN bugs
                    ON bugs.bug_id = longdescs.bug_id
-                WHERE longdescs.bug_id IN ($buglist) $date_bits } .
-            $dbh->sql_group_by('longdescs.bug_id, login_name', 'longdescs.bug_when') .
-           qq{ HAVING SUM(work_time) > 0}, {Slice => {}}, @$date_values);
-
-    my %list;
-    # What this loop does is to push data having the same key in an array.
-    push(@{$list{ $_->{$keyname} }}, $_) foreach @$data;
-    return \%list;
+                WHERE longdescs.bug_id IN ($buglist) $date_bits }
+      . $dbh->sql_group_by('longdescs.bug_id, login_name', 'longdescs.bug_when')
+      . qq{ HAVING SUM(work_time) > 0}, {Slice => {}}, @$date_values
+  );
+
+  my %list;
+
+  # What this loop does is to push data having the same key in an array.
+  push(@{$list{$_->{$keyname}}}, $_) foreach @$data;
+  return \%list;
 }
 
 # Return bugs which had no activity (a.k.a work_time = 0) during the given time range.
 sub get_inactive_bugs {
-    my ($bugids, $start_date, $end_date) = @_;
-    my $dbh = Bugzilla->dbh;
-    my ($date_bits, $date_values) = sqlize_dates($start_date, $end_date);
-    my $buglist = join(", ", @$bugids);
+  my ($bugids, $start_date, $end_date) = @_;
+  my $dbh = Bugzilla->dbh;
+  my ($date_bits, $date_values) = sqlize_dates($start_date, $end_date);
+  my $buglist = join(", ", @$bugids);
 
-    my $bugs = $dbh->selectcol_arrayref(
-        "SELECT bug_id
+  my $bugs = $dbh->selectcol_arrayref(
+    "SELECT bug_id
            FROM bugs
           WHERE bugs.bug_id IN ($buglist)
             AND NOT EXISTS (
                 SELECT 1
                   FROM longdescs
                  WHERE bugs.bug_id = longdescs.bug_id
-                   AND work_time > 0 $date_bits)",
-         undef, @$date_values);
+                   AND work_time > 0 $date_bits)", undef, @$date_values
+  );
 
-    return $bugs;
+  return $bugs;
 }
 
 # Return 1st day of the month of the earliest activity date for a given list of bugs.
 sub get_earliest_activity_date {
-    my ($bugids) = @_;
-    my $dbh = Bugzilla->dbh;
+  my ($bugids) = @_;
+  my $dbh = Bugzilla->dbh;
 
-    my ($date) = $dbh->selectrow_array(
-        'SELECT ' . $dbh->sql_date_format('MIN(bug_when)', '%Y-%m-01')
-       . ' FROM longdescs
-          WHERE ' . $dbh->sql_in('bug_id', $bugids)
-                  . ' AND work_time > 0');
+  my ($date) = $dbh->selectrow_array(
+        'SELECT '
+      . $dbh->sql_date_format('MIN(bug_when)', '%Y-%m-01')
+      . ' FROM longdescs
+          WHERE ' . $dbh->sql_in('bug_id', $bugids) . ' AND work_time > 0'
+  );
 
-    return $date;
+  return $date;
 }
 
 #
@@ -243,122 +253,133 @@ sub get_earliest_activity_date {
 
 my $user = Bugzilla->login(LOGIN_REQUIRED);
 
-my $cgi = Bugzilla->cgi;
+my $cgi      = Bugzilla->cgi;
 my $template = Bugzilla->template;
-my $vars = {};
+my $vars     = {};
 
 Bugzilla->switch_to_shadow_db();
 
-$user->is_timetracker
-    || ThrowUserError("auth_failure", {group  => "time-tracking",
-                                       action => "access",
-                                       object => "timetracking_summaries"});
+$user->is_timetracker || ThrowUserError(
+  "auth_failure",
+  {
+    group  => "time-tracking",
+    action => "access",
+    object => "timetracking_summaries"
+  }
+);
 
 my @ids = split(",", $cgi->param('id') || '');
 @ids = map { Bugzilla::Bug->check($_)->id } @ids;
 scalar(@ids) || ThrowUserError('no_bugs_chosen', {action => 'summarize'});
 
-my $group_by = $cgi->param('group_by') || "number";
-my $monthly = $cgi->param('monthly');
-my $detailed = $cgi->param('detailed');
-my $do_report = $cgi->param('do_report');
-my $inactive = $cgi->param('inactive');
+my $group_by   = $cgi->param('group_by') || "number";
+my $monthly    = $cgi->param('monthly');
+my $detailed   = $cgi->param('detailed');
+my $do_report  = $cgi->param('do_report');
+my $inactive   = $cgi->param('inactive');
 my $do_depends = $cgi->param('do_depends');
-my $ctype = $cgi->param('ctype');
+my $ctype      = $cgi->param('ctype');
 
 my ($start_date, $end_date);
 if ($do_report) {
-    my @bugs = @ids;
-
-    # Dependency mode requires a single bug and grabs dependents.
-    if ($do_depends) {
-        if (scalar(@bugs) != 1) {
-            ThrowCodeError("bad_arg", { argument=>"id",
-                                        function=>"summarize_time"});
-        }
-        @bugs = get_blocker_ids($bugs[0]);
-        @bugs = @{ $user->visible_bugs(\@bugs) };
-    }
+  my @bugs = @ids;
 
-    $start_date = trim(scalar $cgi->param('start_date'));
-    $end_date = trim(scalar $cgi->param('end_date'));
-
-    foreach my $date ($start_date, $end_date) {
-        next unless $date;
-        validate_date($date)
-          || ThrowUserError('illegal_date', {date => $date, format => 'YYYY-MM-DD'});
-    }
-    # Swap dates in case the user put an end_date before the start_date
-    if ($start_date && $end_date && 
-        str2time($start_date) > str2time($end_date)) {
-        $vars->{'warn_swap_dates'} = 1;
-        ($start_date, $end_date) = ($end_date, $start_date);
-    }
-
-    # Store dates in a session cookie so re-visiting the page
-    # for other bugs keeps them around.
-    $cgi->send_cookie(-name => 'time-summary-dates',
-                      -value => join ";", ($start_date, $end_date));
-
-    my (@parts, $part_data, @part_list);
-
-    # Break dates apart into months if necessary; if not, we use the
-    # same @parts list to allow us to use a common codepath.
-    if ($monthly) {
-        # Calculate the earliest activity date if the user doesn't
-        # specify a start date.
-        if (!$start_date) {
-            $start_date = get_earliest_activity_date(\@bugs);
-        }
-        # Provide a default end date. Note that this differs in semantics
-        # from the open-ended queries we use when start/end_date aren't
-        # provided -- and clock skews will make this evident!
-        @parts = split_by_month($start_date, 
-                                $end_date || format_time(scalar localtime(time()), '%Y-%m-%d'));
-    } else {
-        @parts = ([$start_date, $end_date]);
+  # Dependency mode requires a single bug and grabs dependents.
+  if ($do_depends) {
+    if (scalar(@bugs) != 1) {
+      ThrowCodeError("bad_arg", {argument => "id", function => "summarize_time"});
     }
-
-    # For each of the separate divisions, grab the relevant data.
-    my $keyname = ($group_by eq 'owner') ? 'login_name' : 'bug_id';
-    foreach my $part (@parts) {
-        my ($sub_start, $sub_end) = @$part;
-        $part_data = get_list(\@bugs, $sub_start, $sub_end, $keyname);
-        push(@part_list, $part_data);
-    }
-
-    # Do we want to see inactive bugs?
-    if ($inactive) {
-        $vars->{'null'} = get_inactive_bugs(\@bugs, $start_date, $end_date);
-    } else {
-        $vars->{'null'} = {};
+    @bugs = get_blocker_ids($bugs[0]);
+    @bugs = @{$user->visible_bugs(\@bugs)};
+  }
+
+  $start_date = trim(scalar $cgi->param('start_date'));
+  $end_date   = trim(scalar $cgi->param('end_date'));
+
+  foreach my $date ($start_date, $end_date) {
+    next unless $date;
+    validate_date($date)
+      || ThrowUserError('illegal_date', {date => $date, format => 'YYYY-MM-DD'});
+  }
+
+  # Swap dates in case the user put an end_date before the start_date
+  if ($start_date && $end_date && str2time($start_date) > str2time($end_date)) {
+    $vars->{'warn_swap_dates'} = 1;
+    ($start_date, $end_date) = ($end_date, $start_date);
+  }
+
+  # Store dates in a session cookie so re-visiting the page
+  # for other bugs keeps them around.
+  $cgi->send_cookie(
+    -name  => 'time-summary-dates',
+    -value => join ";",
+    ($start_date, $end_date)
+  );
+
+  my (@parts, $part_data, @part_list);
+
+  # Break dates apart into months if necessary; if not, we use the
+  # same @parts list to allow us to use a common codepath.
+  if ($monthly) {
+
+    # Calculate the earliest activity date if the user doesn't
+    # specify a start date.
+    if (!$start_date) {
+      $start_date = get_earliest_activity_date(\@bugs);
     }
 
-    # Convert bug IDs to bug objects.
-    @bugs = map {new Bugzilla::Bug($_)} @bugs;
-
-    $vars->{'part_list'} = \@part_list;
-    $vars->{'parts'} = \@parts;
-    # We pass the list of bugs as a hashref.
-    $vars->{'bugs'} = {map { $_->id => $_ } @bugs};
+    # Provide a default end date. Note that this differs in semantics
+    # from the open-ended queries we use when start/end_date aren't
+    # provided -- and clock skews will make this evident!
+    @parts = split_by_month($start_date,
+      $end_date || format_time(scalar localtime(time()), '%Y-%m-%d'));
+  }
+  else {
+    @parts = ([$start_date, $end_date]);
+  }
+
+  # For each of the separate divisions, grab the relevant data.
+  my $keyname = ($group_by eq 'owner') ? 'login_name' : 'bug_id';
+  foreach my $part (@parts) {
+    my ($sub_start, $sub_end) = @$part;
+    $part_data = get_list(\@bugs, $sub_start, $sub_end, $keyname);
+    push(@part_list, $part_data);
+  }
+
+  # Do we want to see inactive bugs?
+  if ($inactive) {
+    $vars->{'null'} = get_inactive_bugs(\@bugs, $start_date, $end_date);
+  }
+  else {
+    $vars->{'null'} = {};
+  }
+
+  # Convert bug IDs to bug objects.
+  @bugs = map { new Bugzilla::Bug($_) } @bugs;
+
+  $vars->{'part_list'} = \@part_list;
+  $vars->{'parts'}     = \@parts;
+
+  # We pass the list of bugs as a hashref.
+  $vars->{'bugs'} = {map { $_->id => $_ } @bugs};
 }
 elsif ($cgi->cookie("time-summary-dates")) {
-    ($start_date, $end_date) = split ";", $cgi->cookie('time-summary-dates');
+  ($start_date, $end_date) = split ";", $cgi->cookie('time-summary-dates');
 }
 
-$vars->{'ids'} = \@ids;
+$vars->{'ids'}        = \@ids;
 $vars->{'start_date'} = $start_date;
-$vars->{'end_date'} = $end_date;
-$vars->{'group_by'} = $group_by;
-$vars->{'monthly'} = $monthly;
-$vars->{'detailed'} = $detailed;
-$vars->{'inactive'} = $inactive;
-$vars->{'do_report'} = $do_report;
+$vars->{'end_date'}   = $end_date;
+$vars->{'group_by'}   = $group_by;
+$vars->{'monthly'}    = $monthly;
+$vars->{'detailed'}   = $detailed;
+$vars->{'inactive'}   = $inactive;
+$vars->{'do_report'}  = $do_report;
 $vars->{'do_depends'} = $do_depends;
 
 my $format = $template->get_format("bug/summarize-time", undef, $ctype);
 
 # Get the proper content-type
-print $cgi->header(-type=> $format->{'ctype'});
+print $cgi->header(-type => $format->{'ctype'});
 $template->process("$format->{'template'}", $vars)
   || ThrowTemplateError($template->error());
index d9ec1b28da9267c158a01dd8fc997755db1e10f2..037ecd364ff33a8d81c005da99e9a406a975e357 100644 (file)
@@ -18,49 +18,49 @@ use lib qw(. lib local/lib/perl5 t);
 use Config;
 use Support::Files;
 use Test::More tests => scalar(@Support::Files::testitems)
-                        + scalar(@Support::Files::test_files);
+  + scalar(@Support::Files::test_files);
 
-BEGIN { 
-    use_ok('Bugzilla::Constants');
-    use_ok('Bugzilla::Install::Requirements');
-    use_ok('Bugzilla');
+BEGIN {
+  use_ok('Bugzilla::Constants');
+  use_ok('Bugzilla::Install::Requirements');
+  use_ok('Bugzilla');
 }
 Bugzilla->usage_mode(USAGE_MODE_TEST);
 
 sub compile_file {
-    my ($file) = @_;
+  my ($file) = @_;
 
-    # Don't allow CPAN.pm to modify the global @INC, which the version
-    # shipped with Perl 5.8.8 does. (It gets loaded by 
-    # Bugzilla::Install::CPAN.)
-    local @INC = @INC;
+  # Don't allow CPAN.pm to modify the global @INC, which the version
+  # shipped with Perl 5.8.8 does. (It gets loaded by
+  # Bugzilla::Install::CPAN.)
+  local @INC = @INC;
 
-    if ($file =~ s/\.pm$//) {
-        $file =~ s{/}{::}g;
-        use_ok($file);
-        return;
-    }
+  if ($file =~ s/\.pm$//) {
+    $file =~ s{/}{::}g;
+    use_ok($file);
+    return;
+  }
 
-    open(my $fh, $file);
-    my $bang = <$fh>;
-    close $fh;
+  open(my $fh, $file);
+  my $bang = <$fh>;
+  close $fh;
 
-    my $T = "";
-    if ($bang =~ m/#!\S*perl\s+-.*T/) {
-        $T = "T";
-    }
+  my $T = "";
+  if ($bang =~ m/#!\S*perl\s+-.*T/) {
+    $T = "T";
+  }
 
-    my $libs = '-It ';
-    if ($ENV{PERL5LIB}) {
-       $libs .= join " ", map { "-I\"$_\"" } split /$Config{path_sep}/, $ENV{PERL5LIB};
-    }
-    my $perl = qq{"$^X"};
-    my $output = `$perl $libs -c$T -MSupport::Systemexec $file 2>&1`;
-    chomp($output);
-    my $return_val = $?;
-    $output =~ s/^\Q$file\E syntax OK$//ms;
-    diag($output) if $output;
-    ok(!$return_val, $file) or diag('--ERROR');
+  my $libs = '-It ';
+  if ($ENV{PERL5LIB}) {
+    $libs .= join " ", map {"-I\"$_\""} split /$Config{path_sep}/, $ENV{PERL5LIB};
+  }
+  my $perl   = qq{"$^X"};
+  my $output = `$perl $libs -c$T -MSupport::Systemexec $file 2>&1`;
+  chomp($output);
+  my $return_val = $?;
+  $output =~ s/^\Q$file\E syntax OK$//ms;
+  diag($output) if $output;
+  ok(!$return_val, $file) or diag('--ERROR');
 }
 
 my @testitems = (@Support::Files::testitems, @Support::Files::test_files);
@@ -68,28 +68,28 @@ my $file_features = map_files_to_features();
 
 # Test the scripts by compiling them
 foreach my $file (@testitems) {
-    # These were already compiled, above.
-    next if ($file eq 'Bugzilla.pm' 
-             or $file eq 'Bugzilla/Constants.pm'
-             or $file eq 'Bugzilla/Install/Requirements.pm');
-    SKIP: {
-        if ($file eq 'mod_perl.pl') {
-            skip 'mod_perl.pl cannot be compiled from the command line', 1;
-        }
-        my $feature = $file_features->{$file};
-        if ($feature and !Bugzilla->feature($feature)) {
-            skip "$file: $feature not enabled", 1;
-        }
-
-        # Check that we have a DBI module to support the DB, if this 
-        # is a database module (but not Schema)
-        if ($file =~ m{Bugzilla/DB/([^/]+)\.pm$}
-            and $file ne "Bugzilla/DB/Schema.pm") 
-        {
-            my $module = lc($1);
-            Bugzilla->feature($module) or skip "$file: Driver for $module not installed", 1;
-        }
-
-        compile_file($file);
+
+  # These were already compiled, above.
+  next
+    if ($file eq 'Bugzilla.pm'
+    or $file eq 'Bugzilla/Constants.pm'
+    or $file eq 'Bugzilla/Install/Requirements.pm');
+SKIP: {
+    if ($file eq 'mod_perl.pl') {
+      skip 'mod_perl.pl cannot be compiled from the command line', 1;
+    }
+    my $feature = $file_features->{$file};
+    if ($feature and !Bugzilla->feature($feature)) {
+      skip "$file: $feature not enabled", 1;
     }
-}      
+
+    # Check that we have a DBI module to support the DB, if this
+    # is a database module (but not Schema)
+    if ($file =~ m{Bugzilla/DB/([^/]+)\.pm$} and $file ne "Bugzilla/DB/Schema.pm") {
+      my $module = lc($1);
+      Bugzilla->feature($module) or skip "$file: Driver for $module not installed", 1;
+    }
+
+    compile_file($file);
+  }
+}
index 400483a5a01a5440a9c7c159ce8cb1772b6fcbfe..a81dfb1bb8a9452ee684272ef0ec2e499a93a751 100644 (file)
@@ -18,156 +18,171 @@ use lib qw(. lib local/lib/perl5 t);
 
 use Support::Files;
 
-use Test::More tests => (scalar(@Support::Files::testitems)
-                         + scalar(@Support::Files::test_files)) * 6;
+use Test::More tests =>
+  (scalar(@Support::Files::testitems) + scalar(@Support::Files::test_files))
+  * 6;
 
 my @testitems = (@Support::Files::test_files, @Support::Files::testitems);
 my @require_taint = qw(email_in.pl importxml.pl mod_perl.pl whine.pl);
 
 foreach my $file (@testitems) {
-    $file =~ s/\s.*$//; # nuke everything after the first space (#comment)
-    next if (!$file); # skip null entries
-    if (! open (FILE, $file)) {
-        ok(0,"could not open $file --WARNING");
+  $file =~ s/\s.*$//;    # nuke everything after the first space (#comment)
+  next if (!$file);      # skip null entries
+  if (!open(FILE, $file)) {
+    ok(0, "could not open $file --WARNING");
+  }
+  my $file_line1 = <FILE>;
+  close(FILE);
+
+  $file =~ m/.*\.(.*)/;
+  my $ext = $1;
+
+  if ($file_line1 !~ m/^#\!/) {
+    ok(1, "$file does not have a shebang");
+  }
+  else {
+    my $flags;
+    if (!defined $ext || $ext eq 'pl' || $ext eq 'psgi') {
+
+      # standalone programs aren't taint checked yet
+      if (grep { $file eq $_ } @require_taint) {
+        $flags = 'T';
+      }
+      else {
+        $flags = '';
+      }
     }
-    my $file_line1 = <FILE>;
-    close (FILE);
-
-    $file =~ m/.*\.(.*)/;
-    my $ext = $1;
-
-    if ($file_line1 !~ m/^#\!/) {
-        ok(1,"$file does not have a shebang");
-    } else {
-        my $flags;
-        if (!defined $ext || $ext eq 'pl' || $ext eq 'psgi') {
-            # standalone programs aren't taint checked yet
-            if (grep { $file eq $_ } @require_taint) {
-                $flags = 'T';
-            }
-            else {
-                $flags = '';
-            }
-        } elsif ($ext eq "pm") {
-            ok(0, "$file is a module, but has a shebang");
-            next;
-        } elsif ($ext eq "cgi") {
-            # cgi files must be taint checked
-            $flags = 'T';
-        } else {
-            ok(0, "$file has shebang but unknown extension");
-            next;
-        }
-
-        if ($file_line1 =~ m#^\#\!/usr/bin/perl(?:\s-(\w+))?$#) {
-            my $file_flags = $1 || '';
-            if ($flags eq $file_flags) {
-                ok(1, "$file uses standard perl location" . ($flags ? " and -$flags flag" : ""));
-            }
-            elsif ($flags) {
-                ok(0, "$file is MISSING -$flags flag --WARNING");
-            }
-            else {
-                ok(0, "$file has unexpected -$file_flags flag --WARNING");
-            }
-        } else {
-            ok(0,"$file uses non-standard perl location");
-        }
+    elsif ($ext eq "pm") {
+      ok(0, "$file is a module, but has a shebang");
+      next;
     }
-}
+    elsif ($ext eq "cgi") {
 
-foreach my $file (@testitems) {
-    my $found_use_perl = 0;
-    my $found_use_strict = 0;
-    my $found_use_warnings = 0;
-
-    $file =~ s/\s.*$//; # nuke everything after the first space (#comment)
-    next if (!$file); # skip null entries
-    if (! open (FILE, $file)) {
-        ok(0,"could not open $file --WARNING");
-        next;
-    }
-    while (my $file_line = <FILE>) {
-        $found_use_perl = 1 if $file_line =~ m/^\s*use 5\.14\.0/;
-        $found_use_strict = 1 if $file_line =~ m/^\s*use strict/;
-        $found_use_warnings = 1 if $file_line =~ m/^\s*use warnings/;
-        last if ($found_use_perl && $found_use_strict && $found_use_warnings);
+      # cgi files must be taint checked
+      $flags = 'T';
     }
-    close (FILE);
-    if ($found_use_perl) {
-        ok(1,"$file requires Perl 5.14.0");
-    } else {
-        ok(0,"$file DOES NOT require Perl 5.14.0 --WARNING");
+    else {
+      ok(0, "$file has shebang but unknown extension");
+      next;
     }
 
-    if ($found_use_strict) {
-        ok(1,"$file uses strict");
-    } else {
-        ok(0,"$file DOES NOT use strict --WARNING");
+    if ($file_line1 =~ m#^\#\!/usr/bin/perl(?:\s-(\w+))?$#) {
+      my $file_flags = $1 || '';
+      if ($flags eq $file_flags) {
+        ok(1,
+          "$file uses standard perl location" . ($flags ? " and -$flags flag" : ""));
+      }
+      elsif ($flags) {
+        ok(0, "$file is MISSING -$flags flag --WARNING");
+      }
+      else {
+        ok(0, "$file has unexpected -$file_flags flag --WARNING");
+      }
     }
-
-    if ($found_use_warnings) {
-        ok(1,"$file uses warnings");
-    } else {
-        ok(0,"$file DOES NOT use warnings --WARNING");
+    else {
+      ok(0, "$file uses non-standard perl location");
     }
+  }
+}
+
+foreach my $file (@testitems) {
+  my $found_use_perl     = 0;
+  my $found_use_strict   = 0;
+  my $found_use_warnings = 0;
+
+  $file =~ s/\s.*$//;    # nuke everything after the first space (#comment)
+  next if (!$file);      # skip null entries
+  if (!open(FILE, $file)) {
+    ok(0, "could not open $file --WARNING");
+    next;
+  }
+  while (my $file_line = <FILE>) {
+    $found_use_perl     = 1 if $file_line =~ m/^\s*use 5\.14\.0/;
+    $found_use_strict   = 1 if $file_line =~ m/^\s*use strict/;
+    $found_use_warnings = 1 if $file_line =~ m/^\s*use warnings/;
+    last if ($found_use_perl && $found_use_strict && $found_use_warnings);
+  }
+  close(FILE);
+  if ($found_use_perl) {
+    ok(1, "$file requires Perl 5.14.0");
+  }
+  else {
+    ok(0, "$file DOES NOT require Perl 5.14.0 --WARNING");
+  }
+
+  if ($found_use_strict) {
+    ok(1, "$file uses strict");
+  }
+  else {
+    ok(0, "$file DOES NOT use strict --WARNING");
+  }
+
+  if ($found_use_warnings) {
+    ok(1, "$file uses warnings");
+  }
+  else {
+    ok(0, "$file DOES NOT use warnings --WARNING");
+  }
 }
 
 # Check to see that all error messages use tags (for l10n reasons.)
 foreach my $file (@testitems) {
-    $file =~ s/\s.*$//; # nuke everything after the first space (#comment)
-    next if (!$file); # skip null entries
-    if (! open (FILE, $file)) {
-        ok(0,"could not open $file --WARNING");
-        next;
+  $file =~ s/\s.*$//;    # nuke everything after the first space (#comment)
+  next if (!$file);      # skip null entries
+  if (!open(FILE, $file)) {
+    ok(0, "could not open $file --WARNING");
+    next;
+  }
+  my $lineno = 0;
+  my $error  = 0;
+
+  while (!$error && (my $file_line = <FILE>)) {
+    $lineno++;
+    if ($file_line =~ /Throw.*Error\("(.*?)"/) {
+      if ($1 =~ /\s/) {
+        ok(
+          0, "$file has a Throw*Error call on line $lineno 
+                      which doesn't use a tag --ERROR"
+        );
+        $error = 1;
+      }
     }
-    my $lineno = 0;
-    my $error = 0;
-    
-    while (!$error && (my $file_line = <FILE>)) {
-        $lineno++;
-        if ($file_line =~ /Throw.*Error\("(.*?)"/) {
-            if ($1 =~ /\s/) {
-                ok(0,"$file has a Throw*Error call on line $lineno 
-                      which doesn't use a tag --ERROR");
-                $error = 1;       
-            }
-        }
-    }
-    
-    ok(1,"$file uses Throw*Error calls correctly") if !$error;
-    
-    close(FILE);
+  }
+
+  ok(1, "$file uses Throw*Error calls correctly") if !$error;
+
+  close(FILE);
 }
 
 # Forbird the { foo => $cgi->param() } syntax, for security reasons.
 foreach my $file (@testitems) {
-    $file =~ s/\s.*$//; # nuke everything after the first space (#comment)
-    next unless $file; # skip null entries
-    if (!open(FILE, $file)) {
-        ok(0, "could not open $file --WARNING");
-        next;
+  $file =~ s/\s.*$//;    # nuke everything after the first space (#comment)
+  next unless $file;     # skip null entries
+  if (!open(FILE, $file)) {
+    ok(0, "could not open $file --WARNING");
+    next;
+  }
+  my $lineno = 0;
+  my @unsafe_args;
+
+  while (my $file_line = <FILE>) {
+    $lineno++;
+    $file_line =~ s/^\s*(.+)\s*$/$1/;    # Remove leading and trailing whitespaces.
+    if ($file_line =~ /^[^#]+=> \$cgi\->param/) {
+      push(@unsafe_args, "$file_line on line $lineno");
     }
-    my $lineno = 0;
-    my @unsafe_args;
-
-    while (my $file_line = <FILE>) {
-        $lineno++;
-        $file_line =~ s/^\s*(.+)\s*$/$1/; # Remove leading and trailing whitespaces.
-        if ($file_line =~ /^[^#]+=> \$cgi\->param/) {
-            push(@unsafe_args, "$file_line on line $lineno");
-        }
-    }
-
-    if (@unsafe_args) {
-        ok(0, "$file incorrectly passes a CGI argument to a hash --ERROR\n" .
-              join("\n", @unsafe_args));
-    }
-    else {
-        ok(1, "$file has no vulnerable hash syntax");
-    }
-
-    close(FILE);
+  }
+
+  if (@unsafe_args) {
+    ok(0,
+      "$file incorrectly passes a CGI argument to a hash --ERROR\n"
+        . join("\n", @unsafe_args));
+  }
+  else {
+    ok(1, "$file has no vulnerable hash syntax");
+  }
+
+  close(FILE);
 }
 
 exit 0;
index 5296564ae3f56efad7be870bac7682e2fed4c1ea..cf8de66287eb758a5a2c33ec8d5449fb5c06c8f9 100644 (file)
@@ -22,20 +22,22 @@ use CGI qw(-no_debug);
 
 use File::Spec;
 use Template;
-use Test::More tests => ( scalar(@referenced_files) + 3 * $num_actual_files );
+use Test::More tests => (scalar(@referenced_files) + 3 * $num_actual_files);
 
 # Capture the TESTOUT from Test::More or Test::Builder for printing errors.
 # This will handle verbosity for us automatically.
 my $fh;
 {
-    no warnings qw(unopened);  # Don't complain about non-existent filehandles
-    if (-e \*Test::More::TESTOUT) {
-        $fh = \*Test::More::TESTOUT;
-    } elsif (-e \*Test::Builder::TESTOUT) {
-        $fh = \*Test::Builder::TESTOUT;
-    } else {
-        $fh = \*STDOUT;
-    }
+  no warnings qw(unopened);    # Don't complain about non-existent filehandles
+  if (-e \*Test::More::TESTOUT) {
+    $fh = \*Test::More::TESTOUT;
+  }
+  elsif (-e \*Test::Builder::TESTOUT) {
+    $fh = \*Test::Builder::TESTOUT;
+  }
+  else {
+    $fh = \*STDOUT;
+  }
 }
 
 # Check to make sure all templates that are referenced in Bugzilla
@@ -44,88 +46,97 @@ my $fh;
 # fall back to English if necessary.
 
 foreach my $file (@referenced_files) {
-    my $found = 0;
-    foreach my $path (@english_default_include_paths) {
-        my $pathfile = File::Spec->catfile($path, $file);
-        if (-e $pathfile) {
-            $found = 1;
-            last;
-        }
+  my $found = 0;
+  foreach my $path (@english_default_include_paths) {
+    my $pathfile = File::Spec->catfile($path, $file);
+    if (-e $pathfile) {
+      $found = 1;
+      last;
     }
+  }
 
-    ok($found, "$file found");
+  ok($found, "$file found");
 }
 
 foreach my $include_path (@include_paths) {
-    # Processes all the templates to make sure they have good syntax
-    my $provider = Template::Provider->new(
-    {
-        INCLUDE_PATH => $include_path ,
-        # Need to define filters used in the codebase, they don't
-        # actually have to function in this test, just be defined.
-        # See Template.pm for the actual codebase definitions.
-
-        # Initialize templates (f.e. by loading plugins like Hook).
-        PRE_PROCESS => "global/variables.none.tmpl",
-
-        FILTERS =>
-        {
-            html_linebreak => sub { return $_; },
-            js        => sub { return $_ } ,
-            base64   => sub { return $_ } ,
-            url_quote => sub { return $_ } ,
-            css_class_quote => sub { return $_ } ,
-            xml       => sub { return $_ } ,
-            quoteUrls => sub { return $_ } ,
-            bug_link => [ sub { return sub { return $_; } }, 1] ,
-            csv       => sub { return $_ } ,
-            unitconvert => sub { return $_ },
-            time      => sub { return $_ } ,
-            wrap_comment => sub { return $_ },
-            none      => sub { return $_ } ,
-            ics       => [ sub { return sub { return $_; } }, 1] ,
-            markdown => sub { return $_ } ,
+
+  # Processes all the templates to make sure they have good syntax
+  my $provider = Template::Provider->new({
+    INCLUDE_PATH => $include_path,
+
+    # Need to define filters used in the codebase, they don't
+    # actually have to function in this test, just be defined.
+    # See Template.pm for the actual codebase definitions.
+
+    # Initialize templates (f.e. by loading plugins like Hook).
+    PRE_PROCESS => "global/variables.none.tmpl",
+
+    FILTERS => {
+      html_linebreak  => sub { return $_; },
+      js              => sub { return $_ },
+      base64          => sub { return $_ },
+      url_quote       => sub { return $_ },
+      css_class_quote => sub { return $_ },
+      xml             => sub { return $_ },
+      quoteUrls       => sub { return $_ },
+      bug_link        => [
+        sub {
+          return sub { return $_; }
         },
+        1
+      ],
+      csv          => sub { return $_ },
+      unitconvert  => sub { return $_ },
+      time         => sub { return $_ },
+      wrap_comment => sub { return $_ },
+      none         => sub { return $_ },
+      ics          => [
+        sub {
+          return sub { return $_; }
+        },
+        1
+      ],
+      markdown => sub { return $_ },
+    },
+  });
+
+  foreach my $file (@{$actual_files{$include_path}}) {
+    my $path = File::Spec->catfile($include_path, $file);
+
+    # These are actual files, so there's no need to check for existence.
+
+    my ($data, $err) = $provider->fetch($file);
+
+    if (!$err) {
+      ok(1, "$path syntax ok");
+    }
+    else {
+      ok(0, "$path has bad syntax --ERROR");
+      print $fh $data . "\n";
+    }
+
+    # Make sure no forbidden constructs are present.
+    local $/;
+    open(FILE, '<', $path) or die "Can't open $file: $!\n";
+    $data = <FILE>;
+    close(FILE);
+
+    # Forbid single quotes to delimit URLs, see bug 926085.
+    if ($data =~ /href=\\?'/) {
+      ok(0, "$path contains blacklisted constructs: href='...'");
+    }
+    else {
+      ok(1, "$path contains no blacklisted constructs");
+    }
+
+    # Forbid cgi.param(). cgi_param() must be used instead.
+    if ($data =~ /cgi\.param/) {
+      ok(0, "$path calls cgi.param() instead of cgi_param()");
     }
-    );
-
-    foreach my $file (@{$actual_files{$include_path}}) {
-        my $path = File::Spec->catfile($include_path, $file);
-
-        # These are actual files, so there's no need to check for existence.
-
-        my ($data, $err) = $provider->fetch($file);
-
-        if (!$err) {
-            ok(1, "$path syntax ok");
-        }
-        else {
-            ok(0, "$path has bad syntax --ERROR");
-            print $fh $data . "\n";
-        }
-
-        # Make sure no forbidden constructs are present.
-        local $/;
-        open(FILE, '<', $path) or die "Can't open $file: $!\n";
-        $data = <FILE>;
-        close (FILE);
-
-        # Forbid single quotes to delimit URLs, see bug 926085.
-        if ($data =~ /href=\\?'/) {
-            ok(0, "$path contains blacklisted constructs: href='...'");
-        }
-        else {
-            ok(1, "$path contains no blacklisted constructs");
-        }
-
-        # Forbid cgi.param(). cgi_param() must be used instead.
-        if ($data =~ /cgi\.param/) {
-            ok(0, "$path calls cgi.param() instead of cgi_param()");
-        }
-        else {
-            ok(1, "$path correctly calls CGI parameters");
-        }
+    else {
+      ok(1, "$path correctly calls CGI parameters");
     }
+  }
 }
 
 exit 0;
index 5529392f6f8f16cc3a6a5132daed5685eb47adc0..8008bf5e09782bcb6392eea06b36bd27571d7af0 100644 (file)
@@ -19,49 +19,57 @@ use Support::Files;
 use Support::Templates;
 
 use File::Spec;
-use Test::More tests => (scalar(@Support::Files::testitems)
-                         + scalar(@Support::Files::test_files)
-                         + $Support::Templates::num_actual_files) * 3;
+use Test::More tests => (
+      scalar(@Support::Files::testitems)
+    + scalar(@Support::Files::test_files)
+    + $Support::Templates::num_actual_files)
+  * 3;
 
 my @testitems = (@Support::Files::testitems, @Support::Files::test_files);
 for my $path (@Support::Templates::include_paths) {
-   push(@testitems, map(File::Spec->catfile($path, $_),
-                        Support::Templates::find_actual_files($path)));
+  push(
+    @testitems,
+    map(File::Spec->catfile($path, $_),
+      Support::Templates::find_actual_files($path))
+  );
 }
 
 my %results;
 
 foreach my $file (@testitems) {
-    open (FILE, "$file");
-    my @contents = <FILE>;
-    if (grep /\t/, @contents) {
-        ok(0, "$file contains tabs --WARNING");
-    } else {
-        ok(1, "$file has no tabs");
-    }
-    close (FILE);
+  open(FILE, "$file");
+  my @contents = <FILE>;
+  if (grep /\t/, @contents) {
+    ok(0, "$file contains tabs --WARNING");
+  }
+  else {
+    ok(1, "$file has no tabs");
+  }
+  close(FILE);
 }
 
 foreach my $file (@testitems) {
-    open (FILE, "$file");
-    my @contents = <FILE>;
-    if (grep /\r/, @contents) {
-        ok(0, "$file contains non-OS-conformant line endings --WARNING");
-    } else {
-        ok(1, "All line endings of $file are OS conformant");
-    }
-    close (FILE);
+  open(FILE, "$file");
+  my @contents = <FILE>;
+  if (grep /\r/, @contents) {
+    ok(0, "$file contains non-OS-conformant line endings --WARNING");
+  }
+  else {
+    ok(1, "All line endings of $file are OS conformant");
+  }
+  close(FILE);
 }
 
 foreach my $file (@testitems) {
-    open (FILE, "$file");
-    my $first_line = <FILE>;
-    if ($first_line =~ /\xef\xbb\xbf/) {
-        ok(0, "$file contains Byte Order Mark --WARNING");
-    } else {
-        ok(1, "$file is free of a Byte Order Mark");
-    }
-    close (FILE);
+  open(FILE, "$file");
+  my $first_line = <FILE>;
+  if ($first_line =~ /\xef\xbb\xbf/) {
+    ok(0, "$file contains Byte Order Mark --WARNING");
+  }
+  else {
+    ok(1, "$file is free of a Byte Order Mark");
+  }
+  close(FILE);
 }
 
 exit 0;
index e675398f12b8b89d768d3f16fa3f10a0b4499e69..a5eaf724355119214123a8656f91d4421b973812 100644 (file)
@@ -19,72 +19,76 @@ use Support::Files;
 
 # -1 because 006spellcheck.t must not be checked.
 use Test::More tests => scalar(@Support::Files::testitems)
-                        + scalar(@Support::Files::test_files) - 1;
+  + scalar(@Support::Files::test_files) - 1;
 
 # Capture the TESTOUT from Test::More or Test::Builder for printing errors.
 # This will handle verbosity for us automatically.
 my $fh;
 {
-    no warnings qw(unopened);  # Don't complain about non-existent filehandles
-    if (-e \*Test::More::TESTOUT) {
-        $fh = \*Test::More::TESTOUT;
-    } elsif (-e \*Test::Builder::TESTOUT) {
-        $fh = \*Test::Builder::TESTOUT;
-    } else {
-        $fh = \*STDOUT;
-    }
+  no warnings qw(unopened);    # Don't complain about non-existent filehandles
+  if (-e \*Test::More::TESTOUT) {
+    $fh = \*Test::More::TESTOUT;
+  }
+  elsif (-e \*Test::Builder::TESTOUT) {
+    $fh = \*Test::Builder::TESTOUT;
+  }
+  else {
+    $fh = \*STDOUT;
+  }
 }
 
 my @testitems = (@Support::Files::testitems, @Support::Files::test_files);
 
 #add the words to check here:
 my @evilwords = qw(
-    anyways
-    appearence
-    arbitary
-    cancelled
-    critera
-    databasa
-    dependan
-    existance
-    existant
-    paramater
-    refered
-    repsentation
-    suported
-    varsion
+  anyways
+  appearence
+  arbitary
+  cancelled
+  critera
+  databasa
+  dependan
+  existance
+  existant
+  paramater
+  refered
+  repsentation
+  suported
+  varsion
 );
 
 my $evilwordsregexp = join('|', @evilwords);
 
 foreach my $file (@testitems) {
-    $file =~ s/\s.*$//; # nuke everything after the first space (#comment)
-    next if (!$file); # skip null entries
-    # Do not try to validate this file as it obviously contains a list
-    # of wrongly spelled words.
-    next if ($file eq 't/006spellcheck.t');
-
-    if (open (FILE, $file)) { # open the file for reading
-
-        my $found_word = '';
-
-        while (my $file_line = <FILE>) { # and go through the file line by line
-            if ($file_line =~ /($evilwordsregexp)/i) { # found an evil word
-                $found_word = $1;
-                last;
-            }
-        }
-            
-        close (FILE);
-            
-        if ($found_word) {
-            ok(0,"$file: found SPELLING ERROR $found_word --WARNING");
-        } else {
-            ok(1,"$file does not contain registered spelling errors");
-        }
-    } else {
-        ok(0,"could not open $file for spellcheck --WARNING");
+  $file =~ s/\s.*$//;    # nuke everything after the first space (#comment)
+  next if (!$file);      # skip null entries
+       # Do not try to validate this file as it obviously contains a list
+       # of wrongly spelled words.
+  next if ($file eq 't/006spellcheck.t');
+
+  if (open(FILE, $file)) {    # open the file for reading
+
+    my $found_word = '';
+
+    while (my $file_line = <FILE>) {    # and go through the file line by line
+      if ($file_line =~ /($evilwordsregexp)/i) {    # found an evil word
+        $found_word = $1;
+        last;
+      }
     }
-} 
+
+    close(FILE);
+
+    if ($found_word) {
+      ok(0, "$file: found SPELLING ERROR $found_word --WARNING");
+    }
+    else {
+      ok(1, "$file does not contain registered spelling errors");
+    }
+  }
+  else {
+    ok(0, "could not open $file for spellcheck --WARNING");
+  }
+}
 
 exit 0;
index 5861c2d35064114fc3039fcd5e21835d04ef7113..6c1abf526bf271ae4c727ffb79e539d8f2480e2d 100644 (file)
@@ -18,9 +18,9 @@ use Support::Files;
 use Test::More tests => 17;
 use DateTime;
 
-BEGIN { 
-    use_ok('Bugzilla');
-    use_ok('Bugzilla::Util');
+BEGIN {
+  use_ok('Bugzilla');
+  use_ok('Bugzilla::Util');
 }
 
 # We need to override user preferences so we can get an expected value when
@@ -29,52 +29,75 @@ Bugzilla->user->{'settings'}->{'timezone'}->{'value'} = "local";
 
 # We need to know the local timezone for the date chosen in our tests.
 # Below, tests are run against Nov. 24, 2002.
-my $tz = Bugzilla->local_timezone->short_name_for_datetime(DateTime->new(year => 2002, month => 11, day => 24));
+my $tz = Bugzilla->local_timezone->short_name_for_datetime(
+  DateTime->new(year => 2002, month => 11, day => 24));
 
 # we don't test the taint functions since that's going to take some more work.
 # XXX: test taint functions
 
 #html_quote():
-is(html_quote("<lala&@>"),"&lt;lala&amp;&#64;&gt;",'html_quote');
+is(html_quote("<lala&@>"), "&lt;lala&amp;&#64;&gt;", 'html_quote');
 
 #url_quote():
-is(url_quote("<lala&>gaa\"'[]{\\"),"%3Clala%26%3Egaa%22%27%5B%5D%7B%5C",'url_quote');
+is(url_quote("<lala&>gaa\"'[]{\\"),
+  "%3Clala%26%3Egaa%22%27%5B%5D%7B%5C", 'url_quote');
 
 #trim():
-is(trim(" fg<*\$%>+=~~ "),'fg<*$%>+=~~','trim()');
+is(trim(" fg<*\$%>+=~~ "), 'fg<*$%>+=~~', 'trim()');
 
 #format_time();
-is(format_time("2002.11.24 00:05"), "2002-11-24 00:05 $tz",'format_time("2002.11.24 00:05") is ' . format_time("2002.11.24 00:05"));
-is(format_time("2002.11.24 00:05:56"), "2002-11-24 00:05:56 $tz",'format_time("2002.11.24 00:05:56")');
-is(format_time("2002.11.24 00:05:56", "%Y-%m-%d %R"), '2002-11-24 00:05', 'format_time("2002.11.24 00:05:56", "%Y-%m-%d %R") (with no timezone)');
-is(format_time("2002.11.24 00:05:56", "%Y-%m-%d %R %Z"), "2002-11-24 00:05 $tz", 'format_time("2002.11.24 00:05:56", "%Y-%m-%d %R %Z") (with timezone)');
+is(
+  format_time("2002.11.24 00:05"),
+  "2002-11-24 00:05 $tz",
+  'format_time("2002.11.24 00:05") is ' . format_time("2002.11.24 00:05")
+);
+is(
+  format_time("2002.11.24 00:05:56"),
+  "2002-11-24 00:05:56 $tz",
+  'format_time("2002.11.24 00:05:56")'
+);
+is(
+  format_time("2002.11.24 00:05:56", "%Y-%m-%d %R"),
+  '2002-11-24 00:05',
+  'format_time("2002.11.24 00:05:56", "%Y-%m-%d %R") (with no timezone)'
+);
+is(
+  format_time("2002.11.24 00:05:56", "%Y-%m-%d %R %Z"),
+  "2002-11-24 00:05 $tz",
+  'format_time("2002.11.24 00:05:56", "%Y-%m-%d %R %Z") (with timezone)'
+);
 
 # email_filter
 my %email_strings = (
-    'somebody@somewhere.com' => 'somebody',
-    'Somebody <somebody@somewhere.com>' => 'Somebody <somebody>',
-    'One Person <one@person.com>, Two Person <two@person.com>' 
-        => 'One Person <one>, Two Person <two>',
-    'This string contains somebody@somewhere.com and also this@that.com'
-        => 'This string contains somebody and also this',
+  'somebody@somewhere.com'            => 'somebody',
+  'Somebody <somebody@somewhere.com>' => 'Somebody <somebody>',
+  'One Person <one@person.com>, Two Person <two@person.com>' =>
+    'One Person <one>, Two Person <two>',
+  'This string contains somebody@somewhere.com and also this@that.com' =>
+    'This string contains somebody and also this',
 );
+
 foreach my $input (keys %email_strings) {
-    is(Bugzilla::Util::email_filter($input), $email_strings{$input}, 
-       "email_filter('$input')");
+  is(Bugzilla::Util::email_filter($input),
+    $email_strings{$input}, "email_filter('$input')");
 }
 
 # validate_email_syntax. We need to override some parameters.
 my $params = Bugzilla->params;
 $params->{emailregexp} = '.*';
 my $ascii_email = 'admin@company.com';
+
 # U+0430 returns the Cyrillic "а", which looks similar to the ASCII "a".
 my $utf8_email = "\N{U+0430}dmin\@company.com";
-ok(validate_email_syntax($ascii_email), 'correctly formatted ASCII-only email address is valid');
-ok(!validate_email_syntax($utf8_email), 'correctly formatted email address with non-ASCII characters is rejected');
+ok(validate_email_syntax($ascii_email),
+  'correctly formatted ASCII-only email address is valid');
+ok(!validate_email_syntax($utf8_email),
+  'correctly formatted email address with non-ASCII characters is rejected');
 
 # diff_arrays():
 my @old_array = qw(alpha beta alpha gamma gamma beta alpha delta epsilon gamma);
 my @new_array = qw(alpha alpha beta gamma epsilon delta beta delta);
+
 # The order is not relevant when comparing both arrays for matching items,
 # i.e. (foo bar) and (bar foo) are the same arrays (same items).
 # But when returning data, we try to respect the initial order.
@@ -82,5 +105,6 @@ my @new_array = qw(alpha alpha beta gamma epsilon delta beta delta);
 # Removed (in this order): gamma alpha gamma.
 # Added (in this order): delta
 my ($removed, $added) = diff_arrays(\@old_array, \@new_array);
-is_deeply($removed, [qw(gamma alpha gamma)], 'diff_array(\@old, \@new) (check removal)');
+is_deeply($removed, [qw(gamma alpha gamma)],
+  'diff_array(\@old, \@new) (check removal)');
 is_deeply($added, [qw(delta)], 'diff_array(\@old, \@new) (check addition)');
index 0da18c38a9987737e49f4ca590ccc613a0ef9aa5..c48aab30ba5a35f06a8b6ed3b167808b125538e9 100644 (file)
@@ -11,7 +11,7 @@
 
 # This test scans all our templates for every directive. Having eliminated
 # those which cannot possibly cause XSS problems, it then checks the rest
-# against the safe list stored in the filterexceptions.pl file. 
+# against the safe list stored in the filterexceptions.pl file.
 
 # Sample exploit code: '>"><script>alert('Oh dear...')</script>
 
@@ -29,192 +29,196 @@ use Cwd;
 
 # Undefine the record separator so we can read in whole files at once
 my $oldrecsep = $/;
-my $topdir = cwd;
+my $topdir    = cwd;
 $/ = undef;
 our %safe;
 
 foreach my $path (@Support::Templates::include_paths) {
-    $path =~ s|\\|/|g if ON_WINDOWS;  # convert \ to / in path if on windows
-    $path =~ m|template/([^/]+)/([^/]+)|;
-    my $lang = $1;
-    my $flavor = $2;
-
-    chdir $topdir; # absolute path
-    my @testitems = Support::Templates::find_actual_files($path);
-    chdir $topdir; # absolute path
-    
-    next unless @testitems;
-    
-    # Some people require this, others don't. No-one knows why.
-    chdir $path; # relative path
-    
-    # We load a %safe list of acceptable exceptions.
-    if (-r "filterexceptions.pl") {
-        do "filterexceptions.pl";
-        if (ON_WINDOWS) {
-          # filterexceptions.pl uses / separated paths, while 
-          # find_actual_files returns \ separated ones on Windows.
-          # Here, we convert the filter exception hash to use \.
-          foreach my $file (keys %safe) {
-            my $orig_file = $file;
-            $file =~ s|/|\\|g;
-            if ($file ne $orig_file) {
-              $safe{$file} = $safe{$orig_file};
-              delete $safe{$orig_file};
-            }
-          }
+  $path =~ s|\\|/|g if ON_WINDOWS;    # convert \ to / in path if on windows
+  $path =~ m|template/([^/]+)/([^/]+)|;
+  my $lang   = $1;
+  my $flavor = $2;
+
+  chdir $topdir;                      # absolute path
+  my @testitems = Support::Templates::find_actual_files($path);
+  chdir $topdir;                      # absolute path
+
+  next unless @testitems;
+
+  # Some people require this, others don't. No-one knows why.
+  chdir $path;                        # relative path
+
+  # We load a %safe list of acceptable exceptions.
+  if (-r "filterexceptions.pl") {
+    do "filterexceptions.pl";
+    if (ON_WINDOWS) {
+
+      # filterexceptions.pl uses / separated paths, while
+      # find_actual_files returns \ separated ones on Windows.
+      # Here, we convert the filter exception hash to use \.
+      foreach my $file (keys %safe) {
+        my $orig_file = $file;
+        $file =~ s|/|\\|g;
+        if ($file ne $orig_file) {
+          $safe{$file} = $safe{$orig_file};
+          delete $safe{$orig_file};
         }
+      }
     }
-    
-    # We preprocess the %safe hash of lists into a hash of hashes. This allows
-    # us to flag which members were not found, and report that as a warning, 
-    # thereby keeping the lists clean.
-    foreach my $file (keys %safe) {
-        if (ref $safe{$file} eq 'ARRAY') {
-            my $list = $safe{$file};
-            $safe{$file} = {};
-            foreach my $directive (@$list) {
-                $safe{$file}{$directive} = 0;    
-            }
-        }
+  }
+
+  # We preprocess the %safe hash of lists into a hash of hashes. This allows
+  # us to flag which members were not found, and report that as a warning,
+  # thereby keeping the lists clean.
+  foreach my $file (keys %safe) {
+    if (ref $safe{$file} eq 'ARRAY') {
+      my $list = $safe{$file};
+      $safe{$file} = {};
+      foreach my $directive (@$list) {
+        $safe{$file}{$directive} = 0;
+      }
     }
+  }
 
-    foreach my $file (@testitems) {
-        # There are some files we don't check, because there is no need to
-        # filter their contents due to their content-type.
-        if ($file =~ /\.(pm|txt|rst|png)\.tmpl$/) {
-            ok(1, "($lang/$flavor) $file is filter-safe");
-            next;
-        }
+  foreach my $file (@testitems) {
 
-        # Read the entire file into a string
-        open (FILE, "<$file") || die "Can't open $file: $!\n";
-        my $slurp = <FILE>;
-        close (FILE);
+    # There are some files we don't check, because there is no need to
+    # filter their contents due to their content-type.
+    if ($file =~ /\.(pm|txt|rst|png)\.tmpl$/) {
+      ok(1, "($lang/$flavor) $file is filter-safe");
+      next;
+    }
 
-        my @unfiltered;
+    # Read the entire file into a string
+    open(FILE, "<$file") || die "Can't open $file: $!\n";
+    my $slurp = <FILE>;
+    close(FILE);
 
-        # /g means we execute this loop for every match
-        # /s means we ignore linefeeds in the regexp matches
-        while ($slurp =~ /\[%(?:-|\+|~|=)?(.*?)(?:-|\+|~|=)?%\]/gs) {
-            my $directive = $1;
+    my @unfiltered;
 
-            my @lineno = ($` =~ m/\n/gs);
-            my $lineno = scalar(@lineno) + 1;
+    # /g means we execute this loop for every match
+    # /s means we ignore linefeeds in the regexp matches
+    while ($slurp =~ /\[%(?:-|\+|~|=)?(.*?)(?:-|\+|~|=)?%\]/gs) {
+      my $directive = $1;
 
-            if (!directive_ok($file, $directive)) {
+      my @lineno = ($` =~ m/\n/gs);
+      my $lineno = scalar(@lineno) + 1;
 
-              # This intentionally makes no effort to eliminate duplicates; to do
-              # so would merely make it more likely that the user would not 
-              # escape all instances when attempting to correct an error.
-              push(@unfiltered, "$lineno:$directive");
-            }
-        }  
+      if (!directive_ok($file, $directive)) {
 
-        my $fullpath = File::Spec->catfile($path, $file);
-        
-        if (@unfiltered) {
-            my $uflist = join("\n  ", @unfiltered);
-            ok(0, "($lang/$flavor) $fullpath has unfiltered directives:\n  $uflist\n--ERROR");
-        }
-        else {
-            # Find any members of the exclusion list which were not found
-            my @notfound;
-            foreach my $directive (keys %{$safe{$file}}) {
-                push(@notfound, $directive) if ($safe{$file}{$directive} == 0);    
-            }
-
-            if (@notfound) {
-                my $nflist = join("\n  ", @notfound);
-                ok(0, "($lang/$flavor) $fullpath - filterexceptions.pl has extra members:\n  $nflist\n" . 
-                                                                  "--WARNING");
-            }
-            else {
-                # Don't use the full path here - it's too long and unwieldy.
-                ok(1, "($lang/$flavor) $file is filter-safe");
-            }
-        }
+        # This intentionally makes no effort to eliminate duplicates; to do
+        # so would merely make it more likely that the user would not
+        # escape all instances when attempting to correct an error.
+        push(@unfiltered, "$lineno:$directive");
+      }
     }
+
+    my $fullpath = File::Spec->catfile($path, $file);
+
+    if (@unfiltered) {
+      my $uflist = join("\n  ", @unfiltered);
+      ok(0,
+        "($lang/$flavor) $fullpath has unfiltered directives:\n  $uflist\n--ERROR");
+    }
+    else {
+      # Find any members of the exclusion list which were not found
+      my @notfound;
+      foreach my $directive (keys %{$safe{$file}}) {
+        push(@notfound, $directive) if ($safe{$file}{$directive} == 0);
+      }
+
+      if (@notfound) {
+        my $nflist = join("\n  ", @notfound);
+        ok(0,
+          "($lang/$flavor) $fullpath - filterexceptions.pl has extra members:\n  $nflist\n"
+            . "--WARNING");
+      }
+      else {
+        # Don't use the full path here - it's too long and unwieldy.
+        ok(1, "($lang/$flavor) $file is filter-safe");
+      }
+    }
+  }
 }
 
 sub directive_ok {
-    my ($file, $directive) = @_;
+  my ($file, $directive) = @_;
 
-    # Comments
-    return 1 if $directive =~ /^#/;        
+  # Comments
+  return 1 if $directive =~ /^#/;
 
-    # Remove any leading/trailing whitespace.
-    $directive =~ s/^\s*//;
-    $directive =~ s/\s*$//;
+  # Remove any leading/trailing whitespace.
+  $directive =~ s/^\s*//;
+  $directive =~ s/\s*$//;
 
-    # Empty directives are ok; they are usually line break helpers
-    return 1 if $directive eq '';
+  # Empty directives are ok; they are usually line break helpers
+  return 1 if $directive eq '';
 
-    # Make sure we're not looking for ./ in the $safe hash
-    $file =~ s#^\./##;
+  # Make sure we're not looking for ./ in the $safe hash
+  $file =~ s#^\./##;
 
-    # Exclude those on the nofilter list
-    if (defined($safe{$file}{$directive})) {
-        $safe{$file}{$directive}++;
-        return 1;
-    };
+  # Exclude those on the nofilter list
+  if (defined($safe{$file}{$directive})) {
+    $safe{$file}{$directive}++;
+    return 1;
+  }
 
-    # Directives
-    return 1 if $directive =~ /^(IF|END|UNLESS|FOREACH|PROCESS|INCLUDE|
+  # Directives
+  return 1 if $directive =~ /^(IF|END|UNLESS|FOREACH|PROCESS|INCLUDE|
                                  BLOCK|USE|ELSE|NEXT|LAST|DEFAULT|CALL|
                                  ELSIF|SET|SWITCH|CASE|WHILE|RETURN|STOP|
                                  TRY|CATCH|FINAL|THROW|CLEAR|MACRO|FILTER)/x;
 
-    # ? :
-    if ($directive =~ /.+\?(.+):(.+)/) {
-        return 1 if directive_ok($file, $1) && directive_ok($file, $2);
-    }
+  # ? :
+  if ($directive =~ /.+\?(.+):(.+)/) {
+    return 1 if directive_ok($file, $1) && directive_ok($file, $2);
+  }
+
+  # + - * /
+  return 1 if $directive =~ /[+\-*\/]/;
+
+  # Numbers
+  return 1 if $directive =~ /^[0-9]+$/;
+
+  # Simple assignments
+  return 1 if $directive =~ /^[\w\.\$\{\}]+\s+=\s+/;
+
+  # Conditional literals with either sort of quotes
+  # There must be no $ in the string for it to be a literal
+  return 1 if $directive =~ /^(["'])[^\$]*[^\\]\1/;
+  return 1 if $directive =~ /^(["'])\1/;
+
+  # Special values always used for numbers
+  return 1 if $directive =~ /^[ijkn]$/;
+  return 1 if $directive =~ /^count$/;
+
+  # Params
+  return 1 if $directive =~ /^Param\(/;
+
+  # Hooks
+  return 1 if $directive =~ /^Hook.process\(/;
+
+  # Other functions guaranteed to return OK output
+  return 1 if $directive =~ /^(time2str|url)\(/;
+
+  # Safe Template Toolkit virtual methods
+  return 1 if $directive =~ /\.(length$|size$|push\(|unshift\(|delete\()/;
+
+  # Special Template Toolkit loop variable
+  return 1 if $directive =~ /^loop\.(index|count)$/;
+
+  # Branding terms
+  return 1 if $directive =~ /^terms\./;
 
-    # + - * /
-    return 1 if $directive =~ /[+\-*\/]/;
-
-    # Numbers
-    return 1 if $directive =~ /^[0-9]+$/;
-
-    # Simple assignments
-    return 1 if $directive =~ /^[\w\.\$\{\}]+\s+=\s+/;
-
-    # Conditional literals with either sort of quotes 
-    # There must be no $ in the string for it to be a literal
-    return 1 if $directive =~ /^(["'])[^\$]*[^\\]\1/;
-    return 1 if $directive =~ /^(["'])\1/;
-
-    # Special values always used for numbers
-    return 1 if $directive =~ /^[ijkn]$/;
-    return 1 if $directive =~ /^count$/;
-    
-    # Params
-    return 1 if $directive =~ /^Param\(/;
-    
-    # Hooks
-    return 1 if $directive =~ /^Hook.process\(/;
-
-    # Other functions guaranteed to return OK output
-    return 1 if $directive =~ /^(time2str|url)\(/;
-
-    # Safe Template Toolkit virtual methods
-    return 1 if $directive =~ /\.(length$|size$|push\(|unshift\(|delete\()/;
-
-    # Special Template Toolkit loop variable
-    return 1 if $directive =~ /^loop\.(index|count)$/;
-    
-    # Branding terms
-    return 1 if $directive =~ /^terms\./;
-            
-    # Things which are already filtered
-    # Note: If a single directive prints two things, and only one is 
-    # filtered, we may not catch that case.
-    return 1 if $directive =~ /FILTER\ (html|csv|js|base64|css_class_quote|ics|
+  # Things which are already filtered
+  # Note: If a single directive prints two things, and only one is
+  # filtered, we may not catch that case.
+  return 1 if $directive =~ /FILTER\ (html|csv|js|base64|css_class_quote|ics|
                                         quoteUrls|time|uri|xml|html_light|
                                         unitconvert|txt|html_linebreak|markdown|
                                         none|null)\b/x;
 
-    return 0;
+  return 0;
 }
 
 $/ = $oldrecsep;
index af83d1eac60ec6238ab4ee6b073d22c33cc016bb..40db00fe29b30579c8c8e9fc792df44c7e63f372 100644 (file)
@@ -9,9 +9,9 @@
 #Bugzilla Test 9#
 ####bugwords#####
 
-# Bugzilla has a mechanism for taking various words, including "bug", "bugs", 
+# Bugzilla has a mechanism for taking various words, including "bug", "bugs",
 # and "a bug" and automatically replacing them in the templates with the local
-# terminology. It does this by using the 'terms' hash, so "bug" becomes 
+# terminology. It does this by using the 'terms' hash, so "bug" becomes
 # "[% terms.bug %]". This test makes sure the relevant words aren't used
 # bare.
 
@@ -27,53 +27,57 @@ use Bugzilla::Util;
 
 use File::Spec;
 
-use Test::More tests => ($Support::Templates::num_actual_files); 
+use Test::More tests => ($Support::Templates::num_actual_files);
 
 # Find all the templates
 my @testitems;
 for my $path (@Support::Templates::include_paths) {
-    push(@testitems, map(File::Spec->catfile($path, $_),
-                         Support::Templates::find_actual_files($path)));
+  push(
+    @testitems,
+    map(File::Spec->catfile($path, $_),
+      Support::Templates::find_actual_files($path))
+  );
 }
 
 foreach my $file (@testitems) {
-    my @errors;
-    
-    # Read the entire file into a string
-    local $/;
-    open (FILE, "<$file") || die "Can't open $file: $!\n";    
-    my $slurp = <FILE>;
-    close (FILE);
-
-    # /g means we execute this loop for every match
-    # /s means we ignore linefeeds in the regexp matches
-    # This extracts everything which is _not_ a directive.
-    while ($slurp =~ /%\](.*?)(\[%|$)/gs) {
-        my $text = $1;
-
-        my @lineno = ($` =~ m/\n/gs);
-        my $lineno = scalar(@lineno) + 1;
-    
-        # "a bug", "bug", "bugs"
-        if (grep /(a?[\s>]bugs?[\s.:;,<])/i, $text) {
-            # Exclude variable assignment.
-            unless (grep /bugs =/, $text) {
-                push(@errors, [$lineno, $text]);
-                next;
-            }
-        }
-    }
-        
-    if (scalar(@errors)) {
-      ok(0, "$file contains invalid bare words (e.g. 'bug') --WARNING");
-      
-      foreach my $error (@errors) {
-        print "$error->[0]: $error->[1]\n";
+  my @errors;
+
+  # Read the entire file into a string
+  local $/;
+  open(FILE, "<$file") || die "Can't open $file: $!\n";
+  my $slurp = <FILE>;
+  close(FILE);
+
+  # /g means we execute this loop for every match
+  # /s means we ignore linefeeds in the regexp matches
+  # This extracts everything which is _not_ a directive.
+  while ($slurp =~ /%\](.*?)(\[%|$)/gs) {
+    my $text = $1;
+
+    my @lineno = ($` =~ m/\n/gs);
+    my $lineno = scalar(@lineno) + 1;
+
+    # "a bug", "bug", "bugs"
+    if (grep /(a?[\s>]bugs?[\s.:;,<])/i, $text) {
+
+      # Exclude variable assignment.
+      unless (grep /bugs =/, $text) {
+        push(@errors, [$lineno, $text]);
+        next;
       }
-    } 
-    else {
-      ok(1, "$file has no invalid barewords");
     }
+  }
+
+  if (scalar(@errors)) {
+    ok(0, "$file contains invalid bare words (e.g. 'bug') --WARNING");
+
+    foreach my $error (@errors) {
+      print "$error->[0]: $error->[1]\n";
+    }
+  }
+  else {
+    ok(1, "$file has no invalid barewords");
+  }
 }
 
 exit 0;
index 1b6c270a8c4c6d95d10caa376e672790466a345b..6af24ce2b03fbff952ab1949721f88b0a7d1f603 100644 (file)
@@ -30,7 +30,8 @@ use constant MODULE_REGEX => qr/
     ['"]?
     ([\w:\.\\]+)
 /x;
-use constant BASE_REGEX => qr/^use (?:base|parent) (?:-norequire, )?qw\(([^\)]+)/;
+use constant BASE_REGEX =>
+  qr/^use (?:base|parent) (?:-norequire, )?qw\(([^\)]+)/;
 
 # Extract all Perl modules.
 foreach my $file (@Support::Files::testitems) {
@@ -42,46 +43,47 @@ foreach my $file (@Support::Files::testitems) {
 }
 
 foreach my $module (keys %mods) {
-    my $reading = 1;
-    my @use;
-
-    open(SOURCE, $mods{$module});
-    while (my $line = <SOURCE>) {
-      last if ($line =~ /^__END__/);
-      if ($line =~ /^=cut/) {
-        $reading = 1;
-        next;
-      }
-      next unless $reading;
-      if ($line =~ /^=(head|over|item|back|pod|begin|end|for)/) {
-        $reading = 0;
-        next;
-      }
-      if ($line =~ /^package\s+([^;]);/) {
-        $module = $1;
-      }
-      elsif ($line =~ BASE_REGEX or $line =~ MODULE_REGEX) {
-        my $used_string = $1;
-        # "use base"/"use parent" can have multiple modules
-        my @used_array = split(/\s+/, $used_string);
-        foreach my $used (@used_array) {
-            next if $used !~ /^Bugzilla/;
-            $used =~ s#/#::#g;
-            $used =~ s#\.pm$##;
-            $used =~ s#\$module#[^:]+#;
-            $used =~ s#\$\{[^}]+}#[^:]+#;
-            $used =~ s#[" ]##g;
-            push(@use, grep(/^\Q$used\E$/, keys %mods));
-        }
-      }
+  my $reading = 1;
+  my @use;
+
+  open(SOURCE, $mods{$module});
+  while (my $line = <SOURCE>) {
+    last if ($line =~ /^__END__/);
+    if ($line =~ /^=cut/) {
+      $reading = 1;
+      next;
+    }
+    next unless $reading;
+    if ($line =~ /^=(head|over|item|back|pod|begin|end|for)/) {
+      $reading = 0;
+      next;
     }
-    close (SOURCE);
+    if ($line =~ /^package\s+([^;]);/) {
+      $module = $1;
+    }
+    elsif ($line =~ BASE_REGEX or $line =~ MODULE_REGEX) {
+      my $used_string = $1;
 
-    foreach my $u (@use) {
-      if (!grep {$_ eq $u} @{$deps{$module}}) {
-        push(@{$deps{$module}}, $u);
+      # "use base"/"use parent" can have multiple modules
+      my @used_array = split(/\s+/, $used_string);
+      foreach my $used (@used_array) {
+        next if $used !~ /^Bugzilla/;
+        $used =~ s#/#::#g;
+        $used =~ s#\.pm$##;
+        $used =~ s#\$module#[^:]+#;
+        $used =~ s#\$\{[^}]+}#[^:]+#;
+        $used =~ s#[" ]##g;
+        push(@use, grep(/^\Q$used\E$/, keys %mods));
       }
     }
+  }
+  close(SOURCE);
+
+  foreach my $u (@use) {
+    if (!grep { $_ eq $u } @{$deps{$module}}) {
+      push(@{$deps{$module}}, $u);
+    }
+  }
 }
 
 sub creates_loop {
index 8a833967283ebafda38df13415c6f6132025f694..db6e16ad41ef4b76c69c62ae11ab70faa008eaa0 100644 (file)
@@ -21,111 +21,123 @@ use Pod::Checker;
 use Pod::Coverage;
 
 use Test::More tests => scalar(@Support::Files::testitems)
-                        + scalar(@Support::Files::module_files);
+  + scalar(@Support::Files::module_files);
 
 # These methods do not need to be documented by default.
-use constant DEFAULT_WHITELIST => qr/^(?:new|new_from_list|check|run_create_validators|[A-Z_]+)$/;
+use constant DEFAULT_WHITELIST =>
+  qr/^(?:new|new_from_list|check|run_create_validators|[A-Z_]+)$/;
 
 # These subroutines do not need to be documented, generally because
 # you shouldn't call them yourself. No need to include subroutines
 # of the form _foo(); they are already treated as private.
 use constant SUB_WHITELIST => (
-    'Bugzilla::Flag'     => qr/^(?:(force_)?retarget|force_cleanup)$/,
-    'Bugzilla::FlagType' => qr/^sqlify_criteria$/,
-    'Bugzilla::JobQueue' => qr/(?:^work_once|work_until_done|subprocess_worker)$/,
-    'Bugzilla::Search'   => qr/^SPECIAL_PARSING$/,
-    'Bugzilla::Template' => qr/^field_name$/,
-    'Bugzilla::MIME'     => qr/^as_string$/,
+  'Bugzilla::Flag'     => qr/^(?:(force_)?retarget|force_cleanup)$/,
+  'Bugzilla::FlagType' => qr/^sqlify_criteria$/,
+  'Bugzilla::JobQueue' => qr/(?:^work_once|work_until_done|subprocess_worker)$/,
+  'Bugzilla::Search'   => qr/^SPECIAL_PARSING$/,
+  'Bugzilla::Template' => qr/^field_name$/,
+  'Bugzilla::MIME'     => qr/^as_string$/,
 );
 
 # These modules do not need to be documented, generally because they
 # are subclasses of another module which already has all the relevant
 # documentation. Partial names are allowed.
 use constant MODULE_WHITELIST => qw(
-    Bugzilla::Auth::Login::
-    Bugzilla::Auth::Persist::
-    Bugzilla::Auth::Verify::
-    Bugzilla::BugUrl::
-    Bugzilla::Config::
-    Bugzilla::Extension::
-    Bugzilla::Job::
-    Bugzilla::Migrate::
-    docs::lib::Pod::Simple::
+  Bugzilla::Auth::Login::
+  Bugzilla::Auth::Persist::
+  Bugzilla::Auth::Verify::
+  Bugzilla::BugUrl::
+  Bugzilla::Config::
+  Bugzilla::Extension::
+  Bugzilla::Job::
+  Bugzilla::Migrate::
+  docs::lib::Pod::Simple::
 );
 
 # Capture the TESTOUT from Test::More or Test::Builder for printing errors.
 # This will handle verbosity for us automatically.
 my $fh;
 {
-    no warnings qw(unopened);  # Don't complain about non-existent filehandles
-    if (-e \*Test::More::TESTOUT) {
-        $fh = \*Test::More::TESTOUT;
-    } elsif (-e \*Test::Builder::TESTOUT) {
-        $fh = \*Test::Builder::TESTOUT;
-    } else {
-        $fh = \*STDOUT;
-    }
+  no warnings qw(unopened);    # Don't complain about non-existent filehandles
+  if (-e \*Test::More::TESTOUT) {
+    $fh = \*Test::More::TESTOUT;
+  }
+  elsif (-e \*Test::Builder::TESTOUT) {
+    $fh = \*Test::Builder::TESTOUT;
+  }
+  else {
+    $fh = \*STDOUT;
+  }
 }
 
 my @testitems = @Support::Files::testitems;
 
 foreach my $file (@testitems) {
-    $file =~ s/\s.*$//; # nuke everything after the first space (#comment)
-    next if (!$file); # skip null entries
-    my $error_count = podchecker($file, $fh);
-    if ($error_count < 0) {
-        ok(1,"$file does not contain any POD");
-    } elsif ($error_count == 0) {
-        ok(1,"$file has correct POD syntax");
-    } else {
-        ok(0,"$file has incorrect POD syntax --ERROR");
-    }
+  $file =~ s/\s.*$//;    # nuke everything after the first space (#comment)
+  next if (!$file);      # skip null entries
+  my $error_count = podchecker($file, $fh);
+  if ($error_count < 0) {
+    ok(1, "$file does not contain any POD");
+  }
+  elsif ($error_count == 0) {
+    ok(1, "$file has correct POD syntax");
+  }
+  else {
+    ok(0, "$file has incorrect POD syntax --ERROR");
+  }
 }
 
 my %sub_whitelist = SUB_WHITELIST;
-my @module_files = sort @Support::Files::module_files;
+my @module_files  = sort @Support::Files::module_files;
 
 foreach my $file (@module_files) {
-    my $module = $file;
-    $module =~ s/\.pm$//;
-    $module =~ s#/#::#g;
-    $module =~ s/^extensions/Bugzilla::Extension/;
-
-    my @whitelist = (DEFAULT_WHITELIST);
-    push(@whitelist, $sub_whitelist{$module}) if $sub_whitelist{$module};
-
-    # XXX Once all methods are correctly documented, nonwhitespace should
-    # be set to 1.
-    my $cover = Pod::Coverage->new(package => $module, nonwhitespace => 0,
-                                   trustme => \@whitelist);
-    my $coverage = $cover->coverage;
-    my $reason = $cover->why_unrated;
-
-    if (defined $coverage) {
-        if ($coverage == 1) {
-            ok(1, "$file has POD for all methods");
-        }
-        else {
-            ok(0, "$file POD coverage is " . sprintf("%u%%", 100 * $coverage) .
-                  ". Undocumented methods: " . join(', ', $cover->uncovered));
-        }
+  my $module = $file;
+  $module =~ s/\.pm$//;
+  $module =~ s#/#::#g;
+  $module =~ s/^extensions/Bugzilla::Extension/;
+
+  my @whitelist = (DEFAULT_WHITELIST);
+  push(@whitelist, $sub_whitelist{$module}) if $sub_whitelist{$module};
+
+  # XXX Once all methods are correctly documented, nonwhitespace should
+  # be set to 1.
+  my $cover = Pod::Coverage->new(
+    package       => $module,
+    nonwhitespace => 0,
+    trustme       => \@whitelist
+  );
+  my $coverage = $cover->coverage;
+  my $reason   = $cover->why_unrated;
+
+  if (defined $coverage) {
+    if ($coverage == 1) {
+      ok(1, "$file has POD for all methods");
     }
-    # These errors are thrown when the module couldn't be loaded due to
-    # a missing dependency.
-    elsif ($reason =~ /^(?:no public symbols defined|requiring '[^']+' failed)$/) {
-        ok(1, "$file cannot be loaded");
+    else {
+      ok(0,
+            "$file POD coverage is "
+          . sprintf("%u%%", 100 * $coverage)
+          . ". Undocumented methods: "
+          . join(', ', $cover->uncovered));
     }
-    elsif ($reason eq "couldn't find pod") {
-        if (grep { $module =~ /^\Q$_\E/ } MODULE_WHITELIST) {
-            ok(1, "$file does not contain any POD (whitelisted)");
-        }
-        else {
-            ok(0, "$file POD coverage is 0%");
-        }
+  }
+
+  # These errors are thrown when the module couldn't be loaded due to
+  # a missing dependency.
+  elsif ($reason =~ /^(?:no public symbols defined|requiring '[^']+' failed)$/) {
+    ok(1, "$file cannot be loaded");
+  }
+  elsif ($reason eq "couldn't find pod") {
+    if (grep { $module =~ /^\Q$_\E/ } MODULE_WHITELIST) {
+      ok(1, "$file does not contain any POD (whitelisted)");
     }
     else {
-        ok(0, "$file: $reason");
+      ok(0, "$file POD coverage is 0%");
     }
+  }
+  else {
+    ok(0, "$file: $reason");
+  }
 }
 
 exit 0;
index 9d709c6aba0ebac1885a9cc1a62af8176dafe2fb..6471e1da35d90803fce3671bd4c9e4b06e89d2d6 100644 (file)
@@ -6,7 +6,6 @@
 # defined by the Mozilla Public License, v. 2.0.
 
 
-
 ##################
 #Bugzilla Test 12#
 ######Errors######
@@ -32,11 +31,11 @@ push @{$Errors{code}{template_error}{used_in}{'Bugzilla/Error.pm'}}, 0;
 
 # Define files to test. Each file would have a list of error messages, if any.
 my %test_templates = ();
-my %test_modules = ();
+my %test_modules   = ();
 
 # Find all modules
 foreach my $module (@Support::Files::testitems) {
-    $test_modules{$module} = ();
+  $test_modules{$module} = ();
 }
 
 # Find all error templates
@@ -44,20 +43,19 @@ foreach my $module (@Support::Files::testitems) {
 # hairy. But let us do it only once.
 
 foreach my $include_path (@include_paths) {
-    foreach my $path (@{$actual_files{$include_path}}) {
-        my $file = File::Spec->catfile($include_path, $path);
-        $file =~ s/\s.*$//; # nuke everything after the first space
-        $file =~ s|\\|/|g if ON_WINDOWS;  # convert \ to / in path if on windows
-        $test_templates{$file} = () 
-            if $file =~ m#global/(code|user)-error\.html\.tmpl#;
-
-        # Make sure the extension is not disabled
-        if ($file =~ m#^(extensions/[^/]+/)#) {
-            $test_templates{$file} = ()
-                if ! -e "${1}disabled"
-                && $file =~ m#global/(code|user)-error-errors\.html\.tmpl#;
-        }
+  foreach my $path (@{$actual_files{$include_path}}) {
+    my $file = File::Spec->catfile($include_path, $path);
+    $file =~ s/\s.*$//;    # nuke everything after the first space
+    $file =~ s|\\|/|g if ON_WINDOWS;    # convert \ to / in path if on windows
+    $test_templates{$file} = () if $file =~ m#global/(code|user)-error\.html\.tmpl#;
+
+    # Make sure the extension is not disabled
+    if ($file =~ m#^(extensions/[^/]+/)#) {
+      $test_templates{$file} = ()
+        if !-e "${1}disabled"
+        && $file =~ m#global/(code|user)-error-errors\.html\.tmpl#;
     }
+  }
 }
 
 # Count the tests. The +1 is for checking the WS_ERROR_CODE errors.
@@ -69,112 +67,115 @@ plan tests => $tests;
 
 # Collect all errors defined in templates
 foreach my $file (keys %test_templates) {
-    $file =~ m|template/([^/]+).*/global/([^/]+)-error(?:-errors)?\.html\.tmpl|;
-    my $lang = $1;
-    my $errtype = $2;
-
-    if (! open (TMPL, $file)) {
-        Register(\%test_templates, $file, "could not open file --WARNING");
-        next;
+  $file =~ m|template/([^/]+).*/global/([^/]+)-error(?:-errors)?\.html\.tmpl|;
+  my $lang    = $1;
+  my $errtype = $2;
+
+  if (!open(TMPL, $file)) {
+    Register(\%test_templates, $file, "could not open file --WARNING");
+    next;
+  }
+
+  my $lineno = 0;
+  while (my $line = <TMPL>) {
+    $lineno++;
+    if ($line =~ /\[%\s[A-Z]+\s*error\s*==\s*"(.+)"\s*%\]/) {
+      my $errtag = $1;
+      if ($errtag =~ /\s/) {
+        Register(\%test_templates, $file,
+              "has an error definition \"$errtag\" at line $lineno with "
+            . "space(s) embedded --ERROR");
+      }
+      else {
+        push @{$Errors{$errtype}{$errtag}{defined_in}{$lang}{$file}}, $lineno;
+      }
     }
-    
-    my $lineno=0;
-    while (my $line = <TMPL>) {
-        $lineno++;
-        if ($line =~ /\[%\s[A-Z]+\s*error\s*==\s*"(.+)"\s*%\]/) {
-            my $errtag = $1;
-            if ($errtag =~ /\s/) {
-                Register(\%test_templates, $file, 
-                "has an error definition \"$errtag\" at line $lineno with "
-                . "space(s) embedded --ERROR");
-            }
-            else {
-                push @{$Errors{$errtype}{$errtag}{defined_in}{$lang}{$file}}, $lineno;
-            }
-        }
-    }
-    close(TMPL);
+  }
+  close(TMPL);
 }
 
 # Collect all used errors from cgi/pm files
 foreach my $file (keys %test_modules) {
-    $file =~ s/\s.*$//; # nuke everything after the first space (#comment)
-    next if (!$file); # skip null entries
-    if (! open (TMPL, $file)) {
-        Register(\%test_modules, $file, "could not open file --WARNING");
-        next;
+  $file =~ s/\s.*$//;    # nuke everything after the first space (#comment)
+  next if (!$file);      # skip null entries
+  if (!open(TMPL, $file)) {
+    Register(\%test_modules, $file, "could not open file --WARNING");
+    next;
+  }
+
+  my $lineno = 0;
+  while (my $line = <TMPL>) {
+    last if $line =~ /^__END__/;    # skip the POD (at least in
+                                    # Bugzilla/Error.pm)
+    $lineno++;
+    if ($line
+      =~ /^[^#]*\b(Throw(Code|User)Error|(user_)?error\s+=>)\s*\(?\s*["'](.*?)['"]/)
+    {
+      my $errtype;
+
+      # If it's a normal ThrowCode/UserError
+      if ($2) {
+        $errtype = lc($2);
+      }
+
+      # If it's an AUTH_ERROR tag
+      else {
+        $errtype = $3 ? 'user' : 'code';
+      }
+      my $errtag = $4;
+      push @{$Errors{$errtype}{$errtag}{used_in}{$file}}, $lineno;
     }
+  }
 
-    my $lineno = 0;
-    while (my $line = <TMPL>) {
-        last if $line =~ /^__END__/; # skip the POD (at least in
-                                        # Bugzilla/Error.pm)
-        $lineno++;
-        if ($line =~
-/^[^#]*\b(Throw(Code|User)Error|(user_)?error\s+=>)\s*\(?\s*["'](.*?)['"]/) {
-            my $errtype;
-            # If it's a normal ThrowCode/UserError
-            if ($2) {
-                $errtype = lc($2);
-            }
-            # If it's an AUTH_ERROR tag
-            else {
-                $errtype = $3 ? 'user' : 'code';
-            }
-            my $errtag = $4;
-            push @{$Errors{$errtype}{$errtag}{used_in}{$file}}, $lineno;
-        }
-    }
-    
-    close(TMPL);
+  close(TMPL);
 }
 
 # Now let us start the checks
 
 foreach my $errtype (keys %Errors) {
-    foreach my $errtag (keys %{$Errors{$errtype}}) {
-        # Check for undefined tags
-        if (!defined $Errors{$errtype}{$errtag}{defined_in}) {
-            UsedIn($errtype, $errtag, "any");
+  foreach my $errtag (keys %{$Errors{$errtype}}) {
+
+    # Check for undefined tags
+    if (!defined $Errors{$errtype}{$errtag}{defined_in}) {
+      UsedIn($errtype, $errtag, "any");
+    }
+    else {
+      # Check for all languages!!!
+      my @langs = ();
+      foreach my $lang (@languages) {
+        if (!defined $Errors{$errtype}{$errtag}{defined_in}{$lang}) {
+          push @langs, $lang;
         }
-        else {
-            # Check for all languages!!!
-            my @langs = ();
-            foreach my $lang (@languages) {
-                if (!defined $Errors{$errtype}{$errtag}{defined_in}{$lang}) {
-                    push @langs, $lang;
-                }
-            }
-            if (scalar @langs) {
-                UsedIn($errtype, $errtag, join(', ',@langs));
-            }
-            
-            # Now check for tag usage in all DEFINED languages
-            foreach my $lang (keys %{$Errors{$errtype}{$errtag}{defined_in}}) {
-                if (!defined $Errors{$errtype}{$errtag}{used_in}) {
-                    DefinedIn($errtype, $errtag, $lang);
-                }
-            }
+      }
+      if (scalar @langs) {
+        UsedIn($errtype, $errtag, join(', ', @langs));
+      }
+
+      # Now check for tag usage in all DEFINED languages
+      foreach my $lang (keys %{$Errors{$errtype}{$errtag}{defined_in}}) {
+        if (!defined $Errors{$errtype}{$errtag}{used_in}) {
+          DefinedIn($errtype, $errtag, $lang);
         }
+      }
     }
+  }
 }
 
 # And make sure that everything defined in WS_ERROR_CODE
 # is actually a valid error.
 foreach my $err_name (keys %{WS_ERROR_CODE()}) {
-    if (!defined $Errors{'code'}{$err_name} 
-        && !defined $Errors{'user'}{$err_name})
-    {
-        Register(\%test_modules, 'WS_ERROR_CODE',
-            "Error tag '$err_name' is used in WS_ERROR_CODE in"
-            . " Bugzilla/WebService/Constants.pm"
-            . " but not defined in any template, and not used in any code.");
-    }
+  if (!defined $Errors{'code'}{$err_name} && !defined $Errors{'user'}{$err_name})
+  {
+    Register(\%test_modules, 'WS_ERROR_CODE',
+          "Error tag '$err_name' is used in WS_ERROR_CODE in"
+        . " Bugzilla/WebService/Constants.pm"
+        . " but not defined in any template, and not used in any code.");
+  }
 }
 
 # Now report modules results
 foreach my $file (sort keys %test_modules) {
-    Report($file, @{$test_modules{$file}});
+  Report($file, @{$test_modules{$file}});
 }
 
 # And report WS_ERROR_CODE results
@@ -182,56 +183,67 @@ Report('WS_ERROR_CODE', @{$test_modules{'WS_ERROR_CODE'}});
 
 # Now report templates results
 foreach my $file (sort keys %test_templates) {
-    Report($file, @{$test_templates{$file}});
+  Report($file, @{$test_templates{$file}});
 }
 
 sub Register {
-    my ($hash, $file, $message, $warning) = @_;
-    # If set to 1, $warning will avoid the test to fail.
-    $warning ||= 0;
-    push(@{$hash->{$file}}, {'message' => $message, 'warning' => $warning});
+  my ($hash, $file, $message, $warning) = @_;
+
+  # If set to 1, $warning will avoid the test to fail.
+  $warning ||= 0;
+  push(@{$hash->{$file}}, {'message' => $message, 'warning' => $warning});
 }
 
 sub Report {
-    my ($file, @errors) = @_;
-    if (scalar @errors) {
-        # Do we only have warnings to report or also real errors?
-        my @real_errors = grep {$_->{'warning'} == 0} @errors;
-        # Extract error messages.
-        @errors = map {$_->{'message'}} @errors;
-        if (scalar(@real_errors)) {
-            ok(0, "$file has ". scalar(@errors) ." error(s):\n" . join("\n", @errors));
-        }
-        else {
-            ok(1, "--WARNING $file has " . scalar(@errors) .
-                  " unused error tag(s):\n" . join("\n", @errors));
-        }
+  my ($file, @errors) = @_;
+  if (scalar @errors) {
+
+    # Do we only have warnings to report or also real errors?
+    my @real_errors = grep { $_->{'warning'} == 0 } @errors;
+
+    # Extract error messages.
+    @errors = map { $_->{'message'} } @errors;
+    if (scalar(@real_errors)) {
+      ok(0, "$file has " . scalar(@errors) . " error(s):\n" . join("\n", @errors));
     }
     else {
-        # This is used for both code and template files, so let's use
-        # file-independent phrase
-        ok(1, "$file uses error tags correctly");
+      ok(1,
+            "--WARNING $file has "
+          . scalar(@errors)
+          . " unused error tag(s):\n"
+          . join("\n", @errors));
     }
+  }
+  else {
+    # This is used for both code and template files, so let's use
+    # file-independent phrase
+    ok(1, "$file uses error tags correctly");
+  }
 }
 
 sub UsedIn {
-    my ($errtype, $errtag, $lang) = @_;
-    $lang = $lang || "any";
-    foreach my $file (keys %{$Errors{$errtype}{$errtag}{used_in}}) {
-        Register(\%test_modules, $file, 
-            "$errtype error tag '$errtag' is used at line(s) (" 
-            . join (',', @{$Errors{$errtype}{$errtag}{used_in}{$file}}) 
-            . ") but not defined for language(s): $lang");
-    }
+  my ($errtype, $errtag, $lang) = @_;
+  $lang = $lang || "any";
+  foreach my $file (keys %{$Errors{$errtype}{$errtag}{used_in}}) {
+    Register(\%test_modules, $file,
+          "$errtype error tag '$errtag' is used at line(s) ("
+        . join(',', @{$Errors{$errtype}{$errtag}{used_in}{$file}})
+        . ") but not defined for language(s): $lang");
+  }
 }
+
 sub DefinedIn {
-    my ($errtype, $errtag, $lang) = @_;
-    foreach my $file (keys %{$Errors{$errtype}{$errtag}{defined_in}{$lang}}) {
-        Register(\%test_templates, $file, 
-            "$errtype error tag '$errtag' is defined at line(s) ("
-            . join (',', @{$Errors{$errtype}{$errtag}{defined_in}{$lang}{$file}}) 
-            . ") but is not used anywhere", 1);
-    }
+  my ($errtype, $errtag, $lang) = @_;
+  foreach my $file (keys %{$Errors{$errtype}{$errtag}{defined_in}{$lang}}) {
+    Register(
+      \%test_templates,
+      $file,
+      "$errtype error tag '$errtag' is defined at line(s) ("
+        . join(',', @{$Errors{$errtype}{$errtag}{defined_in}{$lang}{$file}})
+        . ") but is not used anywhere",
+      1
+    );
+  }
 }
 
 exit 0;
index 9770593aa2a29839447a02b09430a199ed28d624..4e18b749b0aed25882c4bf28208412c519e53f15 100644 (file)
@@ -23,30 +23,30 @@ use Bugzilla::DB::Schema;
 
 # SQL reserved words
 use constant RESERVED_WORDS => qw(
-    ABSOLUTE ACTION ACTOR ADD AFTER ALL ALLOCATE ALTER ANY AND ARE AS ASC ASSERTION ASYNC AT
-    ATTRIBUTES BEFORE BEGIN BETWEEN BIT BIT_LENGTH BOOLEAN BOTH BREADTH BY CALL CASCADE
-    CASCADED CASE CAST CATALOG CHAR CHARACTER_LENGTH CHAR_LENGTH COLLATE
-    COLLATION COLUMN COMPLETION CONNECT CONNECTION CONSTRAINT CONSTRAINTS
-    CONVERT CORRESPONDING CREATE CROSS CURRENT_DATE CURRENT_PATH CURRENT_TIME
-    CURRENT_TIMESTAMP CURRENT_USER CYCLE DATA DATE DAY DEALLOCATE DECLARE DEFAULT DEFERRABLE
-    DEFERRED DELETE DEPTH DESC DESCRIBE DESCRIPTOR DESTROY DIAGNOSTICS DICTIONARY
-    DISCONNECT DISTINCT DO DOMAIN DROP EACH ELEMENT ELSE ELSEIF END END-EXEC EQUALS EXCEPT
-    EXCEPTION EXECUTE EXTERNAL EXTRACT FACTOR FALSE FIRST FOR FROM FULL GENERAL GET
-    GLOBAL GRANT GROUP HAVING HOLD HOUR IDENTITY IF IGNORE IMMEDIATE IN INITIALLY INNER INPUT
-    INSENSITIVE INSERT INSTEAD INTERSECT INTERVAL IS ISOLATION JOIN LAST LEADING LEAVE
-    LEFT LESS LEVEL LIMIT LIST LOCAL LOOP LOWER MATCH MINUTE MODIFY MONTH NAMES
-    NATIONAL NATURAL NCHAR NEW NEW_TABLE NEXT NO NONE NOT NULL NULLIF OBJECT
-    OCTET_LENGTH OFF OID OLD OLD_TABLE ONLY OPERATION OPERATOR OPERATORS OR ORDER OTHERS
-    OUTER OUTPUT OVERLAPS PAD PARAMETERS PARTIAL PATH PENDANT POSITION POSTFIX
-    PREFIX PREORDER PREPARE PRESERVE PRIOR PRIVATE PROTECTED READ RECURSIVE REF
-    REFERENCING RELATIVE REPLACE RESIGNAL RESTRICT RETURN RETURNS REVOKE RIGHT
-    ROLE ROUTINE ROW ROWS SAVEPOINT SCROLL SEARCH SECOND SELECT SENSITIVE SEQUENCE
-    SESSION SESSION_USER SIGNAL SIMILAR SIZE SPACE SQLEXCEPTION SQLSTATE
-    SQLWARNING START STATE STRUCTURE SUBSTRING SYMBOL SYSTEM_USER TABLE TEMPORARY
-    TERM TEST THEN THERE TIME TIMESTAMP TIMEZONE_HOUR TIMEZONE_MINUTE TRAILING
-    TRANSACTION TRANSLATE TRANSLATION TRIGGER TRIM TRUE TUPLE UNDER
-    UNKNOWN UNION UNIQUE UPDATE UPPER USAGE USING VARCHAR VARIABLE VARYING VIEW VIRTUAL VISIBLE
-    WAIT WHEN WHERE WHILE WITH WITHOUT WRITE YEAR ZONE
+  ABSOLUTE ACTION ACTOR ADD AFTER ALL ALLOCATE ALTER ANY AND ARE AS ASC ASSERTION ASYNC AT
+  ATTRIBUTES BEFORE BEGIN BETWEEN BIT BIT_LENGTH BOOLEAN BOTH BREADTH BY CALL CASCADE
+  CASCADED CASE CAST CATALOG CHAR CHARACTER_LENGTH CHAR_LENGTH COLLATE
+  COLLATION COLUMN COMPLETION CONNECT CONNECTION CONSTRAINT CONSTRAINTS
+  CONVERT CORRESPONDING CREATE CROSS CURRENT_DATE CURRENT_PATH CURRENT_TIME
+  CURRENT_TIMESTAMP CURRENT_USER CYCLE DATA DATE DAY DEALLOCATE DECLARE DEFAULT DEFERRABLE
+  DEFERRED DELETE DEPTH DESC DESCRIBE DESCRIPTOR DESTROY DIAGNOSTICS DICTIONARY
+  DISCONNECT DISTINCT DO DOMAIN DROP EACH ELEMENT ELSE ELSEIF END END-EXEC EQUALS EXCEPT
+  EXCEPTION EXECUTE EXTERNAL EXTRACT FACTOR FALSE FIRST FOR FROM FULL GENERAL GET
+  GLOBAL GRANT GROUP HAVING HOLD HOUR IDENTITY IF IGNORE IMMEDIATE IN INITIALLY INNER INPUT
+  INSENSITIVE INSERT INSTEAD INTERSECT INTERVAL IS ISOLATION JOIN LAST LEADING LEAVE
+  LEFT LESS LEVEL LIMIT LIST LOCAL LOOP LOWER MATCH MINUTE MODIFY MONTH NAMES
+  NATIONAL NATURAL NCHAR NEW NEW_TABLE NEXT NO NONE NOT NULL NULLIF OBJECT
+  OCTET_LENGTH OFF OID OLD OLD_TABLE ONLY OPERATION OPERATOR OPERATORS OR ORDER OTHERS
+  OUTER OUTPUT OVERLAPS PAD PARAMETERS PARTIAL PATH PENDANT POSITION POSTFIX
+  PREFIX PREORDER PREPARE PRESERVE PRIOR PRIVATE PROTECTED READ RECURSIVE REF
+  REFERENCING RELATIVE REPLACE RESIGNAL RESTRICT RETURN RETURNS REVOKE RIGHT
+  ROLE ROUTINE ROW ROWS SAVEPOINT SCROLL SEARCH SECOND SELECT SENSITIVE SEQUENCE
+  SESSION SESSION_USER SIGNAL SIMILAR SIZE SPACE SQLEXCEPTION SQLSTATE
+  SQLWARNING START STATE STRUCTURE SUBSTRING SYMBOL SYSTEM_USER TABLE TEMPORARY
+  TERM TEST THEN THERE TIME TIMESTAMP TIMEZONE_HOUR TIMEZONE_MINUTE TRAILING
+  TRANSACTION TRANSLATE TRANSLATION TRIGGER TRIM TRUE TUPLE UNDER
+  UNKNOWN UNION UNIQUE UPDATE UPPER USAGE USING VARCHAR VARIABLE VARYING VIEW VIRTUAL VISIBLE
+  WAIT WHEN WHERE WHILE WITH WITHOUT WRITE YEAR ZONE
 );
 
 # Few Exceptions are removed from the above list
@@ -57,31 +57,31 @@ our $schema;
 our @tables;
 
 BEGIN {
-    $schema = Bugzilla::DB::Schema->new("Mysql");
-    @tables = $schema->get_table_list();
+  $schema = Bugzilla::DB::Schema->new("Mysql");
+  @tables = $schema->get_table_list();
 }
 
 use Test::More tests => scalar(@tables);
 
 foreach my $table (@tables) {
-    my @reserved;
+  my @reserved;
 
-    if (grep { uc($table) eq $_ } RESERVED_WORDS) {
-        push(@reserved, $table);
-    }
+  if (grep { uc($table) eq $_ } RESERVED_WORDS) {
+    push(@reserved, $table);
+  }
 
-    foreach my $column ($schema->get_table_columns($table)) {
-        if (grep { uc($column) eq $_ } RESERVED_WORDS) {
-            push(@reserved, $column);
-        }
+  foreach my $column ($schema->get_table_columns($table)) {
+    if (grep { uc($column) eq $_ } RESERVED_WORDS) {
+      push(@reserved, $column);
     }
+  }
 
-    if (scalar @reserved) {
-        ok(0, "Table $table use reserved words: " . join(", ", @reserved));
-    }
-    else {
-        ok(1, "Table $table does not use reserved words");
-    }
+  if (scalar @reserved) {
+    ok(0, "Table $table use reserved words: " . join(", ", @reserved));
+  }
+  else {
+    ok(1, "Table $table does not use reserved words");
+  }
 }
 
 exit 0;
index fb3bc279c46803e813e732fabe433f8fd306e965..da97772c4a2e294cae3998900b29b65e97cc423f 100644 (file)
@@ -17,20 +17,15 @@ use Test::More;
 use File::Spec::Functions ':ALL';
 
 BEGIN {
-    # Don't run tests for installs or automated tests
-    unless ( $ENV{RELEASE_TESTING} ) {
-        plan( skip_all => "Author tests not required for installation" );
-    }
-    my $config = catfile('t', 'critic-core.ini');
-    unless ( eval "use Test::Perl::Critic -profile => '$config'; 1" ) {
-        plan skip_all => 'Test::Perl::Critic required to criticise code';
-    }
+  # Don't run tests for installs or automated tests
+  unless ($ENV{RELEASE_TESTING}) {
+    plan(skip_all => "Author tests not required for installation");
+  }
+  my $config = catfile('t', 'critic-core.ini');
+  unless (eval "use Test::Perl::Critic -profile => '$config'; 1") {
+    plan skip_all => 'Test::Perl::Critic required to criticise code';
+  }
 }
 
 # need to skip t/
-all_critic_ok(
-    'Bugzilla.pm',
-    'Bugzilla/',
-    glob("*.cgi"),
-    glob("*.pl"),
-);
+all_critic_ok('Bugzilla.pm', 'Bugzilla/', glob("*.cgi"), glob("*.pl"),);
index a35b887c78137bc414aa00ef799a2bdd85a9872e..6650258aeb2e16ff658351a3c4239a36f8d060be 100644 (file)
@@ -18,67 +18,67 @@ use Bugzilla::Util qw(remote_ip);
 my $params = Bugzilla->params;
 
 {
-    local $params->{inbound_proxies} = '10.0.0.1,10.0.0.2';
-    local $ENV{REMOTE_ADDR} = '10.0.0.2';
-    local $ENV{HTTP_X_FORWARDED_FOR} = '10.42.42.42';
+  local $params->{inbound_proxies} = '10.0.0.1,10.0.0.2';
+  local $ENV{REMOTE_ADDR}          = '10.0.0.2';
+  local $ENV{HTTP_X_FORWARDED_FOR} = '10.42.42.42';
 
-    is(remote_ip(), '10.42.42.42', "from proxy 2");
+  is(remote_ip(), '10.42.42.42', "from proxy 2");
 }
 
 {
-    local $params->{inbound_proxies} = '10.0.0.1,10.0.0.2';
-    local $ENV{REMOTE_ADDR} = '10.0.0.1';
-    local $ENV{HTTP_X_FORWARDED_FOR} = '10.42.42.42';
+  local $params->{inbound_proxies} = '10.0.0.1,10.0.0.2';
+  local $ENV{REMOTE_ADDR}          = '10.0.0.1';
+  local $ENV{HTTP_X_FORWARDED_FOR} = '10.42.42.42';
 
-    is(remote_ip(), '10.42.42.42', "from proxy 1");
+  is(remote_ip(), '10.42.42.42', "from proxy 1");
 }
 
 {
-    local $params->{inbound_proxies} = '10.0.0.1,10.0.0.2';
-    local $ENV{REMOTE_ADDR} = '10.0.0.3';
-    local $ENV{HTTP_X_FORWARDED_FOR} = '10.42.42.42';
+  local $params->{inbound_proxies} = '10.0.0.1,10.0.0.2';
+  local $ENV{REMOTE_ADDR}          = '10.0.0.3';
+  local $ENV{HTTP_X_FORWARDED_FOR} = '10.42.42.42';
 
-    is(remote_ip(), '10.0.0.3', "not a proxy");
+  is(remote_ip(), '10.0.0.3', "not a proxy");
 }
 
 {
-    local $params->{inbound_proxies} = '*';
-    local $ENV{REMOTE_ADDR} = '10.0.0.3';
-    local $ENV{HTTP_X_FORWARDED_FOR} = '10.42.42.42,1.4.9.2';
+  local $params->{inbound_proxies} = '*';
+  local $ENV{REMOTE_ADDR}          = '10.0.0.3';
+  local $ENV{HTTP_X_FORWARDED_FOR} = '10.42.42.42,1.4.9.2';
 
-    is(remote_ip(), '10.42.42.42', "always proxy");
+  is(remote_ip(), '10.42.42.42', "always proxy");
 }
 
 {
-    local $params->{inbound_proxies} = '';
-    local $ENV{REMOTE_ADDR} = '10.9.8.7';
-    local $ENV{HTTP_X_FORWARDED_FOR} = '10.42.42.42,1.4.9.2';
+  local $params->{inbound_proxies} = '';
+  local $ENV{REMOTE_ADDR}          = '10.9.8.7';
+  local $ENV{HTTP_X_FORWARDED_FOR} = '10.42.42.42,1.4.9.2';
 
-    is(remote_ip(), '10.9.8.7', "never proxy");
+  is(remote_ip(), '10.9.8.7', "never proxy");
 }
 
 
 {
-    local $params->{inbound_proxies} = '10.0.0.1,2600:cafe::cafe:ffff:bf42:4998';
-    local $ENV{REMOTE_ADDR} = '2600:cafe::cafe:ffff:bf42:4998';
-    local $ENV{HTTP_X_FORWARDED_FOR} = '2600:cafe::cafe:ffff:bf42:BEEF';
+  local $params->{inbound_proxies} = '10.0.0.1,2600:cafe::cafe:ffff:bf42:4998';
+  local $ENV{REMOTE_ADDR}          = '2600:cafe::cafe:ffff:bf42:4998';
+  local $ENV{HTTP_X_FORWARDED_FOR} = '2600:cafe::cafe:ffff:bf42:BEEF';
 
-    is(remote_ip(), '2600:cafe::cafe:ffff:bf42:BEEF', "from proxy ipv6");
+  is(remote_ip(), '2600:cafe::cafe:ffff:bf42:BEEF', "from proxy ipv6");
 }
 
 {
-    local $params->{inbound_proxies} = '10.0.0.1,2600:cafe::cafe:ffff:bf42:4998';
-    local $ENV{REMOTE_ADDR} = '2600:cafe::cafe:ffff:bf42:DEAD';
-    local $ENV{HTTP_X_FORWARDED_FOR} = '2600:cafe::cafe:ffff:bf42:BEEF';
+  local $params->{inbound_proxies} = '10.0.0.1,2600:cafe::cafe:ffff:bf42:4998';
+  local $ENV{REMOTE_ADDR}          = '2600:cafe::cafe:ffff:bf42:DEAD';
+  local $ENV{HTTP_X_FORWARDED_FOR} = '2600:cafe::cafe:ffff:bf42:BEEF';
 
-    is(remote_ip(), '2600:cafe::cafe:ffff:bf42:DEAD', "invalid proxy ipv6");
+  is(remote_ip(), '2600:cafe::cafe:ffff:bf42:DEAD', "invalid proxy ipv6");
 }
 
 
 {
-    local $params->{inbound_proxies} = '*';
-    local $ENV{REMOTE_ADDR} = '2600:cafe::cafe:ffff:bf42:DEAD';
-    local $ENV{HTTP_X_FORWARDED_FOR} = '';
+  local $params->{inbound_proxies} = '*';
+  local $ENV{REMOTE_ADDR}          = '2600:cafe::cafe:ffff:bf42:DEAD';
+  local $ENV{HTTP_X_FORWARDED_FOR} = '';
 
-    is(remote_ip(), '2600:cafe::cafe:ffff:bf42:DEAD', "always proxy ipv6");
+  is(remote_ip(), '2600:cafe::cafe:ffff:bf42:DEAD', "always proxy ipv6");
 }
index 427522e25720073f4e7ae1097735856339fe8d4c..11bb890f43d2c36f69b6cac9978cab861fe8b8b9 100644 (file)
@@ -21,103 +21,110 @@ use Support::Templates;
 use List::MoreUtils qw(any);
 use Text::Balanced qw(gen_extract_tagged extract_multiple);
 
-unless ( $ENV{CSP_TESTING} ) {
-    plan( skip_all => "CSP tests not yet default" );
-    exit;
+unless ($ENV{CSP_TESTING}) {
+  plan(skip_all => "CSP tests not yet default");
+  exit;
 }
 
 my @event_attrs = qw(
-    onabort onafterprint onbeforeprint onbeforeunload onblur oncanplay oncanplaythrough onchange onclick oncontextmenu
-    oncopy oncuechange oncut ondblclick ondrag ondragend ondragenter ondragleave ondragover ondragstart ondrop
-    ondurationchange onemptied onended onerror onfocus onhashchange oninput oninvalid onkeydown onkeypress onkeyup onload
-    onloadeddata onloadedmetadata onloadstart onmessage onmousedown onmousemove onmouseout onmouseover onmouseup
-    onmousewheel onoffline ononline onpagehide onpageshow onpaste onpause onplay onplaying onpopstate onprogress
-    onratechange onreset onresize onscroll onsearch onseeked onseeking onselect onshow onstalled onstorage onsubmit
-    onsuspend ontimeupdate ontoggle onunload onvolumechange onwaiting onwheel
+  onabort onafterprint onbeforeprint onbeforeunload onblur oncanplay oncanplaythrough onchange onclick oncontextmenu
+  oncopy oncuechange oncut ondblclick ondrag ondragend ondragenter ondragleave ondragover ondragstart ondrop
+  ondurationchange onemptied onended onerror onfocus onhashchange oninput oninvalid onkeydown onkeypress onkeyup onload
+  onloadeddata onloadedmetadata onloadstart onmessage onmousedown onmousemove onmouseout onmouseover onmouseup
+  onmousewheel onoffline ononline onpagehide onpageshow onpaste onpause onplay onplaying onpopstate onprogress
+  onratechange onreset onresize onscroll onsearch onseeked onseeking onselect onshow onstalled onstorage onsubmit
+  onsuspend ontimeupdate ontoggle onunload onvolumechange onwaiting onwheel
 );
 
 my %score;
+
 sub wanted {
-    my $name = $File::Find::name;
-
-    return unless /\.html\.tmpl$/;
-    return unless -f $name;
-    open my $fh, '<', $name or return;
-    my $data = do { local $/ = undef; scalar <$fh> };
-    close $fh;
-
-    my $tt_parser = gen_extract_tagged( "\\[%", "%\\]", undef, { bad => ["\\[%"] } );
-    my @tt_matches = extract_multiple($data, [ $tt_parser ]);
-
-    my $found_tt_javascript = 0;
-    my $found_tt_onload = 0;
-    my $found_script_content = 0;
-    my @found_event_attr;
-    foreach my $match (@tt_matches) {
-        if ($match =~ /^\[%/) {
-            if ($match =~ /javascript\s+=\s+/) {
-                $found_tt_javascript = 1;
-                $score{$name}++;
-            }
-            elsif ($match =~ /onload\s*=/) {
-                $found_tt_onload = 1;
-                $score{$name}++;
-            }
+  my $name = $File::Find::name;
+
+  return unless /\.html\.tmpl$/;
+  return unless -f $name;
+  open my $fh, '<', $name or return;
+  my $data = do { local $/ = undef; scalar <$fh> };
+  close $fh;
+
+  my $tt_parser = gen_extract_tagged("\\[%", "%\\]", undef, {bad => ["\\[%"]});
+  my @tt_matches = extract_multiple($data, [$tt_parser]);
+
+  my $found_tt_javascript  = 0;
+  my $found_tt_onload      = 0;
+  my $found_script_content = 0;
+  my @found_event_attr;
+  foreach my $match (@tt_matches) {
+    if ($match =~ /^\[%/) {
+      if ($match =~ /javascript\s+=\s+/) {
+        $found_tt_javascript = 1;
+        $score{$name}++;
+      }
+      elsif ($match =~ /onload\s*=/) {
+        $found_tt_onload = 1;
+        $score{$name}++;
+      }
+    }
+    else {
+      foreach my $event_attr (@event_attrs) {
+        if ($match =~ /\Q$event_attr\E\s*=\s*['"]/s) {
+          push @found_event_attr, $event_attr;
+          $score{$name}++;
         }
-        else {
-            foreach my $event_attr (@event_attrs) {
-                if ($match =~ /\Q$event_attr\E\s*=\s*['"]/s) {
-                    push @found_event_attr, $event_attr;
-                    $score{$name}++;
-                }
-            }
-            my $tag_parser = gen_extract_tagged();
-            while (my @tag = $tag_parser->($match)) {
-                last unless defined $tag[0];
-
-                if ($tag[3] && $tag[3] =~ /<script/) {
-                    if ($tag[4]) {
-                        $score{$name}++;
-                        $found_script_content = 1;
-                    }
-                }
-
-            }
+      }
+      my $tag_parser = gen_extract_tagged();
+      while (my @tag = $tag_parser->($match)) {
+        last unless defined $tag[0];
+
+        if ($tag[3] && $tag[3] =~ /<script/) {
+          if ($tag[4]) {
+            $score{$name}++;
+            $found_script_content = 1;
+          }
         }
-    }
 
-    my $found_javascript_link = $data =~ /javascript:\S/;
-    $score{$name}++ if $found_javascript_link;
-
-    my $found_problems = $found_tt_javascript || $found_tt_onload || @found_event_attr || $found_script_content || $found_javascript_link;
-    ok(!$found_problems, "checking $name");
-    if ($found_problems) {
-        my $msg = "problems:\n";
-        $msg .= "  found javascript tt var\n" if $found_tt_javascript;
-        $msg .= "  found onload tt var\n" if $found_tt_onload;
-        $msg .= "  found event attributes: " . join(", ", @found_event_attr) . "\n" if @found_event_attr;
-        $msg .= "  found script content\n" if $found_script_content;
-        $msg .= "  found javascript: link\n" if $found_javascript_link;
-        diag $msg;
+      }
     }
+  }
+
+  my $found_javascript_link = $data =~ /javascript:\S/;
+  $score{$name}++ if $found_javascript_link;
+
+  my $found_problems
+    = $found_tt_javascript
+    || $found_tt_onload
+    || @found_event_attr
+    || $found_script_content
+    || $found_javascript_link;
+  ok(!$found_problems, "checking $name");
+  if ($found_problems) {
+    my $msg = "problems:\n";
+    $msg .= "  found javascript tt var\n" if $found_tt_javascript;
+    $msg .= "  found onload tt var\n"     if $found_tt_onload;
+    $msg .= "  found event attributes: " . join(", ", @found_event_attr) . "\n"
+      if @found_event_attr;
+    $msg .= "  found script content\n"   if $found_script_content;
+    $msg .= "  found javascript: link\n" if $found_javascript_link;
+    diag $msg;
+  }
 }
 
 sub check_for_javascript {
-    my ($block) = @_;
-    diag $block;
-    return '';
+  my ($block) = @_;
+  diag $block;
+  return '';
 }
 
 diag @include_paths;
-find({ no_chdir => 1, wanted => \&wanted }, @include_paths);
+find({no_chdir => 1, wanted => \&wanted}, @include_paths);
 
 # print out a json file so we can see how bad we're doing.
 if (my $score_file = $ENV{CSP_SCORE_FILE}) {
-    require JSON::XS;
-    diag "writing scores to $score_file";
-    open my $score_fh, '>', $score_file;
-    print $score_fh JSON::XS->new->pretty->canonical(1)->encode(\%score);
-    close $score_fh;
+  require JSON::XS;
+  diag "writing scores to $score_file";
+  open my $score_fh, '>', $score_file;
+  print $score_fh JSON::XS->new->pretty->canonical(1)->encode(\%score);
+  close $score_fh;
 }
 
 done_testing();
index 217912bf90dbd5d393e883923c657ab431001b71..8fd6c9260335c978bb62b41db6500ef25d08a3cd 100644 (file)
@@ -15,30 +15,32 @@ use lib qw(. lib local/lib/perl5 t);
 use Test2::Tools::Mock;
 use Test::More;
 use Bugzilla::Util;
+
 BEGIN {
-    my $terms = {
-        "bug"               => "bug",
-        "Bug"               => "Bug",
-        "abug"              => "a bug",
-        "Abug"              => "A bug",
-        "aBug"              => "a Bug",
-        "ABug"              => "A Bug",
-        "bugs"              => "bugs",
-        "Bugs"              => "Bugs",
-        "comment"           => "comment",
-        "comments"          => "comments",
-        "zeroSearchResults" => "Zarro Boogs found",
-        "Bugzilla"          => "Bugzilla"
-    };
-    no warnings 'redefine', 'once';
-    *Bugzilla::Util::template_var = sub {
-        my $name = shift;
-        if ($name eq 'terms') {
-            return $terms;
-        } else {
-            die "sorry!";
-        }
-    };
+  my $terms = {
+    "bug"               => "bug",
+    "Bug"               => "Bug",
+    "abug"              => "a bug",
+    "Abug"              => "A bug",
+    "aBug"              => "a Bug",
+    "ABug"              => "A Bug",
+    "bugs"              => "bugs",
+    "Bugs"              => "Bugs",
+    "comment"           => "comment",
+    "comments"          => "comments",
+    "zeroSearchResults" => "Zarro Boogs found",
+    "Bugzilla"          => "Bugzilla"
+  };
+  no warnings 'redefine', 'once';
+  *Bugzilla::Util::template_var = sub {
+    my $name = shift;
+    if ($name eq 'terms') {
+      return $terms;
+    }
+    else {
+      die "sorry!";
+    }
+  };
 }
 use Bugzilla;
 use Bugzilla::Constants;
@@ -52,22 +54,19 @@ use File::Basename;
 Bugzilla->usage_mode(USAGE_MODE_TEST);
 Bugzilla->error_mode(ERROR_MODE_DIE);
 
-my $user_mock = mock 'Bugzilla::User' => (
-    override_constructor => ['new', 'hash'],
-);
+my $user_mock
+  = mock 'Bugzilla::User' => (override_constructor => ['new', 'hash'],);
 
-my $comment_mock = mock 'Bugzilla::Comment' => (
-    add_constructor => ['new', 'hash'],
-);
+my $comment_mock
+  = mock 'Bugzilla::Comment' => (add_constructor => ['new', 'hash'],);
 
-my $bug_mock = mock 'Bugzilla::Bug' => (
-    override_constructor => ['new', 'hash'],
-);
+my $bug_mock
+  = mock 'Bugzilla::Bug' => (override_constructor => ['new', 'hash'],);
 
 # mocked objects just take all constructor args and put them into the hash.
 my $user = Bugzilla::User->new(
-    userid => 33,
-    settings => { use_markdown => { is_enabled => 1, value => 'on' } },
+  userid   => 33,
+  settings => {use_markdown => {is_enabled => 1, value => 'on'}},
 );
 my $bug = Bugzilla::Bug->new(bug_id => 666);
 my $comment = Bugzilla::Comment->new(already_wrapped => 0);
index e4e8f1ff81c391ae63116c310f0698f2bcf0c8dc..2c7f78e0f52fe3e871d7d9040abe8fa4f2ba116e 100644 (file)
@@ -17,40 +17,40 @@ use File::Find;
 our @additional_files = ();
 
 our @files = glob('*');
-find(sub { push(@files, $File::Find::name) if $_ =~ /\.pm$/;}, qw(Bugzilla docs));
+find(sub { push(@files, $File::Find::name) if $_ =~ /\.pm$/; },
+  qw(Bugzilla docs));
 push(@files, 'extensions/create.pl', 'docs/makedocs.pl', 'cpanfile');
 
-our @extensions =
-    grep { $_ ne 'extensions/create.pl' && ! -e "$_/disabled" }
-    glob('extensions/*');
+our @extensions = grep { $_ ne 'extensions/create.pl' && !-e "$_/disabled" }
+  glob('extensions/*');
 
 foreach my $extension (@extensions) {
-    find(sub { push(@files, $File::Find::name) if $_ =~ /\.pm$/;}, $extension);
+  find(sub { push(@files, $File::Find::name) if $_ =~ /\.pm$/; }, $extension);
 }
 
 our @test_files = glob('t/*.t xt/*/*.t');
 
 sub isTestingFile {
-    my ($file) = @_;
-    my $exclude;
-
-    if ($file =~ /\.psgi$|\.cgi$|\.pl$|\.pm$/) {
-        return 1;
-    }
-    my $additional;
-    foreach $additional (@additional_files) {
-        if ($file eq $additional) { return 1; }
-    }
-    return undef;
+  my ($file) = @_;
+  my $exclude;
+
+  if ($file =~ /\.psgi$|\.cgi$|\.pl$|\.pm$/) {
+    return 1;
+  }
+  my $additional;
+  foreach $additional (@additional_files) {
+    if ($file eq $additional) { return 1; }
+  }
+  return undef;
 }
 
 our (@testitems, @module_files);
 
 foreach my $currentfile (@files) {
-    if (isTestingFile($currentfile)) {
-        push(@testitems, $currentfile);
-    }
-    push(@module_files, $currentfile) if $currentfile =~ /\.pm$/;
+  if (isTestingFile($currentfile)) {
+    push(@testitems, $currentfile);
+  }
+  push(@module_files, $currentfile) if $currentfile =~ /\.pm$/;
 }
 
 
index 2f53592a81a9c961ff91390a21cb943bda14079b..ca5b62040bac3694b4d4dae8117bb4bd31b2dac2 100644 (file)
@@ -13,9 +13,9 @@ use warnings;
 
 use lib 't';
 use parent qw(Exporter);
-@Support::Templates::EXPORT =
-    qw(@languages @include_paths @english_default_include_paths
-       @referenced_files %actual_files $num_actual_files);
+@Support::Templates::EXPORT
+  = qw(@languages @include_paths @english_default_include_paths
+  @referenced_files %actual_files $num_actual_files);
 
 use Bugzilla;
 use Bugzilla::Constants;
@@ -26,15 +26,15 @@ use File::Find;
 use File::Spec;
 
 # English default include paths
-our @english_default_include_paths =
-    (File::Spec->catdir(bz_locations()->{'templatedir'}, 'en', 'default'));
+our @english_default_include_paths
+  = (File::Spec->catdir(bz_locations()->{'templatedir'}, 'en', 'default'));
 
 # And the extensions too
 foreach my $extension (@Support::Files::extensions) {
-    my $dir = File::Spec->catdir($extension, 'template', 'en', 'default');
-    if (-e $dir) {
-        push @english_default_include_paths, $dir;
-    }
+  my $dir = File::Spec->catdir($extension, 'template', 'en', 'default');
+  if (-e $dir) {
+    push @english_default_include_paths, $dir;
+  }
 }
 
 # Files which are referenced in the cgi files
@@ -47,37 +47,39 @@ our %actual_files = ();
 our $num_actual_files = 0;
 
 # Set the template available languages and include paths
-our @languages = @{ Bugzilla->languages };
-our @include_paths = @{ template_include_path({ language => Bugzilla->languages }) };
+our @languages = @{Bugzilla->languages};
+our @include_paths
+  = @{template_include_path({language => Bugzilla->languages})};
 
 our @files;
 
 # Local subroutine used with File::Find
 sub find_templates {
-    # Prune CVS directories
-    if (-d $_ && $_ eq 'CVS') {
-        $File::Find::prune = 1;
-        return;
-    }
 
-    # Only include files ending in '.tmpl'
-    if (-f $_ && $_ =~ m/\.tmpl$/i) {
-        my $filename;
-        my $local_dir = File::Spec->abs2rel($File::Find::dir,
-                                            $File::Find::topdir);
+  # Prune CVS directories
+  if (-d $_ && $_ eq 'CVS') {
+    $File::Find::prune = 1;
+    return;
+  }
 
-        # File::Spec 3.13 and newer return "." instead of "" if both
-        # arguments of abs2rel() are identical.
-        $local_dir = "" if ($local_dir eq ".");
+  # Only include files ending in '.tmpl'
+  if (-f $_ && $_ =~ m/\.tmpl$/i) {
+    my $filename;
+    my $local_dir = File::Spec->abs2rel($File::Find::dir, $File::Find::topdir);
 
-        if ($local_dir) {
-            $filename = File::Spec->catfile($local_dir, $_);
-        } else {
-            $filename = $_;
-        }
+    # File::Spec 3.13 and newer return "." instead of "" if both
+    # arguments of abs2rel() are identical.
+    $local_dir = "" if ($local_dir eq ".");
 
-        push(@files, $filename);
+    if ($local_dir) {
+      $filename = File::Spec->catfile($local_dir, $_);
     }
+    else {
+      $filename = $_;
+    }
+
+    push(@files, $filename);
+  }
 }
 
 # Scan the given template include path for templates
@@ -90,7 +92,7 @@ sub find_actual_files {
 
 
 foreach my $include_path (@include_paths) {
-  $actual_files{$include_path} = [ find_actual_files($include_path) ];
+  $actual_files{$include_path} = [find_actual_files($include_path)];
   $num_actual_files += scalar(@{$actual_files{$include_path}});
 }
 
@@ -99,20 +101,21 @@ foreach my $include_path (@include_paths) {
 my %seen;
 
 foreach my $file (@Support::Files::testitems) {
-    open (FILE, $file);
-    my @lines = <FILE>;
-    close (FILE);
-    foreach my $line (@lines) {
-        if ($line =~ m/template->process\(\"(.+?)\", .+?\)/) {
-            my $template = $1;
-            # Ignore templates with $ in the name, since they're
-            # probably vars, not real files
-            next if $template =~ m/\$/;
-            next if $seen{$template};
-            push (@referenced_files, $template);
-            $seen{$template} = 1;
-        }
+  open(FILE, $file);
+  my @lines = <FILE>;
+  close(FILE);
+  foreach my $line (@lines) {
+    if ($line =~ m/template->process\(\"(.+?)\", .+?\)/) {
+      my $template = $1;
+
+      # Ignore templates with $ in the name, since they're
+      # probably vars, not real files
+      next if $template =~ m/\$/;
+      next if $seen{$template};
+      push(@referenced_files, $template);
+      $seen{$template} = 1;
     }
+  }
 }
 
 1;
index 69978a7223683fef9cd82183937f06bdbc7dde22..67eea4cca3d294a20ee1d88ecb10db4687f11639 100644 (file)
 
 %::safe = (
 
-'whine/schedule.html.tmpl' => [
-  'event.key',
-  'query.id',
-  'query.sort',
-  'schedule.id',
-  'option.0',
-  'option.1',
-],
-
-'whine/mail.html.tmpl' => [
-  'bug.bug_id',
-],
-
-'flag/list.html.tmpl' => [
-  'flag.status', 
-  'type.id', 
-],
-
-'search/form.html.tmpl' => [
-  'qv.name',
-  'qv.description',
-],
-
-'search/search-specific.html.tmpl' => [
-  'status.name',
-],
-
-'search/tabs.html.tmpl' => [
-  'content',
-],
-
-'request/queue.html.tmpl' => [
-  'column_headers.$group_field', 
-  'column_headers.$column', 
-  'request.status', 
-  'request.bug_id', 
-  'request.attach_id', 
-],
-
-'reports/keywords.html.tmpl' => [
-  'keyword.bug_count', 
-],
-
-'reports/report-table.csv.tmpl' => [
-  'data.$tbl.$col.$row',
-  'colsepchar',
-],
-
-'reports/report-table.html.tmpl' => [
-  '"&amp;$col_vals" IF col_vals', 
-  '"&amp;$row_vals" IF row_vals', 
-  'urlbase', 
-  'data.$tbl.$col.$row', 
-],
-
-'reports/report.html.tmpl' => [
-  'width', 
-  'height', 
-  'imageurl', 
-  'formaturl', 
-  'other_format.name', 
-  'switchbase',
-  'cumulate',
-],
-
-'reports/chart.html.tmpl' => [
-  'width', 
-  'height', 
-  'imageurl', 
-  'sizeurl', 
-  'height + 100', 
-  'height - 100', 
-  'width + 100', 
-  'width - 100', 
-],
-
-'reports/series-common.html.tmpl' => [
-  'sel.name', 
-  '"onchange=\"$sel.onchange\"" IF sel.onchange', 
-],
-
-'reports/chart.csv.tmpl' => [
-  'data.$j.$i', 
-  'colsepchar',
-],
-
-'reports/create-chart.html.tmpl' => [
-  'series.series_id', 
-  'newidx',
-],
-
-'reports/edit-series.html.tmpl' => [
-  'default.series_id', 
-],
-
-'list/edit-multiple.html.tmpl' => [
-  'group.id', 
-  'menuname',
-],
-
-'list/list.rdf.tmpl' => [
-  'template_version', 
-  'bug.bug_id', 
-  'column', 
-],
-
-'list/table.html.tmpl' => [
-  'tableheader',
-  'bug.bug_id', 
-],
-
-'list/list.csv.tmpl' => [
-  'bug.bug_id', 
-  'colsepchar',
-],
-
-'list/list.js.tmpl' => [
-  'bug.bug_id', 
-],
-
-'global/choose-product.html.tmpl' => [
-  'target',
-],
-
-# You are not permitted to add any values here. Everything in this file should 
+  'whine/schedule.html.tmpl' => [
+    'event.key', 'query.id', 'query.sort', 'schedule.id', 'option.0', 'option.1',
+  ],
+
+  'whine/mail.html.tmpl' => ['bug.bug_id',],
+
+  'flag/list.html.tmpl' => ['flag.status', 'type.id',],
+
+  'search/form.html.tmpl' => ['qv.name', 'qv.description',],
+
+  'search/search-specific.html.tmpl' => ['status.name',],
+
+  'search/tabs.html.tmpl' => ['content',],
+
+  'request/queue.html.tmpl' => [
+    'column_headers.$group_field', 'column_headers.$column',
+    'request.status',              'request.bug_id',
+    'request.attach_id',
+  ],
+
+  'reports/keywords.html.tmpl' => ['keyword.bug_count',],
+
+  'reports/report-table.csv.tmpl' => ['data.$tbl.$col.$row', 'colsepchar',],
+
+  'reports/report-table.html.tmpl' => [
+    '"&amp;$col_vals" IF col_vals', '"&amp;$row_vals" IF row_vals',
+    'urlbase',                      'data.$tbl.$col.$row',
+  ],
+
+  'reports/report.html.tmpl' => [
+    'width',             'height',     'imageurl', 'formaturl',
+    'other_format.name', 'switchbase', 'cumulate',
+  ],
+
+  'reports/chart.html.tmpl' => [
+    'width',        'height',       'imageurl',    'sizeurl',
+    'height + 100', 'height - 100', 'width + 100', 'width - 100',
+  ],
+
+  'reports/series-common.html.tmpl' =>
+    ['sel.name', '"onchange=\"$sel.onchange\"" IF sel.onchange',],
+
+  'reports/chart.csv.tmpl' => ['data.$j.$i', 'colsepchar',],
+
+  'reports/create-chart.html.tmpl' => ['series.series_id', 'newidx',],
+
+  'reports/edit-series.html.tmpl' => ['default.series_id',],
+
+  'list/edit-multiple.html.tmpl' => ['group.id', 'menuname',],
+
+  'list/list.rdf.tmpl' => ['template_version', 'bug.bug_id', 'column',],
+
+  'list/table.html.tmpl' => ['tableheader', 'bug.bug_id',],
+
+  'list/list.csv.tmpl' => ['bug.bug_id', 'colsepchar',],
+
+  'list/list.js.tmpl' => ['bug.bug_id',],
+
+  'global/choose-product.html.tmpl' => ['target',],
+
+# You are not permitted to add any values here. Everything in this file should
 # be filtered unless there's an extremely good reason why not, in which case,
 # use the "none" dummy filter.
-'global/code-error.html.tmpl' => [
-],
-'global/header.html.tmpl' => [
-  'javascript', 
-  'style', 
-  'onload',
-  'title',
-  '" &ndash; $header" IF header',
-  'subheader',
-  'header_addl_info', 
-  'message', 
-],
-
-'global/messages.html.tmpl' => [
-  'series.frequency * 2',
-],
-
-'global/select-menu.html.tmpl' => [
-  'options', 
-  'size', 
-],
-
-'global/tabs.html.tmpl' => [
-  'content', 
-],
-
-# You are not permitted to add any values here. Everything in this file should 
+  'global/code-error.html.tmpl' => [],
+
+  'global/header.html.tmpl' => [
+    'javascript',                   'style',
+    'onload',                       'title',
+    '" &ndash; $header" IF header', 'subheader',
+    'header_addl_info',             'message',
+  ],
+
+  'global/messages.html.tmpl' => ['series.frequency * 2',],
+
+  'global/select-menu.html.tmpl' => ['options', 'size',],
+
+  'global/tabs.html.tmpl' => ['content',],
+
+# You are not permitted to add any values here. Everything in this file should
 # be filtered unless there's an extremely good reason why not, in which case,
 # use the "none" dummy filter.
-'global/user-error.html.tmpl' => [
-],
-
-'global/confirm-user-match.html.tmpl' => [
-  'script',
-],
-
-'bug/comments.html.tmpl' => [
-  'comment.id',
-  'comment.count',
-  'bug.bug_id',
-],
-
-'bug/dependency-graph.html.tmpl' => [
-  'image_map', # We need to continue to make sure this is safe in the CGI
-  'image_url', 
-  'map_url', 
-  'bug_id', 
-],
-
-'bug/dependency-tree.html.tmpl' => [
-  'bugid', 
-  'maxdepth', 
-  'hide_resolved', 
-  'ids.join(",")',
-  'maxdepth + 1', 
-  'maxdepth > 0 && maxdepth <= realdepth ? maxdepth : ""',
-  'maxdepth == 1 ? 1
+  'global/user-error.html.tmpl' => [],
+
+  'global/confirm-user-match.html.tmpl' => ['script',],
+
+  'bug/comments.html.tmpl' => ['comment.id', 'comment.count', 'bug.bug_id',],
+
+  'bug/dependency-graph.html.tmpl' => [
+    'image_map',    # We need to continue to make sure this is safe in the CGI
+    'image_url', 'map_url', 'bug_id',
+  ],
+
+  'bug/dependency-tree.html.tmpl' => [
+    'bugid', 'maxdepth', 'hide_resolved', 'ids.join(",")', 'maxdepth + 1',
+    'maxdepth > 0 && maxdepth <= realdepth ? maxdepth : ""', 'maxdepth == 1 ? 1
                        : ( maxdepth ? maxdepth - 1 : realdepth - 1 )',
-],
-
-'bug/edit.html.tmpl' => [
-  'bug.remaining_time', 
-  'bug.delta_ts', 
-  'bug.bug_id', 
-  'group.bit', 
-  'selname',
-  'inputname',
-  '" colspan=\"$colspan\"" IF colspan',
-  '" size=\"$size\"" IF size',
-  '" maxlength=\"$maxlength\"" IF maxlength',
-  '" spellcheck=\"$spellcheck\"" IF spellcheck',
-],
-
-'bug/show-multiple.html.tmpl' => [
-  'attachment.id', 
-  'flag.status',
-],
-
-'bug/show.html.tmpl' => [
-  'bug.bug_id',
-],
-
-'bug/show.xml.tmpl' => [
-  'constants.BUGZILLA_VERSION', 
-  'a.id', 
-  'field', 
-],
-
-'bug/summarize-time.html.tmpl' => [
-  'global.grand_total FILTER format("%.2f")',
-  'subtotal FILTER format("%.2f")',
-  'work_time FILTER format("%.2f")',
-  'global.total FILTER format("%.2f")',
-  'global.remaining FILTER format("%.2f")',
-  'global.estimated FILTER format("%.2f")',
-  'bugs.$id.remaining_time FILTER format("%.2f")',
-  'bugs.$id.estimated_time FILTER format("%.2f")',
-],
-
-
-'bug/time.html.tmpl' => [
-  "time_unit.replace('0\\Z', '')",
-  '(act / (act + rem)) * 100 
-       FILTER format("%d")', 
-],
-
-'bug/process/results.html.tmpl' => [
-  'title.$type.ucfirst',
-],
-
-'bug/create/create.html.tmpl' => [
-  'cloned_bug_id',
-],
-
-'bug/create/create-guided.html.tmpl' => [
-  'sel',
-],
-
-'bug/activity/table.html.tmpl' => [
-  'change.attachid', 
-],
-
-'attachment/create.html.tmpl' => [
-  'bug.bug_id',
-  'attachment.id', 
-],
-
-'attachment/created.html.tmpl' => [
-  'attachment.id',
-  'attachment.bug_id',
-],
-
-'attachment/edit.html.tmpl' => [
-  'attachment.id', 
-  'attachment.bug_id', 
-  'editable_or_hide',
-  'use_patchviewer',
-],
-
-'attachment/list.html.tmpl' => [
-  'attachment.id', 
-  'flag.status',
-  'bugid',
-  'obsolete_attachments',
-],
-
-'attachment/midair.html.tmpl' => [
-  'attachment.id',
-],
-
-'attachment/show-multiple.html.tmpl' => [
-  'a.id',
-  'flag.status'
-],
-
-'attachment/updated.html.tmpl' => [
-  'attachment.id',
-],
-
-'attachment/diff-header.html.tmpl' => [
-  'attachid',
-  'id',
-  'bugid',
-  'oldid',
-  'newid',
-  'patch.id',
-],
-
-'attachment/diff-file.html.tmpl' => [
-  'file.minus_lines',
-  'file.plus_lines',
-  'section.old_start',
-  'section_num',
-  'current_line_old',
-  'current_line_new',
-],
-
-'admin/admin.html.tmpl' => [
-  'class'
-],
-
-'admin/table.html.tmpl' => [
-  'contentlink'
-],
-
-'admin/custom_fields/cf-js.js.tmpl' => [
-  'constants.FIELD_TYPE_SINGLE_SELECT',
-  'constants.FIELD_TYPE_MULTI_SELECT',
-  'constants.FIELD_TYPE_BUG_ID',
-],
-
-'admin/params/common.html.tmpl' => [
-  'sortlist_separator', 
-],
-
-'admin/products/groupcontrol/confirm-edit.html.tmpl' => [
-  'group.count', 
-],
-
-'admin/products/groupcontrol/edit.html.tmpl' => [
-  'group.id',
-  'constants.CONTROLMAPNA', 
-  'constants.CONTROLMAPSHOWN',
-  'constants.CONTROLMAPDEFAULT',
-  'constants.CONTROLMAPMANDATORY',
-],
-
-'admin/products/list.html.tmpl' => [
-  'classification_url_part', 
-],
-
-'admin/products/footer.html.tmpl' => [
-  'classification_url_part', 
-  'classification_text', 
-],
-
-'admin/flag-type/confirm-delete.html.tmpl' => [
-  'flag_type.flag_count',
-  'flag_type.id', 
-],
-
-'admin/flag-type/edit.html.tmpl' => [
-  'selname',
-],
-
-'admin/flag-type/list.html.tmpl' => [
-  'type.id', 
-],
-
-
-'admin/components/confirm-delete.html.tmpl' => [
-  'comp.bug_count'
-],
-
-'admin/groups/delete.html.tmpl' => [
-  'shared_queries'
-],
-
-'admin/users/confirm-delete.html.tmpl' => [
-  'attachments',
-  'reporter',
-  'assignee_or_qa',
-  'cc',
-  'component_cc',
-  'flags.requestee',
-  'flags.setter',
-  'longdescs',
-  'quips',
-  'series',
-  'watch.watched',
-  'watch.watcher',
-  'whine_events',
-  'whine_schedules',
-  'otheruser.id'
-],
-
-'admin/users/edit.html.tmpl' => [
-  'otheruser.id',
-  'group.id',
-],
-
-'admin/components/edit.html.tmpl' => [
-  'comp.bug_count'
-],
-
-'admin/workflow/edit.html.tmpl' => [
-  'status.id',
-  'new_status.id',
-],
-
-'admin/workflow/comment.html.tmpl' => [
-  'status.id',
-  'new_status.id',
-],
-
-'account/auth/login-small.html.tmpl' => [
-  'qs_suffix',
-],
-
-'account/prefs/email.html.tmpl' => [
-  'relationship.id',
-  'event.id',
-  'prefname',
-],
-
-'account/prefs/prefs.html.tmpl' => [
-  'current_tab.label',
-  'current_tab.name',
-],
-
-'account/prefs/saved-searches.html.tmpl' => [
-  'group.id',
-],
-
-'config.rdf.tmpl' => [
-  'escaped_urlbase',
-  'uri_product_name',
-  'flag_type.id'
-],
+  ],
+
+  'bug/edit.html.tmpl' => [
+    'bug.remaining_time',
+    'bug.delta_ts',
+    'bug.bug_id',
+    'group.bit',
+    'selname',
+    'inputname',
+    '" colspan=\"$colspan\"" IF colspan',
+    '" size=\"$size\"" IF size',
+    '" maxlength=\"$maxlength\"" IF maxlength',
+    '" spellcheck=\"$spellcheck\"" IF spellcheck',
+  ],
+
+  'bug/show-multiple.html.tmpl' => ['attachment.id', 'flag.status',],
+
+  'bug/show.html.tmpl' => ['bug.bug_id',],
+
+  'bug/show.xml.tmpl' => ['constants.BUGZILLA_VERSION', 'a.id', 'field',],
+
+  'bug/summarize-time.html.tmpl' => [
+    'global.grand_total FILTER format("%.2f")',
+    'subtotal FILTER format("%.2f")',
+    'work_time FILTER format("%.2f")',
+    'global.total FILTER format("%.2f")',
+    'global.remaining FILTER format("%.2f")',
+    'global.estimated FILTER format("%.2f")',
+    'bugs.$id.remaining_time FILTER format("%.2f")',
+    'bugs.$id.estimated_time FILTER format("%.2f")',
+  ],
+
+
+  'bug/time.html.tmpl' => [
+    "time_unit.replace('0\\Z', '')", '(act / (act + rem)) * 100 
+       FILTER format("%d")',
+  ],
+
+  'bug/process/results.html.tmpl' => ['title.$type.ucfirst',],
+
+  'bug/create/create.html.tmpl' => ['cloned_bug_id',],
+
+  'bug/create/create-guided.html.tmpl' => ['sel',],
+
+  'bug/activity/table.html.tmpl' => ['change.attachid',],
+
+  'attachment/create.html.tmpl' => ['bug.bug_id', 'attachment.id',],
+
+  'attachment/created.html.tmpl' => ['attachment.id', 'attachment.bug_id',],
+
+  'attachment/edit.html.tmpl' => [
+    'attachment.id', 'attachment.bug_id', 'editable_or_hide', 'use_patchviewer',
+  ],
+
+  'attachment/list.html.tmpl' =>
+    ['attachment.id', 'flag.status', 'bugid', 'obsolete_attachments',],
+
+  'attachment/midair.html.tmpl' => ['attachment.id',],
+
+  'attachment/show-multiple.html.tmpl' => ['a.id', 'flag.status'],
+
+  'attachment/updated.html.tmpl' => ['attachment.id',],
+
+  'attachment/diff-header.html.tmpl' =>
+    ['attachid', 'id', 'bugid', 'oldid', 'newid', 'patch.id',],
+
+  'attachment/diff-file.html.tmpl' => [
+    'file.minus_lines',  'file.plus_lines',
+    'section.old_start', 'section_num',
+    'current_line_old',  'current_line_new',
+  ],
+
+  'admin/admin.html.tmpl' => ['class'],
+
+  'admin/table.html.tmpl' => ['contentlink'],
+
+  'admin/custom_fields/cf-js.js.tmpl' => [
+    'constants.FIELD_TYPE_SINGLE_SELECT', 'constants.FIELD_TYPE_MULTI_SELECT',
+    'constants.FIELD_TYPE_BUG_ID',
+  ],
+
+  'admin/params/common.html.tmpl' => ['sortlist_separator',],
+
+  'admin/products/groupcontrol/confirm-edit.html.tmpl' => ['group.count',],
+
+  'admin/products/groupcontrol/edit.html.tmpl' => [
+    'group.id',                  'constants.CONTROLMAPNA',
+    'constants.CONTROLMAPSHOWN', 'constants.CONTROLMAPDEFAULT',
+    'constants.CONTROLMAPMANDATORY',
+  ],
+
+  'admin/products/list.html.tmpl' => ['classification_url_part',],
+
+  'admin/products/footer.html.tmpl' =>
+    ['classification_url_part', 'classification_text',],
+
+  'admin/flag-type/confirm-delete.html.tmpl' =>
+    ['flag_type.flag_count', 'flag_type.id',],
+
+  'admin/flag-type/edit.html.tmpl' => ['selname',],
+
+  'admin/flag-type/list.html.tmpl' => ['type.id',],
+
+
+  'admin/components/confirm-delete.html.tmpl' => ['comp.bug_count'],
+
+  'admin/groups/delete.html.tmpl' => ['shared_queries'],
+
+  'admin/users/confirm-delete.html.tmpl' => [
+    'attachments',  'reporter',        'assignee_or_qa', 'cc',
+    'component_cc', 'flags.requestee', 'flags.setter',   'longdescs',
+    'quips',        'series',          'watch.watched',  'watch.watcher',
+    'whine_events', 'whine_schedules', 'otheruser.id'
+  ],
+
+  'admin/users/edit.html.tmpl' => ['otheruser.id', 'group.id',],
+
+  'admin/components/edit.html.tmpl' => ['comp.bug_count'],
+
+  'admin/workflow/edit.html.tmpl' => ['status.id', 'new_status.id',],
+
+  'admin/workflow/comment.html.tmpl' => ['status.id', 'new_status.id',],
+
+  'account/auth/login-small.html.tmpl' => ['qs_suffix',],
+
+  'account/prefs/email.html.tmpl' => ['relationship.id', 'event.id', 'prefname',],
+
+  'account/prefs/prefs.html.tmpl' => ['current_tab.label', 'current_tab.name',],
+
+  'account/prefs/saved-searches.html.tmpl' => ['group.id',],
+
+  'config.rdf.tmpl' => ['escaped_urlbase', 'uri_product_name', 'flag_type.id'],
 
 );
index 34bbba2b794f3d1cf253f44b7c464998d7593c51..095a439078930a29c442e240cf18a7be98bdc80d 100644 (file)
 # Please keep the strings in alphabetical order by their name.
 
 %strings = (
-    all_optional_features_require => 'All optional features above require the following modules to be found:',
-    any  => 'any',
-    apachectl_failed => <<END,
+  all_optional_features_require =>
+    'All optional features above require the following modules to be found:',
+  any              => 'any',
+  apachectl_failed => <<END,
 WARNING: We could not check the configuration of Apache. This sometimes
 happens when you are not running checksetup.pl as ##root##. To see the
 problem we ran into, run: ##command##
 END
-    bad_executable => 'not a valid executable: ##bin##',
-    bad_font_file => <<'END',
+  bad_executable => 'not a valid executable: ##bin##',
+  bad_font_file  => <<'END',
 The file ##file## cannot be found. Make sure you typed the full path to the file
 END
-    bad_font_file_name => <<'END',
+  bad_font_file_name => <<'END',
 The file ##file## must point to a TrueType or OpenType font file
 (its extension must be .ttf or .otf).
 END
-    blacklisted => '(blacklisted)',
-    bz_schema_exists_before_220 => <<'END',
+  blacklisted                 => '(blacklisted)',
+  bz_schema_exists_before_220 => <<'END',
 You are upgrading from a version before 2.20, but the bz_schema table
 already exists. This means that you restored a mysqldump into the Bugzilla
 database without first dropping the already-existing Bugzilla database,
@@ -44,32 +45,31 @@ not contain the bz_schema table. If for some reason you cannot do this, you
 can connect to your MySQL database and drop the bz_schema table, as a last
 resort.
 END
-    checking_for => 'Checking for',
-    chmod_failed      => '##path##: Failed to change permissions: ##error##',
-    chown_failed      => '##path##: Failed to change ownership: ##error##',
-    commands_dbd      => <<EOT,
+  checking_for => 'Checking for',
+  chmod_failed => '##path##: Failed to change permissions: ##error##',
+  chown_failed => '##path##: Failed to change ownership: ##error##',
+  commands_dbd => <<EOT,
 YOU MUST RUN ONE OF THE FOLLOWING COMMANDS (depending on which database
 you use):
 EOT
-    commands_optional => 'COMMANDS TO INSTALL OPTIONAL MODULES:',
-    commands_required => <<EOT,
+  commands_optional => 'COMMANDS TO INSTALL OPTIONAL MODULES:',
+  commands_required => <<EOT,
 COMMANDS TO INSTALL REQUIRED MODULES (You *must* run all these commands
 and then re-run checksetup.pl):
 EOT
-    continue_without_answers => <<'END',
+  continue_without_answers => <<'END',
 Re-run checksetup.pl in interactive mode (without an 'answers' file)
 to continue.
 END
-    cpanfile_created => "##file## created",
-    cpan_bugzilla_home => 
-        "WARNING: Using the Bugzilla directory as the CPAN home.",
-    db_enum_setup  => "Setting up choices for standard drop-down fields:",
-    db_schema_init => "Initializing bz_schema...",
-    db_table_new   => "Adding new table ##table##...",
-    db_table_setup => "Creating tables...",
-    done => 'done.',
-    enter_or_ctrl_c => "Press Enter to continue or Ctrl-C to exit...",
-    error_localconfig_read => <<'END',
+  cpanfile_created   => "##file## created",
+  cpan_bugzilla_home => "WARNING: Using the Bugzilla directory as the CPAN home.",
+  db_enum_setup      => "Setting up choices for standard drop-down fields:",
+  db_schema_init     => "Initializing bz_schema...",
+  db_table_new       => "Adding new table ##table##...",
+  db_table_setup     => "Creating tables...",
+  done               => 'done.',
+  enter_or_ctrl_c    => "Press Enter to continue or Ctrl-C to exit...",
+  error_localconfig_read => <<'END',
 An error has occurred while reading the ##localconfig## file.  The text of
 the error message is:
 
@@ -82,24 +82,24 @@ localconfig file:
   $ mv -f localconfig localconfig.old
   $ ./checksetup.pl
 END
-    extension_must_return_name => <<END,
+  extension_must_return_name => <<END,
 ##file## returned ##returned##, which is not a valid name for an extension.
 Extensions must return their name, not <code>1</code> or a number. See
 the documentation of Bugzilla::Extension for details.
 END
-    file_remove => 'Removing ##name##...',
-    file_rename => 'Renaming ##from## to ##to##...',
-    header => "* This is Bugzilla ##bz_ver## on perl ##perl_ver##\n"
-            . "* Running on ##os_name## ##os_ver##",
-    installation_failed => '*** Installation aborted. Read the messages above. ***',
-    install_data_too_long => <<EOT,
+  file_remove => 'Removing ##name##...',
+  file_rename => 'Renaming ##from## to ##to##...',
+  header      => "* This is Bugzilla ##bz_ver## on perl ##perl_ver##\n"
+    . "* Running on ##os_name## ##os_ver##",
+  installation_failed => '*** Installation aborted. Read the messages above. ***',
+  install_data_too_long => <<EOT,
 WARNING: Some of the data in the ##table##.##column## column is longer than
 its new length limit of ##max_length## characters. The data that needs to be
 fixed is printed below with the value of the ##id_column## column first and
 then the value of the ##column## column that needs to be fixed:
 
 EOT
-    lc_new_vars => <<'END',
+  lc_new_vars => <<'END',
 This version of Bugzilla contains some variables that you may want to
 change and adapt to your local settings. The following variables are
 new to ##localconfig## since you last ran checksetup.pl:
@@ -109,11 +109,11 @@ new to ##localconfig## since you last ran checksetup.pl:
 Please edit the file ##localconfig## and then re-run checksetup.pl
 to complete your installation.
 END
-    lc_old_vars => <<'END',
+  lc_old_vars => <<'END',
 The following variables are no longer used in ##localconfig##, and
 have been moved to ##old_file##: ##vars##
 END
-    localconfig_create_htaccess => <<'END',
+  localconfig_create_htaccess => <<'END',
 If you are using Apache as your web server, Bugzilla can create .htaccess
 files for you, which will keep this file (localconfig) and other
 confidential files from being read over the web.
@@ -123,72 +123,72 @@ they don't exist.
 
 If this is set to 0, checksetup.pl will not create .htaccess files.
 END
-    localconfig_db_check => <<'END',
+  localconfig_db_check => <<'END',
 Should checksetup.pl try to verify that your database setup is correct?
 With some combinations of database servers/Perl modules/moonphase this
 doesn't work, and so you can try setting this to 0 to make checksetup.pl
 run.
 END
-    localconfig_db_from_env => <<'END',
+  localconfig_db_from_env => <<'END',
 If this is set, the other db_* values will be ignored and instead the $DATABASE_URL
 will be used to provide the database connection information.
 Note this requires the 'db_from_env' feature to be enabled.
 END
-    localconfig_db_driver => <<'END',
+  localconfig_db_driver => <<'END',
 What SQL database to use. Default is mysql. List of supported databases
 can be obtained by listing Bugzilla/DB directory - every module corresponds
 to one supported database and the name of the module (before ".pm")
 corresponds to a valid value for this variable.
 END
-    localconfig_db_host => <<'END',
+  localconfig_db_host => <<'END',
 The DNS name or IP address of the host that the database server runs on.
 END
-    localconfig_db_name => <<'END',
+  localconfig_db_name => <<'END',
 The name of the database. For Oracle, this is the database's SID. For
 SQLite, this is a name (or path) for the DB file.
 For postgres, this database must already exist and be writable by $db_user.
 END
-    localconfig_db_pass => <<'END',
+  localconfig_db_pass => <<'END',
 Enter your database password here. It's normally advisable to specify
 a password for your bugzilla database user.
 If you use apostrophe (') or a backslash (\) in your password, you'll
 need to escape it by preceding it with a '\' character. (\') or (\)
 (It is far simpler to just not use those characters.)
 END
-    localconfig_db_port => <<'END',
+  localconfig_db_port => <<'END',
 Sometimes the database server is running on a non-standard port. If that's
 the case for your database server, set this to the port number that your
 database server is running on. Setting this to 0 means "use the default
 port for my database server."
 END
-    localconfig_db_sock => <<'END',
+  localconfig_db_sock => <<'END',
 MySQL Only: Enter a path to the unix socket for MySQL. If this is
 blank, then MySQL's compiled-in default will be used. You probably
 want that.
 END
-    localconfig_db_user => "Who we connect to the database as.",
-    localconfig_db_mysql_ssl_ca_file => <<'END',
+  localconfig_db_user              => "Who we connect to the database as.",
+  localconfig_db_mysql_ssl_ca_file => <<'END',
 Path to a PEM file with a list of trusted SSL CA certificates.
 The file must be readable by web server user.
 END
-    localconfig_db_mysql_ssl_ca_path => <<'END',
+  localconfig_db_mysql_ssl_ca_path => <<'END',
 Path to a directory containing trusted SSL CA certificates in PEM format.
 Directory and files inside must be readable by the web server user.
 END
-    localconfig_db_mysql_ssl_client_cert => <<'END',
+  localconfig_db_mysql_ssl_client_cert => <<'END',
 Full path to the client SSL certificate in PEM format we will present to the DB server.
 The file must be readable by web server user.
 END
-    localconfig_db_mysql_ssl_client_key => <<'END',
+  localconfig_db_mysql_ssl_client_key => <<'END',
 Full path to the private key corresponding to the client SSL certificate.
 The file must not be password-protected and must be readable by web server user.
 END
-    localconfig_diffpath => <<'END',
+  localconfig_diffpath => <<'END',
 For the "Difference Between Two Patches" feature to work, we need to know
 what directory the "diff" bin is in. (You only need to set this if you
 are using that feature of the Patch Viewer.)
 END
-    localconfig_font_file => <<'END',
+  localconfig_font_file => <<'END',
 You can specify the full path to a TrueType or OpenType font file, which will
 be used to display text (labels, legends, ...) in charts and graphical reports.
 To support as many languages as possible, we recommend to specify a font such as
@@ -197,7 +197,7 @@ characters in the Basic Multilingual Plane. If you leave this parameter empty,
 a default font will be used, but its support is limited to English characters
 only and so other characters will be displayed incorrectly.
 END
-    localconfig_index_html => <<'END',
+  localconfig_index_html => <<'END',
 Most web servers will allow you to use index.cgi as a directory
 index, and many come preconfigured that way, but if yours doesn't
 then you'll need an index.html file that provides redirection
@@ -207,19 +207,19 @@ NOTE: checksetup.pl will not replace an existing file, so if you
       wish to have checksetup.pl create one for you, you must
       make sure that index.html doesn't already exist.
 END
-    localconfig_interdiffbin => <<'END',
+  localconfig_interdiffbin => <<'END',
 If you want to use the "Difference Between Two Patches" feature of the
 Patch Viewer, please specify the full path to the "interdiff" executable
 here.
 END
-    localconfig_site_wide_secret => <<'END',
+  localconfig_site_wide_secret => <<'END',
 This secret key is used by your installation for the creation and
 validation of encrypted tokens. These tokens are used to implement
 security features in Bugzilla, to protect against certain types of attacks.
 A random string is generated by default. It's very important that this key
 is kept secret. It also must be very long.
 END
-    localconfig_use_suexec => <<'END',
+  localconfig_use_suexec => <<'END',
 Set this to 1 if Bugzilla runs in an Apache SuexecUserGroup environment.
 
 If your web server runs control panel software (cPanel, Plesk or similar),
@@ -234,7 +234,7 @@ a normal webserver environment.
 If set to 1, checksetup.pl will set file permissions so that Bugzilla
 works in a SuexecUserGroup environment. 
 END
-    localconfig_webdotbase => <<'END',
+  localconfig_webdotbase => <<'END',
 It is possible to show graphs of dependent bugs. You may set this parameter to
 any of the following:
 
@@ -249,7 +249,7 @@ files from your webdot directory. On Apache you do this by editing the
 .htaccess file, for other systems the needed measures may vary. You can run
 checksetup.pl to recreate the .htaccess file if it has been lost.
 END
-    localconfig_webservergroup => <<'END',
+  localconfig_webservergroup => <<'END',
 The name of the group that your web server runs as. On Red Hat
 distributions, this is usually "apache". On Debian/Ubuntu, it is 
 usually "www-data".
@@ -269,22 +269,22 @@ and you cannot set this up any other way. YOU HAVE BEEN WARNED!
 If you set this to anything other than "", you will need to run checksetup.pl
 as ##root## or as a user who is a member of the specified group.
 END
-    localconfig_apache_size_limit => <<EOT,
+  localconfig_apache_size_limit => <<EOT,
 This is the max amount of unshared memory the apache process is allowed to use
 before Apache::SizeLimit kills it. This is only applicable when run under mod_perl.
 EOT
-    max_allowed_packet => <<EOT,
+  max_allowed_packet => <<EOT,
 WARNING: You need to set the max_allowed_packet parameter in your MySQL
 configuration to at least ##needed##. Currently it is set to ##current##.
 You can set this parameter in the [mysqld] section of your MySQL
 configuration file.
 EOT
-    min_version_required => "Minimum version required: ",
+  min_version_required => "Minimum version required: ",
 
 # Note: When translating these "modules" messages, don't change the formatting
-# if possible, because there is hardcoded formatting in 
+# if possible, because there is hardcoded formatting in
 # Bugzilla::Install::Requirements to match the box formatting.
-    modules_message_apache => <<END,
+  modules_message_apache => <<END,
 ***********************************************************************
 * APACHE MODULES                                                      *
 ***********************************************************************
@@ -299,7 +299,7 @@ EOT
 * The modules you need to enable are:                                 *
 *                                                                     *
 END
-    modules_message_db => <<EOT,
+  modules_message_db => <<EOT,
 ***********************************************************************
 * DATABASE ACCESS                                                     *
 ***********************************************************************
@@ -308,7 +308,7 @@ END
 * running. See below for the correct command to run to install the    *
 * appropriate module for your database.                               *
 EOT
-    modules_message_optional => <<EOT,
+  modules_message_optional => <<EOT,
 ***********************************************************************
 * OPTIONAL MODULES                                                    *
 ***********************************************************************
@@ -320,7 +320,7 @@ EOT
 * with the name of the feature they enable. Below that table are the  *
 * commands to install each module.                                    *
 EOT
-    modules_message_required => <<EOT,
+  modules_message_required => <<EOT,
 ***********************************************************************
 * REQUIRED MODULES                                                    *
 ***********************************************************************
@@ -329,23 +329,23 @@ EOT
 * See below for commands to install these modules.                    *
 EOT
 
-    module_found => "found v##ver##",
-    module_not_found => "not found",
-    module_ok => 'ok',
-    module_unknown_version => "found unknown version",
-    no_such_module => "There is no Perl module on CPAN named ##module##.",
-    mysql_innodb_disabled => <<'END',
+  module_found           => "found v##ver##",
+  module_not_found       => "not found",
+  module_ok              => 'ok',
+  module_unknown_version => "found unknown version",
+  no_such_module         => "There is no Perl module on CPAN named ##module##.",
+  mysql_innodb_disabled  => <<'END',
 InnoDB is disabled in your MySQL installation.
 Bugzilla requires InnoDB to be enabled.
 Please enable it and then re-run checksetup.pl.
 END
-    mysql_index_renaming => <<'END',
+  mysql_index_renaming => <<'END',
 We are about to rename old indexes. The estimated time to complete
 renaming is ##minutes## minutes. You cannot interrupt this action once
 it has begun. If you would like to cancel, press Ctrl-C now...
 (Waiting 45 seconds...)
 END
-    mysql_utf8_conversion => <<'END',
+  mysql_utf8_conversion => <<'END',
 WARNING: We are about to convert your table storage format to UTF-8. This
          allows Bugzilla to correctly store and sort international characters.
          However, if you have any non-UTF-8 data in your database,
@@ -360,7 +360,7 @@ WARNING: We are about to convert your table storage format to UTF-8. This
          If you ever used a version of Bugzilla before 2.22, we STRONGLY
          recommend that you stop checksetup.pl NOW and run contrib/recode.pl.
 END
-    no_checksetup_from_cgi => <<END,
+  no_checksetup_from_cgi => <<END,
 <!DOCTYPE html>
 <html>
   <head>
@@ -384,49 +384,49 @@ END
   </body>
 </html>
 END
-    patchutils_missing => <<'END',
+  patchutils_missing => <<'END',
 OPTIONAL NOTE: If you want to be able to use the 'difference between two
 patches' feature of Bugzilla (which requires the PatchReader Perl module
 as well), you should install patchutils from:
 
     http://cyberelk.net/tim/software/patchutils/
 END
-    template_precompile   => "Precompiling templates...",
-    template_removal_failed => <<END,
+  template_precompile     => "Precompiling templates...",
+  template_removal_failed => <<END,
 WARNING: The directory '##template_cache##' could not be removed.
          It has been moved into '##deleteme##', which should be
          deleted manually to conserve disk space.
 END
-    template_removing_dir => "Removing existing compiled templates...",
-    update_cf_invalid_name => 
-        "Removing custom field '##field##', because it has an invalid name...",
-    update_flags_bad_name => <<'END',
+  template_removing_dir => "Removing existing compiled templates...",
+  update_cf_invalid_name =>
+    "Removing custom field '##field##', because it has an invalid name...",
+  update_flags_bad_name => <<'END',
 "##flag##" is not a valid name for a flag. Rename it to not have any spaces
 or commas.
 END
-    update_nomail_bad => <<'END',
+  update_nomail_bad => <<'END',
 WARNING: The following users were listed in ##data##/nomail, but do
 not have an account here. The unmatched entries have been moved to 
 ##data##/nomail.bad:
 END
-    update_summary_truncate_comment => 
-        "The original value of the Summary field was longer than 255"
-        . " characters, and so it was truncated during an upgrade."
-        . " The original summary was:\n\n##summary##",
-    update_summary_truncated => <<'END',
+  update_summary_truncate_comment =>
+    "The original value of the Summary field was longer than 255"
+    . " characters, and so it was truncated during an upgrade."
+    . " The original summary was:\n\n##summary##",
+  update_summary_truncated => <<'END',
 WARNING: Some of your bugs had summaries longer than 255 characters.
 They have had their original summary copied into a comment, and then
 the summary was truncated to 255 characters. The affected bug numbers were:
 END
-    update_quips => <<'END',
+  update_quips => <<'END',
 Quips are now stored in the database, rather than in an external file.
 The quips previously stored in ##data##/comments have been copied into
 the database, and that file has been renamed to ##data##/comments.bak
 You may delete the renamed file once you have confirmed that all your
 quips were moved successfully.
 END
-    update_queries_to_tags => "Populating the new 'tag' table:",
-    webdot_bad_htaccess => <<END,
+  update_queries_to_tags => "Populating the new 'tag' table:",
+  webdot_bad_htaccess    => <<END,
 WARNING: Dependency graph images are not accessible.
 Delete ##dir##/.htaccess and re-run checksetup.pl.
 END
index a5e36a825255b2ffcac2dbea09ce3a3fd75dfad2..3f21ebe7a4837d31fbbc1846f242fe2b05d1c2cb 100755 (executable)
@@ -29,61 +29,59 @@ my $lwp = $@ ? 0 : 1;
 eval "require LWP::Protocol::https;";
 my $lwpssl = $@ ? 0 : 1;
 
-if ((@ARGV != 1) || ($ARGV[0] !~ /^https?:/i))
-{
-    say "Usage: $0 <URL to this Bugzilla installation>";
-    say "e.g.:  $0 http://www.mycompany.com/bugzilla";
-    exit(1);
+if ((@ARGV != 1) || ($ARGV[0] !~ /^https?:/i)) {
+  say "Usage: $0 <URL to this Bugzilla installation>";
+  say "e.g.:  $0 http://www.mycompany.com/bugzilla";
+  exit(1);
 }
 
 
 # Try to determine the GID used by the web server.
-my @pscmds = ('ps -eo comm,gid', 'ps -acxo command,gid', 'ps -acxo command,rgid');
+my @pscmds
+  = ('ps -eo comm,gid', 'ps -acxo command,gid', 'ps -acxo command,rgid');
 my $sgid = 0;
 if (!ON_WINDOWS) {
-    foreach my $pscmd (@pscmds) {
-        open PH, '-|', "$pscmd 2>/dev/null";
-        while (my $line = <PH>) {
-            if ($line =~ /^(?:\S*\/)?(?:httpd|apache?)2?\s+(\d+)$/) {
-                $sgid = $1 if $1 > $sgid;
-            }
-        }
-        close(PH);
+  foreach my $pscmd (@pscmds) {
+    open PH, '-|', "$pscmd 2>/dev/null";
+    while (my $line = <PH>) {
+      if ($line =~ /^(?:\S*\/)?(?:httpd|apache?)2?\s+(\d+)$/) {
+        $sgid = $1 if $1 > $sgid;
+      }
     }
+    close(PH);
+  }
 }
 
 # Determine the numeric GID of $webservergroup
-my $webgroupnum = 0;
+my $webgroupnum    = 0;
 my $webservergroup = Bugzilla->localconfig->{webservergroup};
 if ($webservergroup =~ /^(\d+)$/) {
-    $webgroupnum = $1;
+  $webgroupnum = $1;
 }
 else {
-    eval { $webgroupnum = (getgrnam $webservergroup) || 0; };
+  eval { $webgroupnum = (getgrnam $webservergroup) || 0; };
 }
 
 # Check $webservergroup against the server's GID
 if ($sgid > 0) {
-    if ($webservergroup eq "") {
-        say 
-"WARNING \$webservergroup is set to an empty string.
+  if ($webservergroup eq "") {
+    say "WARNING \$webservergroup is set to an empty string.
 That is a very insecure practice. Please refer to the
 Bugzilla documentation.";
-    }
-    elsif ($webgroupnum == $sgid || Bugzilla->localconfig->{use_suexec}) {
-        say "TEST-OK Webserver is running under group id in \$webservergroup.";
-    }
-    else {
-        say 
-"TEST-WARNING Webserver is running under group id not matching \$webservergroup.
+  }
+  elsif ($webgroupnum == $sgid || Bugzilla->localconfig->{use_suexec}) {
+    say "TEST-OK Webserver is running under group id in \$webservergroup.";
+  }
+  else {
+    say
+      "TEST-WARNING Webserver is running under group id not matching \$webservergroup.
 This if the tests below fail, this is probably the problem.
 Please refer to the web server configuration section of the Bugzilla guide. 
 If you are using virtual hosts or suexec, this warning may not apply.";
-    }
+  }
 }
 elsif (!ON_WINDOWS) {
-   say
-"TEST-WARNING Failed to find the GID for the 'httpd' process, unable
+  say "TEST-WARNING Failed to find the GID for the 'httpd' process, unable
 to validate webservergroup.";
 }
 
@@ -92,200 +90,209 @@ to validate webservergroup.";
 $ARGV[0] =~ s/\/$//;
 my $url = $ARGV[0] . "/images/padlock.png";
 if (fetch($url)) {
-    say "TEST-OK Got padlock picture.";
-} else {
-    say 
-"TEST-FAILED Fetch of images/padlock.png failed
+  say "TEST-OK Got padlock picture.";
+}
+else {
+  say "TEST-FAILED Fetch of images/padlock.png failed
 Your web server could not fetch $url.
 Check your web server configuration and try again.";
-    exit(1);
+  exit(1);
 }
 
 # Try to execute a cgi script
 my $response = clean_text(fetch($ARGV[0] . "/testagent.cgi"));
 if ($response =~ /^OK (.*)$/) {
-    say "TEST-OK Webserver is executing CGIs via $1.";
-} elsif ($response =~ /^#!/) {
-    say
-"TEST-FAILED Webserver is fetching rather than executing CGI files.
+  say "TEST-OK Webserver is executing CGIs via $1.";
+}
+elsif ($response =~ /^#!/) {
+  say "TEST-FAILED Webserver is fetching rather than executing CGI files.
 Check the AddHandler statement in your httpd.conf file.";
-    exit(1);
-} else {
-    say "TEST-FAILED Webserver is not executing CGI files.";
+  exit(1);
+}
+else {
+  say "TEST-FAILED Webserver is not executing CGI files.";
 }
 
 # Make sure that the web server is honoring .htaccess files
 my $localconfig = bz_locations()->{'localconfig'};
 $localconfig =~ s~^\./~~;
-$url = $ARGV[0] . "/$localconfig";
+$url      = $ARGV[0] . "/$localconfig";
 $response = fetch($url);
 if ($response) {
-    say
-"TEST-FAILED Webserver is permitting fetch of $url.
+  say "TEST-FAILED Webserver is permitting fetch of $url.
 This is a serious security problem.
 Check your web server configuration.";
-    exit(1);
-} else {
-    say "TEST-OK Webserver is preventing fetch of $url.";
+  exit(1);
+}
+else {
+  say "TEST-OK Webserver is preventing fetch of $url.";
 }
 
 # Test chart generation
 eval 'use GD';
 if ($@ eq '') {
-    undef $/;
-
-    # Ensure major versions of GD and libgd match
-    # Windows's GD module include libgd.dll, guaranteed to match
-    if (!ON_WINDOWS) {
-        my $gdlib = `gdlib-config --version 2>&1` || "";
-        $gdlib =~ s/\n$//;
-        if (!$gdlib) {
-            say "TEST-WARNING Failed to run gdlib-config; can't compare " .
-                  "GD versions.";
-        }
-        else {
-            my $gd = $GD::VERSION;
-    
-            my $verstring = "GD version $gd, libgd version $gdlib";
-    
-            $gdlib =~ s/^([^\.]+)\..*/$1/;
-            $gd =~ s/^([^\.]+)\..*/$1/;
-    
-            if ($gdlib == $gd) {
-                say "TEST-OK $verstring; Major versions match.";
-            } else {
-                say "TEST-FAILED $verstring; Major versions do not match.";
-            }
-        }
+  undef $/;
+
+  # Ensure major versions of GD and libgd match
+  # Windows's GD module include libgd.dll, guaranteed to match
+  if (!ON_WINDOWS) {
+    my $gdlib = `gdlib-config --version 2>&1` || "";
+    $gdlib =~ s/\n$//;
+    if (!$gdlib) {
+      say "TEST-WARNING Failed to run gdlib-config; can't compare " . "GD versions.";
+    }
+    else {
+      my $gd = $GD::VERSION;
+
+      my $verstring = "GD version $gd, libgd version $gdlib";
+
+      $gdlib =~ s/^([^\.]+)\..*/$1/;
+      $gd =~ s/^([^\.]+)\..*/$1/;
+
+      if ($gdlib == $gd) {
+        say "TEST-OK $verstring; Major versions match.";
+      }
+      else {
+        say "TEST-FAILED $verstring; Major versions do not match.";
+      }
     }
+  }
 
-    # Test GD
+  # Test GD
+  eval {
+    my $image = new GD::Image(100, 100);
+    my $black = $image->colorAllocate(0,   0,   0);
+    my $white = $image->colorAllocate(255, 255, 255);
+    my $red   = $image->colorAllocate(255, 0,   0);
+    my $blue  = $image->colorAllocate(0,   0,   255);
+    $image->transparent($white);
+    $image->rectangle(0, 0, 99, 99, $black);
+    $image->arc(50, 50, 95, 75, 0, 360, $blue);
+    $image->fill(50, 50, $red);
+
+    if ($image->can('png')) {
+      create_file("$datadir/testgd-local.png", $image->png);
+      check_image("$datadir/testgd-local.png", 'GD');
+    }
+    else {
+      say "TEST-FAILED GD doesn't support PNG generation.";
+    }
+  };
+  if ($@ ne '') {
+    say "TEST-FAILED GD returned: $@";
+  }
+
+  # Test Chart
+  eval 'use Chart::Lines';
+  if ($@) {
+    say "TEST-FAILED Chart::Lines is not installed.";
+  }
+  else {
     eval {
-        my $image = new GD::Image(100, 100);
-        my $black = $image->colorAllocate(0, 0, 0);
-        my $white = $image->colorAllocate(255, 255, 255);
-        my $red = $image->colorAllocate(255, 0, 0);
-        my $blue = $image->colorAllocate(0, 0, 255);
-        $image->transparent($white);
-        $image->rectangle(0, 0, 99, 99, $black);
-        $image->arc(50, 50, 95, 75, 0, 360, $blue);
-        $image->fill(50, 50, $red);
-
-        if ($image->can('png')) {
-            create_file("$datadir/testgd-local.png", $image->png);
-            check_image("$datadir/testgd-local.png", 'GD');
-        } else {
-            say "TEST-FAILED GD doesn't support PNG generation.";
-        }
+      my $chart = Chart::Lines->new(400, 400);
+
+      $chart->add_pt('foo', 30, 25);
+      $chart->add_pt('bar', 16, 32);
+
+      $chart->png("$datadir/testchart-local.png");
+      check_image("$datadir/testchart-local.png", "Chart");
     };
     if ($@ ne '') {
-        say "TEST-FAILED GD returned: $@";
+      say "TEST-FAILED Chart returned: $@";
     }
+  }
 
-    # Test Chart
-    eval 'use Chart::Lines';
-    if ($@) {
-        say "TEST-FAILED Chart::Lines is not installed.";
-    } else {
-        eval {
-            my $chart = Chart::Lines->new(400, 400);
-
-            $chart->add_pt('foo', 30, 25);
-            $chart->add_pt('bar', 16, 32);
-
-            $chart->png("$datadir/testchart-local.png");
-            check_image("$datadir/testchart-local.png", "Chart");
-        };
-        if ($@ ne '') {
-            say "TEST-FAILED Chart returned: $@";
-        }
-    }
+  eval 'use Template::Plugin::GD::Image';
+  if ($@) {
+    say "TEST-FAILED Template::Plugin::GD is not installed.";
+  }
+  else {
+    say "TEST-OK Template::Plugin::GD is installed.";
+  }
+}
 
-    eval 'use Template::Plugin::GD::Image';
-    if ($@) {
-        say "TEST-FAILED Template::Plugin::GD is not installed.";
+sub fetch {
+  my $url = shift;
+  my $rtn;
+  if ($lwp) {
+    if ($url =~ /^https:/i && !$lwpssl) {
+      die("You need LWP::Protocol::https installed to use https with testserver.pl");
     }
     else {
-        say "TEST-OK Template::Plugin::GD is installed.";
+      my $req = HTTP::Request->new(GET => $url);
+      my $ua  = LWP::UserAgent->new;
+      my $res = $ua->request($req);
+      $rtn = ($res->is_success ? $res->content : undef);
+    }
+  }
+  elsif ($url =~ /^https:/i) {
+    die(
+      "You need LWP (and LWP::Protocol::https, for LWP 6.02 or newer) installed to use https with testserver.pl"
+    );
+  }
+  else {
+    my ($host, $port, $file) = ('', 80, '');
+    if ($url =~ m#^http://([^:]+):(\d+)(/.*)#i) {
+      ($host, $port, $file) = ($1, $2, $3);
+    }
+    elsif ($url =~ m#^http://([^/]+)(/.*)#i) {
+      ($host, $file) = ($1, $2);
+    }
+    else {
+      die("Cannot parse url");
     }
-}
 
-sub fetch {
-    my $url = shift;
-    my $rtn;
-    if ($lwp) {
-        if ($url =~ /^https:/i && !$lwpssl) {
-            die("You need LWP::Protocol::https installed to use https with testserver.pl");
-        } else {
-            my $req = HTTP::Request->new(GET => $url);
-            my $ua = LWP::UserAgent->new;
-            my $res = $ua->request($req);
-            $rtn = ($res->is_success ? $res->content : undef);
-        }
-    } elsif ($url =~ /^https:/i) {
-        die("You need LWP (and LWP::Protocol::https, for LWP 6.02 or newer) installed to use https with testserver.pl");
-    } else {
-        my($host, $port, $file) = ('', 80, '');
-        if ($url =~ m#^http://([^:]+):(\d+)(/.*)#i) {
-            ($host, $port, $file) = ($1, $2, $3);
-        } elsif ($url =~ m#^http://([^/]+)(/.*)#i) {
-            ($host, $file) = ($1, $2);
-        } else {
-            die("Cannot parse url");
-        }
-
-        my $proto = getprotobyname('tcp');
-        socket(SOCK, PF_INET, SOCK_STREAM, $proto);
-        my $sin = sockaddr_in($port, inet_aton($host));
-        if (connect(SOCK, $sin)) {
-            binmode SOCK;
-            select((select(SOCK), $| = 1)[0]);
-
-            # get content
-            print SOCK "GET $file HTTP/1.0\015\012host: $host:$port\015\012\015\012";
-            my $header = '';
-            while (defined(my $line = <SOCK>)) {
-                last if $line eq "\015\012";
-                $header .= $line;
-            }
-            my $content = '';
-            while (defined(my $line = <SOCK>)) {
-                $content .= $line;
-            }
-
-            my ($status) = $header =~ m#^HTTP/\d+\.\d+ (\d+)#;
-            $rtn = (($status =~ /^2\d\d/) ? $content : undef);
-        }
+    my $proto = getprotobyname('tcp');
+    socket(SOCK, PF_INET, SOCK_STREAM, $proto);
+    my $sin = sockaddr_in($port, inet_aton($host));
+    if (connect(SOCK, $sin)) {
+      binmode SOCK;
+      select((select(SOCK), $| = 1)[0]);
+
+      # get content
+      print SOCK "GET $file HTTP/1.0\015\012host: $host:$port\015\012\015\012";
+      my $header = '';
+      while (defined(my $line = <SOCK>)) {
+        last if $line eq "\015\012";
+        $header .= $line;
+      }
+      my $content = '';
+      while (defined(my $line = <SOCK>)) {
+        $content .= $line;
+      }
+
+      my ($status) = $header =~ m#^HTTP/\d+\.\d+ (\d+)#;
+      $rtn = (($status =~ /^2\d\d/) ? $content : undef);
     }
-    return($rtn);
+  }
+  return ($rtn);
 }
 
 sub check_image {
-    my ($local_file, $library) = @_;
-    my $filedata = read_file($local_file);
-    if ($filedata =~ /^\x89\x50\x4E\x47\x0D\x0A\x1A\x0A/) {
-        say "TEST-OK $library library generated a good PNG image.";
-        unlink $local_file;
-    } else {
-        say "TEST-WARNING $library library did not generate a good PNG.";
-    }
+  my ($local_file, $library) = @_;
+  my $filedata = read_file($local_file);
+  if ($filedata =~ /^\x89\x50\x4E\x47\x0D\x0A\x1A\x0A/) {
+    say "TEST-OK $library library generated a good PNG image.";
+    unlink $local_file;
+  }
+  else {
+    say "TEST-WARNING $library library did not generate a good PNG.";
+  }
 }
 
 sub create_file {
-    my ($filename, $content) = @_;
-    open(FH, ">", $filename)
-        or die "Failed to create $filename: $!\n";
-    binmode FH;
-    print FH $content;
-    close FH;
+  my ($filename, $content) = @_;
+  open(FH, ">", $filename) or die "Failed to create $filename: $!\n";
+  binmode FH;
+  print FH $content;
+  close FH;
 }
 
 sub read_file {
-    my ($filename) = @_;
-    open(FH, '<', $filename)
-        or die "Failed to open $filename: $!\n";
-    binmode FH;
-    my $content = <FH>;
-    close FH;
-    return $content;
+  my ($filename) = @_;
+  open(FH, '<', $filename) or die "Failed to open $filename: $!\n";
+  binmode FH;
+  my $content = <FH>;
+  close FH;
+  return $content;
 }
index b2792306e04505c16565eeffc4475e84d7a234a1..0feb74a05fdd35e1d7786564f6a6aa065a0ac815 100755 (executable)
--- a/token.cgi
+++ b/token.cgi
@@ -22,12 +22,12 @@ use Bugzilla::User;
 use Date::Format;
 use Date::Parse;
 
-local our $cgi = Bugzilla->cgi;
+local our $cgi      = Bugzilla->cgi;
 local our $template = Bugzilla->template;
-local our $vars = {};
+local our $vars     = {};
 
 my $action = $cgi->param('a');
-my $token = $cgi->param('t');
+my $token  = $cgi->param('t');
 
 Bugzilla->login(LOGIN_OPTIONAL);
 
@@ -41,66 +41,66 @@ my ($user_id, $date, $data, $tokentype) = Bugzilla::Token::GetTokenData($token);
 # Requesting a new password is the single action which doesn't require a token.
 # XXX Ideally, these checks should be done inside the subroutines themselves.
 unless ($action eq 'reqpw') {
-    $tokentype || ThrowUserError("token_does_not_exist");
-
-    # Make sure the token is the correct type for the action being taken.
-    # The { user_error => 'wrong_token_for_*' } trick is to make 012throwables.t happy.
-    my $error = {};
-    if (grep($action eq $_ , qw(cfmpw cxlpw chgpw)) && $tokentype ne 'password') {
-        $error = { user_error => 'wrong_token_for_changing_passwd' };
-    }
-    elsif ($action eq 'cxlem'
-           && ($tokentype ne 'emailold' && $tokentype ne 'emailnew'))
-    {
-        $error = { user_error => 'wrong_token_for_cancelling_email_change' };
-    }
-    elsif (grep($action eq $_ , qw(cfmem chgem)) && $tokentype ne 'emailnew') {
-        $error = { user_error => 'wrong_token_for_confirming_email_change' };
-    }
-    elsif ($action =~ /^(request|confirm|cancel)_new_account$/
-           && $tokentype ne 'account')
-    {
-        $error = { user_error => 'wrong_token_for_creating_account' };
-    }
-
-    if (my $user_error = $error->{user_error}) {
-        Bugzilla::Token::Cancel($token, $user_error);
-        ThrowUserError($user_error);
-    }
+  $tokentype || ThrowUserError("token_does_not_exist");
+
+# Make sure the token is the correct type for the action being taken.
+# The { user_error => 'wrong_token_for_*' } trick is to make 012throwables.t happy.
+  my $error = {};
+  if (grep($action eq $_, qw(cfmpw cxlpw chgpw)) && $tokentype ne 'password') {
+    $error = {user_error => 'wrong_token_for_changing_passwd'};
+  }
+  elsif ($action eq 'cxlem'
+    && ($tokentype ne 'emailold' && $tokentype ne 'emailnew'))
+  {
+    $error = {user_error => 'wrong_token_for_cancelling_email_change'};
+  }
+  elsif (grep($action eq $_, qw(cfmem chgem)) && $tokentype ne 'emailnew') {
+    $error = {user_error => 'wrong_token_for_confirming_email_change'};
+  }
+  elsif ($action =~ /^(request|confirm|cancel)_new_account$/
+    && $tokentype ne 'account')
+  {
+    $error = {user_error => 'wrong_token_for_creating_account'};
+  }
+
+  if (my $user_error = $error->{user_error}) {
+    Bugzilla::Token::Cancel($token, $user_error);
+    ThrowUserError($user_error);
+  }
 }
 
 if ($action eq 'reqpw') {
-    requestChangePassword();
+  requestChangePassword();
 }
 elsif ($action eq 'cfmpw') {
-    confirmChangePassword($token);
+  confirmChangePassword($token);
 }
 elsif ($action eq 'cxlpw') {
-    cancelChangePassword($token);
+  cancelChangePassword($token);
 }
 elsif ($action eq 'chgpw') {
-    changePassword($user_id, $token);
+  changePassword($user_id, $token);
 }
 elsif ($action eq 'cfmem') {
-    confirmChangeEmail($token);
+  confirmChangeEmail($token);
 }
 elsif ($action eq 'cxlem') {
-    cancelChangeEmail($user_id, $data, $tokentype, $token);
+  cancelChangeEmail($user_id, $data, $tokentype, $token);
 }
 elsif ($action eq 'chgem') {
-    changeEmail($user_id, $data, $token);
+  changeEmail($user_id, $data, $token);
 }
 elsif ($action eq 'request_new_account') {
-    request_create_account($date, $data, $token);
+  request_create_account($date, $data, $token);
 }
 elsif ($action eq 'confirm_new_account') {
-    confirm_create_account($data, $token);
+  confirm_create_account($data, $token);
 }
 elsif ($action eq 'cancel_new_account') {
-    cancel_create_account($data, $token);
+  cancel_create_account($data, $token);
 }
 else {
-    ThrowUserError('unknown_action', {action => $action});
+  ThrowUserError('unknown_action', {action => $action});
 }
 
 exit;
@@ -113,245 +113,256 @@ exit;
 # their login name and it exists in the database, and that the DB module is in
 # the list of allowed verification methods.
 sub requestChangePassword {
-    # check verification methods
-    Bugzilla->user->authorizer->can_change_password
-      || ThrowUserError("password_change_requests_not_allowed");
 
-    # Check the hash token to make sure this user actually submitted
-    # the forgotten password form.
-    my $token = $cgi->param('token');
-    check_hash_token($token, ['reqpw']);
+  # check verification methods
+  Bugzilla->user->authorizer->can_change_password
+    || ThrowUserError("password_change_requests_not_allowed");
 
-    my $email = $cgi->param('email')
-      or ThrowUserError("email_needed_for_password_change");
+  # Check the hash token to make sure this user actually submitted
+  # the forgotten password form.
+  my $token = $cgi->param('token');
+  check_hash_token($token, ['reqpw']);
 
-    my $userid = email_to_id($email, 'throw_error_if_not_exist');
-    my $user = new Bugzilla::User($userid);
+  my $email = $cgi->param('email')
+    or ThrowUserError("email_needed_for_password_change");
 
-    # Make sure the user account is active.
-    if (!$user->is_enabled) {
-        ThrowUserError('account_disabled',
-                       {disabled_reason => get_text('account_disabled', {email => $email})});
-    }
+  my $userid = email_to_id($email, 'throw_error_if_not_exist');
+  my $user = new Bugzilla::User($userid);
+
+  # Make sure the user account is active.
+  if (!$user->is_enabled) {
+    ThrowUserError('account_disabled',
+      {disabled_reason => get_text('account_disabled', {email => $email})});
+  }
 
-    Bugzilla::Token::IssuePasswordToken($user);
+  Bugzilla::Token::IssuePasswordToken($user);
 
-    $vars->{'message'} = "password_change_request";
-    $vars->{'email'} = $email;
+  $vars->{'message'} = "password_change_request";
+  $vars->{'email'}   = $email;
 
-    print $cgi->header();
-    $template->process("global/message.html.tmpl", $vars)
-      || ThrowTemplateError($template->error());
+  print $cgi->header();
+  $template->process("global/message.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
 }
 
 sub confirmChangePassword {
-    my $token = shift;
-    $vars->{'token'} = $token;
+  my $token = shift;
+  $vars->{'token'} = $token;
 
-    print $cgi->header();
-    $template->process("account/password/set-forgotten-password.html.tmpl", $vars)
-      || ThrowTemplateError($template->error());
+  print $cgi->header();
+  $template->process("account/password/set-forgotten-password.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
 }
 
 sub cancelChangePassword {
-    my $token = shift;
-    $vars->{'message'} = "password_change_canceled";
-    Bugzilla::Token::Cancel($token, $vars->{'message'});
+  my $token = shift;
+  $vars->{'message'} = "password_change_canceled";
+  Bugzilla::Token::Cancel($token, $vars->{'message'});
 
-    print $cgi->header();
-    $template->process("global/message.html.tmpl", $vars)
-      || ThrowTemplateError($template->error());
+  print $cgi->header();
+  $template->process("global/message.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
 }
 
 # If the user is changing their password, make sure they submitted a new
 # password and that the new password is valid.
 sub changePassword {
-    my ($user_id, $token) = @_;
-    my $dbh = Bugzilla->dbh;
+  my ($user_id, $token) = @_;
+  my $dbh = Bugzilla->dbh;
+
+  my $password = $cgi->param('password');
+  (defined $password && defined $cgi->param('matchpassword'))
+    || ThrowUserError("require_new_password");
 
-    my $password = $cgi->param('password');
-    (defined $password && defined $cgi->param('matchpassword'))
-      || ThrowUserError("require_new_password");
+  validate_password($password, $cgi->param('matchpassword'));
 
-    validate_password($password, $cgi->param('matchpassword'));
-    # Make sure that these never show up in the UI under any circumstances.
-    $cgi->delete('password', 'matchpassword');
+  # Make sure that these never show up in the UI under any circumstances.
+  $cgi->delete('password', 'matchpassword');
 
-    my $user = Bugzilla::User->check({ id => $user_id });
-    $user->set_password($password);
-    $user->update();
-    delete_token($token);
-    $dbh->do(q{DELETE FROM tokens WHERE userid = ?
-               AND tokentype = 'password'}, undef, $user_id);
+  my $user = Bugzilla::User->check({id => $user_id});
+  $user->set_password($password);
+  $user->update();
+  delete_token($token);
+  $dbh->do(
+    q{DELETE FROM tokens WHERE userid = ?
+               AND tokentype = 'password'}, undef, $user_id
+  );
 
-    Bugzilla->logout_user_by_id($user_id);
+  Bugzilla->logout_user_by_id($user_id);
 
-    $vars->{'message'} = "password_changed";
+  $vars->{'message'} = "password_changed";
 
-    print $cgi->header();
-    $template->process("global/message.html.tmpl", $vars)
-      || ThrowTemplateError($template->error());
+  print $cgi->header();
+  $template->process("global/message.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
 }
 
 sub confirmChangeEmail {
-    my $token = shift;
-    $vars->{'token'} = $token;
+  my $token = shift;
+  $vars->{'token'} = $token;
 
-    print $cgi->header();
-    $template->process("account/email/confirm.html.tmpl", $vars)
-      || ThrowTemplateError($template->error());
+  print $cgi->header();
+  $template->process("account/email/confirm.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
 }
 
 sub changeEmail {
-    my ($userid, $eventdata, $token) = @_;
-    my $dbh = Bugzilla->dbh;
-    my ($old_email, $new_email) = split(/:/,$eventdata);
-
-    $dbh->bz_start_transaction();
-
-    my $user = Bugzilla::User->check({ id => $userid });
-    my $cgipassword  = $cgi->param('password');
-
-    # Make sure the user who wants to change the email address
-    # is the real account owner.
-    $user->check_current_password($cgipassword);
-
-    # The new email address should be available as this was 
-    # confirmed initially so cancel token if it is not still available
-    if (!is_available_email($new_email, $old_email)) {
-        $vars->{'email'} = $new_email; # Needed for Bugzilla::Token::Cancel's mail
-        Bugzilla::Token::Cancel($token, "account_exists", $vars);
-        ThrowUserError("account_exists", { email => $new_email } );
-    } 
-
-    # Update the user's email address in the profiles table.
-    $user->set_email($new_email);
-    $user->update({ keep_session => 1, keep_tokens => 1 });
-    delete_token($token);
-    $dbh->do(q{DELETE FROM tokens WHERE userid = ?
-               AND tokentype = 'emailnew'}, undef, $userid);
-
-    $dbh->bz_commit_transaction();
-
-    # Return HTTP response headers.
-    print $cgi->header();
-
-    # Let the user know their email address has been changed.
-    $vars->{'message'} = "login_changed";
-
-    $template->process("global/message.html.tmpl", $vars)
-      || ThrowTemplateError($template->error());
+  my ($userid, $eventdata, $token) = @_;
+  my $dbh = Bugzilla->dbh;
+  my ($old_email, $new_email) = split(/:/, $eventdata);
+
+  $dbh->bz_start_transaction();
+
+  my $user = Bugzilla::User->check({id => $userid});
+  my $cgipassword = $cgi->param('password');
+
+  # Make sure the user who wants to change the email address
+  # is the real account owner.
+  $user->check_current_password($cgipassword);
+
+  # The new email address should be available as this was
+  # confirmed initially so cancel token if it is not still available
+  if (!is_available_email($new_email, $old_email)) {
+    $vars->{'email'} = $new_email;    # Needed for Bugzilla::Token::Cancel's mail
+    Bugzilla::Token::Cancel($token, "account_exists", $vars);
+    ThrowUserError("account_exists", {email => $new_email});
+  }
+
+  # Update the user's email address in the profiles table.
+  $user->set_email($new_email);
+  $user->update({keep_session => 1, keep_tokens => 1});
+  delete_token($token);
+  $dbh->do(
+    q{DELETE FROM tokens WHERE userid = ?
+               AND tokentype = 'emailnew'}, undef, $userid
+  );
+
+  $dbh->bz_commit_transaction();
+
+  # Return HTTP response headers.
+  print $cgi->header();
+
+  # Let the user know their email address has been changed.
+  $vars->{'message'} = "login_changed";
+
+  $template->process("global/message.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
 }
 
 sub cancelChangeEmail {
-    my ($userid, $eventdata, $tokentype, $token) = @_;
-    my $dbh = Bugzilla->dbh;
+  my ($userid, $eventdata, $tokentype, $token) = @_;
+  my $dbh = Bugzilla->dbh;
 
-    $dbh->bz_start_transaction();
+  $dbh->bz_start_transaction();
 
-    my ($old_email, $new_email) = split(/:/,$eventdata);
+  my ($old_email, $new_email) = split(/:/, $eventdata);
 
-    if ($tokentype eq "emailold") {
-        $vars->{'message'} = "emailold_change_canceled";
-        my $user = Bugzilla::User->check({ id => $userid });
+  if ($tokentype eq "emailold") {
+    $vars->{'message'} = "emailold_change_canceled";
+    my $user = Bugzilla::User->check({id => $userid});
 
-        # check to see if it has been altered
-        if ($user->email ne $old_email) {
-            $user->set_email($old_email);
-            $user->update({ keep_tokens => 1 });
+    # check to see if it has been altered
+    if ($user->email ne $old_email) {
+      $user->set_email($old_email);
+      $user->update({keep_tokens => 1});
 
-            $vars->{'message'} = "email_change_canceled_reinstated";
-        } 
-    } 
-    else {
-        $vars->{'message'} = 'email_change_canceled'
-     }
+      $vars->{'message'} = "email_change_canceled_reinstated";
+    }
+  }
+  else {
+    $vars->{'message'} = 'email_change_canceled';
+  }
 
-    $vars->{'old_email'} = $old_email;
-    $vars->{'new_email'} = $new_email;
-    Bugzilla::Token::Cancel($token, $vars->{'message'}, $vars);
+  $vars->{'old_email'} = $old_email;
+  $vars->{'new_email'} = $new_email;
+  Bugzilla::Token::Cancel($token, $vars->{'message'}, $vars);
 
-    $dbh->do(q{DELETE FROM tokens WHERE userid = ?
-               AND tokentype = 'emailold' OR tokentype = 'emailnew'},
-             undef, $userid);
+  $dbh->do(
+    q{DELETE FROM tokens WHERE userid = ?
+               AND tokentype = 'emailold' OR tokentype = 'emailnew'}, undef, $userid
+  );
 
-    $dbh->bz_commit_transaction();
+  $dbh->bz_commit_transaction();
 
-    # Return HTTP response headers.
-    print $cgi->header();
+  # Return HTTP response headers.
+  print $cgi->header();
 
-    $template->process("global/message.html.tmpl", $vars)
-      || ThrowTemplateError($template->error());
+  $template->process("global/message.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
 }
 
 sub request_create_account {
-    my ($date, $data, $token) = @_;
-
-    Bugzilla->user->check_account_creation_enabled;
-
-    # Be careful! Some logins may contain ":" in them.
-    my ($email, $login) = split(':', $data, 2);
-    $vars = {
-      token => $token,
-      login => $login,
-      email => $email,
-      # Make sure nobody else chose this login meanwhile.
-      login_already_in_use => login_to_id($login) ? 1 : 0,
-      expiration_ts => ctime(str2time($date) + MAX_TOKEN_AGE * 86400)
-    };
-
-    print $cgi->header();
-    $template->process('account/email/confirm-new.html.tmpl', $vars)
-      || ThrowTemplateError($template->error());
+  my ($date, $data, $token) = @_;
+
+  Bugzilla->user->check_account_creation_enabled;
+
+  # Be careful! Some logins may contain ":" in them.
+  my ($email, $login) = split(':', $data, 2);
+  $vars = {
+    token => $token,
+    login => $login,
+    email => $email,
+
+    # Make sure nobody else chose this login meanwhile.
+    login_already_in_use => login_to_id($login) ? 1 : 0,
+    expiration_ts => ctime(str2time($date) + MAX_TOKEN_AGE * 86400)
+  };
+
+  print $cgi->header();
+  $template->process('account/email/confirm-new.html.tmpl', $vars)
+    || ThrowTemplateError($template->error());
 }
 
 sub confirm_create_account {
-    my ($data, $token) = @_;
+  my ($data, $token) = @_;
+
+  Bugzilla->user->check_account_creation_enabled;
 
-    Bugzilla->user->check_account_creation_enabled;
+  my $password = $cgi->param('passwd1') || '';
+  validate_password($password, $cgi->param('passwd2') || '');
 
-    my $password = $cgi->param('passwd1') || '';
-    validate_password($password, $cgi->param('passwd2') || '');
-    # Make sure that these never show up anywhere in the UI.
-    $cgi->delete('passwd1', 'passwd2');
+  # Make sure that these never show up anywhere in the UI.
+  $cgi->delete('passwd1', 'passwd2');
 
-    # Be careful! Some logins may contain ":" in them.
-    my ($email, $login) = split(':', $data, 2);
-    $login = $cgi->param('login') if login_to_id($login);
+  # Be careful! Some logins may contain ":" in them.
+  my ($email, $login) = split(':', $data, 2);
+  $login = $cgi->param('login') if login_to_id($login);
 
-    my $otheruser = Bugzilla::User->create({
-        login_name => $login,
-        email      => $email,
-        realname   => scalar $cgi->param('realname'),
-        cryptpassword => $password});
+  my $otheruser = Bugzilla::User->create({
+    login_name    => $login,
+    email         => $email,
+    realname      => scalar $cgi->param('realname'),
+    cryptpassword => $password
+  });
 
-    # Now delete this token.
-    delete_token($token);
+  # Now delete this token.
+  delete_token($token);
 
-    # Let the user know that their user account has been successfully created.
-    $vars->{'message'} = 'account_created';
-    $vars->{'otheruser'} = $otheruser;
+  # Let the user know that their user account has been successfully created.
+  $vars->{'message'}   = 'account_created';
+  $vars->{'otheruser'} = $otheruser;
 
-    # Log in the new user using credentials they just gave.
-    $cgi->param('Bugzilla_login', $otheruser->login);
-    $cgi->param('Bugzilla_password', $password);
-    Bugzilla->login(LOGIN_OPTIONAL);
+  # Log in the new user using credentials they just gave.
+  $cgi->param('Bugzilla_login',    $otheruser->login);
+  $cgi->param('Bugzilla_password', $password);
+  Bugzilla->login(LOGIN_OPTIONAL);
 
-    print $cgi->header();
+  print $cgi->header();
 
-    $template->process('index.html.tmpl', $vars)
-      || ThrowTemplateError($template->error());
+  $template->process('index.html.tmpl', $vars)
+    || ThrowTemplateError($template->error());
 }
 
 sub cancel_create_account {
-    my ($data, $token) = @_;
+  my ($data, $token) = @_;
+
+  $vars->{'message'} = 'account_creation_canceled';
 
-    $vars->{'message'} = 'account_creation_canceled';
-    # Be careful! Some logins may contain ":" in them.
-    ($vars->{'email'}, $vars->{'login'}) = split(':', $data, 2);
-    Bugzilla::Token::Cancel($token, $vars->{'message'});
+  # Be careful! Some logins may contain ":" in them.
+  ($vars->{'email'}, $vars->{'login'}) = split(':', $data, 2);
+  Bugzilla::Token::Cancel($token, $vars->{'message'});
 
-    print $cgi->header();
-    $template->process('global/message.html.tmpl', $vars)
-      || ThrowTemplateError($template->error());
+  print $cgi->header();
+  $template->process('global/message.html.tmpl', $vars)
+    || ThrowTemplateError($template->error());
 }
index 54cad3c5156d140b1d3d6b6987ff48866ef1aaff..c9a3a16565302b5446f1735e03527bd29bfa8e11 100755 (executable)
@@ -28,548 +28,569 @@ my $template = Bugzilla->template;
 local our $vars = {};
 
 ###############################################################################
-# Each panel has two functions - panel Foo has a DoFoo, to get the data 
-# necessary for displaying the panel, and a SaveFoo, to save the panel's 
-# contents from the form data (if appropriate). 
-# SaveFoo may be called before DoFoo.    
+# Each panel has two functions - panel Foo has a DoFoo, to get the data
+# necessary for displaying the panel, and a SaveFoo, to save the panel's
+# contents from the form data (if appropriate).
+# SaveFoo may be called before DoFoo.
 ###############################################################################
 sub DoAccount {
-    my $dbh = Bugzilla->dbh;
-    my $user = Bugzilla->user;
-
-    if (Bugzilla->params->{'allowemailchange'}
-        && $user->authorizer->can_change_email)
-    {
-       # First delete old tokens.
-       Bugzilla::Token::CleanTokenTable();
-
-        my @token = $dbh->selectrow_array(
-            "SELECT tokentype, " .
-                    $dbh->sql_date_math('issuedate', '+', MAX_TOKEN_AGE, 'DAY')
-                    . ", eventdata
+  my $dbh  = Bugzilla->dbh;
+  my $user = Bugzilla->user;
+
+  if (Bugzilla->params->{'allowemailchange'}
+    && $user->authorizer->can_change_email)
+  {
+    # First delete old tokens.
+    Bugzilla::Token::CleanTokenTable();
+
+    my @token = $dbh->selectrow_array(
+          "SELECT tokentype, "
+        . $dbh->sql_date_math('issuedate', '+', MAX_TOKEN_AGE, 'DAY')
+        . ", eventdata
                FROM tokens
               WHERE userid = ?
                 AND tokentype LIKE 'email%'
-           ORDER BY tokentype ASC " . $dbh->sql_limit(1), undef, $user->id);
-        if (scalar(@token) > 0) {
-            my ($tokentype, $change_date, $eventdata) = @token;
-            $vars->{'email_change_date'} = $change_date;
-
-            if($tokentype eq 'emailnew') {
-                my ($oldemail,$newemail) = split(/:/,$eventdata);
-                $vars->{'new_email'} = $newemail;
-            }
-        }
+           ORDER BY tokentype ASC " . $dbh->sql_limit(1), undef, $user->id
+    );
+    if (scalar(@token) > 0) {
+      my ($tokentype, $change_date, $eventdata) = @token;
+      $vars->{'email_change_date'} = $change_date;
+
+      if ($tokentype eq 'emailnew') {
+        my ($oldemail, $newemail) = split(/:/, $eventdata);
+        $vars->{'new_email'} = $newemail;
+      }
     }
+  }
 }
 
 sub SaveAccount {
-    my $cgi = Bugzilla->cgi;
-    my $dbh = Bugzilla->dbh;
-    
-    $dbh->bz_start_transaction;
-
-    my $user = Bugzilla->user;
-
-    my $oldpassword = $cgi->param('old_password');
-    my $verified_password;
-    my $pwd1 = $cgi->param('new_password1');
-    my $pwd2 = $cgi->param('new_password2');
-    my $new_login = clean_text(scalar $cgi->param('new_login'));
-    my $new_email = clean_text(scalar $cgi->param('new_email'));
-
-    if ($user->authorizer->can_change_password
-        && ($pwd1 ne "" || $pwd2 ne ""))
-    {
-        $user->check_current_password($oldpassword);
-        $verified_password = 1;
+  my $cgi = Bugzilla->cgi;
+  my $dbh = Bugzilla->dbh;
 
-        $pwd1 || ThrowUserError("new_password_missing");
-        validate_password($pwd1, $pwd2);
+  $dbh->bz_start_transaction;
 
-        if ($oldpassword ne $pwd1) {
-            $user->set_password($pwd1);
-            # Invalidate all logins except for the current one
-            Bugzilla->logout(LOGOUT_KEEP_CURRENT);
-        }
-    }
+  my $user = Bugzilla->user;
 
-    # This is used only if email and login are separate
-    if ($user->authorizer->can_change_login
-        && !Bugzilla->params->{"use_email_as_login"}
-        && $new_login
-        && $user->login ne $new_login)
-    {
-        $verified_password || $user->check_current_password($oldpassword);
+  my $oldpassword = $cgi->param('old_password');
+  my $verified_password;
+  my $pwd1      = $cgi->param('new_password1');
+  my $pwd2      = $cgi->param('new_password2');
+  my $new_login = clean_text(scalar $cgi->param('new_login'));
+  my $new_email = clean_text(scalar $cgi->param('new_email'));
 
-        if ($new_login =~ /@/ &&
-            $new_login ne $user->email)
-        {
-            ThrowUserError("login_at_sign_disallowed");
-        }
+  if ($user->authorizer->can_change_password && ($pwd1 ne "" || $pwd2 ne "")) {
+    $user->check_current_password($oldpassword);
+    $verified_password = 1;
 
-        if (Bugzilla::Token::HasEmailChangeToken($user->id)) {
-            ThrowUserError("login_change_during_email_change");
-        }
+    $pwd1 || ThrowUserError("new_password_missing");
+    validate_password($pwd1, $pwd2);
+
+    if ($oldpassword ne $pwd1) {
+      $user->set_password($pwd1);
 
-        $user->set_login($new_login);
+      # Invalidate all logins except for the current one
+      Bugzilla->logout(LOGOUT_KEEP_CURRENT);
+    }
+  }
+
+  # This is used only if email and login are separate
+  if ( $user->authorizer->can_change_login
+    && !Bugzilla->params->{"use_email_as_login"}
+    && $new_login
+    && $user->login ne $new_login)
+  {
+    $verified_password || $user->check_current_password($oldpassword);
+
+    if ($new_login =~ /@/ && $new_login ne $user->email) {
+      ThrowUserError("login_at_sign_disallowed");
     }
 
-    # This is used for the single value if use_email_as_login is true, or for
-    # the email address otherwise.
-    if ($user->authorizer->can_change_email
-        && Bugzilla->params->{"allowemailchange"}
-        && $new_email
-        && $user->email ne $new_email)
-    {
-        $verified_password || $user->check_current_password($oldpassword);
+    if (Bugzilla::Token::HasEmailChangeToken($user->id)) {
+      ThrowUserError("login_change_during_email_change");
+    }
 
-        # Block multiple email changes for the same user.
-        if (Bugzilla::Token::HasEmailChangeToken($user->id)) {
-            ThrowUserError("email_change_in_progress");
-        }
+    $user->set_login($new_login);
+  }
+
+  # This is used for the single value if use_email_as_login is true, or for
+  # the email address otherwise.
+  if ( $user->authorizer->can_change_email
+    && Bugzilla->params->{"allowemailchange"}
+    && $new_email
+    && $user->email ne $new_email)
+  {
+    $verified_password || $user->check_current_password($oldpassword);
+
+    # Block multiple email changes for the same user.
+    if (Bugzilla::Token::HasEmailChangeToken($user->id)) {
+      ThrowUserError("email_change_in_progress");
+    }
 
-        # Before changing to an email address, confirm it does not exist.
-        $user->check_email($new_email);
+    # Before changing to an email address, confirm it does not exist.
+    $user->check_email($new_email);
 
-        $vars->{'email_token'} = Bugzilla::Token::IssueEmailChangeToken($new_email);
-        $vars->{'email_changes_saved'} = 1;
-    }
-    $user->set_name(scalar $cgi->param('realname'));
-    $user->update({ keep_session => 1, keep_tokens => 1 });
-    $dbh->bz_commit_transaction;
+    $vars->{'email_token'} = Bugzilla::Token::IssueEmailChangeToken($new_email);
+    $vars->{'email_changes_saved'} = 1;
+  }
+  $user->set_name(scalar $cgi->param('realname'));
+  $user->update({keep_session => 1, keep_tokens => 1});
+  $dbh->bz_commit_transaction;
 }
 
 
 sub DoSettings {
-    my $user = Bugzilla->user;
+  my $user = Bugzilla->user;
 
-    my $settings = $user->settings;
-    $vars->{'settings'} = $settings;
+  my $settings = $user->settings;
+  $vars->{'settings'} = $settings;
 
-    my @setting_list = sort keys %$settings;
-    $vars->{'setting_names'} = \@setting_list;
+  my @setting_list = sort keys %$settings;
+  $vars->{'setting_names'} = \@setting_list;
 
-    $vars->{'has_settings_enabled'} = 0;
-    # Is there at least one user setting enabled?
-    foreach my $setting_name (@setting_list) {
-        if ($settings->{"$setting_name"}->{'is_enabled'}) {
-            $vars->{'has_settings_enabled'} = 1;
-            last;
-        }
+  $vars->{'has_settings_enabled'} = 0;
+
+  # Is there at least one user setting enabled?
+  foreach my $setting_name (@setting_list) {
+    if ($settings->{"$setting_name"}->{'is_enabled'}) {
+      $vars->{'has_settings_enabled'} = 1;
+      last;
     }
-    $vars->{'dont_show_button'} = !$vars->{'has_settings_enabled'};
+  }
+  $vars->{'dont_show_button'} = !$vars->{'has_settings_enabled'};
 }
 
 sub SaveSettings {
-    my $cgi = Bugzilla->cgi;
-    my $user = Bugzilla->user;
-
-    my $settings = $user->settings;
-    my @setting_list = keys %$settings;
-
-    foreach my $name (@setting_list) {
-        next if ! ($settings->{$name}->{'is_enabled'});
-        my $value = $cgi->param($name);
-        next unless defined $value;
-        my $setting = new Bugzilla::User::Setting($name);
-
-        if ($value eq "${name}-isdefault" ) {
-            if (! $settings->{$name}->{'is_default'}) {
-                $settings->{$name}->reset_to_default;
-            }
-        }
-        else {
-            $setting->validate_value($value);
-            $settings->{$name}->set($value);
-        }
+  my $cgi  = Bugzilla->cgi;
+  my $user = Bugzilla->user;
+
+  my $settings     = $user->settings;
+  my @setting_list = keys %$settings;
+
+  foreach my $name (@setting_list) {
+    next if !($settings->{$name}->{'is_enabled'});
+    my $value = $cgi->param($name);
+    next unless defined $value;
+    my $setting = new Bugzilla::User::Setting($name);
+
+    if ($value eq "${name}-isdefault") {
+      if (!$settings->{$name}->{'is_default'}) {
+        $settings->{$name}->reset_to_default;
+      }
+    }
+    else {
+      $setting->validate_value($value);
+      $settings->{$name}->set($value);
     }
-    $vars->{'settings'} = $user->settings(1);
-    clear_settings_cache($user->id);
+  }
+  $vars->{'settings'} = $user->settings(1);
+  clear_settings_cache($user->id);
 }
 
 sub DoEmail {
-    my $dbh = Bugzilla->dbh;
-    my $user = Bugzilla->user;
-    
-    ###########################################################################
-    # User watching
-    ###########################################################################
-    my $watched_ref = $dbh->selectcol_arrayref(
-        "SELECT profiles.login_name FROM watch INNER JOIN profiles" .
-        " ON watch.watched = profiles.userid" .
-        " WHERE watcher = ?" .
-        " ORDER BY profiles.login_name",
-        undef, $user->id);
-    $vars->{'watchedusers'} = $watched_ref;
-
-    my $watcher_ids = $dbh->selectcol_arrayref(
-        "SELECT watcher FROM watch WHERE watched = ?",
-        undef, $user->id);
-
-    my @watchers;
-    foreach my $watcher_id (@$watcher_ids) {
-        my $watcher = new Bugzilla::User($watcher_id);
-        push(@watchers, Bugzilla::User::identity($watcher));
-    }
-
-    @watchers = sort { lc($a) cmp lc($b) } @watchers;
-    $vars->{'watchers'} = \@watchers;
+  my $dbh  = Bugzilla->dbh;
+  my $user = Bugzilla->user;
+
+  ###########################################################################
+  # User watching
+  ###########################################################################
+  my $watched_ref = $dbh->selectcol_arrayref(
+    "SELECT profiles.login_name FROM watch INNER JOIN profiles"
+      . " ON watch.watched = profiles.userid"
+      . " WHERE watcher = ?"
+      . " ORDER BY profiles.login_name",
+    undef, $user->id
+  );
+  $vars->{'watchedusers'} = $watched_ref;
+
+  my $watcher_ids
+    = $dbh->selectcol_arrayref("SELECT watcher FROM watch WHERE watched = ?",
+    undef, $user->id);
+
+  my @watchers;
+  foreach my $watcher_id (@$watcher_ids) {
+    my $watcher = new Bugzilla::User($watcher_id);
+    push(@watchers, Bugzilla::User::identity($watcher));
+  }
+
+  @watchers = sort { lc($a) cmp lc($b) } @watchers;
+  $vars->{'watchers'} = \@watchers;
 }
 
 sub SaveEmail {
-    my $dbh = Bugzilla->dbh;
-    my $cgi = Bugzilla->cgi;
-    my $user = Bugzilla->user;
-
-    Bugzilla::User::match_field({ 'new_watchedusers' => {'type' => 'multi'} });
-
-    ###########################################################################
-    # Role-based preferences
-    ###########################################################################
-    $dbh->bz_start_transaction();
-
-    my $sth_insert = $dbh->prepare('INSERT INTO email_setting
-                                    (user_id, relationship, event) VALUES (?, ?, ?)');
-
-    my $sth_delete = $dbh->prepare('DELETE FROM email_setting
-                                    WHERE user_id = ? AND relationship = ? AND event = ?');
-    # Load current email preferences into memory before updating them.
-    my $settings = $user->mail_settings;
-
-    # Update the table - first, with normal events in the
-    # relationship/event matrix.
-    my %relationships = Bugzilla::BugMail::relationships();
-    foreach my $rel (keys %relationships) {
-        next if ($rel == REL_QA && !Bugzilla->params->{'useqacontact'});
-        # Positive events: a ticked box means "send me mail."
-        foreach my $event (POS_EVENTS) {
-            my $is_set = $cgi->param("email-$rel-$event");
-            if ($is_set xor $settings->{$rel}{$event}) {
-                if ($is_set) {
-                    $sth_insert->execute($user->id, $rel, $event);
-                }
-                else {
-                    $sth_delete->execute($user->id, $rel, $event);
-                }
-            }
+  my $dbh  = Bugzilla->dbh;
+  my $cgi  = Bugzilla->cgi;
+  my $user = Bugzilla->user;
+
+  Bugzilla::User::match_field({'new_watchedusers' => {'type' => 'multi'}});
+
+  ###########################################################################
+  # Role-based preferences
+  ###########################################################################
+  $dbh->bz_start_transaction();
+
+  my $sth_insert = $dbh->prepare(
+    'INSERT INTO email_setting
+                                    (user_id, relationship, event) VALUES (?, ?, ?)'
+  );
+
+  my $sth_delete = $dbh->prepare(
+    'DELETE FROM email_setting
+                                    WHERE user_id = ? AND relationship = ? AND event = ?'
+  );
+
+  # Load current email preferences into memory before updating them.
+  my $settings = $user->mail_settings;
+
+  # Update the table - first, with normal events in the
+  # relationship/event matrix.
+  my %relationships = Bugzilla::BugMail::relationships();
+  foreach my $rel (keys %relationships) {
+    next if ($rel == REL_QA && !Bugzilla->params->{'useqacontact'});
+
+    # Positive events: a ticked box means "send me mail."
+    foreach my $event (POS_EVENTS) {
+      my $is_set = $cgi->param("email-$rel-$event");
+      if ($is_set xor $settings->{$rel}{$event}) {
+        if ($is_set) {
+          $sth_insert->execute($user->id, $rel, $event);
         }
-        
-        # Negative events: a ticked box means "don't send me mail."
-        foreach my $event (NEG_EVENTS) {
-            my $is_set = $cgi->param("neg-email-$rel-$event");
-            if (!$is_set xor $settings->{$rel}{$event}) {
-                if (!$is_set) {
-                    $sth_insert->execute($user->id, $rel, $event);
-                }
-                else {
-                    $sth_delete->execute($user->id, $rel, $event);
-                }
-            }
+        else {
+          $sth_delete->execute($user->id, $rel, $event);
         }
+      }
     }
 
-    # Global positive events: a ticked box means "send me mail."
-    foreach my $event (GLOBAL_EVENTS) {
-        my $is_set = $cgi->param("email-" . REL_ANY . "-$event");
-        if ($is_set xor $settings->{+REL_ANY}{$event}) {
-            if ($is_set) {
-                $sth_insert->execute($user->id, REL_ANY, $event);
-            }
-            else {
-                $sth_delete->execute($user->id, REL_ANY, $event);
-            }
+    # Negative events: a ticked box means "don't send me mail."
+    foreach my $event (NEG_EVENTS) {
+      my $is_set = $cgi->param("neg-email-$rel-$event");
+      if (!$is_set xor $settings->{$rel}{$event}) {
+        if (!$is_set) {
+          $sth_insert->execute($user->id, $rel, $event);
+        }
+        else {
+          $sth_delete->execute($user->id, $rel, $event);
         }
+      }
     }
+  }
+
+  # Global positive events: a ticked box means "send me mail."
+  foreach my $event (GLOBAL_EVENTS) {
+    my $is_set = $cgi->param("email-" . REL_ANY . "-$event");
+    if ($is_set xor $settings->{+REL_ANY}{$event}) {
+      if ($is_set) {
+        $sth_insert->execute($user->id, REL_ANY, $event);
+      }
+      else {
+        $sth_delete->execute($user->id, REL_ANY, $event);
+      }
+    }
+  }
 
-    $dbh->bz_commit_transaction();
-
-    # We have to clear the cache about email preferences.
-    delete $user->{'mail_settings'};
-
-    ###########################################################################
-    # User watching
-    ###########################################################################
-    if (defined $cgi->param('new_watchedusers')
-        || defined $cgi->param('remove_watched_users'))
-    {
-        $dbh->bz_start_transaction();
-
-        # Use this to protect error messages on duplicate submissions
-        my $old_watch_ids =
-            $dbh->selectcol_arrayref("SELECT watched FROM watch"
-                                   . " WHERE watcher = ?", undef, $user->id);
+  $dbh->bz_commit_transaction();
 
-        # The new information given to us by the user.
-        my $new_watched_users = join(',', $cgi->param('new_watchedusers')) || '';
-        my @new_watch_names = split(/[,\s]+/, $new_watched_users);
-        my %new_watch_ids;
+  # We have to clear the cache about email preferences.
+  delete $user->{'mail_settings'};
 
-        foreach my $username (@new_watch_names) {
-            my $watched_userid = login_to_id(trim($username), THROW_ERROR);
-            $new_watch_ids{$watched_userid} = 1;
-        }
+  ###########################################################################
+  # User watching
+  ###########################################################################
+  if ( defined $cgi->param('new_watchedusers')
+    || defined $cgi->param('remove_watched_users'))
+  {
+    $dbh->bz_start_transaction();
 
-        # Add people who were added.
-        my $insert_sth = $dbh->prepare('INSERT INTO watch (watched, watcher)'
-                                     . ' VALUES (?, ?)');
-        foreach my $add_me (keys(%new_watch_ids)) {
-            next if grep($_ == $add_me, @$old_watch_ids);
-            $insert_sth->execute($add_me, $user->id);
-        }
+    # Use this to protect error messages on duplicate submissions
+    my $old_watch_ids
+      = $dbh->selectcol_arrayref("SELECT watched FROM watch" . " WHERE watcher = ?",
+      undef, $user->id);
 
-        if (defined $cgi->param('remove_watched_users')) {
-            my @removed = $cgi->multi_param('watched_by_you');
-            # Remove people who were removed.
-            my $delete_sth = $dbh->prepare('DELETE FROM watch WHERE watched = ?'
-                                         . ' AND watcher = ?');
-            
-            my %remove_watch_ids;
-            foreach my $username (@removed) {
-                my $watched_userid = login_to_id(trim($username), THROW_ERROR);
-                $remove_watch_ids{$watched_userid} = 1;
-            }
-            foreach my $remove_me (keys(%remove_watch_ids)) {
-                $delete_sth->execute($remove_me, $user->id);
-            }
-        }
+    # The new information given to us by the user.
+    my $new_watched_users = join(',', $cgi->param('new_watchedusers')) || '';
+    my @new_watch_names = split(/[,\s]+/, $new_watched_users);
+    my %new_watch_ids;
 
-        $dbh->bz_commit_transaction();
+    foreach my $username (@new_watch_names) {
+      my $watched_userid = login_to_id(trim($username), THROW_ERROR);
+      $new_watch_ids{$watched_userid} = 1;
     }
 
-    ###########################################################################
-    # Ignore Bugs
-    ###########################################################################
-    my %ignored_bugs = map { $_->{'id'} => 1 } @{$user->bugs_ignored};
-
-    # Validate the new bugs to ignore by checking that they exist and also
-    # if the user gave an alias
-    my @add_ignored = split(/[\s,]+/, $cgi->param('add_ignored_bugs'));
-    @add_ignored = map { Bugzilla::Bug->check($_)->id } @add_ignored;
-    map { $ignored_bugs{$_} = 1 } @add_ignored;
-
-    # Remove any bug ids the user no longer wants to ignore
-    foreach my $key (grep(/^remove_ignored_bug_/, $cgi->multi_param())) {
-        my ($bug_id) = $key =~ /(\d+)$/a;
-        delete $ignored_bugs{$bug_id};
+    # Add people who were added.
+    my $insert_sth
+      = $dbh->prepare('INSERT INTO watch (watched, watcher)' . ' VALUES (?, ?)');
+    foreach my $add_me (keys(%new_watch_ids)) {
+      next if grep($_ == $add_me, @$old_watch_ids);
+      $insert_sth->execute($add_me, $user->id);
     }
 
-    # Update the database with any changes made
-    my ($removed, $added) = diff_arrays([ map { $_->{'id'} } @{$user->bugs_ignored} ],
-                                        [ keys %ignored_bugs ]);
+    if (defined $cgi->param('remove_watched_users')) {
+      my @removed = $cgi->multi_param('watched_by_you');
+
+      # Remove people who were removed.
+      my $delete_sth
+        = $dbh->prepare('DELETE FROM watch WHERE watched = ?' . ' AND watcher = ?');
+
+      my %remove_watch_ids;
+      foreach my $username (@removed) {
+        my $watched_userid = login_to_id(trim($username), THROW_ERROR);
+        $remove_watch_ids{$watched_userid} = 1;
+      }
+      foreach my $remove_me (keys(%remove_watch_ids)) {
+        $delete_sth->execute($remove_me, $user->id);
+      }
+    }
 
-    if (scalar @$removed || scalar @$added) {
-        $dbh->bz_start_transaction();
+    $dbh->bz_commit_transaction();
+  }
+
+  ###########################################################################
+  # Ignore Bugs
+  ###########################################################################
+  my %ignored_bugs = map { $_->{'id'} => 1 } @{$user->bugs_ignored};
+
+  # Validate the new bugs to ignore by checking that they exist and also
+  # if the user gave an alias
+  my @add_ignored = split(/[\s,]+/, $cgi->param('add_ignored_bugs'));
+  @add_ignored = map { Bugzilla::Bug->check($_)->id } @add_ignored;
+  map { $ignored_bugs{$_} = 1 } @add_ignored;
+
+  # Remove any bug ids the user no longer wants to ignore
+  foreach my $key (grep(/^remove_ignored_bug_/, $cgi->multi_param())) {
+    my ($bug_id) = $key =~ /(\d+)$/a;
+    delete $ignored_bugs{$bug_id};
+  }
+
+  # Update the database with any changes made
+  my ($removed, $added)
+    = diff_arrays([map { $_->{'id'} } @{$user->bugs_ignored}],
+    [keys %ignored_bugs]);
+
+  if (scalar @$removed || scalar @$added) {
+    $dbh->bz_start_transaction();
 
-        if (scalar @$removed) {
-            $dbh->do('DELETE FROM email_bug_ignore WHERE user_id = ? AND ' . 
-                     $dbh->sql_in('bug_id', $removed),
-                     undef, $user->id);
-        }
-        if (scalar @$added) {
-            my $sth = $dbh->prepare('INSERT INTO email_bug_ignore
-                                     (user_id, bug_id) VALUES (?, ?)');
-            $sth->execute($user->id, $_) foreach @$added;
-        }
+    if (scalar @$removed) {
+      $dbh->do(
+        'DELETE FROM email_bug_ignore WHERE user_id = ? AND '
+          . $dbh->sql_in('bug_id', $removed),
+        undef, $user->id
+      );
+    }
+    if (scalar @$added) {
+      my $sth = $dbh->prepare(
+        'INSERT INTO email_bug_ignore
+                                     (user_id, bug_id) VALUES (?, ?)'
+      );
+      $sth->execute($user->id, $_) foreach @$added;
+    }
 
-        # Reset the cache of ignored bugs if the list changed.
-        delete $user->{bugs_ignored};
+    # Reset the cache of ignored bugs if the list changed.
+    delete $user->{bugs_ignored};
 
-        $dbh->bz_commit_transaction();
-    }
+    $dbh->bz_commit_transaction();
+  }
 }
 
 
 sub DoPermissions {
-    my $dbh = Bugzilla->dbh;
-    my $user = Bugzilla->user;
-    my (@has_bits, @set_bits);
-
-    my $groups = $dbh->selectall_arrayref(
-               "SELECT DISTINCT name, description FROM groups WHERE id IN (" .
-               $user->groups_as_string . ") ORDER BY name");
-    foreach my $group (@$groups) {
-        my ($nam, $desc) = @$group;
-        push(@has_bits, {"desc" => $desc, "name" => $nam});
-    }
-    $groups = $dbh->selectall_arrayref('SELECT DISTINCT id, name, description
+  my $dbh  = Bugzilla->dbh;
+  my $user = Bugzilla->user;
+  my (@has_bits, @set_bits);
+
+  my $groups
+    = $dbh->selectall_arrayref(
+        "SELECT DISTINCT name, description FROM groups WHERE id IN ("
+      . $user->groups_as_string
+      . ") ORDER BY name");
+  foreach my $group (@$groups) {
+    my ($nam, $desc) = @$group;
+    push(@has_bits, {"desc" => $desc, "name" => $nam});
+  }
+  $groups = $dbh->selectall_arrayref(
+    'SELECT DISTINCT id, name, description
                                           FROM groups
-                                         ORDER BY name');
-    foreach my $group (@$groups) {
-        my ($group_id, $nam, $desc) = @$group;
-        if ($user->can_bless($group_id)) {
-            push(@set_bits, {"desc" => $desc, "name" => $nam});
-        }
+                                         ORDER BY name'
+  );
+  foreach my $group (@$groups) {
+    my ($group_id, $nam, $desc) = @$group;
+    if ($user->can_bless($group_id)) {
+      push(@set_bits, {"desc" => $desc, "name" => $nam});
     }
+  }
 
-    # If the user has product specific privileges, inform them about that.
-    foreach my $privs (PER_PRODUCT_PRIVILEGES) {
-        next if $user->in_group($privs);
-        $vars->{"local_$privs"} = $user->get_products_by_permission($privs);
-    }
+  # If the user has product specific privileges, inform them about that.
+  foreach my $privs (PER_PRODUCT_PRIVILEGES) {
+    next if $user->in_group($privs);
+    $vars->{"local_$privs"} = $user->get_products_by_permission($privs);
+  }
 
-    $vars->{'has_bits'} = \@has_bits;
-    $vars->{'set_bits'} = \@set_bits;    
+  $vars->{'has_bits'} = \@has_bits;
+  $vars->{'set_bits'} = \@set_bits;
 }
 
 # No SavePermissions() because this panel has no changeable fields.
 
 
 sub DoSavedSearches {
-    my $dbh = Bugzilla->dbh;
-    my $user = Bugzilla->user;
-
-    if ($user->queryshare_groups_as_string) {
-        $vars->{'queryshare_groups'} =
-            Bugzilla::Group->new_from_list($user->queryshare_groups);
-    }
-    $vars->{'bless_group_ids'} = [map { $_->id } @{$user->bless_groups}];
+  my $dbh  = Bugzilla->dbh;
+  my $user = Bugzilla->user;
+
+  if ($user->queryshare_groups_as_string) {
+    $vars->{'queryshare_groups'}
+      = Bugzilla::Group->new_from_list($user->queryshare_groups);
+  }
+  $vars->{'bless_group_ids'} = [map { $_->id } @{$user->bless_groups}];
 }
 
 sub SaveSavedSearches {
-    my $cgi = Bugzilla->cgi;
-    my $dbh = Bugzilla->dbh;
-    my $user = Bugzilla->user;
+  my $cgi  = Bugzilla->cgi;
+  my $dbh  = Bugzilla->dbh;
+  my $user = Bugzilla->user;
 
-    # We'll need this in a loop, so do the call once.
-    my $user_id = $user->id;
+  # We'll need this in a loop, so do the call once.
+  my $user_id = $user->id;
 
-    my $sth_insert_nl = $dbh->prepare('INSERT INTO namedqueries_link_in_footer
+  my $sth_insert_nl = $dbh->prepare(
+    'INSERT INTO namedqueries_link_in_footer
                                        (namedquery_id, user_id)
-                                       VALUES (?, ?)');
-    my $sth_delete_nl = $dbh->prepare('DELETE FROM namedqueries_link_in_footer
+                                       VALUES (?, ?)'
+  );
+  my $sth_delete_nl = $dbh->prepare(
+    'DELETE FROM namedqueries_link_in_footer
                                              WHERE namedquery_id = ?
-                                               AND user_id = ?');
-    my $sth_insert_ngm = $dbh->prepare('INSERT INTO namedquery_group_map
+                                               AND user_id = ?'
+  );
+  my $sth_insert_ngm = $dbh->prepare(
+    'INSERT INTO namedquery_group_map
                                         (namedquery_id, group_id)
-                                        VALUES (?, ?)');
-    my $sth_update_ngm = $dbh->prepare('UPDATE namedquery_group_map
+                                        VALUES (?, ?)'
+  );
+  my $sth_update_ngm = $dbh->prepare(
+    'UPDATE namedquery_group_map
                                            SET group_id = ?
-                                         WHERE namedquery_id = ?');
-    my $sth_delete_ngm = $dbh->prepare('DELETE FROM namedquery_group_map
-                                              WHERE namedquery_id = ?');
-
-    # Update namedqueries_link_in_footer for this user.
-    foreach my $q (@{$user->queries}, @{$user->queries_available}) {
-        if (defined $cgi->param("link_in_footer_" . $q->id)) {
-            $sth_insert_nl->execute($q->id, $user_id) if !$q->link_in_footer;
-        }
-        else {
-            $sth_delete_nl->execute($q->id, $user_id) if $q->link_in_footer;
-        }
+                                         WHERE namedquery_id = ?'
+  );
+  my $sth_delete_ngm = $dbh->prepare(
+    'DELETE FROM namedquery_group_map
+                                              WHERE namedquery_id = ?'
+  );
+
+  # Update namedqueries_link_in_footer for this user.
+  foreach my $q (@{$user->queries}, @{$user->queries_available}) {
+    if (defined $cgi->param("link_in_footer_" . $q->id)) {
+      $sth_insert_nl->execute($q->id, $user_id) if !$q->link_in_footer;
     }
+    else {
+      $sth_delete_nl->execute($q->id, $user_id) if $q->link_in_footer;
+    }
+  }
 
-    # For user's own queries, update namedquery_group_map.
-    foreach my $q (@{$user->queries}) {
-        my $group_id;
+  # For user's own queries, update namedquery_group_map.
+  foreach my $q (@{$user->queries}) {
+    my $group_id;
 
-        if ($user->in_group(Bugzilla->params->{'querysharegroup'})) {
-            $group_id = $cgi->param("share_" . $q->id) || '';
-        }
+    if ($user->in_group(Bugzilla->params->{'querysharegroup'})) {
+      $group_id = $cgi->param("share_" . $q->id) || '';
+    }
 
-        if ($group_id) {
-            # Don't allow the user to share queries with groups they're not
-            # allowed to.
-            next unless grep($_ eq $group_id, @{$user->queryshare_groups});
-
-            # $group_id is now definitely a valid ID of a group the
-            # user can share queries with, so we can trick_taint.
-            detaint_natural($group_id);
-            if ($q->shared_with_group) {
-                $sth_update_ngm->execute($group_id, $q->id);
-            }
-            else {
-                $sth_insert_ngm->execute($q->id, $group_id);
-            }
-
-            # If we're sharing our query with a group we can bless, we 
-            # have the ability to add link to our search to the footer of
-            # direct group members automatically.
-            if ($user->can_bless($group_id) && $cgi->param('force_' . $q->id)) {
-                my $group = new Bugzilla::Group($group_id);
-                my $members = $group->members_non_inherited;
-                foreach my $member (@$members) {
-                    next if $member->id == $user->id;
-                    $sth_insert_nl->execute($q->id, $member->id)
-                        if !$q->link_in_footer($member);
-                }
-            }
-        }
-        else {
-            # They have unshared that query.
-            if ($q->shared_with_group) {
-                $sth_delete_ngm->execute($q->id);
-            }
-
-            # Don't remove namedqueries_link_in_footer entries for users
-            # subscribing to the shared query. The idea is that they will
-            # probably want to be subscribers again should the sharing
-            # user choose to share the query again.
+    if ($group_id) {
+
+      # Don't allow the user to share queries with groups they're not
+      # allowed to.
+      next unless grep($_ eq $group_id, @{$user->queryshare_groups});
+
+      # $group_id is now definitely a valid ID of a group the
+      # user can share queries with, so we can trick_taint.
+      detaint_natural($group_id);
+      if ($q->shared_with_group) {
+        $sth_update_ngm->execute($group_id, $q->id);
+      }
+      else {
+        $sth_insert_ngm->execute($q->id, $group_id);
+      }
+
+      # If we're sharing our query with a group we can bless, we
+      # have the ability to add link to our search to the footer of
+      # direct group members automatically.
+      if ($user->can_bless($group_id) && $cgi->param('force_' . $q->id)) {
+        my $group   = new Bugzilla::Group($group_id);
+        my $members = $group->members_non_inherited;
+        foreach my $member (@$members) {
+          next if $member->id == $user->id;
+          $sth_insert_nl->execute($q->id, $member->id) if !$q->link_in_footer($member);
         }
+      }
+    }
+    else {
+      # They have unshared that query.
+      if ($q->shared_with_group) {
+        $sth_delete_ngm->execute($q->id);
+      }
+
+      # Don't remove namedqueries_link_in_footer entries for users
+      # subscribing to the shared query. The idea is that they will
+      # probably want to be subscribers again should the sharing
+      # user choose to share the query again.
     }
+  }
 
-    $user->flush_queries_cache;
+  $user->flush_queries_cache;
 
-    # Update profiles.mybugslink.
-    my $showmybugslink = defined($cgi->param("showmybugslink")) ? 1 : 0;
-    $dbh->do("UPDATE profiles SET mybugslink = ? WHERE userid = ?",
-             undef, ($showmybugslink, $user->id));
-    $user->{'showmybugslink'} = $showmybugslink;
-    Bugzilla->memcached->clear({ table => 'profiles', id => $user->id });
+  # Update profiles.mybugslink.
+  my $showmybugslink = defined($cgi->param("showmybugslink")) ? 1 : 0;
+  $dbh->do("UPDATE profiles SET mybugslink = ? WHERE userid = ?",
+    undef, ($showmybugslink, $user->id));
+  $user->{'showmybugslink'} = $showmybugslink;
+  Bugzilla->memcached->clear({table => 'profiles', id => $user->id});
 }
 
 
 sub DoApiKey {
-    my $user = Bugzilla->user;
+  my $user = Bugzilla->user;
 
-    my $api_keys = Bugzilla::User::APIKey->match({ user_id => $user->id });
-    $vars->{api_keys} = $api_keys;
-    $vars->{any_revoked} = grep { $_->revoked } @$api_keys;
+  my $api_keys = Bugzilla::User::APIKey->match({user_id => $user->id});
+  $vars->{api_keys} = $api_keys;
+  $vars->{any_revoked} = grep { $_->revoked } @$api_keys;
 }
 
 sub SaveApiKey {
-    my $cgi = Bugzilla->cgi;
-    my $dbh = Bugzilla->dbh;
-    my $user = Bugzilla->user;
-
-    # Do it in a transaction.
-    $dbh->bz_start_transaction;
-
-    # Update any existing keys
-    my $api_keys = Bugzilla::User::APIKey->match({ user_id => $user->id });
-    foreach my $api_key (@$api_keys) {
-        my $description = $cgi->param('description_' . $api_key->id);
-        my $revoked = $cgi->param('revoked_' . $api_key->id);
-
-        if ($description ne $api_key->description
-            || $revoked != $api_key->revoked)
-        {
-            $api_key->set_all({
-                description => $description,
-                revoked     => $revoked,
-            });
-            $api_key->update();
-        }
-    }
-
-    # Create a new API key if requested.
-    if ($cgi->param('new_key')) {
-        $vars->{new_key} = Bugzilla::User::APIKey->create({
-            user_id     => $user->id,
-            description => scalar $cgi->param('new_description'),
-        });
-
-        # As a security precaution, we always sent out an e-mail when
-        # an API key is created
-        my $template = Bugzilla->template_inner($user->setting('lang'));
-        my $message;
-        $template->process('email/new-api-key.txt.tmpl', $vars, \$message)
-          || ThrowTemplateError($template->error());
-
-        MessageToMTA($message);
+  my $cgi  = Bugzilla->cgi;
+  my $dbh  = Bugzilla->dbh;
+  my $user = Bugzilla->user;
+
+  # Do it in a transaction.
+  $dbh->bz_start_transaction;
+
+  # Update any existing keys
+  my $api_keys = Bugzilla::User::APIKey->match({user_id => $user->id});
+  foreach my $api_key (@$api_keys) {
+    my $description = $cgi->param('description_' . $api_key->id);
+    my $revoked     = $cgi->param('revoked_' . $api_key->id);
+
+    if ($description ne $api_key->description || $revoked != $api_key->revoked) {
+      $api_key->set_all({description => $description, revoked => $revoked,});
+      $api_key->update();
     }
-
-    $dbh->bz_commit_transaction;
+  }
+
+  # Create a new API key if requested.
+  if ($cgi->param('new_key')) {
+    $vars->{new_key}
+      = Bugzilla::User::APIKey->create({
+      user_id => $user->id, description => scalar $cgi->param('new_description'),
+      });
+
+    # As a security precaution, we always sent out an e-mail when
+    # an API key is created
+    my $template = Bugzilla->template_inner($user->setting('lang'));
+    my $message;
+    $template->process('email/new-api-key.txt.tmpl', $vars, \$message)
+      || ThrowTemplateError($template->error());
+
+    MessageToMTA($message);
+  }
+
+  $dbh->bz_commit_transaction;
 }
 
 ###############################################################################
@@ -586,9 +607,10 @@ $cgi->delete('GoAheadAndLogIn');
 my $user = Bugzilla->login(LOGIN_OPTIONAL);
 
 if (!$user->id) {
-    # Use credentials given in the form if login cookies are not available.
-    $cgi->param('Bugzilla_login', $cgi->param('old_login'));
-    $cgi->param('Bugzilla_password', $cgi->param('old_password'));
+
+  # Use credentials given in the form if login cookies are not available.
+  $cgi->param('Bugzilla_login',    $cgi->param('old_login'));
+  $cgi->param('Bugzilla_password', $cgi->param('old_password'));
 }
 Bugzilla->login(LOGIN_REQUIRED);
 
@@ -608,52 +630,55 @@ check_token_data($token, 'edit_user_prefs') if $save_changes;
 # Do any saving, and then display the current tab.
 SWITCH: for ($current_tab_name) {
 
-    # Extensions must set it to 1 to confirm the tab is valid.
-    my $handled = 0;
-    Bugzilla::Hook::process('user_preferences',
-                            { 'vars'       => $vars,
-                              save_changes => $save_changes,
-                              current_tab  => $current_tab_name,
-                              handled      => \$handled });
-    last SWITCH if $handled;
-
-    /^account$/ && do {
-        SaveAccount() if $save_changes;
-        DoAccount();
-        last SWITCH;
-    };
-    /^settings$/ && do {
-        SaveSettings() if $save_changes;
-        DoSettings();
-        last SWITCH;
-    };
-    /^email$/ && do {
-        SaveEmail() if $save_changes;
-        DoEmail();
-        last SWITCH;
-    };
-    /^permissions$/ && do {
-        DoPermissions();
-        last SWITCH;
-    };
-    /^saved-searches$/ && do {
-        SaveSavedSearches() if $save_changes;
-        DoSavedSearches();
-        last SWITCH;
-    };
-    /^apikey$/ && do {
-        SaveApiKey() if $save_changes;
-        DoApiKey();
-        last SWITCH;
-    };
-
-    ThrowUserError("unknown_tab",
-                   { current_tab_name => $current_tab_name });
+  # Extensions must set it to 1 to confirm the tab is valid.
+  my $handled = 0;
+  Bugzilla::Hook::process(
+    'user_preferences',
+    {
+      'vars'       => $vars,
+      save_changes => $save_changes,
+      current_tab  => $current_tab_name,
+      handled      => \$handled
+    }
+  );
+  last SWITCH if $handled;
+
+  /^account$/ && do {
+    SaveAccount() if $save_changes;
+    DoAccount();
+    last SWITCH;
+  };
+  /^settings$/ && do {
+    SaveSettings() if $save_changes;
+    DoSettings();
+    last SWITCH;
+  };
+  /^email$/ && do {
+    SaveEmail() if $save_changes;
+    DoEmail();
+    last SWITCH;
+  };
+  /^permissions$/ && do {
+    DoPermissions();
+    last SWITCH;
+  };
+  /^saved-searches$/ && do {
+    SaveSavedSearches() if $save_changes;
+    DoSavedSearches();
+    last SWITCH;
+  };
+  /^apikey$/ && do {
+    SaveApiKey() if $save_changes;
+    DoApiKey();
+    last SWITCH;
+  };
+
+  ThrowUserError("unknown_tab", {current_tab_name => $current_tab_name});
 }
 
 delete_token($token) if $save_changes;
 if ($current_tab_name ne 'permissions') {
-    $vars->{'token'} = issue_session_token('edit_user_prefs');
+  $vars->{'token'} = issue_session_token('edit_user_prefs');
 }
 
 # Generate and return the UI (HTML page) from the appropriate template.
index d4c5c08e3f5c08372a5286e4da61394c44dd080e..f1d7e25adedfc8f251631032dae6f6a2cdebd8a2 100755 (executable)
@@ -21,9 +21,8 @@ use Storable qw(read_magic thaw);
 
 my $user = Bugzilla->login(LOGIN_REQUIRED);
 $user->in_group("admin")
-    || ThrowUserError("auth_failure", { group  => "admin",
-                                        action => "access",
-                                        object => "job_queue" });
+  || ThrowUserError("auth_failure",
+  {group => "admin", action => "access", object => "job_queue"});
 
 my $vars = {};
 generate_report($vars);
@@ -31,14 +30,14 @@ generate_report($vars);
 print Bugzilla->cgi->header();
 my $template = Bugzilla->template;
 $template->process('admin/reports/job_queue.html.tmpl', $vars)
-    || ThrowTemplateError($template->error());
+  || ThrowTemplateError($template->error());
 
 sub generate_report {
-    my ($vars) = @_;
-    my $dbh = Bugzilla->dbh;
-    my $user = Bugzilla->user;
+  my ($vars) = @_;
+  my $dbh    = Bugzilla->dbh;
+  my $user   = Bugzilla->user;
 
-    my $query = "
+  my $query = "
         SELECT
             j.jobid,
             j.arg,
@@ -66,56 +65,59 @@ sub generate_report {
             j.run_after, j.grabbed_until, j.insert_time, j.jobid
         " . $dbh->sql_limit(JOB_QUEUE_VIEW_MAX_JOBS + 1);
 
-    $vars->{jobs} = $dbh->selectall_arrayref($query, { Slice => {} });
-    if (@{ $vars->{jobs} } == JOB_QUEUE_VIEW_MAX_JOBS + 1) {
-        pop @{ $vars->{jobs} };
-        $vars->{job_count} = $dbh->selectrow_array("SELECT COUNT(*) FROM ts_job");
-        $vars->{too_many_jobs} = 1;
-    }
+  $vars->{jobs} = $dbh->selectall_arrayref($query, {Slice => {}});
+  if (@{$vars->{jobs}} == JOB_QUEUE_VIEW_MAX_JOBS + 1) {
+    pop @{$vars->{jobs}};
+    $vars->{job_count}     = $dbh->selectrow_array("SELECT COUNT(*) FROM ts_job");
+    $vars->{too_many_jobs} = 1;
+  }
 
-    my $bug_word = template_var('terms')->{bug};
-    foreach my $job (@{ $vars->{jobs} }) {
-        my ($recipient, $description);
-        eval {
-            if ($job->{func} eq 'Bugzilla::Job::BugMail') {
-                my $arg = _cond_thaw(delete $job->{arg});
-                next unless $arg;
-                my $vars = $arg->{vars};
-                $recipient = $vars->{to_user}->{login_name};
-                $description = "[$bug_word " . $vars->{bug}->{bug_id} . '] '
-                               . $vars->{bug}->{short_desc};
-            }
+  my $bug_word = template_var('terms')->{bug};
+  foreach my $job (@{$vars->{jobs}}) {
+    my ($recipient, $description);
+    eval {
+      if ($job->{func} eq 'Bugzilla::Job::BugMail') {
+        my $arg = _cond_thaw(delete $job->{arg});
+        next unless $arg;
+        my $vars = $arg->{vars};
+        $recipient = $vars->{to_user}->{login_name};
+        $description
+          = "[$bug_word " . $vars->{bug}->{bug_id} . '] ' . $vars->{bug}->{short_desc};
+      }
 
-            elsif ($job->{func} eq 'Bugzilla::Job::Mailer') {
-                my $arg = _cond_thaw(delete $job->{arg});
-                next unless $arg;
-                my $msg = $arg->{msg};
-                if (ref($msg) && blessed($msg) eq 'Email::MIME') {
-                    $recipient = $msg->header('to');
-                    $description = $msg->header('subject');
-                } else {
-                    ($recipient) = $msg =~ /\nTo: ([^\n]+)/i;
-                    ($description) = $msg =~ /\nSubject: ([^\n]+)/i;
-                }
-            }
-        };
-        if ($recipient) {
-            $job->{subject} = "<$recipient> $description";
+      elsif ($job->{func} eq 'Bugzilla::Job::Mailer') {
+        my $arg = _cond_thaw(delete $job->{arg});
+        next unless $arg;
+        my $msg = $arg->{msg};
+        if (ref($msg) && blessed($msg) eq 'Email::MIME') {
+          $recipient   = $msg->header('to');
+          $description = $msg->header('subject');
+        }
+        else {
+          ($recipient)   = $msg =~ /\nTo: ([^\n]+)/i;
+          ($description) = $msg =~ /\nSubject: ([^\n]+)/i;
         }
+      }
+    };
+    if ($recipient) {
+      $job->{subject} = "<$recipient> $description";
     }
+  }
 }
 
 sub _cond_thaw {
-    my $data = shift;
-    my $magic = eval { read_magic($data); };
-    if ($magic && $magic->{major} && $magic->{major} >= 2 && $magic->{major} <= 5) {
-        my $thawed = eval { thaw($data) };
-        if ($@) {
-            # false alarm... looked like a Storable, but wasn't
-            return undef;
-        }
-        return $thawed;
-    } else {
-        return undef;
+  my $data = shift;
+  my $magic = eval { read_magic($data); };
+  if ($magic && $magic->{major} && $magic->{major} >= 2 && $magic->{major} <= 5) {
+    my $thawed = eval { thaw($data) };
+    if ($@) {
+
+      # false alarm... looked like a Storable, but wasn't
+      return undef;
     }
+    return $thawed;
+  }
+  else {
+    return undef;
+  }
 }
index 87322c8e9213d1943467f7ad961d4fbba567f9dc..4030ee452e98ec8f5bef05370fe3d6aa7a4f87ac 100755 (executable)
--- a/votes.cgi
+++ b/votes.cgi
@@ -18,22 +18,22 @@ use lib qw(. lib local/lib/perl5);
 use Bugzilla;
 use Bugzilla::Error;
 
-my $is_enabled = grep { $_->NAME eq 'Voting' } @{ Bugzilla->extensions };
-$is_enabled || ThrowUserError('extension_disabled', { name => 'Voting' });
+my $is_enabled = grep { $_->NAME eq 'Voting' } @{Bugzilla->extensions};
+$is_enabled || ThrowUserError('extension_disabled', {name => 'Voting'});
 
 my $cgi = Bugzilla->cgi;
 my $action = $cgi->param('action') || 'show_user';
 
 if ($action eq "show_bug") {
-    $cgi->delete('action');
-    $cgi->param('id', 'voting/bug.html');
-} 
+  $cgi->delete('action');
+  $cgi->param('id', 'voting/bug.html');
+}
 elsif ($action eq "show_user" or $action eq 'vote') {
-    $cgi->delete('action') unless $action eq 'vote';
-    $cgi->param('id', 'voting/user.html');
+  $cgi->delete('action') unless $action eq 'vote';
+  $cgi->param('id', 'voting/user.html');
 }
 else {
-    ThrowUserError('unknown_action', {action => $action});
+  ThrowUserError('unknown_action', {action => $action});
 }
 
 print $cgi->redirect('page.cgi?' . $cgi->query_string);
index a1c110c7ca4e4bd0597718cdf1dcffd2b484e0f3..34fc2633a5dbd4b5dd07a8d6481648f1c581436f 100755 (executable)
--- a/whine.pl
+++ b/whine.pl
@@ -38,38 +38,35 @@ my @seen_schedules = ();
 
 # These statement handles should live outside of their functions in order to
 # allow the database to keep their SQL compiled.
-my $sth_run_queries =
-    $dbh->prepare("SELECT " .
-                  "query_name, title, onemailperbug " .
-                  "FROM whine_queries " .
-                  "WHERE eventid=? " .
-                  "ORDER BY sortkey");
-my $sth_get_query =
-    $dbh->prepare("SELECT query FROM namedqueries " .
-                  "WHERE userid = ? AND name = ?");
+my $sth_run_queries
+  = $dbh->prepare("SELECT "
+    . "query_name, title, onemailperbug "
+    . "FROM whine_queries "
+    . "WHERE eventid=? "
+    . "ORDER BY sortkey");
+my $sth_get_query = $dbh->prepare(
+  "SELECT query FROM namedqueries " . "WHERE userid = ? AND name = ?");
 
 # get the event that's scheduled with the lowest run_next value
-my $sth_next_scheduled_event = $dbh->prepare(
-    "SELECT " .
-    " whine_schedules.eventid, " .
-    " whine_events.owner_userid, " .
-    " whine_events.subject, " .
-    " whine_events.body, " .
-    " whine_events.mailifnobugs " .
-    "FROM whine_schedules " .
-    "LEFT JOIN whine_events " .
-    " ON whine_events.id = whine_schedules.eventid " .
-    "WHERE run_next <= NOW() " .
-    "ORDER BY run_next " .
-    $dbh->sql_limit(1)
-);
+my $sth_next_scheduled_event
+  = $dbh->prepare("SELECT "
+    . " whine_schedules.eventid, "
+    . " whine_events.owner_userid, "
+    . " whine_events.subject, "
+    . " whine_events.body, "
+    . " whine_events.mailifnobugs "
+    . "FROM whine_schedules "
+    . "LEFT JOIN whine_events "
+    . " ON whine_events.id = whine_schedules.eventid "
+    . "WHERE run_next <= NOW() "
+    . "ORDER BY run_next "
+    . $dbh->sql_limit(1));
 
 # get all pending schedules matching an eventid
-my $sth_schedules_by_event = $dbh->prepare(
-    "SELECT id, mailto_type, mailto " .
-    "FROM whine_schedules " .
-    "WHERE eventid=? AND run_next <= NOW()"
-);
+my $sth_schedules_by_event
+  = $dbh->prepare("SELECT id, mailto_type, mailto "
+    . "FROM whine_schedules "
+    . "WHERE eventid=? AND run_next <= NOW()");
 
 
 ################################################################################
@@ -89,20 +86,25 @@ my $sth_schedules_by_event = $dbh->prepare(
 my $fromaddress = Bugzilla->params->{'mailfrom'};
 
 # get the current date and time
-my ($now_sec, $now_minute, $now_hour, $now_day, $now_month, $now_year, 
-    $now_weekday) = localtime;
+my (
+  $now_sec,   $now_minute, $now_hour, $now_day,
+  $now_month, $now_year,   $now_weekday
+) = localtime;
+
 # Convert year to two digits
 $now_year = sprintf("%02d", $now_year % 100);
+
 # Convert the month to January being "1" instead of January being "0".
 $now_month++;
 
 my @daysinmonth = qw(0 31 28 31 30 31 30 31 31 30 31 30 31);
+
 # Alter February in case of a leap year.  This simple way to do it only
 # applies if you won't be looking at February of next year, which whining
 # doesn't need to do.
-if (($now_year % 4 == 0) &&
-    (($now_year % 100 != 0) || ($now_year % 400 == 0))) {
-    $daysinmonth[2] = 29;
+if (($now_year % 4 == 0) && (($now_year % 100 != 0) || ($now_year % 400 == 0)))
+{
+  $daysinmonth[2] = 29;
 }
 
 # run_day can contain either a calendar day (1, 2, 3...), a day of the week
@@ -114,67 +116,79 @@ if (($now_year % 4 == 0) &&
 #
 # We go over each uninitialized schedule record and use its settings to
 # determine what the next time it runs should be
-my $sched_h = $dbh->prepare("SELECT id, run_day, run_time " .
-                            "FROM whine_schedules " .
-                            "WHERE run_next IS NULL" );
+my $sched_h
+  = $dbh->prepare("SELECT id, run_day, run_time "
+    . "FROM whine_schedules "
+    . "WHERE run_next IS NULL");
 $sched_h->execute();
 while (my ($schedule_id, $day, $time) = $sched_h->fetchrow_array) {
-    # fill in some defaults in case they're blank
-    $day  ||= '0';
-    $time ||= '0';
-
-    # If this schedule is supposed to run today, we see if it's supposed to be
-    # run at a particular hour.  If so, we set it for that hour, and if not,
-    # it runs at an interval over the course of a day, which means we should
-    # set it to run immediately.
-    if (&check_today($day)) {
-        # Values that are not entirely numeric are intervals, like "30min"
-        if ($time !~ /^\d+$/) {
-            # set it to now
-            $sth = $dbh->prepare( "UPDATE whine_schedules " .
-                                  "SET run_next=NOW() " .
-                                  "WHERE id=?");
-            $sth->execute($schedule_id);
-        }
-        # A time greater than now means it still has to run today
-        elsif ($time >= $now_hour) {
-            # set it to today + number of hours
-            $sth = $dbh->prepare(
-                "UPDATE whine_schedules " .
-                   "SET run_next = " .
-                        $dbh->sql_date_math('CURRENT_DATE', '+', '?', 'HOUR') .
-                " WHERE id = ?");
-            $sth->execute($time, $schedule_id);
-        }
-        # the target time is less than the current time
-        else { # set it for the next applicable day
-            $day = &get_next_date($day);
-            my $run_next = $dbh->sql_date_math('(' 
-                . $dbh->sql_date_math('CURRENT_DATE', '+', '?', 'DAY')
-                . ')', '+', '?', 'HOUR');
-            $sth = $dbh->prepare("UPDATE whine_schedules " .
-                                    "SET run_next = $run_next
-                                   WHERE id = ?");
-            $sth->execute($day, $time, $schedule_id);
-        }
 
+  # fill in some defaults in case they're blank
+  $day  ||= '0';
+  $time ||= '0';
+
+  # If this schedule is supposed to run today, we see if it's supposed to be
+  # run at a particular hour.  If so, we set it for that hour, and if not,
+  # it runs at an interval over the course of a day, which means we should
+  # set it to run immediately.
+  if (&check_today($day)) {
+
+    # Values that are not entirely numeric are intervals, like "30min"
+    if ($time !~ /^\d+$/) {
+
+      # set it to now
+      $sth = $dbh->prepare(
+        "UPDATE whine_schedules " . "SET run_next=NOW() " . "WHERE id=?");
+      $sth->execute($schedule_id);
     }
-    # If the schedule is not supposed to run today, we set it to run on the
-    # appropriate date and time
-    else {
-        my $target_date = &get_next_date($day);
-        # If configured for a particular time, set it to that, otherwise
-        # midnight
-        my $target_time = ($time =~ /^\d+$/) ? $time : 0;
-
-       my $run_next = $dbh->sql_date_math('(' 
-            . $dbh->sql_date_math('CURRENT_DATE', '+', '?', 'DAY') 
-            . ')', '+', '?', 'HOUR');
-        $sth = $dbh->prepare("UPDATE whine_schedules " .
-                                "SET run_next = $run_next
-                               WHERE id = ?");
-        $sth->execute($target_date, $target_time, $schedule_id);
+
+    # A time greater than now means it still has to run today
+    elsif ($time >= $now_hour) {
+
+      # set it to today + number of hours
+      $sth
+        = $dbh->prepare("UPDATE whine_schedules "
+          . "SET run_next = "
+          . $dbh->sql_date_math('CURRENT_DATE', '+', '?', 'HOUR')
+          . " WHERE id = ?");
+      $sth->execute($time, $schedule_id);
     }
+
+    # the target time is less than the current time
+    else {    # set it for the next applicable day
+      $day = &get_next_date($day);
+      my $run_next
+        = $dbh->sql_date_math(
+        '(' . $dbh->sql_date_math('CURRENT_DATE', '+', '?', 'DAY') . ')',
+        '+', '?', 'HOUR');
+      $sth = $dbh->prepare(
+        "UPDATE whine_schedules " . "SET run_next = $run_next
+                                   WHERE id = ?"
+      );
+      $sth->execute($day, $time, $schedule_id);
+    }
+
+  }
+
+  # If the schedule is not supposed to run today, we set it to run on the
+  # appropriate date and time
+  else {
+    my $target_date = &get_next_date($day);
+
+    # If configured for a particular time, set it to that, otherwise
+    # midnight
+    my $target_time = ($time =~ /^\d+$/) ? $time : 0;
+
+    my $run_next
+      = $dbh->sql_date_math(
+      '(' . $dbh->sql_date_math('CURRENT_DATE', '+', '?', 'DAY') . ')',
+      '+', '?', 'HOUR');
+    $sth = $dbh->prepare(
+      "UPDATE whine_schedules " . "SET run_next = $run_next
+                               WHERE id = ?"
+    );
+    $sth->execute($target_date, $target_time, $schedule_id);
+  }
 }
 $sched_h->finish();
 
@@ -188,7 +202,7 @@ $sched_h->finish();
 #   5. Return an event hashref
 #
 # The event hashref consists of:
-#   eventid - ID of the event 
+#   eventid - ID of the event
 #   author  - user object for the event's creator
 #   users   - array of user objects for recipients
 #   subject - Subject line for the email
@@ -196,91 +210,85 @@ $sched_h->finish();
 #   mailifnobugs - send message even if there are no query or query results
 
 sub get_next_event {
-    my $event = {};
-
-    # Loop until there's something to return
-    until (scalar keys %{$event}) {
-
-        $dbh->bz_start_transaction();
-
-        # Get the event ID for the first pending schedule
-        $sth_next_scheduled_event->execute;
-        my $fetched = $sth_next_scheduled_event->fetch;
-        $sth_next_scheduled_event->finish;
-        return undef unless $fetched;
-        my ($eventid, $owner_id, $subject, $body, $mailifnobugs) = @{$fetched};
-
-        my $owner = Bugzilla::User->new($owner_id);
-
-        my $whineatothers = $owner->in_group('bz_canusewhineatothers');
-
-        my %user_objects;   # Used for keeping track of who has been added
-
-        # Get all schedules that match that event ID and are pending
-        $sth_schedules_by_event->execute($eventid);
-
-        # Add the users from those schedules to the list
-        while (my $row = $sth_schedules_by_event->fetch) {
-            my ($sid, $mailto_type, $mailto) = @{$row};
-
-            # Only bother doing any work if this user has whine permission
-            if ($owner->in_group('bz_canusewhines')) {
-
-                if ($mailto_type == MAILTO_USER) {
-                    if (not defined $user_objects{$mailto}) {
-                        if ($mailto == $owner_id) {
-                            $user_objects{$mailto} = $owner;
-                        }
-                        elsif ($whineatothers) {
-                            $user_objects{$mailto} = Bugzilla::User->new($mailto);
-                        }
-                    }
-                }
-                elsif ($mailto_type == MAILTO_GROUP) {
-                    my $sth = $dbh->prepare("SELECT name FROM groups " .
-                                            "WHERE id=?");
-                    $sth->execute($mailto);
-                    my $groupname = $sth->fetch->[0];
-                    my $group_id = Bugzilla::Group::ValidateGroupName(
-                        $groupname, $owner);
-                    if ($group_id) {
-                        my $glist = join(',',
-                            @{Bugzilla::Group->flatten_group_membership(
-                            $group_id)});
-                        $sth = $dbh->prepare("SELECT user_id FROM " .
-                                             "user_group_map " .
-                                             "WHERE group_id IN ($glist)");
-                        $sth->execute();
-                        for my $row (@{$sth->fetchall_arrayref}) {
-                            if (not defined $user_objects{$row->[0]}) {
-                                $user_objects{$row->[0]} =
-                                    Bugzilla::User->new($row->[0]);
-                            }
-                        }
-                    }
-                }
+  my $event = {};
 
-            }
+  # Loop until there's something to return
+  until (scalar keys %{$event}) {
 
-            reset_timer($sid);
-        }
+    $dbh->bz_start_transaction();
+
+    # Get the event ID for the first pending schedule
+    $sth_next_scheduled_event->execute;
+    my $fetched = $sth_next_scheduled_event->fetch;
+    $sth_next_scheduled_event->finish;
+    return undef unless $fetched;
+    my ($eventid, $owner_id, $subject, $body, $mailifnobugs) = @{$fetched};
+
+    my $owner = Bugzilla::User->new($owner_id);
+
+    my $whineatothers = $owner->in_group('bz_canusewhineatothers');
 
-        $dbh->bz_commit_transaction();
-
-        # Only set $event if the user is allowed to do whining
-        if ($owner->in_group('bz_canusewhines')) {
-            my @users = values %user_objects;
-            $event = {
-                    'eventid' => $eventid,
-                    'author'  => $owner,
-                    'mailto'  => \@users,
-                    'subject' => $subject,
-                    'body'    => $body,
-                    'mailifnobugs' => $mailifnobugs,
-            };
+    my %user_objects;    # Used for keeping track of who has been added
+
+    # Get all schedules that match that event ID and are pending
+    $sth_schedules_by_event->execute($eventid);
+
+    # Add the users from those schedules to the list
+    while (my $row = $sth_schedules_by_event->fetch) {
+      my ($sid, $mailto_type, $mailto) = @{$row};
+
+      # Only bother doing any work if this user has whine permission
+      if ($owner->in_group('bz_canusewhines')) {
+
+        if ($mailto_type == MAILTO_USER) {
+          if (not defined $user_objects{$mailto}) {
+            if ($mailto == $owner_id) {
+              $user_objects{$mailto} = $owner;
+            }
+            elsif ($whineatothers) {
+              $user_objects{$mailto} = Bugzilla::User->new($mailto);
+            }
+          }
         }
+        elsif ($mailto_type == MAILTO_GROUP) {
+          my $sth = $dbh->prepare("SELECT name FROM groups " . "WHERE id=?");
+          $sth->execute($mailto);
+          my $groupname = $sth->fetch->[0];
+          my $group_id = Bugzilla::Group::ValidateGroupName($groupname, $owner);
+          if ($group_id) {
+            my $glist = join(',', @{Bugzilla::Group->flatten_group_membership($group_id)});
+            $sth = $dbh->prepare(
+              "SELECT user_id FROM " . "user_group_map " . "WHERE group_id IN ($glist)");
+            $sth->execute();
+            for my $row (@{$sth->fetchall_arrayref}) {
+              if (not defined $user_objects{$row->[0]}) {
+                $user_objects{$row->[0]} = Bugzilla::User->new($row->[0]);
+              }
+            }
+          }
+        }
+
+      }
+
+      reset_timer($sid);
     }
-    return $event;
+
+    $dbh->bz_commit_transaction();
+
+    # Only set $event if the user is allowed to do whining
+    if ($owner->in_group('bz_canusewhines')) {
+      my @users = values %user_objects;
+      $event = {
+        'eventid'      => $eventid,
+        'author'       => $owner,
+        'mailto'       => \@users,
+        'subject'      => $subject,
+        'body'         => $body,
+        'mailifnobugs' => $mailifnobugs,
+      };
+    }
+  }
+  return $event;
 }
 
 # Run the queries for each event
@@ -294,38 +302,38 @@ sub get_next_event {
 #   mailifnobugs (send message even if there are no query or query results)
 while (my $event = get_next_event) {
 
-    my $eventid = $event->{'eventid'};
-
-    # We loop for each target user because some of the queries will be using
-    # subjective pronouns
-    $dbh = Bugzilla->switch_to_shadow_db();
-    for my $target (@{$event->{'mailto'}}) {
-        my $args = {
-            'subject'     => $event->{'subject'},
-            'body'        => $event->{'body'},
-            'eventid'     => $event->{'eventid'},
-            'author'      => $event->{'author'},
-            'recipient'   => $target,
-            'from'        => $fromaddress,
-        };
-
-        # run the queries for this schedule
-        my $queries = run_queries($args);
-
-        # If mailifnobugs is false, make sure there is something to output
-        if (!$event->{'mailifnobugs'}) {
-            my $there_are_bugs = 0;
-            for my $query (@{$queries}) {
-                $there_are_bugs = 1 if scalar @{$query->{'bugs'}};
-            }
-            next unless $there_are_bugs;
-        }
+  my $eventid = $event->{'eventid'};
+
+  # We loop for each target user because some of the queries will be using
+  # subjective pronouns
+  $dbh = Bugzilla->switch_to_shadow_db();
+  for my $target (@{$event->{'mailto'}}) {
+    my $args = {
+      'subject'   => $event->{'subject'},
+      'body'      => $event->{'body'},
+      'eventid'   => $event->{'eventid'},
+      'author'    => $event->{'author'},
+      'recipient' => $target,
+      'from'      => $fromaddress,
+    };
+
+    # run the queries for this schedule
+    my $queries = run_queries($args);
+
+    # If mailifnobugs is false, make sure there is something to output
+    if (!$event->{'mailifnobugs'}) {
+      my $there_are_bugs = 0;
+      for my $query (@{$queries}) {
+        $there_are_bugs = 1 if scalar @{$query->{'bugs'}};
+      }
+      next unless $there_are_bugs;
+    }
 
-        $args->{'queries'} = $queries;
+    $args->{'queries'} = $queries;
 
-        mail($args);
-    }
-    $dbh = Bugzilla->switch_to_main_db();
+    mail($args);
+  }
+  $dbh = Bugzilla->switch_to_main_db();
 }
 
 ################################################################################
@@ -348,111 +356,114 @@ while (my $event = get_next_event) {
 #  - recipient      user object for the recipient
 #  - author         user object of the person who created the whine event
 sub mail {
-    my $args = shift;
-    # Don't send mail to someone whose bugmail notification is disabled.
-    return if $args->{recipient}->email_disabled;
-
-    $args->{to_user} = $args->{recipient};
-    MessageToMTA(generate_email(
-        $args,
-        {
-            header => 'whine/header.txt.tmpl',
-            text   => 'whine/mail.txt.tmpl',
-            html   => 'whine/mail.html.tmpl',
-        }
-    ));
+  my $args = shift;
+
+  # Don't send mail to someone whose bugmail notification is disabled.
+  return if $args->{recipient}->email_disabled;
+
+  $args->{to_user} = $args->{recipient};
+  MessageToMTA(generate_email(
+    $args,
+    {
+      header => 'whine/header.txt.tmpl',
+      text   => 'whine/mail.txt.tmpl',
+      html   => 'whine/mail.html.tmpl',
+    }
+  ));
 }
 
 # run_queries runs all of the queries associated with a schedule ID, adding
 # the results to $args or mailing off the template if a query wants individual
 # messages for each bug
 sub run_queries {
-    my $args = shift;
-
-    my $return_queries = [];
-
-    $sth_run_queries->execute($args->{'eventid'});
-    my @queries = ();
-    for (@{$sth_run_queries->fetchall_arrayref}) {
-        push(@queries,
-            {
-              'name'          => $_->[0],
-              'title'         => $_->[1],
-              'onemailperbug' => $_->[2],
-              'columnlist'    => [],
-              'bugs'          => [],
-            }
-        );
+  my $args = shift;
+
+  my $return_queries = [];
+
+  $sth_run_queries->execute($args->{'eventid'});
+  my @queries = ();
+  for (@{$sth_run_queries->fetchall_arrayref}) {
+    push(
+      @queries,
+      {
+        'name'          => $_->[0],
+        'title'         => $_->[1],
+        'onemailperbug' => $_->[2],
+        'columnlist'    => [],
+        'bugs'          => [],
+      }
+    );
+  }
+
+  foreach my $thisquery (@queries) {
+    next unless $thisquery->{'name'};    # named query is blank
+
+    my $savedquery = get_query($thisquery->{'name'}, $args->{'author'});
+    next unless $savedquery;             # silently ignore missing queries
+
+    # Execute the saved query
+    my @searchfields = ('bug_id', DEFAULT_COLUMN_LIST);
+
+    # A new Bugzilla::CGI object needs to be created to allow
+    # Bugzilla::Search to execute a saved query.  It's exceedingly weird,
+    # but that's how it works.
+    my $searchparams = new Bugzilla::CGI($savedquery);
+
+    # Use the columnlist for the saved query, if it exists, and make
+    # sure bug_id is always in the list.
+    if (my $columnlist = $searchparams->param('columnlist')) {
+      @searchfields = split(/[\s,]+/, $columnlist);
+      unshift(@searchfields, 'bug_id') unless grep { $_ eq 'bug_id' } @searchfields;
+    }
+    push @{$thisquery->{'columnlist'}}, @searchfields;
+
+    my @orderstrings = split(/,\s*/, $searchparams->param('order') || '');
+    my $search = new Bugzilla::Search(
+      'fields' => \@searchfields,
+      'params' => scalar $searchparams->Vars,
+      'user'   => $args->{'recipient'},         # the search runs as the recipient
+      'order'  => \@orderstrings
+    );
+
+    # If a query fails for whatever reason, it shouldn't kill the script.
+    my $data = eval { $search->data };
+    if ($@) {
+      print STDERR get_text('whine_query_failed',
+        {query_name => $thisquery->{'name'}, author => $args->{'author'}, reason => $@})
+        . "\n";
+      next;
     }
 
-    foreach my $thisquery (@queries) {
-        next unless $thisquery->{'name'};   # named query is blank
-
-        my $savedquery = get_query($thisquery->{'name'}, $args->{'author'});
-        next unless $savedquery;    # silently ignore missing queries
-
-        # Execute the saved query
-        my @searchfields = ('bug_id', DEFAULT_COLUMN_LIST);
-
-        # A new Bugzilla::CGI object needs to be created to allow
-        # Bugzilla::Search to execute a saved query.  It's exceedingly weird,
-        # but that's how it works.
-        my $searchparams = new Bugzilla::CGI($savedquery);
-
-        # Use the columnlist for the saved query, if it exists, and make
-        # sure bug_id is always in the list.
-        if (my $columnlist = $searchparams->param('columnlist')) {
-            @searchfields = split(/[\s,]+/, $columnlist);
-            unshift(@searchfields, 'bug_id') unless grep { $_ eq 'bug_id' } @searchfields;
-        }
-        push @{$thisquery->{'columnlist'}}, @searchfields;
-
-        my @orderstrings = split(/,\s*/, $searchparams->param('order') || '');
-        my $search = new Bugzilla::Search(
-            'fields' => \@searchfields,
-            'params' => scalar $searchparams->Vars,
-            'user'   => $args->{'recipient'}, # the search runs as the recipient
-            'order'  => \@orderstrings
-        );
-        # If a query fails for whatever reason, it shouldn't kill the script.
-        my $data = eval { $search->data };
-        if ($@) {
-            print STDERR get_text('whine_query_failed', { query_name => $thisquery->{'name'},
-                                                          author => $args->{'author'},
-                                                          reason => $@ }) . "\n";
-            next;
-        }
-
-        foreach my $row (@$data) {
-            my $bug = {};
-            for my $field (@searchfields) {
-                my $fieldname = $field;
-                $fieldname =~ s/^bugs\.//;  # No need for bugs.whatever
-                $bug->{$fieldname} = shift @$row;
-            }
-
-            if ($thisquery->{'onemailperbug'}) {
-                $args->{'queries'} = [
-                    {
-                        'name' => $thisquery->{'name'},
-                        'title' => $thisquery->{'title'},
-                        'columnlist' => $thisquery->{'columnlist'},
-                        'bugs' => [ $bug ],
-                    },
-                ];
-                mail($args);
-                delete $args->{'queries'};
-            }
-            else {  # It belongs in one message with any other lists
-                push @{$thisquery->{'bugs'}}, $bug;
-            }
-        }
-        if (!$thisquery->{'onemailperbug'} && @{$thisquery->{'bugs'}}) {
-            push @{$return_queries}, $thisquery;
-        }
+    foreach my $row (@$data) {
+      my $bug = {};
+      for my $field (@searchfields) {
+        my $fieldname = $field;
+        $fieldname =~ s/^bugs\.//;    # No need for bugs.whatever
+        $bug->{$fieldname} = shift @$row;
+      }
+
+      if ($thisquery->{'onemailperbug'}) {
+        $args->{'queries'} = [
+          {
+            'name'       => $thisquery->{'name'},
+            'title'      => $thisquery->{'title'},
+            'columnlist' => $thisquery->{'columnlist'},
+            'bugs'       => [$bug],
+          },
+        ];
+        mail($args);
+        delete $args->{'queries'};
+      }
+      else {    # It belongs in one message with any other lists
+        push @{$thisquery->{'bugs'}}, $bug;
+      }
+    }
+    if (!$thisquery->{'onemailperbug'} && @{$thisquery->{'bugs'}}) {
+      push @{$return_queries}, $thisquery;
     }
+  }
 
-    return $return_queries;
+  return $return_queries;
 }
 
 # get_query gets the namedquery.  It's similar to LookupNamedQuery (in
@@ -460,12 +471,12 @@ sub run_queries {
 # individual named queries might go away without the whine_queries that point
 # to them being removed.
 sub get_query {
-    my ($name, $user) = @_;
-    my $qname = $name;
-    $sth_get_query->execute($user->id, $qname);
-    my $fetched = $sth_get_query->fetch;
-    $sth_get_query->finish;
-    return $fetched ? $fetched->[0] : '';
+  my ($name, $user) = @_;
+  my $qname = $name;
+  $sth_get_query->execute($user->id, $qname);
+  my $fetched = $sth_get_query->fetch;
+  $sth_get_query->finish;
+  return $fetched ? $fetched->[0] : '';
 }
 
 # check_today gets a run day from the schedule and sees if it matches today
@@ -477,25 +488,23 @@ sub get_query {
 #   - 'MF' for every weekday
 
 sub check_today {
-    my $run_day  = shift;
-
-    if (($run_day eq 'MF')
-     && ($now_weekday > 0)
-     && ($now_weekday < 6)) {
-        return 1;
-    }
-    elsif (
-         length($run_day) == 3 &&
-         index("SunMonTueWedThuFriSat", $run_day)/3 == $now_weekday) {
-        return 1;
-    }
-    elsif  (($run_day eq 'All')
-         || (($run_day eq 'last')  &&
-             ($now_day == $daysinmonth[$now_month] ))
-         || ($run_day eq $now_day)) {
-        return 1;
-    }
-    return 0;
+  my $run_day = shift;
+
+  if (($run_day eq 'MF') && ($now_weekday > 0) && ($now_weekday < 6)) {
+    return 1;
+  }
+  elsif (length($run_day) == 3
+    && index("SunMonTueWedThuFriSat", $run_day) / 3 == $now_weekday)
+  {
+    return 1;
+  }
+  elsif (($run_day eq 'All')
+    || (($run_day eq 'last') && ($now_day == $daysinmonth[$now_month]))
+    || ($run_day eq $now_day))
+  {
+    return 1;
+  }
+  return 0;
 }
 
 # reset_timer sets the next time a whine is supposed to run, assuming it just
@@ -504,95 +513,101 @@ sub check_today {
 # reset_timer does not lock the whine_schedules table.  Anything that calls it
 # should do that itself.
 sub reset_timer {
-    my $schedule_id = shift;
-
-    # Schedules may not be executed more than once for each invocation of
-    # whine.pl -- there are legitimate circumstances that can cause this, like
-    # a set of whines that take a very long time to execute, so it's done
-    # quietly.
-    if (grep($_ == $schedule_id, @seen_schedules)) {
-        null_schedule($schedule_id);
-        return;
+  my $schedule_id = shift;
+
+  # Schedules may not be executed more than once for each invocation of
+  # whine.pl -- there are legitimate circumstances that can cause this, like
+  # a set of whines that take a very long time to execute, so it's done
+  # quietly.
+  if (grep($_ == $schedule_id, @seen_schedules)) {
+    null_schedule($schedule_id);
+    return;
+  }
+  push @seen_schedules, $schedule_id;
+
+  $sth = $dbh->prepare(
+    "SELECT run_day, run_time FROM whine_schedules " . "WHERE id=?");
+  $sth->execute($schedule_id);
+  my ($run_day, $run_time) = $sth->fetchrow_array;
+
+  # It may happen that the run_time field is NULL or blank due to
+  # a bug in editwhines.cgi when this field was initially 0.
+  $run_time ||= 0;
+
+  my $run_today     = 0;
+  my $minute_offset = 0;
+
+  # If the schedule is to run today, and it runs many times per day,
+  # it shall be set to run immediately.
+  $run_today = &check_today($run_day);
+  if (($run_today) && ($run_time !~ /^\d+$/)) {
+
+    # The default of 60 catches any bad value
+    my $minute_interval = 60;
+    if ($run_time =~ /^(\d+)min$/i) {
+      $minute_interval = $1;
     }
-    push @seen_schedules, $schedule_id;
-
-    $sth = $dbh->prepare( "SELECT run_day, run_time FROM whine_schedules " .
-                          "WHERE id=?" );
-    $sth->execute($schedule_id);
-    my ($run_day, $run_time) = $sth->fetchrow_array;
-
-    # It may happen that the run_time field is NULL or blank due to
-    # a bug in editwhines.cgi when this field was initially 0.
-    $run_time ||= 0;
-
-    my $run_today = 0;
-    my $minute_offset = 0;
-
-    # If the schedule is to run today, and it runs many times per day,
-    # it shall be set to run immediately.
-    $run_today = &check_today($run_day);
-    if (($run_today) && ($run_time !~ /^\d+$/)) {
-        # The default of 60 catches any bad value
-        my $minute_interval = 60;
-        if ($run_time =~ /^(\d+)min$/i) {
-            $minute_interval = $1;
-        }
 
-        # set the minute offset to the next interval point
-        $minute_offset = $minute_interval - ($now_minute % $minute_interval);
-    }
-    elsif (($run_today) && ($run_time > $now_hour)) {
-        # timed event for later today
-        # (This should only happen if, for example, an 11pm scheduled event
-        #  didn't happen until after midnight)
-        $minute_offset = (60 * ($run_time - $now_hour)) - $now_minute;
-    }
-    else {
-        # it's not something that runs later today.
-        $minute_offset = 0;
-
-        # Set the target time if it's a specific hour
-        my $target_time = ($run_time =~ /^\d+$/) ? $run_time : 0;
-
-        my $nextdate = &get_next_date($run_day);
-        my $run_next = $dbh->sql_date_math('('
-            . $dbh->sql_date_math('CURRENT_DATE', '+', '?', 'DAY')
-            . ')', '+', '?', 'HOUR');
-        $sth = $dbh->prepare("UPDATE whine_schedules " .
-                                "SET run_next = $run_next
-                               WHERE id = ?");
-        $sth->execute($nextdate, $target_time, $schedule_id);
-        return;
-    }
-
-    if ($minute_offset > 0) {
-        # Scheduling is done in terms of whole minutes.
-        
-        my $next_run = $dbh->selectrow_array(
-            'SELECT ' . $dbh->sql_date_math('NOW()', '+', '?', 'MINUTE'),
-            undef, $minute_offset);
-        $next_run = format_time($next_run, "%Y-%m-%d %R");
-
-        $sth = $dbh->prepare("UPDATE whine_schedules " .
-                             "SET run_next = ? WHERE id = ?");
-        $sth->execute($next_run, $schedule_id);
-    } else {
-        # The minute offset is zero or less, which is not supposed to happen.
-        # complain to STDERR
-        null_schedule($schedule_id);
-        print STDERR "Error: bad minute_offset for schedule ID $schedule_id\n";
-    }
+    # set the minute offset to the next interval point
+    $minute_offset = $minute_interval - ($now_minute % $minute_interval);
+  }
+  elsif (($run_today) && ($run_time > $now_hour)) {
+
+    # timed event for later today
+    # (This should only happen if, for example, an 11pm scheduled event
+    #  didn't happen until after midnight)
+    $minute_offset = (60 * ($run_time - $now_hour)) - $now_minute;
+  }
+  else {
+    # it's not something that runs later today.
+    $minute_offset = 0;
+
+    # Set the target time if it's a specific hour
+    my $target_time = ($run_time =~ /^\d+$/) ? $run_time : 0;
+
+    my $nextdate = &get_next_date($run_day);
+    my $run_next
+      = $dbh->sql_date_math(
+      '(' . $dbh->sql_date_math('CURRENT_DATE', '+', '?', 'DAY') . ')',
+      '+', '?', 'HOUR');
+    $sth = $dbh->prepare(
+      "UPDATE whine_schedules " . "SET run_next = $run_next
+                               WHERE id = ?"
+    );
+    $sth->execute($nextdate, $target_time, $schedule_id);
+    return;
+  }
+
+  if ($minute_offset > 0) {
+
+    # Scheduling is done in terms of whole minutes.
+
+    my $next_run
+      = $dbh->selectrow_array(
+      'SELECT ' . $dbh->sql_date_math('NOW()', '+', '?', 'MINUTE'),
+      undef, $minute_offset);
+    $next_run = format_time($next_run, "%Y-%m-%d %R");
+
+    $sth
+      = $dbh->prepare("UPDATE whine_schedules " . "SET run_next = ? WHERE id = ?");
+    $sth->execute($next_run, $schedule_id);
+  }
+  else {
+    # The minute offset is zero or less, which is not supposed to happen.
+    # complain to STDERR
+    null_schedule($schedule_id);
+    print STDERR "Error: bad minute_offset for schedule ID $schedule_id\n";
+  }
 }
 
 # null_schedule is used to safeguard against infinite loops.  Schedules with
 # run_next set to NULL will not be available to get_next_event until they are
 # rescheduled, which only happens when whine.pl starts.
 sub null_schedule {
-    my $schedule_id = shift;
-    $sth = $dbh->prepare("UPDATE whine_schedules " .
-                         "SET run_next = NULL " .
-                         "WHERE id=?");
-    $sth->execute($schedule_id);
+  my $schedule_id = shift;
+  $sth = $dbh->prepare(
+    "UPDATE whine_schedules " . "SET run_next = NULL " . "WHERE id=?");
+  $sth->execute($schedule_id);
 }
 
 # get_next_date determines the difference in days between now and the next
@@ -601,56 +616,58 @@ sub null_schedule {
 # It takes a run_day argument (see check_today, above, for an explanation),
 # and returns an integer, representing a number of days.
 sub get_next_date {
-    my $day = shift;
+  my $day = shift;
+
+  my $add_days = 0;
 
-    my $add_days = 0;
+  if ($day eq 'All') {
+    $add_days = 1;
+  }
+  elsif ($day eq 'last') {
 
-    if ($day eq 'All') {
-        $add_days = 1;
+    # next_date should contain the last day of this month, or next month
+    # if it's today
+    if ($daysinmonth[$now_month] == $now_day) {
+      my $month = $now_month + 1;
+      $month = 1 if $month > 12;
+      $add_days = $daysinmonth[$month] + 1;
     }
-    elsif ($day eq 'last') {
-        # next_date should contain the last day of this month, or next month
-        # if it's today
-        if ($daysinmonth[$now_month] == $now_day) {
-            my $month = $now_month + 1;
-            $month = 1 if $month > 12;
-            $add_days = $daysinmonth[$month] + 1;
-        }
-        else {
-            $add_days = $daysinmonth[$now_month] - $now_day;
-        }
+    else {
+      $add_days = $daysinmonth[$now_month] - $now_day;
     }
-    elsif ($day eq 'MF') { # any day Monday through Friday
-        if ($now_weekday < 5) { # Sun-Thurs
-            $add_days = 1;
-        }
-        elsif ($now_weekday == 5) { # Friday
-            $add_days = 3;
-        }
-        else { # it's 6, Saturday
-            $add_days = 2;
-        }
+  }
+  elsif ($day eq 'MF') {    # any day Monday through Friday
+    if ($now_weekday < 5) {    # Sun-Thurs
+      $add_days = 1;
+    }
+    elsif ($now_weekday == 5) {    # Friday
+      $add_days = 3;
+    }
+    else {                         # it's 6, Saturday
+      $add_days = 2;
     }
-    elsif ($day !~ /^\d+$/) { # A specific day of the week
+  }
+  elsif ($day !~ /^\d+$/) {        # A specific day of the week
         # The default is used if there is a bad value in the database, in
         # which case we mark it to a less-popular day (Sunday)
-        my $day_num = 0;
+    my $day_num = 0;
 
-        if (length($day) == 3) {
-            $day_num = (index("SunMonTueWedThuFriSat", $day)/3) or 0;
-        }
+    if (length($day) == 3) {
+      $day_num = (index("SunMonTueWedThuFriSat", $day) / 3) or 0;
+    }
 
-        $add_days = $day_num - $now_weekday;
-        if ($add_days <= 0) { # it's next week
-            $add_days += 7;
-        }
+    $add_days = $day_num - $now_weekday;
+    if ($add_days <= 0) {    # it's next week
+      $add_days += 7;
     }
-    else { # it's a number, so we set it for that calendar day
-        $add_days = $day - $now_day;
-        # If it's already beyond that day this month, set it to the next one
-        if ($add_days <= 0) {
-            $add_days += $daysinmonth[$now_month];
-        }
+  }
+  else {                     # it's a number, so we set it for that calendar day
+    $add_days = $day - $now_day;
+
+    # If it's already beyond that day this month, set it to the next one
+    if ($add_days <= 0) {
+      $add_days += $daysinmonth[$now_month];
     }
-    return $add_days;
+  }
+  return $add_days;
 }
index 2113bb819bea4e20a71bbdbb44359b449c136348..f940b14789a4dfd27a78683294c597dca9418d10 100755 (executable)
@@ -7,8 +7,8 @@
 # defined by the Mozilla Public License, v. 2.0.
 
 
-# This is a script suitable for running once a day from a cron job.  It 
-# looks at all the bugs, and sends whiny mail to anyone who has a bug 
+# This is a script suitable for running once a day from a cron job.  It
+# looks at all the bugs, and sends whiny mail to anyone who has a bug
 # assigned to them that has status CONFIRMED, NEW, or REOPENED that has not
 # been touched for more than the number of days specified in the whinedays
 # param. (We have NEW and REOPENED in there to keep compatibility with old
@@ -28,58 +28,59 @@ use Bugzilla::User;
 # Whining is disabled if whinedays is zero
 exit unless Bugzilla->params->{'whinedays'} >= 1;
 
-my $dbh = Bugzilla->dbh;
+my $dbh   = Bugzilla->dbh;
 my $query = q{SELECT bug_id, short_desc, login_name
                 FROM bugs
           INNER JOIN profiles
                   ON userid = assigned_to
                WHERE bug_status IN (?,?,?)
                  AND disable_mail = 0
-                 AND } . $dbh->sql_to_days('NOW()') . " - " .
-                       $dbh->sql_to_days('delta_ts') . " > " .
-                       Bugzilla->params->{'whinedays'} .
-          " ORDER BY bug_id";
+                 AND }
+  . $dbh->sql_to_days('NOW()') . " - "
+  . $dbh->sql_to_days('delta_ts') . " > "
+  . Bugzilla->params->{'whinedays'}
+  . " ORDER BY bug_id";
 
 my %bugs;
 my %desc;
 
-my $slt_bugs = $dbh->selectall_arrayref($query, undef, 'CONFIRMED', 'NEW',
-                                                       'REOPENED');
+my $slt_bugs
+  = $dbh->selectall_arrayref($query, undef, 'CONFIRMED', 'NEW', 'REOPENED');
 
 foreach my $bug (@$slt_bugs) {
-    my ($id, $desc, $login) = @$bug;
-    if (!defined $bugs{$login}) {
-        $bugs{$login} = [];
-    }
-    if (!defined $desc{$login}) {
-        $desc{$login} = [];
-    }
-    push @{$bugs{$login}}, $id;
-    push @{$desc{$login}}, $desc;
+  my ($id, $desc, $login) = @$bug;
+  if (!defined $bugs{$login}) {
+    $bugs{$login} = [];
+  }
+  if (!defined $desc{$login}) {
+    $desc{$login} = [];
+  }
+  push @{$bugs{$login}}, $id;
+  push @{$desc{$login}}, $desc;
 }
 
 
 foreach my $login (sort (keys %bugs)) {
-    my $user = new Bugzilla::User({name => $login});
-    next if $user->email_disabled;
+  my $user = new Bugzilla::User({name => $login});
+  next if $user->email_disabled;
 
-    my $vars = {'email' => $user->email};
+  my $vars = {'email' => $user->email};
 
-    my @bugs = ();
-    foreach my $i (@{$bugs{$login}}) {
-        my $bug = {};
-        $bug->{'summary'} = shift(@{$desc{$login}});
-        $bug->{'id'} = $i;
-        push @bugs, $bug;
-    }
-    $vars->{'bugs'} = \@bugs;
+  my @bugs = ();
+  foreach my $i (@{$bugs{$login}}) {
+    my $bug = {};
+    $bug->{'summary'} = shift(@{$desc{$login}});
+    $bug->{'id'}      = $i;
+    push @bugs, $bug;
+  }
+  $vars->{'bugs'} = \@bugs;
 
-    my $msg;
-    my $template = Bugzilla->template_inner($user->setting('lang'));
-    $template->process("email/whine.txt.tmpl", $vars, \$msg)
-      or die($template->error());
+  my $msg;
+  my $template = Bugzilla->template_inner($user->setting('lang'));
+  $template->process("email/whine.txt.tmpl", $vars, \$msg)
+    or die($template->error());
 
-    MessageToMTA($msg);
+  MessageToMTA($msg);
 
-    say $user->email . "      " . join(" ", @{$bugs{$login}});
+  say $user->email . "      " . join(" ", @{$bugs{$login}});
 }
index b4a035cda905bfbd433c24cc7cf66d43bf1a8d9c..2341bb04eaeb6069dcfacd3c1ce8e744e781c71a 100755 (executable)
@@ -16,10 +16,11 @@ use Bugzilla;
 use Bugzilla::Constants;
 use Bugzilla::Error;
 use Bugzilla::WebService::Constants;
+
 BEGIN {
-    if (!Bugzilla->feature('xmlrpc')) {
-        ThrowUserError('feature_disabled', { feature => 'xmlrpc' });
-    }
+  if (!Bugzilla->feature('xmlrpc')) {
+    ThrowUserError('feature_disabled', {feature => 'xmlrpc'});
+  }
 }
 use Bugzilla::WebService::Server::XMLRPC;
 
@@ -28,6 +29,7 @@ Bugzilla->usage_mode(USAGE_MODE_XMLRPC);
 # Fix the error code that SOAP::Lite uses for Perl errors.
 local $SOAP::Constants::FAULT_SERVER;
 $SOAP::Constants::FAULT_SERVER = ERROR_UNKNOWN_FATAL;
+
 # The line above is used, this one is ignored, but SOAP::Lite
 # might start using this constant (the correct one) for XML-RPC someday.
 local $XMLRPC::Constants::FAULT_SERVER;
@@ -35,8 +37,8 @@ $XMLRPC::Constants::FAULT_SERVER = ERROR_UNKNOWN_FATAL;
 
 local @INC = (bz_locations()->{extensionsdir}, @INC);
 my $server = new Bugzilla::WebService::Server::XMLRPC;
-# We use a sub for on_action because that gets us the info about what 
-# class is being called. Note that this is a hack--this is technically 
+
+# We use a sub for on_action because that gets us the info about what
+# class is being called. Note that this is a hack--this is technically
 # for setting SOAPAction, which isn't used by XML-RPC.
-$server->on_action(sub { $server->handle_login(WS_DISPATCH, @_) })
-       ->handle();
+$server->on_action(sub { $server->handle_login(WS_DISPATCH, @_) })->handle();
index 71344d51a370c6797a2aaab2cfca764f37b16516..d56c1467299cb8882637e78301729251aa71e394 100755 (executable)
@@ -19,16 +19,15 @@ my $conf_path;
 my $config;
 
 BEGIN {
-    say 'reading the config file...';
-    my $conf_file = 'selenium_test.conf';
-    $config = do "$conf_file"
-        or die "can't read configuration '$conf_file': $!$@";
+  say 'reading the config file...';
+  my $conf_file = 'selenium_test.conf';
+  $config = do "$conf_file" or die "can't read configuration '$conf_file': $!$@";
 
-    $conf_path = $config->{bugzilla_path};
+  $conf_path = $config->{bugzilla_path};
 
-    # We don't want randomly-generated keys. We want the ones specified
-    # in the config file so that we can use them in tests scripts.
-    *Bugzilla::User::APIKey::_check_api_key = sub { return $_[1]; };
+  # We don't want randomly-generated keys. We want the ones specified
+  # in the config file so that we can use them in tests scripts.
+  *Bugzilla::User::APIKey::_check_api_key = sub { return $_[1]; };
 }
 
 use lib $conf_path, "$conf_path/local/lib/perl5";
@@ -65,24 +64,24 @@ $urlbase = $config->{browser_url} . '/' . $config->{bugzilla_installation};
 $urlbase .= '/' unless $urlbase =~ /\/$/;
 
 if ($urlbase =~ /^https/) {
-    $sslbase = $urlbase;
-    $urlbase =~ s/^https(.+)$/http$1/;
+  $sslbase = $urlbase;
+  $urlbase =~ s/^https(.+)$/http$1/;
 }
 
 my %set_params = (
-    urlbase => $urlbase,
-    sslbase => $sslbase,
-    useqacontact  => 1,
-    mail_delivery_method => 'Test',
-    maxattachmentsize => 256,
+  urlbase              => $urlbase,
+  sslbase              => $sslbase,
+  useqacontact         => 1,
+  mail_delivery_method => 'Test',
+  maxattachmentsize    => 256,
 );
 
 my $params_modified;
 foreach my $param (keys %set_params) {
-    my $value = $set_params{$param};
-    next unless defined $value && Bugzilla->params->{$param} ne $value;
-    SetParam($param, $value);
-    $params_modified = 1;
+  my $value = $set_params{$param};
+  next unless defined $value && Bugzilla->params->{$param} ne $value;
+  SetParam($param, $value);
+  $params_modified = 1;
 }
 
 write_params() if $params_modified;
@@ -97,8 +96,8 @@ write_params() if $params_modified;
 my %user_prefs = (post_bug_submit_action => 'nothing');
 
 foreach my $pref (keys %user_prefs) {
-    my $value = $user_prefs{$pref};
-    Bugzilla::User::Setting::set_default($pref, $value, 0);
+  my $value = $user_prefs{$pref};
+  Bugzilla::User::Setting::set_default($pref, $value, 0);
 }
 
 ##########################################################################
@@ -106,64 +105,61 @@ foreach my $pref (keys %user_prefs) {
 ##########################################################################
 
 # First of all, remove the default .* regexp for the editbugs group.
-my $group = Bugzilla::Group->new({ name => 'editbugs' });
+my $group = Bugzilla::Group->new({name => 'editbugs'});
 $group->set_user_regexp('');
 $group->update();
 
 my @usernames = (
-    'admin',            'no-privs',
-    'QA-Selenium-TEST', 'canconfirm',
-    'tweakparams',      'permanent_user',
-    'editbugs',         'disabled',
+  'admin',       'no-privs',       'QA-Selenium-TEST', 'canconfirm',
+  'tweakparams', 'permanent_user', 'editbugs',         'disabled',
 );
 
 say 'creating user accounts...';
 foreach my $username (@usernames) {
-    my ($password, $login);
-
-    my $prefix = $username;
-    if ($username eq 'permanent_user') {
-        $password = $config->{admin_user_passwd};
-        $login = $config->{$username};
-    }
-    elsif ($username eq 'no-privs') {
-        $prefix = 'unprivileged';
+  my ($password, $login);
+
+  my $prefix = $username;
+  if ($username eq 'permanent_user') {
+    $password = $config->{admin_user_passwd};
+    $login    = $config->{$username};
+  }
+  elsif ($username eq 'no-privs') {
+    $prefix = 'unprivileged';
+  }
+  elsif ($username eq 'QA-Selenium-TEST') {
+    $prefix = 'QA_Selenium_TEST';
+  }
+
+  $password ||= $config->{"${prefix}_user_passwd"};
+  $login    ||= $config->{"${prefix}_user_login"};
+  my $api_key = $config->{"${prefix}_user_api_key"};
+
+  if (is_available_email($login)) {
+    my %extra_args;
+    if ($username eq 'disabled') {
+      $extra_args{disabledtext} = '!!This is the text!!';
     }
-    elsif ($username eq 'QA-Selenium-TEST') {
-        $prefix = 'QA_Selenium_TEST';
+
+    my $user = Bugzilla::User->create({
+      login_name    => $login,
+      email         => $login,
+      realname      => $username,
+      cryptpassword => $password,
+      %extra_args,
+    });
+
+    if ($api_key) {
+      Bugzilla::User::APIKey->create({
+        user_id     => $user->id,
+        description => 'API key for QA tests',
+        api_key     => $api_key
+      });
     }
 
-    $password ||= $config->{"${prefix}_user_passwd"};
-    $login ||= $config->{"${prefix}_user_login"};
-    my $api_key = $config->{"${prefix}_user_api_key"};
-
-    if (is_available_email($login)) {
-        my %extra_args;
-        if ($username eq 'disabled') {
-            $extra_args{disabledtext} = '!!This is the text!!';
-        }
-
-        my $user = Bugzilla::User->create(
-            { login_name    => $login,
-              email         => $login,
-              realname      => $username,
-              cryptpassword => $password,
-              %extra_args,
-            }
-        );
-
-        if ($api_key) {
-            Bugzilla::User::APIKey->create(
-                { user_id     => $user->id,
-                  description => 'API key for QA tests',
-                  api_key     => $api_key }
-            );
-        }
-
-        if ($username eq 'admin' or $username eq 'permanent_user') {
-            Bugzilla::Install::make_admin($login);
-        }
+    if ($username eq 'admin' or $username eq 'permanent_user') {
+      Bugzilla::Install::make_admin($login);
     }
+  }
 }
 
 ##########################################################################
@@ -175,182 +171,193 @@ my $admin_user = Bugzilla::User->check($config->{admin_user_login});
 Bugzilla->set_user($admin_user);
 
 my %field_values = (
-    'priority'     => 'Highest',
-    'bug_status'   => 'CONFIRMED',
-    'version'      => 'unspecified',
-    'bug_file_loc' => '',
-    'comment'      => 'please ignore this bug',
-    'component'    => 'TestComponent',
-    'rep_platform' => 'All',
-    'short_desc'   => 'This is a testing bug only',
-    'product'      => 'TestProduct',
-    'op_sys'       => 'Linux',
-    'bug_severity' => 'normal',
+  'priority'     => 'Highest',
+  'bug_status'   => 'CONFIRMED',
+  'version'      => 'unspecified',
+  'bug_file_loc' => '',
+  'comment'      => 'please ignore this bug',
+  'component'    => 'TestComponent',
+  'rep_platform' => 'All',
+  'short_desc'   => 'This is a testing bug only',
+  'product'      => 'TestProduct',
+  'op_sys'       => 'Linux',
+  'bug_severity' => 'normal',
 );
 
 say 'creating bugs...';
-my $bug = Bugzilla::Bug->create( \%field_values );
+my $bug = Bugzilla::Bug->create(\%field_values);
 say 'Bug ' . $bug->id . ' created';
 if (Bugzilla::Bug->new('public_bug')->{error}) {
-    # The deadline must be set so that this bug can be used to test
-    # timetracking fields using WebServices.
-    $bug = Bugzilla::Bug->create({ %field_values, alias => 'public_bug', deadline => '2010-01-01' });
-    say 'Bug ' . $bug->id . ' (alias: public_bug) created';
+
+  # The deadline must be set so that this bug can be used to test
+  # timetracking fields using WebServices.
+  $bug = Bugzilla::Bug->create(
+    {%field_values, alias => 'public_bug', deadline => '2010-01-01'});
+  say 'Bug ' . $bug->id . ' (alias: public_bug) created';
 }
 
 ##########################################################################
 # Create Classifications
 ##########################################################################
 
-my @classifications = ({ name        => 'Class2_QA',
-                         description => "required by Selenium... DON'T DELETE" },
+my @classifications = (
+  {name => 'Class2_QA', description => "required by Selenium... DON'T DELETE"},
 );
 
 say 'creating classifications...';
 for my $class (@classifications) {
-    my $new_class = Bugzilla::Classification->new({ name => $class->{name} });
-    if (!$new_class) {
-        $dbh->do('INSERT INTO classifications (name, description) VALUES (?, ?)',
-                 undef, ($class->{name}, $class->{description}));
-    }
+  my $new_class = Bugzilla::Classification->new({name => $class->{name}});
+  if (!$new_class) {
+    $dbh->do('INSERT INTO classifications (name, description) VALUES (?, ?)',
+      undef, ($class->{name}, $class->{description}));
+  }
 }
 ##########################################################################
 # Create Products
 ##########################################################################
 
 my @products = (
-    {   product_name     => 'QA-Selenium-TEST',
-        description      => "used by Selenium test.. DON'T DELETE",
-        versions         => ['unspecified', 'QAVersion'],
-        milestones       => ['QAMilestone'],
-        defaultmilestone => '---',
-        components       => [
-            {   name             => 'QA-Selenium-TEST',
-                description      => "used by Selenium test.. DON'T DELETE",
-                initialowner     => $config->{QA_Selenium_TEST_user_login},
-                initialqacontact => $config->{QA_Selenium_TEST_user_login},
-                initial_cc       => [$config->{QA_Selenium_TEST_user_login}],
-            }
-        ],
-    },
-
-    {   product_name => 'Another Product',
-        description  => 'Alternate product used by Selenium. <b>Do not edit!</b>',
-        versions         => ['unspecified', 'Another1', 'Another2'],
-        milestones       => ['AnotherMS1', 'AnotherMS2', 'Milestone'],
-        defaultmilestone => '---',
-        components       => [
-            {   name             => 'c1',
-                description      => 'c1',
-                initialowner     => $config->{permanent_user},
-                initialqacontact => '',
-                initial_cc       => [],
-            },
-            {   name             => 'c2',
-                description      => 'c2',
-                initialowner     => $config->{permanent_user},
-                initialqacontact => '',
-                initial_cc       => [],
-            },
-        ],
-    },
-
-    {   product_name     => 'C2 Forever',
-        description      => 'I must remain in the Class2_QA classification ' .
-                            'in all cases! Do not edit!',
-        classification   => 'Class2_QA',
-        versions         => ['unspecified', 'C2Ver'],
-        milestones       => ['C2Mil'],
-        defaultmilestone => '---',
-        components       => [
-            {   name             => 'Helium',
-                description      => 'Feel free to add bugs to me',
-                initialowner     => $config->{permanent_user},
-                initialqacontact => '',
-                initial_cc       => [],
-            }
-        ],
-    },
-
-    {   product_name     => 'QA Entry Only',
-        description      => 'Only the QA group may enter bugs here.',
-        versions         => ['unspecified'],
-        milestones       => [],
-        defaultmilestone => '---',
-        components       => [
-            {   name             => 'c1',
-                description      => "Same name as Another Product's component",
-                initialowner     => $config->{QA_Selenium_TEST_user_login},
-                initialqacontact => '',
-                initial_cc       => [],
-            }
-        ],
-    },
-
-    {   product_name     => 'QA Search Only',
-        description      => 'Only the QA group may search for bugs here.',
-        versions         => ['unspecified'],
-        milestones       => [],
-        defaultmilestone => '---',
-        components       => [
-            {   name             => 'c1',
-                description      => 'Still same name as the Another component',
-                initialowner     => $config->{QA_Selenium_TEST_user_login},
-                initialqacontact => '',
-                initial_cc       => [],
-            }
-        ],
-    },
+  {
+    product_name     => 'QA-Selenium-TEST',
+    description      => "used by Selenium test.. DON'T DELETE",
+    versions         => ['unspecified', 'QAVersion'],
+    milestones       => ['QAMilestone'],
+    defaultmilestone => '---',
+    components       => [{
+      name             => 'QA-Selenium-TEST',
+      description      => "used by Selenium test.. DON'T DELETE",
+      initialowner     => $config->{QA_Selenium_TEST_user_login},
+      initialqacontact => $config->{QA_Selenium_TEST_user_login},
+      initial_cc       => [$config->{QA_Selenium_TEST_user_login}],
+    }],
+  },
+
+  {
+    product_name     => 'Another Product',
+    description      => 'Alternate product used by Selenium. <b>Do not edit!</b>',
+    versions         => ['unspecified', 'Another1', 'Another2'],
+    milestones       => ['AnotherMS1', 'AnotherMS2', 'Milestone'],
+    defaultmilestone => '---',
+    components       => [
+      {
+        name             => 'c1',
+        description      => 'c1',
+        initialowner     => $config->{permanent_user},
+        initialqacontact => '',
+        initial_cc       => [],
+      },
+      {
+        name             => 'c2',
+        description      => 'c2',
+        initialowner     => $config->{permanent_user},
+        initialqacontact => '',
+        initial_cc       => [],
+      },
+    ],
+  },
+
+  {
+    product_name => 'C2 Forever',
+    description  => 'I must remain in the Class2_QA classification '
+      . 'in all cases! Do not edit!',
+    classification   => 'Class2_QA',
+    versions         => ['unspecified', 'C2Ver'],
+    milestones       => ['C2Mil'],
+    defaultmilestone => '---',
+    components       => [{
+      name             => 'Helium',
+      description      => 'Feel free to add bugs to me',
+      initialowner     => $config->{permanent_user},
+      initialqacontact => '',
+      initial_cc       => [],
+    }],
+  },
+
+  {
+    product_name     => 'QA Entry Only',
+    description      => 'Only the QA group may enter bugs here.',
+    versions         => ['unspecified'],
+    milestones       => [],
+    defaultmilestone => '---',
+    components       => [{
+      name             => 'c1',
+      description      => "Same name as Another Product's component",
+      initialowner     => $config->{QA_Selenium_TEST_user_login},
+      initialqacontact => '',
+      initial_cc       => [],
+    }],
+  },
+
+  {
+    product_name     => 'QA Search Only',
+    description      => 'Only the QA group may search for bugs here.',
+    versions         => ['unspecified'],
+    milestones       => [],
+    defaultmilestone => '---',
+    components       => [{
+      name             => 'c1',
+      description      => 'Still same name as the Another component',
+      initialowner     => $config->{QA_Selenium_TEST_user_login},
+      initialqacontact => '',
+      initial_cc       => [],
+    }],
+  },
 );
 
 say 'creating products...';
 foreach my $product (@products) {
-    my $new_product = Bugzilla::Product->new({ name => $product->{product_name} });
-    if (!$new_product) {
-        my $class_id = 1;
-        if ($product->{classification}) {
-            $class_id = Bugzilla::Classification->new({ name => $product->{classification} })->id;
-        }
-        $dbh->do('INSERT INTO products (name, description, classification_id) VALUES (?, ?, ?)',
-            undef, ($product->{product_name}, $product->{description}, $class_id));
-
-        $new_product = Bugzilla::Product->new({ name => $product->{product_name} });
-
-        $dbh->do('INSERT INTO milestones (product_id, value) VALUES (?, ?)',
-            undef, ($new_product->id, $product->{defaultmilestone} ));
-
-        # Now clear the internal list of accessible products.
-        delete Bugzilla->user->{selectable_products};
-
-        foreach my $component (@{ $product->{components} }) {
-            Bugzilla::Component->create(
-                {   name             => $component->{name},
-                    product          => $new_product,
-                    description      => $component->{description},
-                    initialowner     => $component->{initialowner},
-                    initialqacontact => $component->{initialqacontact},
-                    initial_cc       => $component->{initial_cc},
-                }
-            );
-        }
+  my $new_product = Bugzilla::Product->new({name => $product->{product_name}});
+  if (!$new_product) {
+    my $class_id = 1;
+    if ($product->{classification}) {
+      $class_id
+        = Bugzilla::Classification->new({name => $product->{classification}})->id;
     }
+    $dbh->do(
+      'INSERT INTO products (name, description, classification_id) VALUES (?, ?, ?)',
+      undef,
+      ($product->{product_name}, $product->{description}, $class_id)
+    );
+
+    $new_product = Bugzilla::Product->new({name => $product->{product_name}});
+
+    $dbh->do('INSERT INTO milestones (product_id, value) VALUES (?, ?)',
+      undef, ($new_product->id, $product->{defaultmilestone}));
+
+    # Now clear the internal list of accessible products.
+    delete Bugzilla->user->{selectable_products};
+
+    foreach my $component (@{$product->{components}}) {
+      Bugzilla::Component->create({
+        name             => $component->{name},
+        product          => $new_product,
+        description      => $component->{description},
+        initialowner     => $component->{initialowner},
+        initialqacontact => $component->{initialqacontact},
+        initial_cc       => $component->{initial_cc},
+      });
+    }
+  }
 
-    foreach my $version (@{ $product->{versions} }) {
-        my $new_version = Bugzilla::Version->new({ name => $version, product => $new_product });
-        if (!$new_version) {
-            Bugzilla::Version->create({ value => $version, product => $new_product });
-        }
+  foreach my $version (@{$product->{versions}}) {
+    my $new_version
+      = Bugzilla::Version->new({name => $version, product => $new_product});
+    if (!$new_version) {
+      Bugzilla::Version->create({value => $version, product => $new_product});
     }
+  }
+
+  foreach my $milestone (@{$product->{milestones}}) {
+    my $new_milestone
+      = Bugzilla::Milestone->new({name => $milestone, product => $new_product});
+    if (!$new_milestone) {
 
-    foreach my $milestone (@{ $product->{milestones} }) {
-        my $new_milestone = Bugzilla::Milestone->new({ name => $milestone, product => $new_product });
-        if (!$new_milestone) {
-            # We don't use Bugzilla::Milestone->create because we want to
-            # bypass security checks.
-            $dbh->do('INSERT INTO milestones (product_id, value) VALUES (?,?)',
-                     undef, $new_product->id, $milestone);
-        }
+      # We don't use Bugzilla::Milestone->create because we want to
+      # bypass security checks.
+      $dbh->do('INSERT INTO milestones (product_id, value) VALUES (?,?)',
+        undef, $new_product->id, $milestone);
     }
+  }
 }
 
 ##########################################################################
@@ -358,29 +365,36 @@ foreach my $product (@products) {
 ##########################################################################
 
 # create Master group
-my ($group_name, $group_desc) = ('Master', 'Master Selenium Group <b>DO NOT EDIT!</b>');
+my ($group_name, $group_desc)
+  = ('Master', 'Master Selenium Group <b>DO NOT EDIT!</b>');
 
 say 'creating groups...';
-my $new_group = Bugzilla::Group->new({ name => $group_name });
+my $new_group = Bugzilla::Group->new({name => $group_name});
 if (!$new_group) {
-    my $group = Bugzilla::Group->create({ name => $group_name,
-                                          description => $group_desc,
-                                          isbuggroup => 1});
+  my $group
+    = Bugzilla::Group->create({
+    name => $group_name, description => $group_desc, isbuggroup => 1
+    });
 
-    $dbh->do('INSERT INTO group_control_map
+  $dbh->do(
+    'INSERT INTO group_control_map
               (group_id, product_id, entry, membercontrol, othercontrol, canedit)
-              SELECT ?, products.id, 0, ?, ?, 0 FROM products',
-              undef, ($group->id, CONTROLMAPSHOWN, CONTROLMAPSHOWN));
+              SELECT ?, products.id, 0, ?, ?, 0 FROM products', undef,
+    ($group->id, CONTROLMAPSHOWN, CONTROLMAPSHOWN)
+  );
 }
 
 # create QA-Selenium-TEST group. Do not use Group->create() so that
 # the admin group doesn't inherit membership (yes, that's what we want!).
-($group_name, $group_desc) = ('QA-Selenium-TEST', "used by Selenium test.. DON'T DELETE");
+($group_name, $group_desc)
+  = ('QA-Selenium-TEST', "used by Selenium test.. DON'T DELETE");
 
-$new_group = Bugzilla::Group->new({ name => $group_name });
+$new_group = Bugzilla::Group->new({name => $group_name});
 if (!$new_group) {
-    $dbh->do('INSERT INTO groups (name, description, isbuggroup, isactive)
-              VALUES (?, ?, 1, 1)', undef, ($group_name, $group_desc));
+  $dbh->do(
+    'INSERT INTO groups (name, description, isbuggroup, isactive)
+              VALUES (?, ?, 1, 1)', undef, ($group_name, $group_desc)
+  );
 }
 
 ##########################################################################
@@ -388,22 +402,24 @@ if (!$new_group) {
 ##########################################################################
 
 my @users_groups = (
-    { user => $config->{QA_Selenium_TEST_user_login}, group => 'QA-Selenium-TEST' },
-    { user => $config->{tweakparams_user_login},      group => 'tweakparams' },
-    { user => $config->{canconfirm_user_login},       group => 'canconfirm' },
-    { user => $config->{editbugs_user_login},         group => 'editbugs' },
+  {user => $config->{QA_Selenium_TEST_user_login}, group => 'QA-Selenium-TEST'},
+  {user => $config->{tweakparams_user_login},      group => 'tweakparams'},
+  {user => $config->{canconfirm_user_login},       group => 'canconfirm'},
+  {user => $config->{editbugs_user_login},         group => 'editbugs'},
 );
 
 say 'adding users to groups...';
 foreach my $user_group (@users_groups) {
-    my $group = Bugzilla::Group->new({ name => $user_group->{group} });
-    my $user = Bugzilla::User->new({ name => $user_group->{user} });
-
-    my $sth_add_mapping =
-      $dbh->prepare('INSERT INTO user_group_map (user_id, group_id, isbless, grant_type)
-                     VALUES (?, ?, ?, ?)');
-    # Don't crash if the entry already exists.
-    eval { $sth_add_mapping->execute($user->id, $group->id, 0, GRANT_DIRECT); };
+  my $group = Bugzilla::Group->new({name => $user_group->{group}});
+  my $user = Bugzilla::User->new({name => $user_group->{user}});
+
+  my $sth_add_mapping = $dbh->prepare(
+    'INSERT INTO user_group_map (user_id, group_id, isbless, grant_type)
+                     VALUES (?, ?, ?, ?)'
+  );
+
+  # Don't crash if the entry already exists.
+  eval { $sth_add_mapping->execute($user->id, $group->id, 0, GRANT_DIRECT); };
 }
 
 ##########################################################################
@@ -411,64 +427,100 @@ foreach my $user_group (@users_groups) {
 ##########################################################################
 
 # Associate the QA-Selenium-TEST group with the QA-Selenium-TEST.
-my $created_group   = Bugzilla::Group->new({ name => 'QA-Selenium-TEST' });
-my $secret_product = Bugzilla::Product->new({ name => 'QA-Selenium-TEST' });
-my $no_entry = Bugzilla::Product->new({ name => 'QA Entry Only' });
-my $no_search = Bugzilla::Product->new({ name => 'QA Search Only' });
+my $created_group = Bugzilla::Group->new({name => 'QA-Selenium-TEST'});
+my $secret_product = Bugzilla::Product->new({name => 'QA-Selenium-TEST'});
+my $no_entry       = Bugzilla::Product->new({name => 'QA Entry Only'});
+my $no_search      = Bugzilla::Product->new({name => 'QA Search Only'});
 
 say 'restricting products to groups...';
+
 # Don't crash if the entries already exist.
-my $sth = $dbh->prepare('INSERT INTO group_control_map
+my $sth = $dbh->prepare(
+  'INSERT INTO group_control_map
                          (group_id, product_id, entry, membercontrol, othercontrol, canedit)
-                         VALUES (?, ?, ?, ?, ?, ?)');
-eval { $sth->execute($created_group->id, $secret_product->id, 1, CONTROLMAPMANDATORY, CONTROLMAPMANDATORY, 0); };
-eval { $sth->execute($created_group->id, $no_entry->id,       1, CONTROLMAPNA,        CONTROLMAPNA,        0); };
-eval { $sth->execute($created_group->id, $no_search->id,      0, CONTROLMAPMANDATORY, CONTROLMAPMANDATORY, 0); };
+                         VALUES (?, ?, ?, ?, ?, ?)'
+);
+eval {
+  $sth->execute($created_group->id, $secret_product->id, 1, CONTROLMAPMANDATORY,
+    CONTROLMAPMANDATORY, 0);
+};
+eval {
+  $sth->execute($created_group->id, $no_entry->id, 1, CONTROLMAPNA, CONTROLMAPNA,
+    0);
+};
+eval {
+  $sth->execute($created_group->id, $no_search->id, 0, CONTROLMAPMANDATORY,
+    CONTROLMAPMANDATORY, 0);
+};
 
 ##########################################################################
 # Create flag types
 ##########################################################################
 
 my @flagtypes = (
-    {name => 'spec_multi_flag', desc => 'Specifically requestable and multiplicable bug flag',
-     is_requestable => 1, is_requesteeble => 1, is_multiplicable => 1, grant_group => 'editbugs',
-     target_type => 'b', cc_list => '', inclusions => ['Another Product:c1']},
+  {
+    name             => 'spec_multi_flag',
+    desc             => 'Specifically requestable and multiplicable bug flag',
+    is_requestable   => 1,
+    is_requesteeble  => 1,
+    is_multiplicable => 1,
+    grant_group      => 'editbugs',
+    target_type      => 'b',
+    cc_list          => '',
+    inclusions       => ['Another Product:c1']
+  },
 );
 
 say 'creating flag types...';
 foreach my $flag (@flagtypes) {
-    # The name is not unique, even within a single product/component, so there is NO WAY
-    # to know if the existing flag type is the one we want or not.
-    # As our Selenium scripts would be confused anyway if there is already such a flag name,
-    # we simply skip it and assume the existing flag type is the one we want.
-    next if Bugzilla::FlagType->new({ name => $flag->{name} });
-
-    my $grant_group_id = $flag->{grant_group} ? Bugzilla::Group->new({ name => $flag->{grant_group} })->id : undef;
-    my $request_group_id = $flag->{request_group} ? Bugzilla::Group->new({ name => $flag->{request_group} })->id : undef;
 
-    $dbh->do('INSERT INTO flagtypes (name, description, cc_list, target_type, is_requestable,
+# The name is not unique, even within a single product/component, so there is NO WAY
+# to know if the existing flag type is the one we want or not.
+# As our Selenium scripts would be confused anyway if there is already such a flag name,
+# we simply skip it and assume the existing flag type is the one we want.
+  next if Bugzilla::FlagType->new({name => $flag->{name}});
+
+  my $grant_group_id
+    = $flag->{grant_group}
+    ? Bugzilla::Group->new({name => $flag->{grant_group}})->id
+    : undef;
+  my $request_group_id
+    = $flag->{request_group}
+    ? Bugzilla::Group->new({name => $flag->{request_group}})->id
+    : undef;
+
+  $dbh->do(
+    'INSERT INTO flagtypes (name, description, cc_list, target_type, is_requestable,
                                      is_requesteeble, is_multiplicable, grant_group_id, request_group_id)
                              VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)',
-             undef, ($flag->{name}, $flag->{desc}, $flag->{cc_list}, $flag->{target_type},
-                     $flag->{is_requestable}, $flag->{is_requesteeble}, $flag->{is_multiplicable},
-                     $grant_group_id, $request_group_id));
-
-    my $type_id = $dbh->bz_last_key('flagtypes', 'id');
-
-    foreach my $inclusion (@{$flag->{inclusions}}) {
-        my ($product, $component) = split(':', $inclusion);
-        my ($prod_id, $comp_id);
-        if ($product) {
-            my $prod_obj = Bugzilla::Product->new({ name => $product });
-            $prod_id = $prod_obj->id;
-            if ($component) {
-                $comp_id = Bugzilla::Component->new({ name => $component, product => $prod_obj})->id;
-            }
-        }
-        $dbh->do('INSERT INTO flaginclusions (type_id, product_id, component_id)
-                  VALUES (?, ?, ?)',
-                 undef, ($type_id, $prod_id, $comp_id));
+    undef,
+    (
+      $flag->{name},             $flag->{desc},
+      $flag->{cc_list},          $flag->{target_type},
+      $flag->{is_requestable},   $flag->{is_requesteeble},
+      $flag->{is_multiplicable}, $grant_group_id,
+      $request_group_id
+    )
+  );
+
+  my $type_id = $dbh->bz_last_key('flagtypes', 'id');
+
+  foreach my $inclusion (@{$flag->{inclusions}}) {
+    my ($product, $component) = split(':', $inclusion);
+    my ($prod_id, $comp_id);
+    if ($product) {
+      my $prod_obj = Bugzilla::Product->new({name => $product});
+      $prod_id = $prod_obj->id;
+      if ($component) {
+        $comp_id
+          = Bugzilla::Component->new({name => $component, product => $prod_obj})->id;
+      }
     }
+    $dbh->do(
+      'INSERT INTO flaginclusions (type_id, product_id, component_id)
+                  VALUES (?, ?, ?)', undef, ($type_id, $prod_id, $comp_id)
+    );
+  }
 }
 
 ##########################################################################
@@ -476,47 +528,51 @@ foreach my $flag (@flagtypes) {
 ##########################################################################
 
 my @fields = (
-    { name        => 'cf_QA_status',
-      description => 'QA Status',
-      type        => FIELD_TYPE_MULTI_SELECT,
-      sortkey     => 100,
-      mailhead    => 0,
-      enter_bug   => 1,
-      obsolete    => 0,
-      custom      => 1,
-      values      => ['verified', 'in progress', 'untested']
-    },
-    { name        => 'cf_single_select',
-      description => 'SingSel',
-      type        => FIELD_TYPE_SINGLE_SELECT,
-      mailhead    => 0,
-      enter_bug   => 1,
-      custom      => 1,
-      values      => [qw(one two three)],
-    },
+  {
+    name        => 'cf_QA_status',
+    description => 'QA Status',
+    type        => FIELD_TYPE_MULTI_SELECT,
+    sortkey     => 100,
+    mailhead    => 0,
+    enter_bug   => 1,
+    obsolete    => 0,
+    custom      => 1,
+    values      => ['verified', 'in progress', 'untested']
+  },
+  {
+    name        => 'cf_single_select',
+    description => 'SingSel',
+    type        => FIELD_TYPE_SINGLE_SELECT,
+    mailhead    => 0,
+    enter_bug   => 1,
+    custom      => 1,
+    values      => [qw(one two three)],
+  },
 );
 
 say 'creating custom fields...';
 foreach my $f (@fields) {
-    # Skip existing custom fields.
-    next if Bugzilla::Field->new({ name => $f->{name} });
-
-    my @values;
-    if (exists $f->{values}) {
-        @values = @{$f->{values}};
-        # We have to delete this key, else create() will complain
-        # that 'values' is not an existing column name.
-        delete $f->{values};
-    }
-    my $field = Bugzilla::Field->create($f);
 
-    # Now populate the table with valid values, if necessary.
-    next unless scalar @values;
+  # Skip existing custom fields.
+  next if Bugzilla::Field->new({name => $f->{name}});
 
-    my $sth = $dbh->prepare('INSERT INTO ' . $field->name . ' (value) VALUES (?)');
-    foreach my $value (@values) {
-        $sth->execute($value);
-    }
+  my @values;
+  if (exists $f->{values}) {
+    @values = @{$f->{values}};
+
+    # We have to delete this key, else create() will complain
+    # that 'values' is not an existing column name.
+    delete $f->{values};
+  }
+  my $field = Bugzilla::Field->create($f);
+
+  # Now populate the table with valid values, if necessary.
+  next unless scalar @values;
+
+  my $sth = $dbh->prepare('INSERT INTO ' . $field->name . ' (value) VALUES (?)');
+  foreach my $value (@values) {
+    $sth->execute($value);
+  }
 }
 
 ####################################################################
@@ -524,13 +580,13 @@ foreach my $f (@fields) {
 ####################################################################
 
 if (Bugzilla->params->{insidergroup} ne 'QA-Selenium-TEST') {
-    SetParam('insidergroup', 'QA-Selenium-TEST');
-    write_params();
+  SetParam('insidergroup', 'QA-Selenium-TEST');
+  write_params();
 }
 
 if (Bugzilla->params->{timetrackinggroup} ne 'editbugs') {
-    SetParam('timetrackinggroup', 'editbugs');
-    write_params();
+  SetParam('timetrackinggroup', 'editbugs');
+  write_params();
 }
 
 ########################
@@ -539,19 +595,19 @@ if (Bugzilla->params->{timetrackinggroup} ne 'editbugs') {
 
 my $test_user = Bugzilla::User->check($config->{QA_Selenium_TEST_user_login});
 $test_user->{'groups'} = [
-    Bugzilla::Group->new({ name => 'editbugs' }),
-    Bugzilla::Group->new({ name => 'QA-Selenium-TEST' })
-]; # editbugs is needed for alias creation
+  Bugzilla::Group->new({name => 'editbugs'}),
+  Bugzilla::Group->new({name => 'QA-Selenium-TEST'})
+];    # editbugs is needed for alias creation
 Bugzilla->set_user($test_user);
 
 if (Bugzilla::Bug->new('private_bug')->{error}) {
-    say 'Creating private bug...';
-    my %priv_values = %field_values;
-    $priv_values{alias} = 'private_bug';
-    $priv_values{product} = 'QA-Selenium-TEST';
-    $priv_values{component} = 'QA-Selenium-TEST';
-    my $bug = Bugzilla::Bug->create(\%priv_values);
-    say 'Bug ' . $bug->id . ' (alias: private_bug) created';
+  say 'Creating private bug...';
+  my %priv_values = %field_values;
+  $priv_values{alias}     = 'private_bug';
+  $priv_values{product}   = 'QA-Selenium-TEST';
+  $priv_values{component} = 'QA-Selenium-TEST';
+  my $bug = Bugzilla::Bug->create(\%priv_values);
+  say 'Bug ' . $bug->id . ' (alias: private_bug) created';
 }
 
 ######################
@@ -559,28 +615,29 @@ if (Bugzilla::Bug->new('private_bug')->{error}) {
 ######################
 
 say 'creating attachments...';
+
 # We use the contents of this script as the attachment.
 open(my $attachment_fh, '<', __FILE__) or die __FILE__ . ": $!";
 my $attachment_contents;
 {
-    local $/;
-    $attachment_contents = <$attachment_fh>;
+  local $/;
+  $attachment_contents = <$attachment_fh>;
 }
 close($attachment_fh);
 
 foreach my $alias (qw(public_bug private_bug)) {
-    my $bug = Bugzilla::Bug->new($alias);
-
-    foreach my $is_private (0, 1) {
-        Bugzilla::Attachment->create({
-            bug  => $bug,
-            data => $attachment_contents,
-            description => "${alias}_${is_private}",
-            filename  => "${alias}_${is_private}.pl",
-            mimetype  => 'application/x-perl',
-            isprivate => $is_private,
-        });
-    }
+  my $bug = Bugzilla::Bug->new($alias);
+
+  foreach my $is_private (0, 1) {
+    Bugzilla::Attachment->create({
+      bug         => $bug,
+      data        => $attachment_contents,
+      description => "${alias}_${is_private}",
+      filename    => "${alias}_${is_private}.pl",
+      mimetype    => 'application/x-perl',
+      isprivate   => $is_private,
+    });
+  }
 }
 
 ###################
@@ -588,16 +645,20 @@ foreach my $alias (qw(public_bug private_bug)) {
 ###################
 
 my @keywords = (
-    { name => 'test-keyword-1',
-      description => 'Created for Bugzilla QA Tests, Keyword 1' },
-    { name => 'test-keyword-2',
-      description => 'Created for Bugzilla QA Tests, Keyword 2' },
+  {
+    name        => 'test-keyword-1',
+    description => 'Created for Bugzilla QA Tests, Keyword 1'
+  },
+  {
+    name        => 'test-keyword-2',
+    description => 'Created for Bugzilla QA Tests, Keyword 2'
+  },
 );
 
 say 'creating keywords...';
 foreach my $kw (@keywords) {
-    next if Bugzilla::Keyword->new({ name => $kw->{name} });
-    Bugzilla::Keyword->create($kw);
+  next if Bugzilla::Keyword->new({name => $kw->{name}});
+  Bugzilla::Keyword->create($kw);
 }
 
 ############################
index c234e12c00df14c75ff4f2d8f3f47745dc0d4905..cb9ee3bb55332cc982e179f62ec7ae142145341f 100644 (file)
@@ -13,10 +13,8 @@ use warnings;
 
 use constant NAME => 'QA';
 
-use constant REQUIRED_MODULES => [
-];
+use constant REQUIRED_MODULES => [];
 
-use constant OPTIONAL_MODULES => [
-];
+use constant OPTIONAL_MODULES => [];
 
 __PACKAGE__->NAME;
index 2259cfecc8c7277b8dc99bf0fd84445ad1eef65f..38f2ec0b08a1f20d755ad1e610cf41e0f85fa433 100644 (file)
@@ -23,52 +23,52 @@ use Bugzilla::User;
 our $VERSION = '1.0';
 
 sub page_before_template {
-    my ($self, $args) = @_;
-    return if $args->{page_id} ne 'qa/email_in.html';
-
-    my $template = Bugzilla->template;
-    my $cgi = Bugzilla->cgi;
-    print $cgi->header;
-
-    # Needed to make sure he can access and edit bugs.
-    my $user = Bugzilla::User->check(scalar $cgi->param('sender'));
-    Bugzilla->set_user($user);
-
-    my ($output, $tmpl_file);
-    my $action = $cgi->param('action') || '';
-    my $vars = { sender => $user, action => $action, pid => $$ };
-
-    if ($action eq 'create') {
-        $tmpl_file = 'qa/create_bug.txt.tmpl';
-    }
-    elsif ($action eq 'create_with_headers') {
-        $tmpl_file = 'qa/create_bug_with_headers.txt.tmpl';
-    }
-    elsif ($action =~ /^update(_with_headers)?$/) {
-        my $f = $1 || '';
-        $tmpl_file = "qa/update_bug$f.txt.tmpl";
-        my $bug = Bugzilla::Bug->check(scalar $cgi->param('bug_id'));
-        $vars->{bug_id} = $bug->id;
-    }
-    else {
-        ThrowUserError('unknown_action', { action => $action });
-    }
-
-    $template->process($tmpl_file, $vars, \$output)
-      or ThrowTemplateError($template->error());
-
-    my $file = "/tmp/email_in_$$.txt";
-    open(FH, '>', $file);
-    print FH $output;
-    close FH;
-
-    $output = `email_in.pl -v < $file 2>&1`;
-    unlink $file;
-
-    parse_output($output, $vars);
-
-    $template->process('qa/results.html.tmpl', $vars)
-      or ThrowTemplateError($template->error());
+  my ($self, $args) = @_;
+  return if $args->{page_id} ne 'qa/email_in.html';
+
+  my $template = Bugzilla->template;
+  my $cgi      = Bugzilla->cgi;
+  print $cgi->header;
+
+  # Needed to make sure he can access and edit bugs.
+  my $user = Bugzilla::User->check(scalar $cgi->param('sender'));
+  Bugzilla->set_user($user);
+
+  my ($output, $tmpl_file);
+  my $action = $cgi->param('action') || '';
+  my $vars = {sender => $user, action => $action, pid => $$};
+
+  if ($action eq 'create') {
+    $tmpl_file = 'qa/create_bug.txt.tmpl';
+  }
+  elsif ($action eq 'create_with_headers') {
+    $tmpl_file = 'qa/create_bug_with_headers.txt.tmpl';
+  }
+  elsif ($action =~ /^update(_with_headers)?$/) {
+    my $f = $1 || '';
+    $tmpl_file = "qa/update_bug$f.txt.tmpl";
+    my $bug = Bugzilla::Bug->check(scalar $cgi->param('bug_id'));
+    $vars->{bug_id} = $bug->id;
+  }
+  else {
+    ThrowUserError('unknown_action', {action => $action});
+  }
+
+  $template->process($tmpl_file, $vars, \$output)
+    or ThrowTemplateError($template->error());
+
+  my $file = "/tmp/email_in_$$.txt";
+  open(FH, '>', $file);
+  print FH $output;
+  close FH;
+
+  $output = `email_in.pl -v < $file 2>&1`;
+  unlink $file;
+
+  parse_output($output, $vars);
+
+  $template->process('qa/results.html.tmpl', $vars)
+    or ThrowTemplateError($template->error());
 }
 
 __PACKAGE__->NAME;
index 48ae944ef6d2772392c8f8859b91723a10b07628..c83952a982b83d28bf5b38c15d7ddcc8d2010f37 100644 (file)
@@ -14,15 +14,15 @@ use warnings;
 use parent qw(Exporter);
 
 our @EXPORT = qw(
-    parse_output
+  parse_output
 );
 
 sub parse_output {
-    my ($output, $vars) = @_;
+  my ($output, $vars) = @_;
 
-    $vars->{error} = ($output =~ /software error/i) ? 1 : 0;
-    $vars->{output} = $output;
-    $vars->{bug_id} ||= ($output =~ /Created bug (\d+)/i) ? $1 : undef;
+  $vars->{error} = ($output =~ /software error/i) ? 1 : 0;
+  $vars->{output} = $output;
+  $vars->{bug_id} ||= ($output =~ /Created bug (\d+)/i) ? $1 : undef;
 }
 
 1;
index 23d4f4433d49b367f2bf1e97d062eec9d98a6fc6..11f0d98cac7602bd7128fef76e3a3de11073a33b 100644 (file)
@@ -23,45 +23,48 @@ use QA::Util;
 use parent qw(LWP::UserAgent Exporter);
 
 @QA::REST::EXPORT = qw(
-    MUST_FAIL
-    get_rest_client
+  MUST_FAIL
+  get_rest_client
 );
 
 use constant MUST_FAIL => 1;
 
 sub get_rest_client {
-    my $rest_client = LWP::UserAgent->new( ssl_opts => { verify_hostname => 0 } );
-    bless($rest_client, 'QA::REST');
-    my $config = $rest_client->{bz_config} = get_config();
-    $rest_client->{bz_url} = $config->{browser_url} . '/' . $config->{bugzilla_installation} . '/rest/';
-    $rest_client->{bz_default_headers} = {'Accept' => 'application/json', 'Content-Type' => 'application/json'};
-    return $rest_client;
+  my $rest_client = LWP::UserAgent->new(ssl_opts => {verify_hostname => 0});
+  bless($rest_client, 'QA::REST');
+  my $config = $rest_client->{bz_config} = get_config();
+  $rest_client->{bz_url}
+    = $config->{browser_url} . '/' . $config->{bugzilla_installation} . '/rest/';
+  $rest_client->{bz_default_headers}
+    = {'Accept' => 'application/json', 'Content-Type' => 'application/json'};
+  return $rest_client;
 }
 
 sub bz_config { return $_[0]->{bz_config}; }
 
 sub call {
-    my ($self, $method, $data, $http_verb, $expect_to_fail) = @_;
-    $http_verb = lc($http_verb || 'GET');
-    $data //= {};
+  my ($self, $method, $data, $http_verb, $expect_to_fail) = @_;
+  $http_verb = lc($http_verb || 'GET');
+  $data //= {};
 
-    my %args = %{ $self->{bz_default_headers} };
-    # We do not pass the API key in the URL, so that it's not logged by the web server.
-    if ($http_verb eq 'get' && $data->{api_key}) {
-        $args{'X-BUGZILLA-API-KEY'} = $data->{api_key};
-    }
-    elsif ($http_verb ne 'get') {
-        $args{Content} = encode_json($data);
-    }
+  my %args = %{$self->{bz_default_headers}};
 
-    my $response = $self->$http_verb($self->{bz_url} . $method, %args);
-    my $res = decode_json($response->decoded_content);
-    if ($response->is_success xor $expect_to_fail) {
-        return $res;
-    }
-    else {
-        die 'error ' . $res->{code} . ': ' . $res->{message} . "\n";
-    }
+# We do not pass the API key in the URL, so that it's not logged by the web server.
+  if ($http_verb eq 'get' && $data->{api_key}) {
+    $args{'X-BUGZILLA-API-KEY'} = $data->{api_key};
+  }
+  elsif ($http_verb ne 'get') {
+    $args{Content} = encode_json($data);
+  }
+
+  my $response = $self->$http_verb($self->{bz_url} . $method, %args);
+  my $res = decode_json($response->decoded_content);
+  if ($response->is_success xor $expect_to_fail) {
+    return $res;
+  }
+  else {
+    die 'error ' . $res->{code} . ': ' . $res->{message} . "\n";
+  }
 }
 
 1;
index 93414e2313d5296981d8560d2149812d4523d6f1..d7954bf2c19ae3c0c35f90a3ab41e2c6cca818d5 100644 (file)
@@ -23,9 +23,9 @@ use Storable qw(dclone);
 use Test::More;
 
 sub bz_config {
-    my $self = shift;
-    $self->{bz_config} ||= QA::Util::get_config();
-    return $self->{bz_config};
+  my $self = shift;
+  $self->{bz_config} ||= QA::Util::get_config();
+  return $self->{bz_config};
 }
 
 # True if we're doing calls over GET instead of POST.
@@ -36,12 +36,12 @@ sub bz_get_mode { return 0 }
 # and Bugzilla_password with every future call until User.logout is called
 # (which actually just calls _bz_clear_credentials, under GET).
 sub _bz_credentials {
-    my ($self, $user, $pass) = @_;
-    if (@_ == 3) {
-        $self->{_bz_credentials}->{user} = $user;
-        $self->{_bz_credentials}->{pass} = $pass;
-    }
-    return $self->{_bz_credentials};
+  my ($self, $user, $pass) = @_;
+  if (@_ == 3) {
+    $self->{_bz_credentials}->{user} = $user;
+    $self->{_bz_credentials}->{pass} = $pass;
+  }
+  return $self->{_bz_credentials};
 }
 sub _bz_clear_credentials { delete $_[0]->{_bz_credentials} }
 
@@ -50,247 +50,265 @@ sub _bz_clear_credentials { delete $_[0]->{_bz_credentials} }
 ################################
 
 sub bz_log_in {
-    my ($self, $user) = @_;
-    my $username = $self->bz_config->{"${user}_user_login"};
-    my $password = $self->bz_config->{"${user}_user_passwd"};
-
-    if ($self->bz_get_mode) {
-        $self->_bz_credentials($username, $password);
-        return;
-    }
-
-    my $call = $self->bz_call_success(
-        'User.login', { login => $username, password => $password });
-    cmp_ok($call->result->{id}, 'gt', 0, $self->TYPE . ": Logged in as $user");
-    $self->{_bz_credentials}->{token} = $call->result->{token};
+  my ($self, $user) = @_;
+  my $username = $self->bz_config->{"${user}_user_login"};
+  my $password = $self->bz_config->{"${user}_user_passwd"};
+
+  if ($self->bz_get_mode) {
+    $self->_bz_credentials($username, $password);
+    return;
+  }
+
+  my $call = $self->bz_call_success('User.login',
+    {login => $username, password => $password});
+  cmp_ok($call->result->{id}, 'gt', 0, $self->TYPE . ": Logged in as $user");
+  $self->{_bz_credentials}->{token} = $call->result->{token};
 }
 
 sub bz_call_success {
-    my ($self, $method, $orig_args, $test_name) = @_;
-    my $args = $orig_args ? dclone($orig_args) : {};
-
-    if ($self->bz_get_mode and $method eq 'User.logout') {
-        $self->_bz_clear_credentials();
-        return;
-    }
-
-    my $call;
-    # Under XMLRPC::Lite, if we pass undef as the second argument,
-    # it sends a single param <value />, which shows up as an
-    # empty string on the Bugzilla side.
-    if ($self->{_bz_credentials}->{token}) {
-        $args->{Bugzilla_token} = $self->{_bz_credentials}->{token};
-    }
-
-    if (scalar keys %$args) {
-        $call = $self->call($method, $args);
-    }
-    else {
-        $call = $self->call($method);
-    }
-    $test_name ||= "$method returned successfully";
-    $self->_handle_undef_response($test_name) if !$call;
-    ok(!$call->fault, $self->TYPE . ": $test_name")
-        or diag($call->faultstring);
-
-    if ($method eq 'User.logout') {
-        delete $self->{_bz_credentials}->{token};
-    }
-    return $call;
+  my ($self, $method, $orig_args, $test_name) = @_;
+  my $args = $orig_args ? dclone($orig_args) : {};
+
+  if ($self->bz_get_mode and $method eq 'User.logout') {
+    $self->_bz_clear_credentials();
+    return;
+  }
+
+  my $call;
+
+  # Under XMLRPC::Lite, if we pass undef as the second argument,
+  # it sends a single param <value />, which shows up as an
+  # empty string on the Bugzilla side.
+  if ($self->{_bz_credentials}->{token}) {
+    $args->{Bugzilla_token} = $self->{_bz_credentials}->{token};
+  }
+
+  if (scalar keys %$args) {
+    $call = $self->call($method, $args);
+  }
+  else {
+    $call = $self->call($method);
+  }
+  $test_name ||= "$method returned successfully";
+  $self->_handle_undef_response($test_name) if !$call;
+  ok(!$call->fault, $self->TYPE . ": $test_name") or diag($call->faultstring);
+
+  if ($method eq 'User.logout') {
+    delete $self->{_bz_credentials}->{token};
+  }
+  return $call;
 }
 
 sub bz_call_fail {
-    my ($self, $method, $orig_args, $faultstring, $test_name) = @_;
-    my $args = $orig_args ? dclone($orig_args) : {};
-
-    if ($self->{_bz_credentials}->{token}) {
-        $args->{Bugzilla_token} = $self->{_bz_credentials}->{token};
-    }
-
-    $test_name ||= "$method failed (as intended)";
-    my $call = $self->call($method, $args);
-    $self->_handle_undef_response($test_name) if !$call;
-    ok($call->fault, $self->TYPE . ": $test_name")
-        or diag("Returned: " . Dumper($call->result));
-    if (defined $faultstring) {
-        cmp_ok(trim($call->faultstring), '=~', $faultstring,
-               $self->TYPE . ": Got correct fault for $method");
-    }
-    ok($call->faultcode
-       && (($call->faultcode < 32000 && $call->faultcode > -32000)
-           # Fault codes 32610 and above are OK because they are errors
-           # that we expect and test for sometimes.
-           || $call->faultcode >= 32610),
-       $self->TYPE . ': Fault code is set properly')
-        or diag("Code: " . $call->faultcode
-                . " Message: " . $call->faultstring);
-
-    return $call;
+  my ($self, $method, $orig_args, $faultstring, $test_name) = @_;
+  my $args = $orig_args ? dclone($orig_args) : {};
+
+  if ($self->{_bz_credentials}->{token}) {
+    $args->{Bugzilla_token} = $self->{_bz_credentials}->{token};
+  }
+
+  $test_name ||= "$method failed (as intended)";
+  my $call = $self->call($method, $args);
+  $self->_handle_undef_response($test_name) if !$call;
+  ok($call->fault, $self->TYPE . ": $test_name")
+    or diag("Returned: " . Dumper($call->result));
+  if (defined $faultstring) {
+    cmp_ok(trim($call->faultstring),
+      '=~', $faultstring, $self->TYPE . ": Got correct fault for $method");
+  }
+  ok(
+    $call->faultcode && (
+      ($call->faultcode < 32000 && $call->faultcode > -32000)
+
+      # Fault codes 32610 and above are OK because they are errors
+      # that we expect and test for sometimes.
+      || $call->faultcode >= 32610
+    ),
+    $self->TYPE . ': Fault code is set properly'
+  ) or diag("Code: " . $call->faultcode . " Message: " . $call->faultstring);
+
+  return $call;
 }
 
 sub _handle_undef_response {
-    my ($self, $test_name) = @_;
-    my $response = $self->transport->http_response;
-    die "$test_name:\n", $response->as_string;
+  my ($self, $test_name) = @_;
+  my $response = $self->transport->http_response;
+  die "$test_name:\n", $response->as_string;
 }
 
 sub bz_get_products {
-    my ($self) = @_;
-    $self->bz_log_in('QA_Selenium_TEST');
-
-    my $accessible = $self->bz_call_success('Product.get_accessible_products');
-    my $prod_call = $self->bz_call_success('Product.get', $accessible->result);
-    my %products;
-    foreach my $prod (@{ $prod_call->result->{products} }) {
-        $products{$prod->{name}} = $prod->{id};
-    }
-
-    $self->bz_call_success('User.logout');
-    return \%products;
+  my ($self) = @_;
+  $self->bz_log_in('QA_Selenium_TEST');
+
+  my $accessible = $self->bz_call_success('Product.get_accessible_products');
+  my $prod_call = $self->bz_call_success('Product.get', $accessible->result);
+  my %products;
+  foreach my $prod (@{$prod_call->result->{products}}) {
+    $products{$prod->{name}} = $prod->{id};
+  }
+
+  $self->bz_call_success('User.logout');
+  return \%products;
 }
 
-sub _string_array { map { random_string() } (1..$_[0]) }
+sub _string_array {
+  map { random_string() } (1 .. $_[0]);
+}
 
 sub bz_create_test_bugs {
-    my ($self, $second_private) = @_;
-    my $config = $self->bz_config;
-
-    my @whiteboard_strings = _string_array(3);
-    my @summary_strings = _string_array(3);
-
-    my $public_bug = create_bug_fields($config);
-    $public_bug->{whiteboard} = join(' ', @whiteboard_strings);
-    $public_bug->{summary} = join(' ', @summary_strings);
-
-    my $private_bug = dclone($public_bug);
-    if ($second_private) {
-        $private_bug->{product}   = 'QA-Selenium-TEST';
-        $private_bug->{component} = 'QA-Selenium-TEST';
-        $private_bug->{target_milestone} = 'QAMilestone';
-        $private_bug->{version} = 'QAVersion';
-        # Although we don't directly use this, this helps some tests that
-        # depend on the values in $private_bug.
-        $private_bug->{creator} = $config->{PRIVATE_BUG_USER . '_user_login'};
-    }
-
-    my @create_bugs = (
-        { user => 'editbugs',
-          args => $public_bug,
-          test => 'Create a public bug' },
-        { user => $second_private ? PRIVATE_BUG_USER : 'editbugs',
-          args => $private_bug,
-          test => $second_private ? 'Create a private bug'
-                                  : 'Create a second public bug' },
-    );
-
-    my $post_success = sub {
-        my ($call, $t) = @_;
-        my $id = $call->result->{id};
-        $t->{args}->{id} = $id;
-    };
-
-    # Creating the bugs isn't really a test, it's just preliminary work
-    # for the tests. So we just run it with one of the RPC clients.
-    $self->bz_run_tests(tests => \@create_bugs, method => 'Bug.create',
-                        post_success => $post_success);
-
-    return ($public_bug, $private_bug);
+  my ($self, $second_private) = @_;
+  my $config = $self->bz_config;
+
+  my @whiteboard_strings = _string_array(3);
+  my @summary_strings    = _string_array(3);
+
+  my $public_bug = create_bug_fields($config);
+  $public_bug->{whiteboard} = join(' ', @whiteboard_strings);
+  $public_bug->{summary}    = join(' ', @summary_strings);
+
+  my $private_bug = dclone($public_bug);
+  if ($second_private) {
+    $private_bug->{product}          = 'QA-Selenium-TEST';
+    $private_bug->{component}        = 'QA-Selenium-TEST';
+    $private_bug->{target_milestone} = 'QAMilestone';
+    $private_bug->{version}          = 'QAVersion';
+
+    # Although we don't directly use this, this helps some tests that
+    # depend on the values in $private_bug.
+    $private_bug->{creator} = $config->{PRIVATE_BUG_USER . '_user_login'};
+  }
+
+  my @create_bugs = (
+    {user => 'editbugs', args => $public_bug, test => 'Create a public bug'},
+    {
+      user => $second_private ? PRIVATE_BUG_USER : 'editbugs',
+      args => $private_bug,
+      test => $second_private ? 'Create a private bug' : 'Create a second public bug'
+    },
+  );
+
+  my $post_success = sub {
+    my ($call, $t) = @_;
+    my $id = $call->result->{id};
+    $t->{args}->{id} = $id;
+  };
+
+  # Creating the bugs isn't really a test, it's just preliminary work
+  # for the tests. So we just run it with one of the RPC clients.
+  $self->bz_run_tests(
+    tests        => \@create_bugs,
+    method       => 'Bug.create',
+    post_success => $post_success
+  );
+
+  return ($public_bug, $private_bug);
 }
 
 sub bz_run_tests {
-    my ($self, %params) = @_;
-    # Required params
-    my $config = $self->bz_config;
-    my $tests  = $params{tests};
-    my $method = $params{method};
-
-    # Optional params
-    my $post_success = $params{post_success};
-    my $pre_call = $params{pre_call};
-
-    my $former_user = '';
-    foreach my $t (@$tests) {
-        # Only logout/login if the user has changed since the last test
-        # (this saves us LOTS of needless logins).
-        my $user = $t->{user} || '';
-        if ($former_user ne $user) {
-            $self->bz_call_success('User.logout') if $former_user;
-            $self->bz_log_in($user) if $user;
-            $former_user = $user;
-        }
-
-        $pre_call->($t, $self) if $pre_call;
-
-        if ($t->{error}) {
-            $self->bz_call_fail($method, $t->{args}, $t->{error}, $t->{test});
-        }
-        else {
-            my $call = $self->bz_call_success($method, $t->{args}, $t->{test});
-            if ($call->result && $post_success) {
-                $post_success->($call, $t, $self);
-            }
-        }
+  my ($self, %params) = @_;
+
+  # Required params
+  my $config = $self->bz_config;
+  my $tests  = $params{tests};
+  my $method = $params{method};
+
+  # Optional params
+  my $post_success = $params{post_success};
+  my $pre_call     = $params{pre_call};
+
+  my $former_user = '';
+  foreach my $t (@$tests) {
+
+    # Only logout/login if the user has changed since the last test
+    # (this saves us LOTS of needless logins).
+    my $user = $t->{user} || '';
+    if ($former_user ne $user) {
+      $self->bz_call_success('User.logout') if $former_user;
+      $self->bz_log_in($user) if $user;
+      $former_user = $user;
+    }
+
+    $pre_call->($t, $self) if $pre_call;
+
+    if ($t->{error}) {
+      $self->bz_call_fail($method, $t->{args}, $t->{error}, $t->{test});
+    }
+    else {
+      my $call = $self->bz_call_success($method, $t->{args}, $t->{test});
+      if ($call->result && $post_success) {
+        $post_success->($call, $t, $self);
+      }
     }
+  }
 
-    $self->bz_call_success('User.logout') if $former_user;
+  $self->bz_call_success('User.logout') if $former_user;
 }
 
 sub bz_test_bug {
-    my ($self, $fields, $bug, $orig_expect, $t, $creation_time) = @_;
-
-    my $expect = dclone($orig_expect);
-
-    foreach my $field (sort @$fields) {
-        # "description" is used by Bug.create but comments are not returned
-        # by Bug.get or Bug.search.
-        next if $field eq 'description';
-
-        my @include = @{ $t->{args}->{include_fields} || [] };
-        my @exclude = @{ $t->{args}->{exclude_fields} || [] };
-        if ( (@include and !grep($_ eq $field, @include))
-             or (@exclude and grep($_ eq $field, @exclude)) )
-        {
-            ok(!exists $bug->{$field}, "$field is not included")
-              or diag Dumper($bug);
-            next;
-        }
-
-       foreach my $field (qw(assigned_to creator qa_contact)) {
-            if (!$t->{user}) {
-                $expect->{$field} = email_filter($expect->{$field});
-            }
-       }
-
-        if ($field =~ /^is_/) {
-            ok(defined $bug->{$field}, $self->TYPE . ": $field is not null");
-            is($bug->{$field} ? 1 : 0, $expect->{$field} ? 1 : 0,
-               $self->TYPE . ": $field has the right boolean value");
-        }
-        elsif ($field eq 'cc') {
-            foreach my $cc_item (@{ $expect->{cc} || [] }) {
-               $cc_item = email_filter($cc_item) if !$t->{user};
-                ok(grep($_ eq $cc_item, @{ $bug->{cc} }),
-                   $self->TYPE . ": $field contains $cc_item");
-            }
-        }
-        elsif ($field eq 'creation_time' or $field eq 'last_change_time') {
-            my $creation_day;
-            # XML-RPC and JSON-RPC have different date formats.
-            if ($self->isa('QA::RPC::XMLRPC')) {
-                $creation_day = $creation_time->ymd('');
-            }
-            else {
-                $creation_day = $creation_time->ymd;
-            }
-
-            like($bug->{$field}, qr/^\Q${creation_day}\ET\d\d:\d\d:\d\d/,
-                 $self->TYPE . ": $field has the right format");
-        }
-        else {
-            is_deeply($bug->{$field}, $expect->{$field},
-                      $self->TYPE . ": $field value is correct");
-        }
+  my ($self, $fields, $bug, $orig_expect, $t, $creation_time) = @_;
+
+  my $expect = dclone($orig_expect);
+
+  foreach my $field (sort @$fields) {
+
+    # "description" is used by Bug.create but comments are not returned
+    # by Bug.get or Bug.search.
+    next if $field eq 'description';
+
+    my @include = @{$t->{args}->{include_fields} || []};
+    my @exclude = @{$t->{args}->{exclude_fields} || []};
+    if ( (@include and !grep($_ eq $field, @include))
+      or (@exclude and grep($_ eq $field, @exclude)))
+    {
+      ok(!exists $bug->{$field}, "$field is not included") or diag Dumper($bug);
+      next;
+    }
+
+    foreach my $field (qw(assigned_to creator qa_contact)) {
+      if (!$t->{user}) {
+        $expect->{$field} = email_filter($expect->{$field});
+      }
+    }
+
+    if ($field =~ /^is_/) {
+      ok(defined $bug->{$field}, $self->TYPE . ": $field is not null");
+      is(
+        $bug->{$field}    ? 1 : 0,
+        $expect->{$field} ? 1 : 0,
+        $self->TYPE . ": $field has the right boolean value"
+      );
+    }
+    elsif ($field eq 'cc') {
+      foreach my $cc_item (@{$expect->{cc} || []}) {
+        $cc_item = email_filter($cc_item) if !$t->{user};
+        ok(
+          grep($_ eq $cc_item, @{$bug->{cc}}),
+          $self->TYPE . ": $field contains $cc_item"
+        );
+      }
+    }
+    elsif ($field eq 'creation_time' or $field eq 'last_change_time') {
+      my $creation_day;
+
+      # XML-RPC and JSON-RPC have different date formats.
+      if ($self->isa('QA::RPC::XMLRPC')) {
+        $creation_day = $creation_time->ymd('');
+      }
+      else {
+        $creation_day = $creation_time->ymd;
+      }
+
+      like(
+        $bug->{$field},
+        qr/^\Q${creation_day}\ET\d\d:\d\d:\d\d/,
+        $self->TYPE . ": $field has the right format"
+      );
+    }
+    else {
+      is_deeply($bug->{$field}, $expect->{$field},
+        $self->TYPE . ": $field value is correct");
     }
+  }
 }
 
 1;
index 10e0d3a69c3287f1532a0bead8177c5fe1a0e7cf..9dc0d614fe1ff495d36cc54479c4342a7800e622 100644 (file)
@@ -17,24 +17,26 @@ use FindBin qw($RealBin);
 use lib "$RealBin/../../../lib", "$RealBin/../../../../local/lib/perl5";
 
 use QA::RPC;
-BEGIN {
-    our @ISA = qw(QA::RPC);
 
-    if (eval { require JSON::RPC::Client }) {
-        push(@ISA, 'JSON::RPC::Client');
-    }
-    else {
-        require JSON::RPC::Legacy::Client;
-        push(@ISA, 'JSON::RPC::Legacy::Client');
-    }
+BEGIN {
+  our @ISA = qw(QA::RPC);
+
+  if (eval { require JSON::RPC::Client }) {
+    push(@ISA, 'JSON::RPC::Client');
+  }
+  else {
+    require JSON::RPC::Legacy::Client;
+    push(@ISA, 'JSON::RPC::Legacy::Client');
+  }
 }
 
 use URI::Escape;
 
 use constant DATETIME_REGEX => qr/^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\dZ$/;
+
 sub TYPE {
-    my ($self) = @_;
-    return $self->bz_get_mode ? 'JSON-RPC GET' : 'JSON-RPC';
+  my ($self) = @_;
+  return $self->bz_get_mode ? 'JSON-RPC GET' : 'JSON-RPC';
 }
 
 #################################
@@ -42,85 +44,88 @@ sub TYPE {
 #################################
 
 sub ua {
-    my $self = shift;
-    if ($self->{ua} and not $self->{ua}->isa('QA::RPC::UserAgent')) {
-        bless $self->{ua}, 'QA::RPC::UserAgent';
-    }
-    return $self->SUPER::ua(@_);
+  my $self = shift;
+  if ($self->{ua} and not $self->{ua}->isa('QA::RPC::UserAgent')) {
+    bless $self->{ua}, 'QA::RPC::UserAgent';
+  }
+  return $self->SUPER::ua(@_);
 }
 sub transport { $_[0]->ua }
 
 sub bz_get_mode {
-    my ($self, $value) = @_;
-    $self->{bz_get_mode} = $value if @_ > 1;
-    return $self->{bz_get_mode};
+  my ($self, $value) = @_;
+  $self->{bz_get_mode} = $value if @_ > 1;
+  return $self->{bz_get_mode};
 }
 
 sub _bz_callback {
-    my ($self, $value) = @_;
-    $self->{bz_callback} = $value if @_ > 1;
-    return $self->{bz_callback};
+  my ($self, $value) = @_;
+  $self->{bz_callback} = $value if @_ > 1;
+  return $self->{bz_callback};
 }
 
 sub call {
-    my $self = shift;
-    my ($method, $args) = @_;
-    my %params = ( method => $method );
-    $params{params} = $args ? [$args] : [];
-
-    my $config = $self->bz_config;
-    my $url = $config->{browser_url} . "/"
-              . $config->{bugzilla_installation} . "/jsonrpc.cgi";
-    my $result;
-    if ($self->bz_get_mode) {
-        my $method_escaped = uri_escape($method);
-        $url .= "?method=$method_escaped";
-        if (my $cred = $self->_bz_credentials) {
-            $args->{Bugzilla_login} = $cred->{user}
-                if !exists $args->{Bugzilla_login};
-            $args->{Bugzilla_password} = $cred->{pass}
-                if !exists $args->{Bugzilla_password};
-        }
-        if ($args) {
-            my $params_json = $self->json->encode($args);
-            my $params_escaped = uri_escape($params_json);
-            $url .= "&params=$params_escaped";
-        }
-        if ($self->version eq '1.1') {
-            $url .= "&version=1.1";
-        }
-        my $callback = delete $args->{callback};
-        if (defined $callback) {
-            $self->_bz_callback($callback);
-            $url .= "&callback=" . uri_escape($callback);
-        }
-        $result = $self->SUPER::call($url);
+  my $self = shift;
+  my ($method, $args) = @_;
+  my %params = (method => $method);
+  $params{params} = $args ? [$args] : [];
+
+  my $config = $self->bz_config;
+  my $url
+    = $config->{browser_url} . "/"
+    . $config->{bugzilla_installation}
+    . "/jsonrpc.cgi";
+  my $result;
+  if ($self->bz_get_mode) {
+    my $method_escaped = uri_escape($method);
+    $url .= "?method=$method_escaped";
+    if (my $cred = $self->_bz_credentials) {
+      $args->{Bugzilla_login} = $cred->{user} if !exists $args->{Bugzilla_login};
+      $args->{Bugzilla_password} = $cred->{pass}
+        if !exists $args->{Bugzilla_password};
     }
-    else {
-        $result = $self->SUPER::call($url, \%params);
+    if ($args) {
+      my $params_json    = $self->json->encode($args);
+      my $params_escaped = uri_escape($params_json);
+      $url .= "&params=$params_escaped";
     }
-
-    if ($result) {
-        bless $result, 'QA::RPC::JSONRPC::ReturnObject';
+    if ($self->version eq '1.1') {
+      $url .= "&version=1.1";
+    }
+    my $callback = delete $args->{callback};
+    if (defined $callback) {
+      $self->_bz_callback($callback);
+      $url .= "&callback=" . uri_escape($callback);
     }
-    return $result;
+    $result = $self->SUPER::call($url);
+  }
+  else {
+    $result = $self->SUPER::call($url, \%params);
+  }
+
+  if ($result) {
+    bless $result, 'QA::RPC::JSONRPC::ReturnObject';
+  }
+  return $result;
 }
 
 sub _get {
-    my $self = shift;
-    my $result = $self->SUPER::_get(@_);
-    # Simple JSONP support for tests. We just remove the callback from
-    # the return value.
-    my $callback = $self->_bz_callback;
-    if (defined $callback and $result->is_success) {
-        my $content = $result->content;
-        $content =~ s/^(?:\/\*\*\/)?\Q$callback(\E(.*)\)$/$1/s;
-        $result->content($content);
-        # We don't need this anymore, and we don't want it to affect
-        # future calls.
-        delete $self->{bz_callback};
-    }
-    return $result;
+  my $self   = shift;
+  my $result = $self->SUPER::_get(@_);
+
+  # Simple JSONP support for tests. We just remove the callback from
+  # the return value.
+  my $callback = $self->_bz_callback;
+  if (defined $callback and $result->is_success) {
+    my $content = $result->content;
+    $content =~ s/^(?:\/\*\*\/)?\Q$callback(\E(.*)\)$/$1/s;
+    $result->content($content);
+
+    # We don't need this anymore, and we don't want it to affect
+    # future calls.
+    delete $self->{bz_callback};
+  }
+  return $result;
 }
 
 1;
@@ -132,13 +137,13 @@ use strict;
 use warnings;
 
 BEGIN {
-    if (eval { require JSON::RPC::Client }) {
-        our @ISA = qw(JSON::RPC::ReturnObject);
-    }
-    else {
-        require JSON::RPC::Legacy::Client;
-        our @ISA = qw(JSON::RPC::Legacy::ReturnObject);
-    }
+  if (eval { require JSON::RPC::Client }) {
+    our @ISA = qw(JSON::RPC::ReturnObject);
+  }
+  else {
+    require JSON::RPC::Legacy::Client;
+    our @ISA = qw(JSON::RPC::Legacy::ReturnObject);
+  }
 }
 
 #################################
@@ -146,8 +151,8 @@ BEGIN {
 #################################
 
 sub faultstring { $_[0]->{content}->{error}->{message} }
-sub faultcode   { $_[0]->{content}->{error}->{code}    }
-sub fault { $_[0]->is_error }
+sub faultcode   { $_[0]->{content}->{error}->{code} }
+sub fault       { $_[0]->is_error }
 
 1;
 
@@ -157,27 +162,28 @@ use 5.14.0;
 use strict;
 use warnings;
 
-use parent  qw(LWP::UserAgent);
+use parent qw(LWP::UserAgent);
 
 ########################################
 # Consistency with XMLRPC::Lite's ->ua #
 ########################################
 
 sub send_request {
-    my $self = shift;
-    my $response = $self->SUPER::send_request(@_);
-    $self->http_response($response);
-    # JSON::RPC::Client can't handle 500 responses, even though
-    # they're required by the JSON-RPC spec.
-    $response->code(200);
-    return $response;
+  my $self     = shift;
+  my $response = $self->SUPER::send_request(@_);
+  $self->http_response($response);
+
+  # JSON::RPC::Client can't handle 500 responses, even though
+  # they're required by the JSON-RPC spec.
+  $response->code(200);
+  return $response;
 }
 
 # Copied directly from SOAP::Lite::Transport::HTTP.
 sub http_response {
-    my $self = shift;
-    if (@_) { $self->{'_http_response'} = shift; return $self }
-    return $self->{'_http_response'};
+  my $self = shift;
+  if (@_) { $self->{'_http_response'} = shift; return $self }
+  return $self->{'_http_response'};
 }
 
 1;
index 7e4bcac2402c29e85d626fe08cfad89c47bf6e16..1dc84a16dabb7423d34fb2f23914f33490269849 100644 (file)
@@ -18,7 +18,7 @@ use lib "$RealBin/../../../lib", "$RealBin/../../../../local/lib/perl5";
 
 use parent qw(QA::RPC XMLRPC::Lite);
 
-use constant TYPE => 'XML-RPC';
+use constant TYPE           => 'XML-RPC';
 use constant DATETIME_REGEX => qr/^\d{8}T\d\d:\d\d:\d\d$/;
 
 1;
index a76f0601bb76eb39facd1159cd14a5c4ba34a87e..da82e6d58c70eed9c2bc09f1c3e06008c957f98e 100644 (file)
@@ -19,96 +19,108 @@ use lib "$RealBin/../../lib", "$RealBin/../../../local/lib/perl5";
 use parent qw(Exporter);
 
 our @EXPORT_OK = qw(
-    PRIVATE_BUG_USER
-    STANDARD_BUG_TESTS
-    bug_tests
-    create_bug_fields
+  PRIVATE_BUG_USER
+  STANDARD_BUG_TESTS
+  bug_tests
+  create_bug_fields
 );
 
-use constant INVALID_BUG_ID => -1;
+use constant INVALID_BUG_ID    => -1;
 use constant INVALID_BUG_ALIAS => 'aaaaaaa12345';
-use constant PRIVATE_BUG_USER => 'QA_Selenium_TEST';
+use constant PRIVATE_BUG_USER  => 'QA_Selenium_TEST';
 
 use constant CREATE_BUG => {
-    'priority'     => 'Highest',
-    'status'       => 'CONFIRMED',
-    'version'      => 'unspecified',
-    'creator'      => 'editbugs',
-    'description'  => '-- Comment Created By Bugzilla XML-RPC Tests --',
-    'cc'           => ['unprivileged'],
-    'component'    => 'c1',
-    'platform'     => 'PC',
-    # It's necessary to assign the bug to somebody who isn't in the
-    # timetracking group, for the Bug.update tests.
-    'assigned_to'  => PRIVATE_BUG_USER,
-    'summary'      => 'WebService Test Bug',
-    'product'      => 'Another Product',
-    'op_sys'       => 'Linux',
-    'severity'     => 'normal',
-    'qa_contact'   => 'canconfirm',
-     version       => 'Another1',
-     url           => 'http://www.bugzilla.org/',
-     target_milestone => 'AnotherMS1',
+  'priority'    => 'Highest',
+  'status'      => 'CONFIRMED',
+  'version'     => 'unspecified',
+  'creator'     => 'editbugs',
+  'description' => '-- Comment Created By Bugzilla XML-RPC Tests --',
+  'cc'          => ['unprivileged'],
+  'component'   => 'c1',
+  'platform'    => 'PC',
+
+  # It's necessary to assign the bug to somebody who isn't in the
+  # timetracking group, for the Bug.update tests.
+  'assigned_to'    => PRIVATE_BUG_USER,
+  'summary'        => 'WebService Test Bug',
+  'product'        => 'Another Product',
+  'op_sys'         => 'Linux',
+  'severity'       => 'normal',
+  'qa_contact'     => 'canconfirm',
+  version          => 'Another1',
+  url              => 'http://www.bugzilla.org/',
+  target_milestone => 'AnotherMS1',
 };
 
 sub create_bug_fields {
-    my ($config) = @_;
-    my %bug = %{ CREATE_BUG() };
-    foreach my $field (qw(creator assigned_to qa_contact)) {
-        my $value = $bug{$field};
-        $bug{$field} = $config->{"${value}_user_login"};
-    }
-    $bug{cc} = [map { $config->{$_ . "_user_login"} } @{ $bug{cc} }];
-    return \%bug;
+  my ($config) = @_;
+  my %bug = %{CREATE_BUG()};
+  foreach my $field (qw(creator assigned_to qa_contact)) {
+    my $value = $bug{$field};
+    $bug{$field} = $config->{"${value}_user_login"};
+  }
+  $bug{cc} = [map { $config->{$_ . "_user_login"} } @{$bug{cc}}];
+  return \%bug;
 }
 
 sub bug_tests {
-    my ($public_id, $private_id) = @_;
-    return [
-    { args  => { ids => [$private_id] },
+  my ($public_id, $private_id) = @_;
+  return [
+    {
+      args  => {ids => [$private_id]},
       error => "You are not authorized to access",
       test  => 'Logged-out user cannot access a private bug',
     },
-    { args => { ids => [$public_id] },
+    {
+      args => {ids => [$public_id]},
       test => 'Logged-out user can access a public bug.',
     },
-    { args  => { ids => [INVALID_BUG_ID] },
+    {
+      args  => {ids => [INVALID_BUG_ID]},
       error => "not a valid bug number",
       test  => 'Passing invalid bug id returns error "Invalid Bug ID"',
     },
-    { args  => { ids => [undef] },
+    {
+      args  => {ids => [undef]},
       error => "You must enter a valid bug number",
       test  => 'Passing undef as bug id param returns error "Invalid Bug ID"',
     },
-    { args  => { ids => [INVALID_BUG_ALIAS] },
+    {
+      args  => {ids => [INVALID_BUG_ALIAS]},
       error => "nor an alias to a bug",
       test  => 'Passing invalid bug alias returns error "Invalid Bug Alias"',
     },
 
-    { user  => 'editbugs',
-      args  => { ids => [$private_id] },
+    {
+      user  => 'editbugs',
+      args  => {ids => [$private_id]},
       error => "You are not authorized to access",
       test  => 'Access to a private bug is denied to a user without privs',
     },
-    { user => 'unprivileged',
-      args => { ids => [$public_id] },
+    {
+      user => 'unprivileged',
+      args => {ids => [$public_id]},
       test => 'User without privs can access a public bug',
     },
-    { user => 'admin',
-      args => { ids => [$public_id] },
+    {
+      user => 'admin',
+      args => {ids => [$public_id]},
       test => 'Admin can access a public bug.',
     },
-    { user => PRIVATE_BUG_USER,
-      args => { ids => [$private_id] },
+    {
+      user => PRIVATE_BUG_USER,
+      args => {ids => [$private_id]},
       test => 'User with privs can successfully access a private bug',
     },
+
     # This helps webservice_bug_attachment get private attachment ids
     # from the public bug, and doesn't hurt for the other tests.
-    { user => PRIVATE_BUG_USER,
-      args => { ids => [$public_id] },
+    {
+      user => PRIVATE_BUG_USER,
+      args => {ids => [$public_id]},
       test => 'User with privs can also access the public bug',
     },
-    ];
+  ];
 }
 
 use constant STANDARD_BUG_TESTS => bug_tests('public_bug', 'private_bug');
index eb68a0b5bef2246948ef5ae8bf0490732e1e975b..19b821b17b6d3931f4dba55191e50f1de20af923 100644 (file)
@@ -23,64 +23,65 @@ use Test::More;
 
 use parent qw(Exporter);
 @QA::Util::EXPORT = qw(
-    trim
-    url_quote
-    random_string
-    email_filter
-
-    log_in
-    logout
-    file_bug_in_product
-    create_bug
-    edit_bug
-    edit_bug_and_return
-    go_to_bug
-    go_to_home
-    go_to_admin
-    edit_product
-    add_product
-    open_advanced_search_page
-    set_parameters
-
-    get_selenium
-    get_rpc_clients
-    get_config
-
-    WAIT_TIME
-    CHROME_MODE
+  trim
+  url_quote
+  random_string
+  email_filter
+
+  log_in
+  logout
+  file_bug_in_product
+  create_bug
+  edit_bug
+  edit_bug_and_return
+  go_to_bug
+  go_to_home
+  go_to_admin
+  edit_product
+  add_product
+  open_advanced_search_page
+  set_parameters
+
+  get_selenium
+  get_rpc_clients
+  get_config
+
+  WAIT_TIME
+  CHROME_MODE
 );
 
 # How long we wait for pages to load.
-use constant WAIT_TIME => 60000;
-use constant CONF_FILE =>  "$RealBin/../config/selenium_test.conf";
+use constant WAIT_TIME   => 60000;
+use constant CONF_FILE   => "$RealBin/../config/selenium_test.conf";
 use constant CHROME_MODE => 1;
-use constant NDASH => chr(0x2013);
+use constant NDASH       => chr(0x2013);
 
 #####################
 # Utility Functions #
 #####################
 
 sub random_string {
-    my $size = shift || 30; # default to 30 chars if nothing specified
-    return join("", map{ ('0'..'9','a'..'z','A'..'Z')[rand 62] } (1..$size));
+  my $size = shift || 30;    # default to 30 chars if nothing specified
+  return
+    join("", map { ('0' .. '9', 'a' .. 'z', 'A' .. 'Z')[rand 62] } (1 .. $size));
 }
 
 # Remove consecutive as well as leading and trailing whitespaces.
 sub trim {
-    my ($str) = @_;
-    if ($str) {
-      $str =~ s/[\r\n\t\s]+/ /g;
-      $str =~ s/^\s+//g;
-      $str =~ s/\s+$//g;
-    }
-    return $str;
+  my ($str) = @_;
+  if ($str) {
+    $str =~ s/[\r\n\t\s]+/ /g;
+    $str =~ s/^\s+//g;
+    $str =~ s/\s+$//g;
+  }
+  return $str;
 }
 
 # This originally came from CGI.pm, by Lincoln D. Stein
 sub url_quote {
-    my ($toencode) = (@_);
-    $toencode =~ s/([^a-zA-Z0-9_\-.])/uc sprintf("%%%02x",ord($1))/eg;
-    return $toencode;
+  my ($toencode) = (@_);
+  $toencode =~ s/([^a-zA-Z0-9_\-.])/uc sprintf("%%%02x",ord($1))/eg;
+  return $toencode;
 }
 
 ###################
@@ -88,61 +89,67 @@ sub url_quote {
 ###################
 
 sub get_config {
-    # read the test configuration file
-    my $conf_file = CONF_FILE;
-    my $config = do($conf_file)
-        or die "can't read configuration '$conf_file': $!$@";
-    return $config;
+
+  # read the test configuration file
+  my $conf_file = CONF_FILE;
+  my $config    = do($conf_file)
+    or die "can't read configuration '$conf_file': $!$@";
+  return $config;
 }
 
 sub get_selenium {
-    my $chrome_mode = shift;
-    my $config = get_config();
-
-    require Test::WWW::Selenium;
-    require WWW::Selenium::Util;
-
-    if (!WWW::Selenium::Util::server_is_running()) {
-        die "Selenium Server isn't running!";
-    }
-
-    my $sel = Test::WWW::Selenium->new(
-        host        => $config->{host},
-        port        => $config->{port},
-        browser     => $chrome_mode ? $config->{experimental_browser_launcher} : $config->{browser},
-        browser_url => $config->{browser_url}
-    );
-
-    return ($sel, $config);
+  my $chrome_mode = shift;
+  my $config      = get_config();
+
+  require Test::WWW::Selenium;
+  require WWW::Selenium::Util;
+
+  if (!WWW::Selenium::Util::server_is_running()) {
+    die "Selenium Server isn't running!";
+  }
+
+  my $sel = Test::WWW::Selenium->new(
+    host    => $config->{host},
+    port    => $config->{port},
+    browser => $chrome_mode
+    ? $config->{experimental_browser_launcher}
+    : $config->{browser},
+    browser_url => $config->{browser_url}
+  );
+
+  return ($sel, $config);
 }
 
 sub get_xmlrpc_client {
-    my $config = get_config();
-    my $xmlrpc_url = $config->{browser_url} . "/" .
-                     $config->{bugzilla_installation} . "/xmlrpc.cgi";
-
-    require QA::RPC::XMLRPC;
-    my $rpc = QA::RPC::XMLRPC->new(proxy => $xmlrpc_url);
-    return ($rpc, $config);
+  my $config = get_config();
+  my $xmlrpc_url
+    = $config->{browser_url} . "/"
+    . $config->{bugzilla_installation}
+    . "/xmlrpc.cgi";
+
+  require QA::RPC::XMLRPC;
+  my $rpc = QA::RPC::XMLRPC->new(proxy => $xmlrpc_url);
+  return ($rpc, $config);
 }
 
 sub get_jsonrpc_client {
-    my ($get_mode) = @_;
-    require QA::RPC::JSONRPC;
-    my $rpc = QA::RPC::JSONRPC->new();
-    # If we don't set a long timeout, then the Bug.add_comment test
-    # where we add a too-large comment fails.
-    $rpc->transport->timeout(180);
-    $rpc->version($get_mode ? '1.1' : '1.0');
-    $rpc->bz_get_mode($get_mode);
-    return $rpc;
+  my ($get_mode) = @_;
+  require QA::RPC::JSONRPC;
+  my $rpc = QA::RPC::JSONRPC->new();
+
+  # If we don't set a long timeout, then the Bug.add_comment test
+  # where we add a too-large comment fails.
+  $rpc->transport->timeout(180);
+  $rpc->version($get_mode ? '1.1' : '1.0');
+  $rpc->bz_get_mode($get_mode);
+  return $rpc;
 }
 
 sub get_rpc_clients {
-    my ($xmlrpc, $config) = get_xmlrpc_client();
-    my $jsonrpc = get_jsonrpc_client();
-    my $jsonrpc_get = get_jsonrpc_client('GET');
-    return ($config, $xmlrpc, $jsonrpc, $jsonrpc_get);
+  my ($xmlrpc, $config) = get_xmlrpc_client();
+  my $jsonrpc     = get_jsonrpc_client();
+  my $jsonrpc_get = get_jsonrpc_client('GET');
+  return ($config, $xmlrpc, $jsonrpc, $jsonrpc_get);
 }
 
 ################################
@@ -150,166 +157,188 @@ sub get_rpc_clients {
 ################################
 
 sub go_to_home {
-    my ($sel, $config) = @_;
-    $sel->open_ok("/$config->{bugzilla_installation}/", undef, "Go to the home page");
-    $sel->title_is("Bugzilla Main Page");
+  my ($sel, $config) = @_;
+  $sel->open_ok("/$config->{bugzilla_installation}/",
+    undef, "Go to the home page");
+  $sel->title_is("Bugzilla Main Page");
 }
 
 # Go to the home/login page and log in.
 sub log_in {
-    my ($sel, $config, $user) = @_;
-
-    go_to_home($sel, $config);
-    $sel->type_ok("Bugzilla_login_top", $config->{"${user}_user_login"}, "Enter $user login name");
-    $sel->type_ok("Bugzilla_password_top", $config->{"${user}_user_passwd"}, "Enter $user password");
-    $sel->click_ok("log_in_top", undef, "Submit credentials");
-    $sel->wait_for_page_to_load(WAIT_TIME);
-    $sel->title_is("Bugzilla Main Page", "User is logged in");
+  my ($sel, $config, $user) = @_;
+
+  go_to_home($sel, $config);
+  $sel->type_ok(
+    "Bugzilla_login_top",
+    $config->{"${user}_user_login"},
+    "Enter $user login name"
+  );
+  $sel->type_ok(
+    "Bugzilla_password_top",
+    $config->{"${user}_user_passwd"},
+    "Enter $user password"
+  );
+  $sel->click_ok("log_in_top", undef, "Submit credentials");
+  $sel->wait_for_page_to_load(WAIT_TIME);
+  $sel->title_is("Bugzilla Main Page", "User is logged in");
 }
 
 # Log out. Will fail if you are not logged in.
 sub logout {
-    my $sel = shift;
+  my $sel = shift;
 
-    $sel->click_ok("link=Log out", undef, "Logout");
-    $sel->wait_for_page_to_load_ok(WAIT_TIME);
-    $sel->title_is("Logged Out");
+  $sel->click_ok("link=Log out", undef, "Logout");
+  $sel->wait_for_page_to_load_ok(WAIT_TIME);
+  $sel->title_is("Logged Out");
 }
 
 # Display the bug form to enter a bug in the given product.
 sub file_bug_in_product {
-    my ($sel, $product, $classification) = @_;
-
-    $classification ||= "Unclassified";
-    $sel->click_ok("link=New", undef, "Go create a new bug");
+  my ($sel, $product, $classification) = @_;
+
+  $classification ||= "Unclassified";
+  $sel->click_ok("link=New", undef, "Go create a new bug");
+  $sel->wait_for_page_to_load(WAIT_TIME);
+  my $title = $sel->get_title();
+  if ($sel->is_text_present("Select Classification")) {
+    ok(1,
+      "More than one enterable classification available. Display them in a list");
+    $sel->click_ok("link=$classification", undef, "Choose $classification");
     $sel->wait_for_page_to_load(WAIT_TIME);
-    my $title = $sel->get_title();
-    if ($sel->is_text_present("Select Classification")) {
-        ok(1, "More than one enterable classification available. Display them in a list");
-        $sel->click_ok("link=$classification", undef, "Choose $classification");
-        $sel->wait_for_page_to_load(WAIT_TIME);
-        $title = $sel->get_title();
-    }
-    if ($title eq "Enter Bug") {
-        ok(1, "Display the list of enterable products");
-        $sel->click_ok("link=$product", undef, "Choose $product");
-        $sel->wait_for_page_to_load(WAIT_TIME);
-    }
-    else {
-        ok(1, "Only one product available in $classification. Skipping the 'Choose product' page.")
-    }
-    $sel->title_is("Enter Bug: $product", "Display form to enter bug data");
+    $title = $sel->get_title();
+  }
+  if ($title eq "Enter Bug") {
+    ok(1, "Display the list of enterable products");
+    $sel->click_ok("link=$product", undef, "Choose $product");
+    $sel->wait_for_page_to_load(WAIT_TIME);
+  }
+  else {
+    ok(1,
+      "Only one product available in $classification. Skipping the 'Choose product' page."
+    );
+  }
+  $sel->title_is("Enter Bug: $product", "Display form to enter bug data");
 }
 
 sub create_bug {
-    my ($sel, $bug_summary) = @_;
-    my $ndash = NDASH;
-
-    $sel->click_ok('commit');
-    $sel->wait_for_page_to_load_ok(WAIT_TIME);
-    my $bug_id = $sel->get_value('//input[@name="id" and @type="hidden"]');
-    $sel->title_like(qr/$bug_id $ndash( \(.*\))? $bug_summary/, "Bug $bug_id created with summary '$bug_summary'");
-    return $bug_id;
+  my ($sel, $bug_summary) = @_;
+  my $ndash = NDASH;
+
+  $sel->click_ok('commit');
+  $sel->wait_for_page_to_load_ok(WAIT_TIME);
+  my $bug_id = $sel->get_value('//input[@name="id" and @type="hidden"]');
+  $sel->title_like(
+    qr/$bug_id $ndash( \(.*\))? $bug_summary/,
+    "Bug $bug_id created with summary '$bug_summary'"
+  );
+  return $bug_id;
 }
 
 sub edit_bug {
-    my ($sel, $bug_id, $bug_summary, $options) = @_;
-    my $ndash = NDASH;
-    my $btn_id = $options ? $options->{id} : 'commit';
-
-    $sel->click_ok($btn_id);
-    $sel->wait_for_page_to_load_ok(WAIT_TIME);
-    $sel->title_is("$bug_id $ndash $bug_summary", "Changes submitted to bug $bug_id");
-    # If the web browser doesn't support history.ReplaceState or has it turned off,
-    # "Bug FIXME processed" is displayed instead (as in Bugzilla 4.0 and older).
-    # $sel->title_is("Bug $bug_id processed", "Changes submitted to bug $bug_id");
+  my ($sel, $bug_id, $bug_summary, $options) = @_;
+  my $ndash = NDASH;
+  my $btn_id = $options ? $options->{id} : 'commit';
+
+  $sel->click_ok($btn_id);
+  $sel->wait_for_page_to_load_ok(WAIT_TIME);
+  $sel->title_is("$bug_id $ndash $bug_summary",
+    "Changes submitted to bug $bug_id");
+
+  # If the web browser doesn't support history.ReplaceState or has it turned off,
+  # "Bug FIXME processed" is displayed instead (as in Bugzilla 4.0 and older).
+  # $sel->title_is("Bug $bug_id processed", "Changes submitted to bug $bug_id");
 }
 
 sub edit_bug_and_return {
-    my ($sel, $bug_id, $bug_summary, $options) = @_;
-    my $ndash = NDASH;
-    edit_bug($sel, $bug_id, $bug_summary, $options);
-    $sel->click_ok("//a[contains(\@href, 'show_bug.cgi?id=$bug_id')]");
-    $sel->wait_for_page_to_load_ok(WAIT_TIME);
-    $sel->title_is("$bug_id $ndash $bug_summary", "Returning back to bug $bug_id");
+  my ($sel, $bug_id, $bug_summary, $options) = @_;
+  my $ndash = NDASH;
+  edit_bug($sel, $bug_id, $bug_summary, $options);
+  $sel->click_ok("//a[contains(\@href, 'show_bug.cgi?id=$bug_id')]");
+  $sel->wait_for_page_to_load_ok(WAIT_TIME);
+  $sel->title_is("$bug_id $ndash $bug_summary", "Returning back to bug $bug_id");
 }
 
 # Go to show_bug.cgi.
 sub go_to_bug {
-    my ($sel, $bug_id) = @_;
-
-    $sel->type_ok("quicksearch_top", $bug_id);
-    $sel->click_ok("find_top", undef, "Go to bug $bug_id");
-    $sel->wait_for_page_to_load_ok(WAIT_TIME);
-    my $bug_title = $sel->get_title();
-    utf8::encode($bug_title) if utf8::is_utf8($bug_title);
-    $sel->title_like(qr/^$bug_id /, $bug_title);
+  my ($sel, $bug_id) = @_;
+
+  $sel->type_ok("quicksearch_top", $bug_id);
+  $sel->click_ok("find_top", undef, "Go to bug $bug_id");
+  $sel->wait_for_page_to_load_ok(WAIT_TIME);
+  my $bug_title = $sel->get_title();
+  utf8::encode($bug_title) if utf8::is_utf8($bug_title);
+  $sel->title_like(qr/^$bug_id /, $bug_title);
 }
 
 # Go to admin.cgi.
 sub go_to_admin {
-    my $sel = shift;
+  my $sel = shift;
 
-    $sel->click_ok("link=Administration", undef, "Go to the Admin page");
-    $sel->wait_for_page_to_load(WAIT_TIME);
-    $sel->title_like(qr/^Administer your installation/, "Display admin.cgi");
+  $sel->click_ok("link=Administration", undef, "Go to the Admin page");
+  $sel->wait_for_page_to_load(WAIT_TIME);
+  $sel->title_like(qr/^Administer your installation/, "Display admin.cgi");
 }
 
 # Go to editproducts.cgi and display the given product.
 sub edit_product {
-    my ($sel, $product, $classification) = @_;
-
-    $classification ||= "Unclassified";
-    go_to_admin($sel);
-    $sel->click_ok("link=Products", undef, "Go to the Products page");
-    $sel->wait_for_page_to_load(WAIT_TIME);
-    my $title = $sel->get_title();
-    if ($title eq "Select Classification") {
-        ok(1, "More than one enterable classification available. Display them in a list");
-        $sel->click_ok("link=$classification", undef, "Choose $classification");
-        $sel->wait_for_page_to_load(WAIT_TIME);
-    }
-    else {
-        $sel->title_is("Select product", "Display the list of enterable products");
-    }
-    $sel->click_ok("link=$product", undef, "Choose $product");
+  my ($sel, $product, $classification) = @_;
+
+  $classification ||= "Unclassified";
+  go_to_admin($sel);
+  $sel->click_ok("link=Products", undef, "Go to the Products page");
+  $sel->wait_for_page_to_load(WAIT_TIME);
+  my $title = $sel->get_title();
+  if ($title eq "Select Classification") {
+    ok(1,
+      "More than one enterable classification available. Display them in a list");
+    $sel->click_ok("link=$classification", undef, "Choose $classification");
     $sel->wait_for_page_to_load(WAIT_TIME);
-    $sel->title_is("Edit Product '$product'", "Display properties of $product");
+  }
+  else {
+    $sel->title_is("Select product", "Display the list of enterable products");
+  }
+  $sel->click_ok("link=$product", undef, "Choose $product");
+  $sel->wait_for_page_to_load(WAIT_TIME);
+  $sel->title_is("Edit Product '$product'", "Display properties of $product");
 }
 
 sub add_product {
-    my ($sel, $classification) = @_;
-
-    $classification ||= "Unclassified";
-    go_to_admin($sel);
-    $sel->click_ok("link=Products", undef, "Go to the Products page");
-    $sel->wait_for_page_to_load(WAIT_TIME);
-    my $title = $sel->get_title();
-    if ($title eq "Select Classification") {
-        ok(1, "More than one enterable classification available. Display them in a list");
-        $sel->click_ok("//a[contains(\@href, 'editproducts.cgi?action=add&classification=$classification')]",
-                       undef, "Add product to $classification");
-    }
-    else {
-        $sel->title_is("Select product", "Display the list of enterable products");
-        $sel->click_ok("link=Add", undef, "Add a new product");
-    }
-    $sel->wait_for_page_to_load(WAIT_TIME);
-    $sel->title_is("Add Product", "Display the new product form");
+  my ($sel, $classification) = @_;
+
+  $classification ||= "Unclassified";
+  go_to_admin($sel);
+  $sel->click_ok("link=Products", undef, "Go to the Products page");
+  $sel->wait_for_page_to_load(WAIT_TIME);
+  my $title = $sel->get_title();
+  if ($title eq "Select Classification") {
+    ok(1,
+      "More than one enterable classification available. Display them in a list");
+    $sel->click_ok(
+      "//a[contains(\@href, 'editproducts.cgi?action=add&classification=$classification')]",
+      undef,
+      "Add product to $classification"
+    );
+  }
+  else {
+    $sel->title_is("Select product", "Display the list of enterable products");
+    $sel->click_ok("link=Add", undef, "Add a new product");
+  }
+  $sel->wait_for_page_to_load(WAIT_TIME);
+  $sel->title_is("Add Product", "Display the new product form");
 }
 
 sub open_advanced_search_page {
-    my $sel = shift;
-
-    $sel->click_ok("link=Search");
+  my $sel = shift;
+
+  $sel->click_ok("link=Search");
+  $sel->wait_for_page_to_load(WAIT_TIME);
+  my $title = $sel->get_title();
+  if ($title eq "Simple Search") {
+    ok(1, "Display the simple search form");
+    $sel->click_ok("link=Advanced Search");
     $sel->wait_for_page_to_load(WAIT_TIME);
-    my $title = $sel->get_title();
-    if ($title eq "Simple Search") {
-        ok(1, "Display the simple search form");
-        $sel->click_ok("link=Advanced Search");
-        $sel->wait_for_page_to_load(WAIT_TIME);
-    }
-    $sel->title_is("Search for bugs", "Display the Advanced search form");
+  }
+  $sel->title_is("Search for bugs", "Display the Advanced search form");
 }
 
 # $params is a hashref of the form:
@@ -325,60 +354,61 @@ sub open_advanced_search_page {
 # undef is for radio buttons (in which case the parameter must be the ID of the radio button)
 # value => 'foo' is the value of the parameter (either text or label)
 sub set_parameters {
-    my ($sel, $params) = @_;
-
-    go_to_admin($sel);
-    $sel->click_ok("link=Parameters", undef, "Go to the Config Parameters page");
-    $sel->wait_for_page_to_load(WAIT_TIME);
-    $sel->title_is("Configuration: Required Settings");
-    my $last_section = "Required Settings";
-
-    foreach my $section (keys %$params) {
-        if ($section ne $last_section) {
-            $sel->click_ok("link=$section");
-            $sel->wait_for_page_to_load_ok(WAIT_TIME);
-            $sel->title_is("Configuration: $section");
-            $last_section = $section;
+  my ($sel, $params) = @_;
+
+  go_to_admin($sel);
+  $sel->click_ok("link=Parameters", undef, "Go to the Config Parameters page");
+  $sel->wait_for_page_to_load(WAIT_TIME);
+  $sel->title_is("Configuration: Required Settings");
+  my $last_section = "Required Settings";
+
+  foreach my $section (keys %$params) {
+    if ($section ne $last_section) {
+      $sel->click_ok("link=$section");
+      $sel->wait_for_page_to_load_ok(WAIT_TIME);
+      $sel->title_is("Configuration: $section");
+      $last_section = $section;
+    }
+    my $param_list = $params->{$section};
+    foreach my $param (keys %$param_list) {
+      my $data = $param_list->{$param};
+      if (defined $data) {
+        my $type  = $data->{type};
+        my $value = $data->{value};
+
+        if ($type eq 'text') {
+          $sel->type_ok($param, $value);
+        }
+        elsif ($type eq 'select') {
+          $sel->select_ok($param, "label=$value");
         }
-        my $param_list = $params->{$section};
-        foreach my $param (keys %$param_list) {
-            my $data = $param_list->{$param};
-            if (defined $data) {
-                my $type = $data->{type};
-                my $value = $data->{value};
-
-                if ($type eq 'text') {
-                    $sel->type_ok($param, $value);
-                }
-                elsif ($type eq 'select') {
-                    $sel->select_ok($param, "label=$value");
-                }
-                else {
-                    ok(0, "Unknown parameter type: $type");
-                }
-            }
-            else {
-                # If the value is undefined, then the param name is
-                # the ID of the radio button.
-                $sel->click_ok($param);
-            }
+        else {
+          ok(0, "Unknown parameter type: $type");
         }
-        $sel->click_ok('//input[@type="submit" and @value="Save Changes"]', undef, "Save Changes");
-        $sel->wait_for_page_to_load_ok(WAIT_TIME);
-        $sel->title_is("Parameters Updated");
+      }
+      else {
+        # If the value is undefined, then the param name is
+        # the ID of the radio button.
+        $sel->click_ok($param);
+      }
     }
+    $sel->click_ok('//input[@type="submit" and @value="Save Changes"]',
+      undef, "Save Changes");
+    $sel->wait_for_page_to_load_ok(WAIT_TIME);
+    $sel->title_is("Parameters Updated");
+  }
 }
 
 sub email_filter {
-    my ($toencode) = @_;
-    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) = @_;
+  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;
 }
 
 1;
index 7fcfff2d512f6a54b34fdcd6da52de4854fd0ffc..ea9b5915bdcbf9af7ff12fc3a91cfb36af7ef9b8 100644 (file)
@@ -19,7 +19,7 @@ use lib "$RealBin/../lib", "$RealBin/../../local/lib/perl5";
 use Test::More tests => 11;
 use QA::REST;
 
-my $rest = get_rest_client();
+my $rest   = get_rest_client();
 my $config = $rest->bz_config;
 
 my $version = $rest->call('version')->{version};
@@ -28,31 +28,42 @@ ok($version, "GET /rest/version returns $version");
 my $extensions = $rest->call('extensions')->{extensions};
 isa_ok($extensions, 'HASH', 'GET /rest/extensions');
 my @ext_names = sort keys %$extensions;
+
 # There is always at least the QA extension enabled.
-ok(scalar(@ext_names), scalar(@ext_names) . ' extension(s) found: ' . join(', ', @ext_names));
-ok($extensions->{QA}, 'The QA extension is enabled, with version ' . $extensions->{QA}->{version});
+ok(scalar(@ext_names),
+  scalar(@ext_names) . ' extension(s) found: ' . join(', ', @ext_names));
+ok($extensions->{QA},
+  'The QA extension is enabled, with version ' . $extensions->{QA}->{version});
 
 my $timezone = $rest->call('timezone')->{timezone};
 ok($timezone, "GET /rest/timezone returns $timezone");
 
 my $time = $rest->call('time');
 foreach my $type (qw(db_time web_time)) {
-    ok($time->{$type}, "GET /rest/time returns $type = " . $time->{$type});
+  ok($time->{$type}, "GET /rest/time returns $type = " . $time->{$type});
 }
 
 # Logged-out users can only access the maintainer and requirelogin parameters.
-my $params = $rest->call('parameters')->{parameters};
+my $params      = $rest->call('parameters')->{parameters};
 my @param_names = sort keys %$params;
-ok(@param_names == 2 && defined $params->{maintainer} && defined $params->{requirelogin},
-   'Only 2 parameters accessible to logged-out users: ' . join(', ', @param_names));
+ok(
+  @param_names == 2
+    && defined $params->{maintainer}
+    && defined $params->{requirelogin},
+  'Only 2 parameters accessible to logged-out users: ' . join(', ', @param_names)
+);
 
 # Powerless users can access much more parameters.
-$params = $rest->call('parameters', { api_key => $config->{unprivileged_user_api_key} })->{parameters};
+$params
+  = $rest->call('parameters', {api_key => $config->{unprivileged_user_api_key}})
+  ->{parameters};
 @param_names = sort keys %$params;
-ok(@param_names > 2, scalar(@param_names) . ' parameters accessible to powerless users');
+ok(@param_names > 2,
+  scalar(@param_names) . ' parameters accessible to powerless users');
 
 # Admins can access all parameters.
-$params = $rest->call('parameters', { api_key => $config->{admin_user_api_key} })->{parameters};
+$params = $rest->call('parameters', {api_key => $config->{admin_user_api_key}})
+  ->{parameters};
 @param_names = sort keys %$params;
 ok(@param_names > 2, scalar(@param_names) . ' parameters accessible to admins');
 
index ed10abbc3cf16c79646ce750f1fc9a3872c082f8..182e04a9c9d4bd3ee6f257f6c7b512d43a77514b 100644 (file)
@@ -19,43 +19,67 @@ use lib "$RealBin/../lib", "$RealBin/../../local/lib/perl5";
 use Test::More tests => 7;
 use QA::REST;
 
-my $rest = get_rest_client();
+my $rest   = get_rest_client();
 my $config = $rest->bz_config;
-my $args = { api_key => $config->{admin_user_api_key} };
+my $args   = {api_key => $config->{admin_user_api_key}};
 
 my $params = $rest->call('parameters', $args)->{parameters};
 my $use_class = $params->{useclassification};
-ok(defined($use_class), 'Classifications are ' . ($use_class ? 'enabled' : 'disabled'));
+ok(defined($use_class),
+  'Classifications are ' . ($use_class ? 'enabled' : 'disabled'));
 
 # Admins can always access classifications, even when they are disabled.
 my $class = $rest->call('classification/1', $args)->{classifications}->[0];
-ok($class->{id}, "Admin found classification '" . $class->{name} . "' with the description '" . $class->{description} . "'");
-my @products = sort map { $_->{name} } @{ $class->{products} };
-ok(scalar(@products), scalar(@products) . ' product(s) found: ' . join(', ', @products));
+ok($class->{id},
+      "Admin found classification '"
+    . $class->{name}
+    . "' with the description '"
+    . $class->{description}
+    . "'");
+my @products = sort map { $_->{name} } @{$class->{products}};
+ok(scalar(@products),
+  scalar(@products) . ' product(s) found: ' . join(', ', @products));
 
 $class = $rest->call('classification/Class2_QA', $args)->{classifications}->[0];
-ok($class->{id}, "Admin found classification '" . $class->{name} . "' with the description '" . $class->{description} . "'");
-@products = sort map { $_->{name} } @{ $class->{products} };
-ok(scalar(@products), scalar(@products) . ' product(s) found: ' . join(', ', @products));
+ok($class->{id},
+      "Admin found classification '"
+    . $class->{name}
+    . "' with the description '"
+    . $class->{description}
+    . "'");
+@products = sort map { $_->{name} } @{$class->{products}};
+ok(scalar(@products),
+  scalar(@products) . ' product(s) found: ' . join(', ', @products));
 
 if ($use_class) {
-    # When classifications are enabled, everybody can query classifications...
-    # ... including logged-out users.
-    $class = $rest->call('classification/1')->{classifications}->[0];
-    ok($class->{id}, 'Logged-out users can access classification ' . $class->{name});
-    # ... and non-admins.
-    $class = $rest->call('classification/1', { api_key => $config->{editbugs_user_api_key} })->{classifications}->[0];
-    ok($class->{id}, 'Non-admins can access classification ' . $class->{name});
+
+  # When classifications are enabled, everybody can query classifications...
+  # ... including logged-out users.
+  $class = $rest->call('classification/1')->{classifications}->[0];
+  ok($class->{id},
+    'Logged-out users can access classification ' . $class->{name});
+
+  # ... and non-admins.
+  $class = $rest->call('classification/1',
+    {api_key => $config->{editbugs_user_api_key}})->{classifications}->[0];
+  ok($class->{id}, 'Non-admins can access classification ' . $class->{name});
 }
 else {
-    # When classifications are disabled, only users in the 'editclassifications'
-    # group can access this method...
-    # ... logged-out users get an error.
-    my $error = $rest->call('classification/1', undef, undef, MUST_FAIL);
-    ok($error->{error} && $error->{code} == 900,
-       'Logged-out users cannot query classifications when disabled: ' . $error->{message});
-    # ... as well as non-admins.
-    $error = $rest->call('classification/1', { api_key => $config->{editbugs_user_api_key} }, undef, MUST_FAIL);
-    ok($error->{error} && $error->{code} == 900,
-       'Non-admins cannot query classifications when disabled: ' . $error->{message});
+  # When classifications are disabled, only users in the 'editclassifications'
+  # group can access this method...
+  # ... logged-out users get an error.
+  my $error = $rest->call('classification/1', undef, undef, MUST_FAIL);
+  ok(
+    $error->{error} && $error->{code} == 900,
+    'Logged-out users cannot query classifications when disabled: '
+      . $error->{message}
+  );
+
+  # ... as well as non-admins.
+  $error
+    = $rest->call('classification/1',
+    {api_key => $config->{editbugs_user_api_key}},
+    undef, MUST_FAIL);
+  ok($error->{error} && $error->{code} == 900,
+    'Non-admins cannot query classifications when disabled: ' . $error->{message});
 }
index 16ddd58acecec2b369c9a2a7458d368544c3e109..96e67c061a443b990c98a0b3385aa1c363c6dde9 100644 (file)
@@ -19,17 +19,17 @@ use QA::Util;
 my ($sel, $config) = get_selenium();
 
 log_in($sel, $config, 'admin');
-set_parameters($sel, { "Bug Fields" => {"usestatuswhiteboard-on" => undef} });
+set_parameters($sel, {"Bug Fields" => {"usestatuswhiteboard-on" => undef}});
 
 # Clear the saved search, in case this test didn't complete previously.
 if ($sel->is_text_present("My bugs from QA_Selenium")) {
-    $sel->click_ok("link=My bugs from QA_Selenium");
-    $sel->wait_for_page_to_load_ok(WAIT_TIME);
-    $sel->title_is("Bug List: My bugs from QA_Selenium");
-    $sel->click_ok("forget_search");
-    $sel->wait_for_page_to_load_ok(WAIT_TIME);
-    $sel->title_is("Search is gone");
-    $sel->is_text_present_ok("OK, the My bugs from QA_Selenium search is gone");
+  $sel->click_ok("link=My bugs from QA_Selenium");
+  $sel->wait_for_page_to_load_ok(WAIT_TIME);
+  $sel->title_is("Bug List: My bugs from QA_Selenium");
+  $sel->click_ok("forget_search");
+  $sel->wait_for_page_to_load_ok(WAIT_TIME);
+  $sel->title_is("Search is gone");
+  $sel->is_text_present_ok("OK, the My bugs from QA_Selenium search is gone");
 }
 
 # Just in case the test failed before completion previously, reset the CANEDIT bit.
@@ -54,18 +54,18 @@ file_bug_in_product($sel, 'TestProduct');
 my $bug_summary = "Test bug editing";
 $sel->select_ok("bug_severity", "label=critical");
 $sel->type_ok("short_desc", $bug_summary);
-$sel->type_ok("comment", "ploc");
+$sel->type_ok("comment",    "ploc");
 my $bug1_id = create_bug($sel, $bug_summary);
 
 # Now edit field values of the bug you just filed.
 
 $sel->select_ok("rep_platform", "label=Other");
-$sel->select_ok("op_sys", "label=Other");
-$sel->select_ok("priority", "label=Highest");
+$sel->select_ok("op_sys",       "label=Other");
+$sel->select_ok("priority",     "label=Highest");
 $sel->select_ok("bug_severity", "label=blocker");
-$sel->type_ok("bug_file_loc", "foo.cgi?action=bar");
+$sel->type_ok("bug_file_loc",      "foo.cgi?action=bar");
 $sel->type_ok("status_whiteboard", "[Selenium was here]");
-$sel->type_ok("comment", "new comment from me :)");
+$sel->type_ok("comment",           "new comment from me :)");
 $sel->select_ok("bug_status", "label=RESOLVED");
 edit_bug($sel, $bug1_id, $bug_summary);
 
@@ -80,22 +80,31 @@ $sel->click_ok("commit");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Verify New Product Details...");
 $sel->select_ok("component", "label=QA-Selenium-TEST");
-$sel->is_element_present_ok('//input[@type="checkbox" and @name="groups" and @value="QA-Selenium-TEST"]');
-ok(!$sel->is_editable('//input[@type="checkbox" and @name="groups" and @value="QA-Selenium-TEST"]'), "QA-Selenium-TEST group not editable");
-$sel->is_checked_ok('//input[@type="checkbox" and @name="groups" and @value="QA-Selenium-TEST"]');
+$sel->is_element_present_ok(
+  '//input[@type="checkbox" and @name="groups" and @value="QA-Selenium-TEST"]');
+ok(
+  !$sel->is_editable(
+    '//input[@type="checkbox" and @name="groups" and @value="QA-Selenium-TEST"]'),
+  "QA-Selenium-TEST group not editable"
+);
+$sel->is_checked_ok(
+  '//input[@type="checkbox" and @name="groups" and @value="QA-Selenium-TEST"]');
 edit_bug_and_return($sel, $bug1_id, $bug_summary, {id => "change_product"});
 $sel->select_ok("bug_severity", "label=normal");
-$sel->select_ok("priority", "label=High");
+$sel->select_ok("priority",     "label=High");
 $sel->select_ok("rep_platform", "label=All");
-$sel->select_ok("op_sys", "label=All");
+$sel->select_ok("op_sys",       "label=All");
 $sel->click_ok("cc_edit_area_showhide");
-$sel->type_ok("newcc", $config->{admin_user_login});
+$sel->type_ok("newcc",   $config->{admin_user_login});
 $sel->type_ok("comment", "Unchecking the reporter_accessible checkbox");
+
 # This checkbox is checked by default.
 $sel->click_ok("reporter_accessible");
 $sel->select_ok("bug_status", "label=VERIFIED");
 edit_bug_and_return($sel, $bug1_id, $bug_summary);
-$sel->type_ok("comment", "I am the reporter, but I can see the bug anyway as I belong to the mandatory group");
+$sel->type_ok("comment",
+  "I am the reporter, but I can see the bug anyway as I belong to the mandatory group"
+);
 edit_bug($sel, $bug1_id, $bug_summary);
 logout($sel);
 
@@ -105,12 +114,12 @@ logout($sel);
 log_in($sel, $config, 'admin');
 go_to_bug($sel, $bug1_id);
 $sel->select_ok("bug_severity", "label=blocker");
-$sel->select_ok("priority", "label=Highest");
+$sel->select_ok("priority",     "label=Highest");
 $sel->type_ok("status_whiteboard", "[Selenium was here][admin too]");
 $sel->select_ok("bug_status", "label=CONFIRMED");
 $sel->click_ok("bz_assignee_edit_action");
 $sel->type_ok("assigned_to", $config->{admin_user_login});
-$sel->type_ok("comment", "I have editbugs privs. Taking!");
+$sel->type_ok("comment",     "I have editbugs privs. Taking!");
 edit_bug_and_return($sel, $bug1_id, $bug_summary);
 $sel->click_ok("cc_edit_area_showhide");
 $sel->type_ok("newcc", $config->{unprivileged_user_login});
@@ -130,7 +139,9 @@ logout($sel);
 log_in($sel, $config, 'admin');
 go_to_bug($sel, $bug1_id);
 $sel->click_ok("cclist_accessible");
-$sel->type_ok("comment", "I am allowed to turn off cclist_accessible despite not being in the mandatory group");
+$sel->type_ok("comment",
+  "I am allowed to turn off cclist_accessible despite not being in the mandatory group"
+);
 edit_bug($sel, $bug1_id, $bug_summary);
 logout($sel);
 
@@ -149,6 +160,7 @@ logout($sel);
 log_in($sel, $config, 'admin');
 go_to_bug($sel, $bug1_id);
 $sel->select_ok("product", "label=TestProduct");
+
 # When selecting a new product, Bugzilla tries to reassign the bug by default,
 # so we have to uncheck it.
 $sel->click_ok("set_default_assignee");
@@ -158,13 +170,30 @@ $sel->click_ok("commit");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Verify New Product Details...");
 $sel->select_ok("component", "label=TestComponent");
-$sel->is_text_present_ok("These groups are not legal for the 'TestProduct' product or you are not allowed to restrict bugs to these groups");
-$sel->is_element_present_ok('//input[@type="checkbox" and @name="groups" and @value="QA-Selenium-TEST"]');
-ok(!$sel->is_editable('//input[@type="checkbox" and @name="groups" and @value="QA-Selenium-TEST"]'), "QA-Selenium-TEST group not editable");
-ok(!$sel->is_checked('//input[@type="checkbox" and @name="groups" and @value="QA-Selenium-TEST"]'), "QA-Selenium-TEST group not selected");
-$sel->is_element_present_ok('//input[@type="checkbox" and @name="groups" and @value="Master"]');
-$sel->is_editable_ok('//input[@type="checkbox" and @name="groups" and @value="Master"]');
-ok(!$sel->is_checked('//input[@type="checkbox" and @name="groups" and @value="Master"]'), "Master group not selected by default");
+$sel->is_text_present_ok(
+  "These groups are not legal for the 'TestProduct' product or you are not allowed to restrict bugs to these groups"
+);
+$sel->is_element_present_ok(
+  '//input[@type="checkbox" and @name="groups" and @value="QA-Selenium-TEST"]');
+ok(
+  !$sel->is_editable(
+    '//input[@type="checkbox" and @name="groups" and @value="QA-Selenium-TEST"]'),
+  "QA-Selenium-TEST group not editable"
+);
+ok(
+  !$sel->is_checked(
+    '//input[@type="checkbox" and @name="groups" and @value="QA-Selenium-TEST"]'),
+  "QA-Selenium-TEST group not selected"
+);
+$sel->is_element_present_ok(
+  '//input[@type="checkbox" and @name="groups" and @value="Master"]');
+$sel->is_editable_ok(
+  '//input[@type="checkbox" and @name="groups" and @value="Master"]');
+ok(
+  !$sel->is_checked(
+    '//input[@type="checkbox" and @name="groups" and @value="Master"]'),
+  "Master group not selected by default"
+);
 edit_bug($sel, $bug1_id, $bug_summary, {id => "change_product"});
 logout($sel);
 
@@ -173,12 +202,18 @@ logout($sel);
 
 log_in($sel, $config, 'unprivileged');
 go_to_bug($sel, $bug1_id);
-$sel->type_ok("comment", "I have no privs, I can only comment (and remove people from the CC list)");
-ok(!$sel->is_element_present('//select[@name="product"]'), "Product field not editable");
-ok(!$sel->is_element_present('//select[@name="bug_severity"]'), "Severity field not editable");
-ok(!$sel->is_element_present('//select[@name="priority"]'), "Priority field not editable");
-ok(!$sel->is_element_present('//select[@name="op_sys"]'), "OS field not editable");
-ok(!$sel->is_element_present('//select[@name="rep_platform"]'), "Hardware field not editable");
+$sel->type_ok("comment",
+  "I have no privs, I can only comment (and remove people from the CC list)");
+ok(!$sel->is_element_present('//select[@name="product"]'),
+  "Product field not editable");
+ok(!$sel->is_element_present('//select[@name="bug_severity"]'),
+  "Severity field not editable");
+ok(!$sel->is_element_present('//select[@name="priority"]'),
+  "Priority field not editable");
+ok(!$sel->is_element_present('//select[@name="op_sys"]'),
+  "OS field not editable");
+ok(!$sel->is_element_present('//select[@name="rep_platform"]'),
+  "Hardware field not editable");
 $sel->click_ok("cc_edit_area_showhide");
 $sel->add_selection_ok("cc", "label=" . $config->{admin_user_login});
 $sel->click_ok("removecc");
@@ -212,7 +247,8 @@ $sel->type_ok("comment", "Just a comment too...");
 $sel->click_ok("commit");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Product Edit Access Denied");
-$sel->is_text_present_ok("You are not permitted to edit bugs in product TestProduct.");
+$sel->is_text_present_ok(
+  "You are not permitted to edit bugs in product TestProduct.");
 logout($sel);
 
 # Test searches and "format for printing".
@@ -240,7 +276,8 @@ $sel->type_ok("save_newqueryname", "My bugs from QA_Selenium");
 $sel->click_ok("remember");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Search created");
-$sel->is_text_present_ok("OK, you have a new search named My bugs from QA_Selenium.");
+$sel->is_text_present_ok(
+  "OK, you have a new search named My bugs from QA_Selenium.");
 $sel->click_ok("link=My bugs from QA_Selenium");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Bug List: My bugs from QA_Selenium");
@@ -249,7 +286,8 @@ $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Full Text Bug Listing");
 $sel->is_text_present_ok("Bug $bug1_id");
 $sel->is_text_present_ok("Status: CONFIRMED");
-$sel->is_text_present_ok("Reporter: QA-Selenium-TEST ($config->{QA_Selenium_TEST_user_login})");
+$sel->is_text_present_ok(
+  "Reporter: QA-Selenium-TEST ($config->{QA_Selenium_TEST_user_login})");
 $sel->is_text_present_ok("Assignee: admin ($config->{admin_user_login})");
 $sel->is_text_present_ok("Severity: blocker");
 $sel->is_text_present_ok("Priority: Highest");
@@ -264,6 +302,7 @@ file_bug_in_product($sel, 'TestProduct');
 my $bug_summary2 = "New bug from me";
 $sel->select_ok("bug_severity", "label=blocker");
 $sel->type_ok("short_desc", $bug_summary2);
+
 # We turned on the CANEDIT bit for TestProduct.
 $sel->type_ok("comment", "I can enter a new bug, but not edit it, right?");
 my $bug2_id = create_bug($sel, $bug_summary2);
@@ -276,7 +315,9 @@ $sel->title_is("Enter Bug: TestProduct");
 $sel->click_ok("commit");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Suspicious Action");
-$sel->is_text_present_ok("no valid token for the create_bug action while processing the 'post_bug.cgi' script");
+$sel->is_text_present_ok(
+  "no valid token for the create_bug action while processing the 'post_bug.cgi' script"
+);
 $sel->click_ok("confirm");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_like(qr/\d+ \S $bug_summary2/, "Bug created");
@@ -284,7 +325,8 @@ $sel->type_ok("comment", "New comment not allowed");
 $sel->click_ok("commit");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Product Edit Access Denied");
-$sel->is_text_present_ok("You are not permitted to edit bugs in product TestProduct.");
+$sel->is_text_present_ok(
+  "You are not permitted to edit bugs in product TestProduct.");
 logout($sel);
 
 # Reassign the newly created bug to the admin.
@@ -293,7 +335,7 @@ log_in($sel, $config, 'admin');
 go_to_bug($sel, $bug2_id);
 $sel->click_ok("bz_assignee_edit_action");
 $sel->type_ok("assigned_to", $config->{admin_user_login});
-$sel->type_ok("comment", "Taking!");
+$sel->type_ok("comment",     "Taking!");
 edit_bug($sel, $bug2_id, $bug_summary2);
 
 # Test mass-change.
@@ -326,75 +368,90 @@ $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Changes made to bug $bug1_id");
 $sel->is_text_present_ok("URL foo.cgi?action=bar");
 $sel->is_text_present_ok("Severity critical blocker");
-$sel->is_text_present_ok("Whiteboard [Selenium was here] [Selenium was here][admin too]");
+$sel->is_text_present_ok(
+  "Whiteboard [Selenium was here] [Selenium was here][admin too]");
 $sel->is_text_present_ok("Product QA-Selenium-TEST TestProduct");
 $sel->is_text_present_ok("Status CONFIRMED RESOLVED");
 
 # Last step: move bugs to another DB, if the extension is enabled.
 
 if ($config->{test_extensions}) {
-    set_parameters($sel, { "Bug Moving" => {"move-to-url"     => {type => "text", value => 'http://www.foo.com/'},
-                                            "move-to-address" => {type => "text", value => 'import@foo.com'},
-                                            "movers"          => {type => "text", value => $config->{admin_user_login}}
-                                           }
-                         });
-
-    $sel->click_ok("link=My bugs from QA_Selenium");
-    $sel->wait_for_page_to_load_ok(WAIT_TIME);
-    $sel->title_is("Bug List: My bugs from QA_Selenium");
-    $sel->is_text_present_ok("2 bugs found");
-    $sel->click_ok("mass_change");
-    $sel->wait_for_page_to_load_ok(WAIT_TIME);
-    $sel->title_is("Bug List");
-    $sel->click_ok("check_all");
-    $sel->type_ok("comment", "-> moved");
-    $sel->click_ok('oldbugmove');
-    $sel->wait_for_page_to_load_ok(WAIT_TIME);
-    $sel->title_is("Bugs processed");
-    $sel->is_text_present_ok("Changes submitted for bug $bug1_id");
-    $sel->is_text_present_ok("Changes submitted for bug $bug2_id");
-    $sel->click_ok("link=$bug2_id");
-    $sel->wait_for_page_to_load_ok(WAIT_TIME);
-    $sel->title_like(qr/^$bug2_id/);
-    $sel->selected_label_is("resolution", "MOVED");
-    $sel->is_text_present_ok("Bug moved to http://www.foo.com/.");
-
-    # Disable bug moving again.
-    set_parameters($sel, { "Bug Moving" => {"movers" => {type => "text", value => ""}} });
+  set_parameters(
+    $sel,
+    {
+      "Bug Moving" => {
+        "move-to-url"     => {type => "text", value => 'http://www.foo.com/'},
+        "move-to-address" => {type => "text", value => 'import@foo.com'},
+        "movers"          => {type => "text", value => $config->{admin_user_login}}
+      }
+    }
+  );
+
+  $sel->click_ok("link=My bugs from QA_Selenium");
+  $sel->wait_for_page_to_load_ok(WAIT_TIME);
+  $sel->title_is("Bug List: My bugs from QA_Selenium");
+  $sel->is_text_present_ok("2 bugs found");
+  $sel->click_ok("mass_change");
+  $sel->wait_for_page_to_load_ok(WAIT_TIME);
+  $sel->title_is("Bug List");
+  $sel->click_ok("check_all");
+  $sel->type_ok("comment", "-> moved");
+  $sel->click_ok('oldbugmove');
+  $sel->wait_for_page_to_load_ok(WAIT_TIME);
+  $sel->title_is("Bugs processed");
+  $sel->is_text_present_ok("Changes submitted for bug $bug1_id");
+  $sel->is_text_present_ok("Changes submitted for bug $bug2_id");
+  $sel->click_ok("link=$bug2_id");
+  $sel->wait_for_page_to_load_ok(WAIT_TIME);
+  $sel->title_like(qr/^$bug2_id/);
+  $sel->selected_label_is("resolution", "MOVED");
+  $sel->is_text_present_ok("Bug moved to http://www.foo.com/.");
+
+  # Disable bug moving again.
+  set_parameters($sel,
+    {"Bug Moving" => {"movers" => {type => "text", value => ""}}});
 }
 
 # Make sure token checks are working correctly for single bug editing and mass change,
 # first with no token, then with an invalid token.
 
-foreach my $params (["no_token_single_bug", ""], ["invalid_token_single_bug", "&token=1"]) {
-    my ($comment, $token) = @$params;
-    $sel->open_ok("/$config->{bugzilla_installation}/process_bug.cgi?id=$bug1_id&comment=$comment$token",
-                  undef, "Edit a single bug with " . ($token ? "an invalid" : "no") . " token");
-    $sel->title_is("Suspicious Action");
-    $sel->is_text_present_ok($token ? "an invalid token" : "web browser directly");
-    edit_bug_and_return($sel, $bug1_id, $bug_summary, {id => "confirm"});
-    $sel->is_text_present_ok($comment);
+foreach my $params (["no_token_single_bug", ""],
+  ["invalid_token_single_bug", "&token=1"])
+{
+  my ($comment, $token) = @$params;
+  $sel->open_ok(
+    "/$config->{bugzilla_installation}/process_bug.cgi?id=$bug1_id&comment=$comment$token",
+    undef, "Edit a single bug with " . ($token ? "an invalid" : "no") . " token"
+  );
+  $sel->title_is("Suspicious Action");
+  $sel->is_text_present_ok($token ? "an invalid token" : "web browser directly");
+  edit_bug_and_return($sel, $bug1_id, $bug_summary, {id => "confirm"});
+  $sel->is_text_present_ok($comment);
 }
 
-foreach my $params (["no_token_mass_change", ""], ["invalid_token_mass_change", "&token=1"]) {
-    my ($comment, $token) = @$params;
-    $sel->open_ok("/$config->{bugzilla_installation}/process_bug.cgi?id_$bug1_id=1&id_$bug2_id=1&comment=$comment$token",
-                  undef, "Mass change with " . ($token ? "an invalid" : "no") . " token");
-    $sel->title_is("Suspicious Action");
-    $sel->is_text_present_ok("no valid token for the buglist_mass_change action");
-    $sel->click_ok("confirm");
+foreach my $params (["no_token_mass_change", ""],
+  ["invalid_token_mass_change", "&token=1"])
+{
+  my ($comment, $token) = @$params;
+  $sel->open_ok(
+    "/$config->{bugzilla_installation}/process_bug.cgi?id_$bug1_id=1&id_$bug2_id=1&comment=$comment$token",
+    undef, "Mass change with " . ($token ? "an invalid" : "no") . " token"
+  );
+  $sel->title_is("Suspicious Action");
+  $sel->is_text_present_ok("no valid token for the buglist_mass_change action");
+  $sel->click_ok("confirm");
+  $sel->wait_for_page_to_load_ok(WAIT_TIME);
+  $sel->title_is("Bugs processed");
+  foreach my $bug_id ($bug1_id, $bug2_id) {
+    $sel->click_ok("link=$bug_id");
+    $sel->wait_for_page_to_load_ok(WAIT_TIME);
+    $sel->title_like(qr/^$bug_id /);
+    $sel->is_text_present_ok($comment);
+    next if $bug_id == $bug2_id;
+    $sel->go_back_ok();
     $sel->wait_for_page_to_load_ok(WAIT_TIME);
     $sel->title_is("Bugs processed");
-    foreach my $bug_id ($bug1_id, $bug2_id) {
-        $sel->click_ok("link=$bug_id");
-        $sel->wait_for_page_to_load_ok(WAIT_TIME);
-        $sel->title_like(qr/^$bug_id /);
-        $sel->is_text_present_ok($comment);
-        next if $bug_id == $bug2_id;
-        $sel->go_back_ok();
-        $sel->wait_for_page_to_load_ok(WAIT_TIME);
-        $sel->title_is("Bugs processed");
-    }
+  }
 }
 
 # Now move these bugs out of our radar.
@@ -407,7 +464,7 @@ $sel->click_ok("mass_change");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Bug List");
 $sel->click_ok("check_all");
-$sel->type_ok("comment", "Reassigning to the reporter");
+$sel->type_ok("comment",     "Reassigning to the reporter");
 $sel->type_ok("assigned_to", $config->{QA_Selenium_TEST_user_login});
 $sel->click_ok("commit");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
@@ -428,14 +485,14 @@ clear_canedit_on_testproduct($sel, $master_gid);
 logout($sel);
 
 sub clear_canedit_on_testproduct {
-    my ($sel, $master_gid) = @_;
-
-    edit_product($sel, "TestProduct");
-    $sel->click_ok("link=Edit Group Access Controls:");
-    $sel->wait_for_page_to_load_ok(WAIT_TIME);
-    $sel->title_is("Edit Group Controls for TestProduct");
-    $sel->uncheck_ok("canedit_$master_gid");
-    $sel->click_ok("submit");
-    $sel->wait_for_page_to_load_ok(WAIT_TIME);
-    $sel->title_is("Update group access controls for TestProduct");
+  my ($sel, $master_gid) = @_;
+
+  edit_product($sel, "TestProduct");
+  $sel->click_ok("link=Edit Group Access Controls:");
+  $sel->wait_for_page_to_load_ok(WAIT_TIME);
+  $sel->title_is("Edit Group Controls for TestProduct");
+  $sel->uncheck_ok("canedit_$master_gid");
+  $sel->click_ok("submit");
+  $sel->wait_for_page_to_load_ok(WAIT_TIME);
+  $sel->title_is("Update group access controls for TestProduct");
 }
index f606415e2796ad41564408bde71b9a14f6044127..354d771c9bb1685157126c7d15240d9d9993a6c1 100644 (file)
@@ -19,11 +19,14 @@ use QA::Util;
 my ($sel, $config) = get_selenium();
 
 log_in($sel, $config, 'admin');
-set_parameters($sel, { "Bug Change Policies" => {"letsubmitterchoosepriority-off" => undef} });
+set_parameters($sel,
+  {"Bug Change Policies" => {"letsubmitterchoosepriority-off" => undef}});
 file_bug_in_product($sel, "TestProduct");
 ok(!$sel->is_text_present("Priority"), "The Priority label is not present");
-ok(!$sel->is_element_present("//select[\@name='priority']"), "The Priority drop-down menu is not present");
-set_parameters($sel, { "Bug Change Policies" => {"letsubmitterchoosepriority-on" => undef} });
+ok(!$sel->is_element_present("//select[\@name='priority']"),
+  "The Priority drop-down menu is not present");
+set_parameters($sel,
+  {"Bug Change Policies" => {"letsubmitterchoosepriority-on" => undef}});
 file_bug_in_product($sel, "TestProduct");
 $sel->is_text_present_ok("Priority");
 $sel->is_element_present_ok("//select[\@name='priority']");
index fa6f68b547453a41cecc56581b926006554c7fbe..69d7c73b6cc9c3ea1ed6951f1b839ec83e7582ba 100644 (file)
@@ -21,7 +21,7 @@ my ($sel, $config) = get_selenium();
 # Enable classifications
 
 log_in($sel, $config, 'admin');
-set_parameters($sel, { "Bug Fields" => {"useclassification-on" => undef} });
+set_parameters($sel, {"Bug Fields" => {"useclassification-on" => undef}});
 
 # Create a new classification.
 
@@ -34,25 +34,29 @@ $sel->title_is("Select classification");
 # Accessing action=delete directly must 1) trigger the security check page,
 # and 2) automatically reclassify products in this classification.
 if ($sel->is_text_present("cone")) {
-    $sel->open_ok("/$config->{bugzilla_installation}/editclassifications.cgi?action=delete&amp;classification=cone");
-    $sel->title_is("Suspicious Action");
-    $sel->click_ok("confirm");
-    $sel->wait_for_page_to_load_ok(WAIT_TIME);
-    $sel->title_is("Classification Deleted");
+  $sel->open_ok(
+    "/$config->{bugzilla_installation}/editclassifications.cgi?action=delete&amp;classification=cone"
+  );
+  $sel->title_is("Suspicious Action");
+  $sel->click_ok("confirm");
+  $sel->wait_for_page_to_load_ok(WAIT_TIME);
+  $sel->title_is("Classification Deleted");
 }
 if ($sel->is_text_present("ctwo")) {
-    $sel->open_ok("/$config->{bugzilla_installation}/editclassifications.cgi?action=delete&amp;classification=ctwo");
-    $sel->title_is("Suspicious Action");
-    $sel->click_ok("confirm");
-    $sel->wait_for_page_to_load_ok(WAIT_TIME);
-    $sel->title_is("Classification Deleted");
+  $sel->open_ok(
+    "/$config->{bugzilla_installation}/editclassifications.cgi?action=delete&amp;classification=ctwo"
+  );
+  $sel->title_is("Suspicious Action");
+  $sel->click_ok("confirm");
+  $sel->wait_for_page_to_load_ok(WAIT_TIME);
+  $sel->title_is("Classification Deleted");
 }
 
 $sel->click_ok("link=Add a new classification");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Add new classification");
 $sel->type_ok("classification", "cone");
-$sel->type_ok("description", "Classification number 1");
+$sel->type_ok("description",    "Classification number 1");
 $sel->click_ok('//input[@type="submit" and @value="Add"]');
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("New Classification Created");
@@ -65,7 +69,8 @@ $sel->click_ok("add_products");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Reclassify products");
 my @products = $sel->get_select_options("myprodlist");
-ok(scalar @products == 1 && $products[0] eq 'TestProduct', "TestProduct successfully added to 'cone'");
+ok(scalar @products == 1 && $products[0] eq 'TestProduct',
+  "TestProduct successfully added to 'cone'");
 
 # Create a new bug in this product/classification.
 
@@ -105,18 +110,21 @@ go_to_admin($sel);
 $sel->click_ok("link=Classifications");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Select classification");
-$sel->click_ok('//a[@href="editclassifications.cgi?action=del&classification=ctwo"]');
+$sel->click_ok(
+  '//a[@href="editclassifications.cgi?action=del&classification=ctwo"]');
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Error");
 my $error = trim($sel->get_text("error_msg"));
-ok($error =~ /there are products for this classification/, "Reject classification deletion");
+ok($error =~ /there are products for this classification/,
+  "Reject classification deletion");
 
 # Reclassify the product before deleting the classification.
 
 $sel->go_back_ok();
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Select classification");
-$sel->click_ok('//a[@href="editclassifications.cgi?action=reclassify&classification=ctwo"]');
+$sel->click_ok(
+  '//a[@href="editclassifications.cgi?action=reclassify&classification=ctwo"]');
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Reclassify products");
 $sel->add_selection_ok("myprodlist", "label=TestProduct");
@@ -126,7 +134,8 @@ $sel->title_is("Reclassify products");
 $sel->click_ok("link=edit");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Select classification");
-$sel->click_ok('//a[@href="editclassifications.cgi?action=del&classification=ctwo"]');
+$sel->click_ok(
+  '//a[@href="editclassifications.cgi?action=del&classification=ctwo"]');
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Delete classification");
 $sel->is_text_present_ok("Do you really want to delete this classification?");
@@ -136,7 +145,7 @@ $sel->title_is("Classification Deleted");
 
 # Disable classifications and make sure you cannot edit them anymore.
 
-set_parameters($sel, { "Bug Fields" => {"useclassification-off" => undef} });
+set_parameters($sel, {"Bug Fields" => {"useclassification-off" => undef}});
 $sel->open_ok("/$config->{bugzilla_installation}/editclassifications.cgi");
 $sel->title_is("Classification Not Enabled");
 logout($sel);
index 6348b87f20eea580dcf13b57ea3f5437c3cfe11b..edb7a0f6c4c8d7bbb70d682d3eeaebb30510e5e9 100644 (file)
@@ -21,12 +21,13 @@ my ($sel, $config) = get_selenium();
 # Turn on 'requirelogin' and log out.
 
 log_in($sel, $config, 'admin');
-set_parameters($sel, { "User Authentication" => {"requirelogin-on" => undef} });
+set_parameters($sel, {"User Authentication" => {"requirelogin-on" => undef}});
 logout($sel);
 
 # Accessing config.cgi should display no sensitive data.
 
-$sel->open_ok("/$config->{bugzilla_installation}/config.cgi", undef, "Go to config.cgi (JS format)");
+$sel->open_ok("/$config->{bugzilla_installation}/config.cgi",
+  undef, "Go to config.cgi (JS format)");
 $sel->is_text_present_ok("var status = [ ];");
 $sel->is_text_present_ok("var status_open = [ ];");
 $sel->is_text_present_ok("var status_closed = [ ];");
@@ -36,13 +37,14 @@ $sel->is_text_present_ok("var platform = [ ];");
 $sel->is_text_present_ok("var severity = [ ];");
 $sel->is_text_present_ok("var field = [\n];");
 
-ok(!$sel->is_text_present("cf_"), "No custom field displayed");
+ok(!$sel->is_text_present("cf_"),        "No custom field displayed");
 ok(!$sel->is_text_present("component["), "No component displayed");
-ok(!$sel->is_text_present("version["), "No version displayed");
-ok(!$sel->is_text_present("target_milestone["), "No target milestone displayed");
+ok(!$sel->is_text_present("version["),   "No version displayed");
+ok(!$sel->is_text_present("target_milestone["),
+  "No target milestone displayed");
 
 # Turn on 'requirelogin' and log out.
 
 log_in($sel, $config, 'admin');
-set_parameters($sel, { "User Authentication" => {"requirelogin-off" => undef} });
+set_parameters($sel, {"User Authentication" => {"requirelogin-off" => undef}});
 logout($sel);
index 8c19a1882b6d59e2e6baae861cb8b5b313318bfa..76c395652b43db82e14790b974c96a110738ead7 100644 (file)
@@ -21,7 +21,13 @@ my ($sel, $config) = get_selenium();
 # Set the email regexp for new bugzilla accounts to end with @bugzilla.test.
 
 log_in($sel, $config, 'admin');
-set_parameters($sel, { "User Authentication" => {"createemailregexp" => {type => "text", value => '[^@]+@bugzilla\.test$'}} });
+set_parameters(
+  $sel,
+  {
+    "User Authentication" =>
+      {"createemailregexp" => {type => "text", value => '[^@]+@bugzilla\.test$'}}
+  }
+);
 logout($sel);
 
 # Create a valid account. We need to randomize the login address, because a request
@@ -57,40 +63,50 @@ my $error_msg = trim($sel->get_text("error_msg"));
 ok($error_msg =~ /Please wait 10 minutes/, "Too soon for this account");
 
 # These accounts do not pass the regexp.
-my @accounts = ('test@yahoo.com', 'test@bugzilla.net', 'test@bugzilla.test.com');
+my @accounts
+  = ('test@yahoo.com', 'test@bugzilla.net', 'test@bugzilla.test.com');
 foreach my $account (@accounts) {
-    $sel->click_ok("link=New Account");
-    $sel->wait_for_page_to_load_ok(WAIT_TIME);
-    $sel->title_is("Create a new Bugzilla account");
-    $sel->type_ok("email", $account);
-    $sel->click_ok("send");
-    $sel->wait_for_page_to_load_ok(WAIT_TIME);
-    $sel->title_is("Account Creation Restricted");
-    $sel->is_text_present_ok("User account creation has been restricted.");
+  $sel->click_ok("link=New Account");
+  $sel->wait_for_page_to_load_ok(WAIT_TIME);
+  $sel->title_is("Create a new Bugzilla account");
+  $sel->type_ok("email", $account);
+  $sel->click_ok("send");
+  $sel->wait_for_page_to_load_ok(WAIT_TIME);
+  $sel->title_is("Account Creation Restricted");
+  $sel->is_text_present_ok("User account creation has been restricted.");
 }
 
 # These accounts are illegal.
-@accounts = ('test\bugzilla@bugzilla.test', 'test@bugzilla.org@bugzilla.test', 'test@bugzilla..test');
+@accounts = (
+  'test\bugzilla@bugzilla.test', 'test@bugzilla.org@bugzilla.test',
+  'test@bugzilla..test'
+);
+
 # Logins larger than 127 characters must be rejected, for security reasons.
-push  @accounts, 'selenium-' . random_string(110) . '@bugzilla.test';
+push @accounts, 'selenium-' . random_string(110) . '@bugzilla.test';
 
 foreach my $account (@accounts) {
-    $sel->click_ok("link=New Account");
-    $sel->wait_for_page_to_load_ok(WAIT_TIME);
-    $sel->title_is("Create a new Bugzilla account");
-    # Starting with 5.0, the login field is a type=email and is marked "required"
-    # This means that we need to add the novalidate attribute to the enclosing form
-    # so that the illegal login can still be checked by the backend code.
-    my $script = q{
+  $sel->click_ok("link=New Account");
+  $sel->wait_for_page_to_load_ok(WAIT_TIME);
+  $sel->title_is("Create a new Bugzilla account");
+
+  # Starting with 5.0, the login field is a type=email and is marked "required"
+  # This means that we need to add the novalidate attribute to the enclosing form
+  # so that the illegal login can still be checked by the backend code.
+  my $script = q{
         document.getElementById('account_creation_form').setAttribute('novalidate', 1);
     };
-    $sel->run_script($script);
-    $sel->type_ok("email", $account);
-    $sel->click_ok("send");
-    $sel->wait_for_page_to_load_ok(WAIT_TIME);
-    $sel->title_is("Invalid Email Address");
-    my $error_msg = trim($sel->get_text("error_msg"));
-    ok($error_msg =~ /^The e-mail address you entered (\S+) didn't pass our syntax checking/, "Invalid email address detected");
+  $sel->run_script($script);
+  $sel->type_ok("email", $account);
+  $sel->click_ok("send");
+  $sel->wait_for_page_to_load_ok(WAIT_TIME);
+  $sel->title_is("Invalid Email Address");
+  my $error_msg = trim($sel->get_text("error_msg"));
+  ok(
+    $error_msg
+      =~ /^The e-mail address you entered (\S+) didn't pass our syntax checking/,
+    "Invalid email address detected"
+  );
 }
 
 # This account already exists.
@@ -102,11 +118,20 @@ $sel->click_ok("send");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Account Already Exists");
 $error_msg = trim($sel->get_text("error_msg"));
-ok($error_msg eq "There is already an account with the email address $config->{admin_user_login}.", "Account already exists");
+ok(
+  $error_msg eq
+    "There is already an account with the email address $config->{admin_user_login}.",
+  "Account already exists"
+);
 
 # Turn off user account creation.
 log_in($sel, $config, 'admin');
-set_parameters($sel, { "User Authentication" => {"createemailregexp" => {type => "text", value => ''}} });
+set_parameters(
+  $sel,
+  {
+    "User Authentication" => {"createemailregexp" => {type => "text", value => ''}}
+  }
+);
 logout($sel);
 
 # Make sure that links pointing to createaccount.cgi are all deactivated.
@@ -114,17 +139,29 @@ ok(!$sel->is_text_present("New Account"), "No link named 'New Account'");
 $sel->click_ok("link=Home");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Bugzilla Main Page");
-ok(!$sel->is_text_present("Open a New Account"), "No link named 'Open a New Account'");
+ok(
+  !$sel->is_text_present("Open a New Account"),
+  "No link named 'Open a New Account'"
+);
 $sel->open_ok("/$config->{bugzilla_installation}/createaccount.cgi");
 $sel->title_is("Account Creation Disabled");
 $error_msg = trim($sel->get_text("error_msg"));
-ok($error_msg =~ /^User account creation has been disabled. New accounts must be created by an administrator/,
-   "User account creation disabled");
+ok(
+  $error_msg
+    =~ /^User account creation has been disabled. New accounts must be created by an administrator/,
+  "User account creation disabled"
+);
 
 # Re-enable user account creation.
 
 log_in($sel, $config, 'admin');
-set_parameters($sel, { "User Authentication" => {"createemailregexp" => {type => "text", value => '.*'}} });
+set_parameters(
+  $sel,
+  {
+    "User Authentication" =>
+      {"createemailregexp" => {type => "text", value => '.*'}}
+  }
+);
 
 # Make sure selenium-<random_string>@bugzilla.test has not be added to the DB yet.
 go_to_admin($sel);
index d477ace3b3d578de595f198114959f3c7d5dbc19..7fb5ceda478a0167ad58622e396b5603d0da7f74 100644 (file)
@@ -24,7 +24,8 @@ log_in($sel, $config, 'admin');
 file_bug_in_product($sel, 'TestProduct');
 my $bug_summary = "What's your ID?";
 $sel->type_ok("short_desc", $bug_summary);
-$sel->type_ok("comment", "Use the ID of this bug to generate a unique custom field name.");
+$sel->type_ok("comment",
+  "Use the ID of this bug to generate a unique custom field name.");
 $sel->type_ok("bug_severity", "label=normal");
 my $bug1_id = create_bug($sel, $bug_summary);
 
@@ -41,13 +42,16 @@ $sel->type_ok("name", "cf_qa_freetext_$bug1_id");
 $sel->type_ok("desc", "Freetext$bug1_id");
 $sel->select_ok("type", "label=Free Text");
 $sel->type_ok("sortkey", $bug1_id);
+
 # These values are off by default.
 $sel->value_is("enter_bug", "off");
-$sel->value_is("obsolete", "off");
+$sel->value_is("obsolete",  "off");
 $sel->click_ok("create");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Custom Field Created");
-$sel->is_text_present_ok("The new custom field 'cf_qa_freetext_$bug1_id' has been successfully created.");
+$sel->is_text_present_ok(
+  "The new custom field 'cf_qa_freetext_$bug1_id' has been successfully created."
+);
 
 $sel->click_ok("link=Add a new custom field");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
@@ -61,11 +65,12 @@ $sel->value_is("enter_bug", "on");
 sleep(3);
 $sel->click_ok("new_bugmail");
 $sel->value_is("new_bugmail", "on");
-$sel->value_is("obsolete", "off");
+$sel->value_is("obsolete",    "off");
 $sel->click_ok("create");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Custom Field Created");
-$sel->is_text_present_ok("The new custom field 'cf_qa_list_$bug1_id' has been successfully created.");
+$sel->is_text_present_ok(
+  "The new custom field 'cf_qa_list_$bug1_id' has been successfully created.");
 
 $sel->click_ok("link=Add a new custom field");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
@@ -73,15 +78,16 @@ $sel->title_is("Add a new Custom Field");
 $sel->type_ok("name", "cf_qa_bugid_$bug1_id");
 $sel->type_ok("desc", "Reference$bug1_id");
 $sel->select_ok("type", "label=Bug ID");
-$sel->type_ok("sortkey", $bug1_id);
+$sel->type_ok("sortkey",      $bug1_id);
 $sel->type_ok("reverse_desc", "IsRef$bug1_id");
 $sel->click_ok("enter_bug");
 $sel->value_is("enter_bug", "on");
-$sel->value_is("obsolete", "off");
+$sel->value_is("obsolete",  "off");
 $sel->click_ok("create");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Custom Field Created");
-$sel->is_text_present_ok("The new custom field 'cf_qa_bugid_$bug1_id' has been successfully created.");
+$sel->is_text_present_ok(
+  "The new custom field 'cf_qa_bugid_$bug1_id' has been successfully created.");
 
 # Add values to the custom fields.
 
@@ -90,27 +96,32 @@ $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Edit the Custom Field 'cf_qa_list_$bug1_id' (List$bug1_id)");
 $sel->click_ok("link=Edit legal values for this field");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
-$sel->title_is("Select value for the 'List$bug1_id' (cf_qa_list_$bug1_id) field");
+$sel->title_is(
+  "Select value for the 'List$bug1_id' (cf_qa_list_$bug1_id) field");
 
 $sel->click_ok("link=Add");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Add Value for the 'List$bug1_id' (cf_qa_list_$bug1_id) field");
-$sel->type_ok("value", "have fun?");
+$sel->type_ok("value",   "have fun?");
 $sel->type_ok("sortkey", "805");
 $sel->click_ok("create");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("New Field Value Created");
-$sel->is_text_present_ok("The value have fun? has been added as a valid choice for the List$bug1_id (cf_qa_list_$bug1_id) field.");
+$sel->is_text_present_ok(
+  "The value have fun? has been added as a valid choice for the List$bug1_id (cf_qa_list_$bug1_id) field."
+);
 
 $sel->click_ok("link=Add");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Add Value for the 'List$bug1_id' (cf_qa_list_$bug1_id) field");
-$sel->type_ok("value", "storage");
+$sel->type_ok("value",   "storage");
 $sel->type_ok("sortkey", "49");
 $sel->click_ok("create");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("New Field Value Created");
-$sel->is_text_present_ok("The value storage has been added as a valid choice for the List$bug1_id (cf_qa_list_$bug1_id) field.");
+$sel->is_text_present_ok(
+  "The value storage has been added as a valid choice for the List$bug1_id (cf_qa_list_$bug1_id) field."
+);
 
 # Also create a new bug status and a new resolution.
 
@@ -124,7 +135,7 @@ $sel->title_is("Select value for the 'Resolution' (resolution) field");
 $sel->click_ok("link=Add");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Add Value for the 'Resolution' (resolution) field");
-$sel->type_ok("value", "UPSTREAM");
+$sel->type_ok("value",   "UPSTREAM");
 $sel->type_ok("sortkey", 450);
 $sel->click_ok("create");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
@@ -140,7 +151,7 @@ $sel->title_is("Select value for the 'Status' (bug_status) field");
 $sel->click_ok("link=Add");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Add Value for the 'Status' (bug_status) field");
-$sel->type_ok("value", "SUSPENDED");
+$sel->type_ok("value",   "SUSPENDED");
 $sel->type_ok("sortkey", 250);
 $sel->click_ok("open_status");
 $sel->click_ok("create");
@@ -150,7 +161,7 @@ $sel->title_is("New Field Value Created");
 $sel->click_ok("link=Add");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Add Value for the 'Status' (bug_status) field");
-$sel->type_ok("value", "IN_QA");
+$sel->type_ok("value",   "IN_QA");
 $sel->type_ok("sortkey", 550);
 $sel->click_ok("closed_status");
 $sel->click_ok("create");
@@ -160,13 +171,20 @@ $sel->title_is("New Field Value Created");
 $sel->click_ok("link=status workflow page");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Edit Workflow");
-$sel->click_ok('//td[@title="From UNCONFIRMED to SUSPENDED"]//input[@type="checkbox"]');
-$sel->click_ok('//td[@title="From CONFIRMED to SUSPENDED"]//input[@type="checkbox"]');
-$sel->click_ok('//td[@title="From SUSPENDED to CONFIRMED"]//input[@type="checkbox"]');
-$sel->click_ok('//td[@title="From SUSPENDED to IN_PROGRESS"]//input[@type="checkbox"]');
-$sel->click_ok('//td[@title="From RESOLVED to IN_QA"]//input[@type="checkbox"]');
-$sel->click_ok('//td[@title="From IN_QA to VERIFIED"]//input[@type="checkbox"]');
-$sel->click_ok('//td[@title="From IN_QA to CONFIRMED"]//input[@type="checkbox"]');
+$sel->click_ok(
+  '//td[@title="From UNCONFIRMED to SUSPENDED"]//input[@type="checkbox"]');
+$sel->click_ok(
+  '//td[@title="From CONFIRMED to SUSPENDED"]//input[@type="checkbox"]');
+$sel->click_ok(
+  '//td[@title="From SUSPENDED to CONFIRMED"]//input[@type="checkbox"]');
+$sel->click_ok(
+  '//td[@title="From SUSPENDED to IN_PROGRESS"]//input[@type="checkbox"]');
+$sel->click_ok(
+  '//td[@title="From RESOLVED to IN_QA"]//input[@type="checkbox"]');
+$sel->click_ok(
+  '//td[@title="From IN_QA to VERIFIED"]//input[@type="checkbox"]');
+$sel->click_ok(
+  '//td[@title="From IN_QA to CONFIRMED"]//input[@type="checkbox"]');
 $sel->click_ok('//input[@value="Commit Changes"]');
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Edit Workflow");
@@ -178,8 +196,14 @@ $sel->is_text_present_ok("List$bug1_id:");
 $sel->is_element_present_ok("cf_qa_list_$bug1_id");
 $sel->is_text_present_ok("Reference$bug1_id:");
 $sel->is_element_present_ok("cf_qa_bugid_$bug1_id");
-ok(!$sel->is_text_present("Freetext$bug1_id:"), "Freetext$bug1_id is not displayed");
-ok(!$sel->is_element_present("cf_qa_freetext_$bug1_id"), "cf_qa_freetext_$bug1_id is not available");
+ok(
+  !$sel->is_text_present("Freetext$bug1_id:"),
+  "Freetext$bug1_id is not displayed"
+);
+ok(
+  !$sel->is_element_present("cf_qa_freetext_$bug1_id"),
+  "cf_qa_freetext_$bug1_id is not available"
+);
 my $bug_summary2 = "Et de un";
 $sel->type_ok("short_desc", $bug_summary2);
 $sel->select_ok("bug_severity", "critical");
@@ -266,21 +290,26 @@ $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Custom Field Updated");
 
 go_to_bug($sel, $bug1_id);
-$sel->is_element_present_ok("cf_qa_list_$bug1_id", "List$bug1_id is in the DOM of the page...");
-ok(!$sel->is_visible("cf_qa_list_$bug1_id"), "... but is not displayed with severity = 'normal'");
+$sel->is_element_present_ok("cf_qa_list_$bug1_id",
+  "List$bug1_id is in the DOM of the page...");
+ok(!$sel->is_visible("cf_qa_list_$bug1_id"),
+  "... but is not displayed with severity = 'normal'");
 $sel->select_ok("bug_severity", "major");
 ok(!$sel->is_visible("cf_qa_list_$bug1_id"), "... nor with severity = 'major'");
 $sel->select_ok("bug_severity", "critical");
-$sel->is_visible_ok("cf_qa_list_$bug1_id", "... but is visible with severity = 'critical'");
+$sel->is_visible_ok("cf_qa_list_$bug1_id",
+  "... but is visible with severity = 'critical'");
 edit_bug_and_return($sel, $bug1_id, $bug_summary);
 $sel->is_visible_ok("cf_qa_list_$bug1_id");
 
 go_to_bug($sel, $bug2_id);
 $sel->is_visible_ok("cf_qa_list_$bug1_id");
 $sel->select_ok("bug_severity", "minor");
-ok(!$sel->is_visible("cf_qa_list_$bug1_id"), "List$bug1_id is not displayed with severity = 'minor'");
+ok(!$sel->is_visible("cf_qa_list_$bug1_id"),
+  "List$bug1_id is not displayed with severity = 'minor'");
 edit_bug_and_return($sel, $bug2_id, $bug_summary2);
-ok(!$sel->is_visible("cf_qa_list_$bug1_id"), "List$bug1_id is not displayed with severity = 'minor'");
+ok(!$sel->is_visible("cf_qa_list_$bug1_id"),
+  "List$bug1_id is not displayed with severity = 'minor'");
 
 # Add a new value which is only listed under some condition.
 
@@ -300,11 +329,12 @@ $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Edit the Custom Field 'cf_qa_list_$bug1_id' (List$bug1_id)");
 $sel->click_ok("link=Edit legal values for this field");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
-$sel->title_is("Select value for the 'List$bug1_id' (cf_qa_list_$bug1_id) field");
+$sel->title_is(
+  "Select value for the 'List$bug1_id' (cf_qa_list_$bug1_id) field");
 $sel->click_ok("link=Add");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Add Value for the 'List$bug1_id' (cf_qa_list_$bug1_id) field");
-$sel->type_ok("value", "ghost");
+$sel->type_ok("value",   "ghost");
 $sel->type_ok("sortkey", "500");
 $sel->select_ok("visibility_value_id", "label=FIXED");
 $sel->click_ok("id=create");
@@ -316,8 +346,8 @@ my @labels = $sel->get_select_options("cf_qa_list_$bug1_id");
 ok(grep(/^ghost$/, @labels), "ghost is in the DOM of the page...");
 my $disabled = $sel->get_attribute("v4_cf_qa_list_$bug1_id\@disabled");
 ok($disabled, "... but is not available for selection by default");
-$sel->select_ok("bug_status", "label=RESOLVED");
-$sel->select_ok("resolution", "label=FIXED");
+$sel->select_ok("bug_status",          "label=RESOLVED");
+$sel->select_ok("resolution",          "label=FIXED");
 $sel->select_ok("cf_qa_list_$bug1_id", "label=ghost");
 edit_bug_and_return($sel, $bug1_id, $bug_summary);
 $sel->selected_label_is("cf_qa_list_$bug1_id", "ghost");
@@ -330,10 +360,14 @@ $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Edit values for which field?");
 $sel->click_ok("link=List$bug1_id");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
-$sel->title_is("Select value for the 'List$bug1_id' (cf_qa_list_$bug1_id) field");
-$sel->click_ok("//a[contains(\@href, 'editvalues.cgi?action=del&field=cf_qa_list_$bug1_id&value=have%20fun%3F')]");
+$sel->title_is(
+  "Select value for the 'List$bug1_id' (cf_qa_list_$bug1_id) field");
+$sel->click_ok(
+  "//a[contains(\@href, 'editvalues.cgi?action=del&field=cf_qa_list_$bug1_id&value=have%20fun%3F')]"
+);
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
-$sel->title_is("Delete Value 'have fun?' from the 'List$bug1_id' (cf_qa_list_$bug1_id) field");
+$sel->title_is(
+  "Delete Value 'have fun?' from the 'List$bug1_id' (cf_qa_list_$bug1_id) field");
 $sel->is_text_present_ok("Do you really want to delete this value?");
 $sel->click_ok("delete");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
@@ -341,9 +375,12 @@ $sel->title_is("Field Value Deleted");
 
 # This value cannot be deleted as it's in use.
 
-$sel->click_ok("//a[contains(\@href, 'editvalues.cgi?action=del&field=cf_qa_list_$bug1_id&value=storage')]");
+$sel->click_ok(
+  "//a[contains(\@href, 'editvalues.cgi?action=del&field=cf_qa_list_$bug1_id&value=storage')]"
+);
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
-$sel->title_is("Delete Value 'storage' from the 'List$bug1_id' (cf_qa_list_$bug1_id) field");
+$sel->title_is(
+  "Delete Value 'storage' from the 'List$bug1_id' (cf_qa_list_$bug1_id) field");
 $sel->is_text_present_ok("There is 1 bug with this field value");
 
 # Mark the <select> field as obsolete, making it unavailable in bug reports.
@@ -362,7 +399,8 @@ $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Custom Field Updated");
 go_to_bug($sel, $bug1_id);
 $sel->value_is("cf_qa_freetext_$bug1_id", "thanks");
-ok(!$sel->is_element_present("cf_qa_list_$bug1_id"), "The custom list is not visible");
+ok(!$sel->is_element_present("cf_qa_list_$bug1_id"),
+  "The custom list is not visible");
 
 # Custom fields are also viewable by logged out users.
 
@@ -390,7 +428,8 @@ $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Custom Fields");
 $sel->click_ok("link=cf_qa_freetext_$bug1_id");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
-$sel->title_is("Edit the Custom Field 'cf_qa_freetext_$bug1_id' (Freetext$bug1_id)");
+$sel->title_is(
+  "Edit the Custom Field 'cf_qa_freetext_$bug1_id' (Freetext$bug1_id)");
 $sel->click_ok("obsolete");
 $sel->value_is("obsolete", "on");
 $sel->click_ok("edit");
@@ -406,7 +445,8 @@ $sel->title_is("Edit values for which field?");
 $sel->click_ok("link=Status");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Select value for the 'Status' (bug_status) field");
-$sel->click_ok('//a[@href="editvalues.cgi?action=del&field=bug_status&value=SUSPENDED"]');
+$sel->click_ok(
+  '//a[@href="editvalues.cgi?action=del&field=bug_status&value=SUSPENDED"]');
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Delete Value 'SUSPENDED' from the 'Status' (bug_status) field");
 $sel->is_text_present_ok("Sorry, but the 'SUSPENDED' value cannot be deleted");
@@ -429,21 +469,25 @@ $sel->title_is("Edit values for which field?");
 $sel->click_ok("link=Status");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Select value for the 'Status' (bug_status) field");
-$sel->click_ok('//a[@href="editvalues.cgi?action=del&field=bug_status&value=SUSPENDED"]');
+$sel->click_ok(
+  '//a[@href="editvalues.cgi?action=del&field=bug_status&value=SUSPENDED"]');
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Delete Value 'SUSPENDED' from the 'Status' (bug_status) field");
 $sel->click_ok("delete");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Field Value Deleted");
-$sel->is_text_present_ok("The value SUSPENDED of the Status (bug_status) field has been deleted");
+$sel->is_text_present_ok(
+  "The value SUSPENDED of the Status (bug_status) field has been deleted");
 
-$sel->click_ok('//a[@href="editvalues.cgi?action=del&field=bug_status&value=IN_QA"]');
+$sel->click_ok(
+  '//a[@href="editvalues.cgi?action=del&field=bug_status&value=IN_QA"]');
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Delete Value 'IN_QA' from the 'Status' (bug_status) field");
 $sel->click_ok("delete");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Field Value Deleted");
-$sel->is_text_present_ok("The value IN_QA of the Status (bug_status) field has been deleted");
+$sel->is_text_present_ok(
+  "The value IN_QA of the Status (bug_status) field has been deleted");
 
 go_to_admin($sel);
 $sel->click_ok("link=Field Values");
@@ -452,12 +496,15 @@ $sel->title_is("Edit values for which field?");
 $sel->click_ok("link=Resolution");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Select value for the 'Resolution' (resolution) field");
-$sel->click_ok('//a[@href="editvalues.cgi?action=del&field=resolution&value=UPSTREAM"]');
+$sel->click_ok(
+  '//a[@href="editvalues.cgi?action=del&field=resolution&value=UPSTREAM"]');
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
-$sel->title_is("Delete Value 'UPSTREAM' from the 'Resolution' (resolution) field");
+$sel->title_is(
+  "Delete Value 'UPSTREAM' from the 'Resolution' (resolution) field");
 $sel->click_ok("delete");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Field Value Deleted");
-$sel->is_text_present_ok("The value UPSTREAM of the Resolution (resolution) field has been deleted");
+$sel->is_text_present_ok(
+  "The value UPSTREAM of the Resolution (resolution) field has been deleted");
 
 logout($sel);
index a330800bf50e0aa9b2407d4ffe788b8b201955cb..761a1d1f736e0f9a912861a51dab901035e247a1 100644 (file)
@@ -27,30 +27,33 @@ $sel->click_ok("link=Custom Fields");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Custom Fields");
 
-my @types = ("Bug ID", "Large Text Box", "Free Text", "Multiple-Selection Box",
-             "Drop Down", "Date/Time");
+my @types = (
+  "Bug ID", "Large Text Box",
+  "Free Text", "Multiple-Selection Box",
+  "Drop Down", "Date/Time"
+);
 my $counter = int(rand(10000));
 
 foreach my $type (@types) {
-    my $fname = "cf_field" . ++$counter;
-    my $fdesc = "Field" . $counter;
-
-    $sel->click_ok("link=Add a new custom field");
-    $sel->wait_for_page_to_load_ok(WAIT_TIME);
-    $sel->title_is("Add a new Custom Field");
-    $sel->type_ok("name", $fname);
-    $sel->type_ok("desc", $fdesc);
-    $sel->select_ok("type", "label=$type");
-    $sel->click_ok("obsolete");
-    $sel->click_ok("create");
-    $sel->wait_for_page_to_load_ok(WAIT_TIME);
-    $sel->title_is("Custom Field Created");
-    $sel->click_ok("//a[\@href='editfields.cgi?action=del&name=$fname']");
-    $sel->wait_for_page_to_load_ok(WAIT_TIME);
-    $sel->title_is("Delete the Custom Field '$fname' ($fdesc)");
-    $sel->click_ok("link=Delete field '$fdesc'");
-    $sel->wait_for_page_to_load_ok(WAIT_TIME);
-    $sel->title_is("Custom Field Deleted");
+  my $fname = "cf_field" . ++$counter;
+  my $fdesc = "Field" . $counter;
+
+  $sel->click_ok("link=Add a new custom field");
+  $sel->wait_for_page_to_load_ok(WAIT_TIME);
+  $sel->title_is("Add a new Custom Field");
+  $sel->type_ok("name", $fname);
+  $sel->type_ok("desc", $fdesc);
+  $sel->select_ok("type", "label=$type");
+  $sel->click_ok("obsolete");
+  $sel->click_ok("create");
+  $sel->wait_for_page_to_load_ok(WAIT_TIME);
+  $sel->title_is("Custom Field Created");
+  $sel->click_ok("//a[\@href='editfields.cgi?action=del&name=$fname']");
+  $sel->wait_for_page_to_load_ok(WAIT_TIME);
+  $sel->title_is("Delete the Custom Field '$fname' ($fdesc)");
+  $sel->click_ok("link=Delete field '$fdesc'");
+  $sel->wait_for_page_to_load_ok(WAIT_TIME);
+  $sel->title_is("Custom Field Deleted");
 }
 
 logout($sel);
index 29e41eb74514192d3d2f2dde08e93a7908a63c48..922ce5c0d625a070e377ed8fb9d88e441269f063 100644 (file)
@@ -24,15 +24,15 @@ log_in($sel, $config, 'admin');
 file_bug_in_product($sel, "TestProduct");
 my $bug_summary = "Dependency Checks";
 $sel->type_ok("short_desc", $bug_summary);
-$sel->type_ok("comment", "This bug is public");
+$sel->type_ok("comment",    "This bug is public");
 my $bug1_id = create_bug($sel, $bug_summary);
 
 file_bug_in_product($sel, "TestProduct");
 $sel->type_ok("alias", "secret_qa_bug_$bug1_id+1");
 my $bug_summary2 = "Big Ben";
 $sel->type_ok("short_desc", $bug_summary2);
-$sel->type_ok("comment", "This bug is private");
-$sel->type_ok("dependson", $bug1_id);
+$sel->type_ok("comment",    "This bug is private");
+$sel->type_ok("dependson",  $bug1_id);
 $sel->check_ok('//input[@name="groups" and @value="Master"]');
 my $bug2_id = create_bug($sel, $bug_summary2);
 
@@ -48,8 +48,11 @@ logout($sel);
 
 log_in($sel, $config, 'editbugs');
 go_to_bug($sel, $bug1_id);
-ok(!$sel->is_text_present("secret_qa_bug_$bug1_id+1"), "The alias of the private bug is not visible");
-$sel->select_ok("priority", "label=High");
+ok(
+  !$sel->is_text_present("secret_qa_bug_$bug1_id+1"),
+  "The alias of the private bug is not visible"
+);
+$sel->select_ok("priority",   "label=High");
 $sel->select_ok("bug_status", "VERIFIED");
 $sel->type_ok("comment", "Can I still edit this bug?");
 edit_bug($sel, $bug1_id, $bug_summary);
index a0f2f92fb6c1b00e0125c4d4cff8c372ae1bc786..fe098e5ebedd32d2ede20fd7e1cb109850e49fb0 100644 (file)
@@ -18,15 +18,19 @@ use QA::Util;
 
 my ($sel, $config) = get_selenium();
 
-my $admin_user_login = $config->{admin_user_login};
+my $admin_user_login        = $config->{admin_user_login};
 my $unprivileged_user_login = $config->{unprivileged_user_login};
-my $permanent_user = $config->{permanent_user};
+my $permanent_user          = $config->{permanent_user};
 
 log_in($sel, $config, 'admin');
-set_parameters($sel, { "Bug Fields"              => {"useclassification-off" => undef,
-                                                     "usetargetmilestone-on" => undef},
-                       "Administrative Policies" => {"allowbugdeletion-on"   => undef}
-                     });
+set_parameters(
+  $sel,
+  {
+    "Bug Fields" =>
+      {"useclassification-off" => undef, "usetargetmilestone-on" => undef},
+    "Administrative Policies" => {"allowbugdeletion-on" => undef}
+  }
+);
 
 # Create a product and add components to it. Do some cleanup first
 # if the script failed during a previous run.
@@ -34,36 +38,40 @@ set_parameters($sel, { "Bug Fields"              => {"useclassification-off" =>
 go_to_admin($sel);
 $sel->click_ok("link=Products");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
+
 # No risk to get the "Select classification" page. We turned off useclassification.
 $sel->title_is("Select product");
 
 my $text = trim($sel->get_text("bugzilla-body"));
 if ($text =~ /(Kill me!|Kill me nicely)/) {
-    my $product = $1;
-    my $escaped_product = url_quote($product);
-    $sel->click_ok("//a[\@href='editproducts.cgi?action=del&product=$escaped_product']");
-    $sel->wait_for_page_to_load_ok(WAIT_TIME);
-    $sel->title_is("Delete Product '$product'");
-    $sel->click_ok("delete");
-    $sel->wait_for_page_to_load_ok(WAIT_TIME);
-    $sel->title_is("Product Deleted");
+  my $product         = $1;
+  my $escaped_product = url_quote($product);
+  $sel->click_ok(
+    "//a[\@href='editproducts.cgi?action=del&product=$escaped_product']");
+  $sel->wait_for_page_to_load_ok(WAIT_TIME);
+  $sel->title_is("Delete Product '$product'");
+  $sel->click_ok("delete");
+  $sel->wait_for_page_to_load_ok(WAIT_TIME);
+  $sel->title_is("Product Deleted");
 }
 
 $sel->click_ok("link=Add");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Add Product");
 $sel->type_ok("product", "Kill me!");
-$sel->type_ok("description", "I will disappear very soon. Do not add bugs to it.");
+$sel->type_ok("description",
+  "I will disappear very soon. Do not add bugs to it.");
 $sel->type_ok("defaultmilestone", "0.1a");
+
 # Since Bugzilla 4.0, the voting system is in an extension.
 if ($config->{test_extensions}) {
-    $sel->type_ok("votesperuser", "1");
-    $sel->type_ok("maxvotesperbug", "1");
-    $sel->type_ok("votestoconfirm", "10");
+  $sel->type_ok("votesperuser",   "1");
+  $sel->type_ok("maxvotesperbug", "1");
+  $sel->type_ok("votestoconfirm", "10");
 }
-$sel->type_ok("version", "0.1a");
-$sel->type_ok("component", "first comp");
-$sel->type_ok("comp_desc", "comp 1");
+$sel->type_ok("version",      "0.1a");
+$sel->type_ok("component",    "first comp");
+$sel->type_ok("comp_desc",    "comp 1");
 $sel->type_ok("initialowner", $admin_user_login);
 $sel->click_ok("add-product");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
@@ -77,8 +85,8 @@ $sel->title_is("Select component of product 'Kill me!'");
 $sel->click_ok("link=Add");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Add component to the Kill me! product");
-$sel->type_ok("component", "first comp");
-$sel->type_ok("description", "comp 2");
+$sel->type_ok("component",    "first comp");
+$sel->type_ok("description",  "comp 2");
 $sel->type_ok("initialowner", $admin_user_login);
 $sel->click_ok("create");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
@@ -89,9 +97,10 @@ $sel->title_is("Component Already Exists");
 $sel->go_back_ok();
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->type_ok("component", "second comp");
+
 # FIXME - Re-enter the default assignee (regression due to bug 577574)
 $sel->type_ok("initialowner", $admin_user_login);
-$sel->type_ok("initialcc", $permanent_user);
+$sel->type_ok("initialcc",    $permanent_user);
 $sel->click_ok("create");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Component Created");
@@ -121,7 +130,7 @@ $sel->click_ok("link=Add");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Add Milestone to Product 'Kill me!'");
 $sel->type_ok("milestone", "0.2");
-$sel->type_ok("sortkey", "2");
+$sel->type_ok("sortkey",   "2");
 $sel->click_ok("create");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Milestone Created");
@@ -132,6 +141,7 @@ $sel->click_ok("link=Add");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Add Milestone to Product 'Kill me!'");
 $sel->type_ok("milestone", "0.1a");
+
 # Negative sortkeys are valid for milestones.
 $sel->type_ok("sortkey", "-2");
 $sel->click_ok("create");
@@ -147,30 +157,36 @@ $sel->title_is("Milestone Created");
 # Now create an UNCONFIRMED bug and add it to the newly created product.
 
 file_bug_in_product($sel, "Kill me!");
-$sel->select_ok("version", "label=0.1a");
+$sel->select_ok("version",   "label=0.1a");
 $sel->select_ok("component", "label=first comp");
+
 # UNCONFIRMED must be present.
 $sel->select_ok("bug_status", "label=UNCONFIRMED");
-$sel->type_ok("cc", $unprivileged_user_login);
+$sel->type_ok("cc",           $unprivileged_user_login);
 $sel->type_ok("bug_file_loc", "http://www.test.com");
 my $bug_summary = "test create/edit product properties";
 $sel->type_ok("short_desc", $bug_summary);
-$sel->type_ok("comment", "this bug will soon be dead");
+$sel->type_ok("comment",    "this bug will soon be dead");
 my $bug1_id = create_bug($sel, $bug_summary);
 my @cc_list = $sel->get_select_options("cc");
-ok(grep($_ eq $unprivileged_user_login, @cc_list), "$unprivileged_user_login correctly added to the CC list");
-ok(!grep($_ eq $permanent_user, @cc_list), "$permanent_user not in the CC list for 'first comp' by default");
+ok(
+  grep($_ eq $unprivileged_user_login, @cc_list),
+  "$unprivileged_user_login correctly added to the CC list"
+);
+ok(!grep($_ eq $permanent_user, @cc_list),
+  "$permanent_user not in the CC list for 'first comp' by default");
 
 # File a second bug, and make sure users in the default CC list are added.
 file_bug_in_product($sel, "Kill me!");
-$sel->select_ok("version", "label=0.1a");
+$sel->select_ok("version",   "label=0.1a");
 $sel->select_ok("component", "label=second comp");
 my $bug_summary2 = "check default CC list";
 $sel->type_ok("short_desc", $bug_summary2);
-$sel->type_ok("comment", "is the CC list populated correctly?");
+$sel->type_ok("comment",    "is the CC list populated correctly?");
 create_bug($sel, $bug_summary2);
 @cc_list = $sel->get_select_options("cc");
-ok(grep($_ eq $permanent_user, @cc_list), "$permanent_user in the CC list for 'second comp' by default");
+ok(grep($_ eq $permanent_user, @cc_list),
+  "$permanent_user in the CC list for 'second comp' by default");
 
 # Edit product properties and set votes_to_confirm to 0, which has
 # the side-effect to disable auto-confirmation (new behavior compared
@@ -178,70 +194,82 @@ ok(grep($_ eq $permanent_user, @cc_list), "$permanent_user in the CC list for 's
 
 edit_product($sel, "Kill me!");
 $sel->type_ok("product", "Kill me nicely");
-$sel->type_ok("description", "I will disappear very soon. Do not add bugs to it (except for testing).");
+$sel->type_ok("description",
+  "I will disappear very soon. Do not add bugs to it (except for testing).");
 $sel->select_ok("defaultmilestone", "label=0.2");
 if ($config->{test_extensions}) {
-    $sel->type_ok("votesperuser", "2");
-    $sel->type_ok("maxvotesperbug", 5);
-    $sel->type_ok("votestoconfirm", "0");
+  $sel->type_ok("votesperuser",   "2");
+  $sel->type_ok("maxvotesperbug", 5);
+  $sel->type_ok("votestoconfirm", "0");
 }
 $sel->click_ok("update-product");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Updating Product 'Kill me nicely'");
-$sel->is_text_present_ok("Updated product name from 'Kill me!' to 'Kill me nicely'");
+$sel->is_text_present_ok(
+  "Updated product name from 'Kill me!' to 'Kill me nicely'");
 $sel->is_text_present_ok("Updated description");
 $sel->is_text_present_ok("Updated default milestone");
 if ($config->{test_extensions}) {
-    $sel->is_text_present_ok("Updated votes per user");
-    $sel->is_text_present_ok("Updated maximum votes per bug");
-    $sel->is_text_present_ok("Updated number of votes needed to confirm a bug");
-    $text = trim($sel->get_text("bugzilla-body"));
-    # We use .{1} in place of the right arrow character, which fails otherwise.
-    ok($text =~ /Checking unconfirmed bugs in this product for any which now have sufficient votes\.{3} .{1}there were none/,
-       "No bugs confirmed by popular votes (votestoconfirm = 0 disables auto-confirmation)");
-
-    # Now set votestoconfirm to 2, vote for a bug, and then set
-    # this attribute back to 1, to trigger auto-confirmation.
-
-    $sel->click_ok("link=Kill me nicely");
-    $sel->wait_for_page_to_load_ok(WAIT_TIME);
-    $sel->title_is("Edit Product 'Kill me nicely'", "Display properties of Kill me nicely");
-    $sel->type_ok("votestoconfirm", 2);
-    $sel->click_ok("update-product");
-    $sel->wait_for_page_to_load_ok(WAIT_TIME);
-    $sel->title_is("Updating Product 'Kill me nicely'");
-    $sel->is_text_present_ok("Updated number of votes needed to confirm a bug");
-
-    go_to_bug($sel, $bug1_id);
-    $sel->click_ok("link=vote");
-    $sel->wait_for_page_to_load_ok(WAIT_TIME);
-    $sel->title_is("Change Votes");
-    $sel->type_ok("bug_$bug1_id", 1);
-    $sel->click_ok("change");
-    $sel->wait_for_page_to_load_ok(WAIT_TIME);
-    $sel->title_is("Change Votes");
-    $sel->is_text_present_ok("The changes to your votes have been saved");
-
-    edit_product($sel, "Kill me nicely");
-    $sel->type_ok("votestoconfirm", 1);
-    $sel->click_ok("update-product");
-    $sel->wait_for_page_to_load_ok(WAIT_TIME);
-    $sel->title_is("Updating Product 'Kill me nicely'");
-    $sel->is_text_present_ok("Updated number of votes needed to confirm a bug");
-    $text = trim($sel->get_text("bugzilla-body"));
-    ok($text =~ /Bug $bug1_id confirmed by number of votes/, "Bug $bug1_id is confirmed by popular votes");
+  $sel->is_text_present_ok("Updated votes per user");
+  $sel->is_text_present_ok("Updated maximum votes per bug");
+  $sel->is_text_present_ok("Updated number of votes needed to confirm a bug");
+  $text = trim($sel->get_text("bugzilla-body"));
+
+  # We use .{1} in place of the right arrow character, which fails otherwise.
+  ok(
+    $text
+      =~ /Checking unconfirmed bugs in this product for any which now have sufficient votes\.{3} .{1}there were none/,
+    "No bugs confirmed by popular votes (votestoconfirm = 0 disables auto-confirmation)"
+  );
+
+  # Now set votestoconfirm to 2, vote for a bug, and then set
+  # this attribute back to 1, to trigger auto-confirmation.
+
+  $sel->click_ok("link=Kill me nicely");
+  $sel->wait_for_page_to_load_ok(WAIT_TIME);
+  $sel->title_is("Edit Product 'Kill me nicely'",
+    "Display properties of Kill me nicely");
+  $sel->type_ok("votestoconfirm", 2);
+  $sel->click_ok("update-product");
+  $sel->wait_for_page_to_load_ok(WAIT_TIME);
+  $sel->title_is("Updating Product 'Kill me nicely'");
+  $sel->is_text_present_ok("Updated number of votes needed to confirm a bug");
+
+  go_to_bug($sel, $bug1_id);
+  $sel->click_ok("link=vote");
+  $sel->wait_for_page_to_load_ok(WAIT_TIME);
+  $sel->title_is("Change Votes");
+  $sel->type_ok("bug_$bug1_id", 1);
+  $sel->click_ok("change");
+  $sel->wait_for_page_to_load_ok(WAIT_TIME);
+  $sel->title_is("Change Votes");
+  $sel->is_text_present_ok("The changes to your votes have been saved");
+
+  edit_product($sel, "Kill me nicely");
+  $sel->type_ok("votestoconfirm", 1);
+  $sel->click_ok("update-product");
+  $sel->wait_for_page_to_load_ok(WAIT_TIME);
+  $sel->title_is("Updating Product 'Kill me nicely'");
+  $sel->is_text_present_ok("Updated number of votes needed to confirm a bug");
+  $text = trim($sel->get_text("bugzilla-body"));
+  ok(
+    $text =~ /Bug $bug1_id confirmed by number of votes/,
+    "Bug $bug1_id is confirmed by popular votes"
+  );
 }
 
 # Edit the bug.
 
 go_to_bug($sel, $bug1_id);
-$sel->selected_label_is("product", "Kill me nicely");
-$sel->selected_label_is("bug_status", "CONFIRMED") if $config->{test_extensions};
+$sel->selected_label_is("product",    "Kill me nicely");
+$sel->selected_label_is("bug_status", "CONFIRMED")
+  if $config->{test_extensions};
 $sel->select_ok("target_milestone", "label=pre-0.1");
-$sel->select_ok("component", "label=second comp");
+$sel->select_ok("component",        "label=second comp");
 edit_bug_and_return($sel, $bug1_id, $bug_summary);
 @cc_list = $sel->get_select_options("cc");
-ok(grep($_ eq $permanent_user, @cc_list), "User $permanent_user automatically added to the CC list");
+ok(grep($_ eq $permanent_user, @cc_list),
+  "User $permanent_user automatically added to the CC list");
 
 # Delete the milestone the bug belongs to. This should retarget the bug
 # to the default milestone.
@@ -250,17 +278,24 @@ edit_product($sel, "Kill me nicely");
 $sel->click_ok("link=Edit milestones:");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Select milestone of product 'Kill me nicely'");
-$sel->click_ok('//a[@href="editmilestones.cgi?action=del&product=Kill%20me%20nicely&milestone=pre-0.1"]');
+$sel->click_ok(
+  '//a[@href="editmilestones.cgi?action=del&product=Kill%20me%20nicely&milestone=pre-0.1"]'
+);
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Delete Milestone of Product 'Kill me nicely'");
 $text = trim($sel->get_text("bugzilla-body"));
 ok($text =~ /There is 1 bug entered for this milestone/, "Warning displayed");
-ok($text =~ /Do you really want to delete this milestone\?/, "Requesting confirmation");
+ok($text =~ /Do you really want to delete this milestone\?/,
+  "Requesting confirmation");
 $sel->click_ok("delete");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Milestone Deleted");
 $text = trim($sel->get_text("message"));
-ok($text =~ /Bugs targetted to this milestone have been retargetted to the default milestone/, "Bug retargetted");
+ok(
+  $text
+    =~ /Bugs targetted to this milestone have been retargetted to the default milestone/,
+  "Bug retargetted"
+);
 
 # Try deleting the version used by the bug. This action must be rejected.
 
@@ -270,21 +305,27 @@ $sel->title_is("Edit Product 'Kill me nicely'");
 $sel->click_ok("link=Edit versions:");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Select version of product 'Kill me nicely'");
-$sel->click_ok("//a[contains(\@href, 'editversions.cgi?action=del&product=Kill%20me%20nicely&version=0.1a')]");
+$sel->click_ok(
+  "//a[contains(\@href, 'editversions.cgi?action=del&product=Kill%20me%20nicely&version=0.1a')]"
+);
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Delete Version of Product 'Kill me nicely'");
 $text = trim($sel->get_text("bugzilla-body"));
-ok($text =~ /Sorry, there are 2 outstanding bugs for this version/, "Rejecting version deletion");
+ok($text =~ /Sorry, there are 2 outstanding bugs for this version/,
+  "Rejecting version deletion");
 $sel->go_back_ok();
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 
 # Delete an unused version. The action must succeed.
 
-$sel->click_ok('//a[@href="editversions.cgi?action=del&product=Kill%20me%20nicely&version=0.1"]');
+$sel->click_ok(
+  '//a[@href="editversions.cgi?action=del&product=Kill%20me%20nicely&version=0.1"]'
+);
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Delete Version of Product 'Kill me nicely'");
 $text = trim($sel->get_text("bugzilla-body"));
-ok($text =~ /Do you really want to delete this version\?/, "Requesting confirmation");
+ok($text =~ /Do you really want to delete this version\?/,
+  "Requesting confirmation");
 $sel->click_ok("delete");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Version Deleted");
@@ -297,19 +338,26 @@ $sel->title_is("Edit Product 'Kill me nicely'");
 $sel->click_ok("link=Edit components:");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Select component of product 'Kill me nicely'");
-$sel->click_ok("//a[contains(\@href, 'editcomponents.cgi?action=del&product=Kill%20me%20nicely&component=second%20comp')]");
+$sel->click_ok(
+  "//a[contains(\@href, 'editcomponents.cgi?action=del&product=Kill%20me%20nicely&component=second%20comp')]"
+);
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Delete component 'second comp' from 'Kill me nicely' product");
 $text = trim($sel->get_text("bugzilla-body"));
 ok($text =~ /There are 2 bugs entered for this component/, "Warning displayed");
-ok($text =~ /Do you really want to delete this component\?/, "Requesting confirmation");
+ok($text =~ /Do you really want to delete this component\?/,
+  "Requesting confirmation");
 $sel->click_ok("delete");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Component Deleted");
 $text = trim($sel->get_text("bugzilla-body"));
-ok($text =~ /The component second comp has been deleted/, "Component deletion confirmed");
-ok($text =~ /All bugs being in this component and all references to them have also been deleted/,
-   "Bug deletion confirmed");
+ok($text =~ /The component second comp has been deleted/,
+  "Component deletion confirmed");
+ok(
+  $text
+    =~ /All bugs being in this component and all references to them have also been deleted/,
+  "Bug deletion confirmed"
+);
 
 # Only one value for component, version and milestone available. They should
 # be selected by default.
@@ -317,7 +365,7 @@ ok($text =~ /All bugs being in this component and all references to them have al
 file_bug_in_product($sel, "Kill me nicely");
 $bug_summary2 = "bye bye everybody!";
 $sel->type_ok("short_desc", $bug_summary2);
-$sel->type_ok("comment", "I'm dead :(");
+$sel->type_ok("comment",    "I'm dead :(");
 create_bug($sel, $bug_summary2);
 
 # Now delete the product.
@@ -326,12 +374,14 @@ go_to_admin($sel);
 $sel->click_ok("link=Products");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Select product");
-$sel->click_ok("//a[\@href='editproducts.cgi?action=del&product=Kill%20me%20nicely']");
+$sel->click_ok(
+  "//a[\@href='editproducts.cgi?action=del&product=Kill%20me%20nicely']");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Delete Product 'Kill me nicely'");
 $text = trim($sel->get_text("bugzilla-body"));
 ok($text =~ /There is 1 bug entered for this product/, "Warning displayed");
-ok($text =~ /Do you really want to delete this product\?/, "Confirmation request displayed");
+ok($text =~ /Do you really want to delete this product\?/,
+  "Confirmation request displayed");
 $sel->click_ok("delete");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Product Deleted");
index ce7888228138373b5930543c7e378c6f397655fb..848902986428b82b603949a1b3fe64ef814d0c6d 100644 (file)
@@ -26,7 +26,7 @@ use constant RCPT_NONE   => 4;
 my @email_both = ($config->{admin_user_login}, $config->{editbugs_user_login});
 my @email_admin = ($config->{admin_user_login});
 my @email_normal = ($config->{editbugs_user_login});
-my @email_none = ("no one");
+my @email_none   = ("no one");
 
 # Test script to test email preferences.
 # For reference, following bugmail and request mails should be generated.
@@ -67,76 +67,86 @@ $sel->click_ok("link=Email Notifications");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->is_text_present_ok("Email Notifications");
 $sel->click_ok("//input[\@value='Disable All Mail']");
-$sel->click_ok("email-0-1", undef, 'Set "I\'m added to or removed from this capacity" for Assignee role');
-$sel->click_ok("email-0-5", undef, 'Set "The priority, status, severity, or milestone changes" for Assignee role');
-$sel->click_ok("email-0-2", undef, 'Set "New comments are added" for Assignee role');
-$sel->click_ok("email-0-0", undef, 'Set "Any field not mentioned above changes" for Assignee role');
+$sel->click_ok("email-0-1", undef,
+  'Set "I\'m added to or removed from this capacity" for Assignee role');
+$sel->click_ok("email-0-5", undef,
+  'Set "The priority, status, severity, or milestone changes" for Assignee role');
+$sel->click_ok("email-0-2", undef,
+  'Set "New comments are added" for Assignee role');
+$sel->click_ok("email-0-0", undef,
+  'Set "Any field not mentioned above changes" for Assignee role');
 $sel->click_ok("email-3-8", undef, 'Set "The CC field changes" for CCed role');
-$sel->click_ok("email-1-10", undef, 'Set "A new bug is created" for QA Contact role');
-$sel->click_ok("email-100-101", undef, 'Set "Email me when someone sets a flag I asked for" global option');
+$sel->click_ok("email-1-10", undef,
+  'Set "A new bug is created" for QA Contact role');
+$sel->click_ok("email-100-101", undef,
+  'Set "Email me when someone sets a flag I asked for" global option');
+
 # Restore the old 4.2 behavior for 'Disable All Mail'.
-foreach my $col (0..3) {
-    foreach my $row (50..51) {
-        $sel->click_ok("neg-email-$col-$row");
-    }
+foreach my $col (0 .. 3) {
+  foreach my $row (50 .. 51) {
+    $sel->click_ok("neg-email-$col-$row");
+  }
 }
-$sel->value_is("email-0-1", "on");
-$sel->value_is("email-0-10", "off");
-$sel->value_is("email-0-6", "off");
-$sel->value_is("email-0-5", "on");
-$sel->value_is("email-0-2", "on");
-$sel->value_is("email-0-3", "off");
-$sel->value_is("email-0-4", "off");
-$sel->value_is("email-0-7", "off");
-$sel->value_is("email-0-8", "off");
-$sel->value_is("email-0-9", "off");
-$sel->value_is("email-0-0", "on");
+$sel->value_is("email-0-1",      "on");
+$sel->value_is("email-0-10",     "off");
+$sel->value_is("email-0-6",      "off");
+$sel->value_is("email-0-5",      "on");
+$sel->value_is("email-0-2",      "on");
+$sel->value_is("email-0-3",      "off");
+$sel->value_is("email-0-4",      "off");
+$sel->value_is("email-0-7",      "off");
+$sel->value_is("email-0-8",      "off");
+$sel->value_is("email-0-9",      "off");
+$sel->value_is("email-0-0",      "on");
 $sel->value_is("neg-email-0-50", "off");
 $sel->value_is("neg-email-0-51", "off");
-$sel->value_is("email-1-1", "off");
-$sel->value_is("email-1-10", "on");
-$sel->value_is("email-1-6", "off");
-$sel->value_is("email-1-5", "off");
-$sel->value_is("email-1-2", "off");
-$sel->value_is("email-1-3", "off");
-$sel->value_is("email-1-4", "off");
-$sel->value_is("email-1-7", "off");
-$sel->value_is("email-1-8", "off");
-$sel->value_is("email-1-9", "off");
-$sel->value_is("email-1-0", "off");
+$sel->value_is("email-1-1",      "off");
+$sel->value_is("email-1-10",     "on");
+$sel->value_is("email-1-6",      "off");
+$sel->value_is("email-1-5",      "off");
+$sel->value_is("email-1-2",      "off");
+$sel->value_is("email-1-3",      "off");
+$sel->value_is("email-1-4",      "off");
+$sel->value_is("email-1-7",      "off");
+$sel->value_is("email-1-8",      "off");
+$sel->value_is("email-1-9",      "off");
+$sel->value_is("email-1-0",      "off");
 $sel->value_is("neg-email-1-50", "off");
 $sel->value_is("neg-email-1-51", "off");
-ok(!$sel->is_editable("email-2-1"), 'The "I\'m added to or removed from this capacity" for Reporter role is disabled');
-$sel->value_is("email-2-10", "off");
-$sel->value_is("email-2-6", "off");
-$sel->value_is("email-2-5", "off");
-$sel->value_is("email-2-2", "off");
-$sel->value_is("email-2-3", "off");
-$sel->value_is("email-2-4", "off");
-$sel->value_is("email-2-7", "off");
-$sel->value_is("email-2-8", "off");
-$sel->value_is("email-2-9", "off");
-$sel->value_is("email-2-0", "off");
+ok(!$sel->is_editable("email-2-1"),
+  'The "I\'m added to or removed from this capacity" for Reporter role is disabled'
+);
+$sel->value_is("email-2-10",     "off");
+$sel->value_is("email-2-6",      "off");
+$sel->value_is("email-2-5",      "off");
+$sel->value_is("email-2-2",      "off");
+$sel->value_is("email-2-3",      "off");
+$sel->value_is("email-2-4",      "off");
+$sel->value_is("email-2-7",      "off");
+$sel->value_is("email-2-8",      "off");
+$sel->value_is("email-2-9",      "off");
+$sel->value_is("email-2-0",      "off");
 $sel->value_is("neg-email-2-50", "off");
 $sel->value_is("neg-email-2-51", "off");
-$sel->value_is("email-3-1", "off");
-$sel->value_is("email-3-10", "off");
-$sel->value_is("email-3-6", "off");
-$sel->value_is("email-3-5", "off");
-$sel->value_is("email-3-2", "off");
-$sel->value_is("email-3-3", "off");
-$sel->value_is("email-3-4", "off");
-$sel->value_is("email-3-7", "off");
-$sel->value_is("email-3-8", "on");
-$sel->value_is("email-3-9", "off");
-$sel->value_is("email-3-0", "off");
+$sel->value_is("email-3-1",      "off");
+$sel->value_is("email-3-10",     "off");
+$sel->value_is("email-3-6",      "off");
+$sel->value_is("email-3-5",      "off");
+$sel->value_is("email-3-2",      "off");
+$sel->value_is("email-3-3",      "off");
+$sel->value_is("email-3-4",      "off");
+$sel->value_is("email-3-7",      "off");
+$sel->value_is("email-3-8",      "on");
+$sel->value_is("email-3-9",      "off");
+$sel->value_is("email-3-0",      "off");
 $sel->value_is("neg-email-3-50", "off");
 $sel->value_is("neg-email-3-51", "off");
-$sel->value_is("email-100-100", "off");
-$sel->value_is("email-100-101", "on");
+$sel->value_is("email-100-100",  "off");
+$sel->value_is("email-100-101",  "on");
 $sel->click_ok("update", undef, "Submit modified admin email preferences");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
-$sel->is_text_present_ok("The changes to your email notifications have been saved.");
+$sel->is_text_present_ok(
+  "The changes to your email notifications have been saved.");
 
 # Set "After changing a bug" default preference to "Show the updated bug"
 # This simplifies bug changes below
@@ -160,104 +170,130 @@ $sel->wait_for_page_to_load(WAIT_TIME);
 $sel->title_is("Email Notifications");
 $sel->is_text_present_ok("Email Notifications");
 $sel->click_ok("//input[\@value='Enable All Mail']");
-$sel->click_ok("email-3-1", undef, 'Clear "I\'m added to or removed from this capacity" for CCed role');
-$sel->click_ok("email-3-5", undef, 'Clear "The priority, status, severity, or milestone changes" for CCed role');
-$sel->click_ok("email-2-2", undef, 'Clear "New comments are added" for Reporter role');
-$sel->click_ok("email-3-2", undef, 'Clear "New comments are added" for CCed role');
-$sel->click_ok("email-2-8", undef, 'Clear "The CC field changes" for Reporter role');
-$sel->click_ok("email-3-8", undef, 'Clear "The CC field changes" for CCed role');
-$sel->click_ok("email-2-0", undef, 'Clear "Any field not mentioned above changes" for Reporter role');
-$sel->click_ok("email-3-0", undef, 'Clear "Any field not mentioned above changes" for CCed role');
-$sel->click_ok("neg-email-0-51", undef, 'Set "Change was made by me" override for Assignee role');
-$sel->click_ok("email-100-101", undef, 'Clear "Email me when someone sets a flag I asked for" global option');
-$sel->value_is("email-0-1", "on");
-$sel->value_is("email-0-10", "on");
-$sel->value_is("email-0-6", "on");
-$sel->value_is("email-0-5", "on");
-$sel->value_is("email-0-2", "on");
-$sel->value_is("email-0-3", "on");
-$sel->value_is("email-0-4", "on");
-$sel->value_is("email-0-7", "on");
-$sel->value_is("email-0-8", "on");
-$sel->value_is("email-0-9", "on");
-$sel->value_is("email-0-0", "on");
+$sel->click_ok("email-3-1", undef,
+  'Clear "I\'m added to or removed from this capacity" for CCed role');
+$sel->click_ok("email-3-5", undef,
+  'Clear "The priority, status, severity, or milestone changes" for CCed role');
+$sel->click_ok("email-2-2", undef,
+  'Clear "New comments are added" for Reporter role');
+$sel->click_ok("email-3-2", undef,
+  'Clear "New comments are added" for CCed role');
+$sel->click_ok("email-2-8", undef,
+  'Clear "The CC field changes" for Reporter role');
+$sel->click_ok("email-3-8", undef,
+  'Clear "The CC field changes" for CCed role');
+$sel->click_ok("email-2-0", undef,
+  'Clear "Any field not mentioned above changes" for Reporter role');
+$sel->click_ok("email-3-0", undef,
+  'Clear "Any field not mentioned above changes" for CCed role');
+$sel->click_ok("neg-email-0-51", undef,
+  'Set "Change was made by me" override for Assignee role');
+$sel->click_ok("email-100-101", undef,
+  'Clear "Email me when someone sets a flag I asked for" global option');
+$sel->value_is("email-0-1",      "on");
+$sel->value_is("email-0-10",     "on");
+$sel->value_is("email-0-6",      "on");
+$sel->value_is("email-0-5",      "on");
+$sel->value_is("email-0-2",      "on");
+$sel->value_is("email-0-3",      "on");
+$sel->value_is("email-0-4",      "on");
+$sel->value_is("email-0-7",      "on");
+$sel->value_is("email-0-8",      "on");
+$sel->value_is("email-0-9",      "on");
+$sel->value_is("email-0-0",      "on");
 $sel->value_is("neg-email-0-50", "off");
 $sel->value_is("neg-email-0-51", "on");
-$sel->value_is("email-1-1", "on");
-$sel->value_is("email-1-10", "on");
-$sel->value_is("email-1-6", "on");
-$sel->value_is("email-1-5", "on");
-$sel->value_is("email-1-2", "on");
-$sel->value_is("email-1-3", "on");
-$sel->value_is("email-1-4", "on");
-$sel->value_is("email-1-7", "on");
-$sel->value_is("email-1-8", "on");
-$sel->value_is("email-1-9", "on");
-$sel->value_is("email-1-0", "on");
+$sel->value_is("email-1-1",      "on");
+$sel->value_is("email-1-10",     "on");
+$sel->value_is("email-1-6",      "on");
+$sel->value_is("email-1-5",      "on");
+$sel->value_is("email-1-2",      "on");
+$sel->value_is("email-1-3",      "on");
+$sel->value_is("email-1-4",      "on");
+$sel->value_is("email-1-7",      "on");
+$sel->value_is("email-1-8",      "on");
+$sel->value_is("email-1-9",      "on");
+$sel->value_is("email-1-0",      "on");
 $sel->value_is("neg-email-1-50", "off");
 $sel->value_is("neg-email-1-51", "off");
-ok(!$sel->is_editable("email-2-1"), 'The "I\'m added to or removed from this capacity" for Reporter role is disabled');
-$sel->value_is("email-2-10", "on");
-$sel->value_is("email-2-6", "on");
-$sel->value_is("email-2-5", "on");
-$sel->value_is("email-2-2", "off");
-$sel->value_is("email-2-3", "on");
-$sel->value_is("email-2-4", "on");
-$sel->value_is("email-2-7", "on");
-$sel->value_is("email-2-8", "off");
-$sel->value_is("email-2-9", "on");
-$sel->value_is("email-2-0", "off");
+ok(!$sel->is_editable("email-2-1"),
+  'The "I\'m added to or removed from this capacity" for Reporter role is disabled'
+);
+$sel->value_is("email-2-10",     "on");
+$sel->value_is("email-2-6",      "on");
+$sel->value_is("email-2-5",      "on");
+$sel->value_is("email-2-2",      "off");
+$sel->value_is("email-2-3",      "on");
+$sel->value_is("email-2-4",      "on");
+$sel->value_is("email-2-7",      "on");
+$sel->value_is("email-2-8",      "off");
+$sel->value_is("email-2-9",      "on");
+$sel->value_is("email-2-0",      "off");
 $sel->value_is("neg-email-2-50", "off");
 $sel->value_is("neg-email-2-51", "off");
-$sel->value_is("email-3-1", "off");
-$sel->value_is("email-3-10", "on");
-$sel->value_is("email-3-6", "on");
-$sel->value_is("email-3-5", "off");
-$sel->value_is("email-3-2", "off");
-$sel->value_is("email-3-3", "on");
-$sel->value_is("email-3-4", "on");
-$sel->value_is("email-3-7", "on");
-$sel->value_is("email-3-8", "off");
-$sel->value_is("email-3-9", "on");
-$sel->value_is("email-3-0", "off");
+$sel->value_is("email-3-1",      "off");
+$sel->value_is("email-3-10",     "on");
+$sel->value_is("email-3-6",      "on");
+$sel->value_is("email-3-5",      "off");
+$sel->value_is("email-3-2",      "off");
+$sel->value_is("email-3-3",      "on");
+$sel->value_is("email-3-4",      "on");
+$sel->value_is("email-3-7",      "on");
+$sel->value_is("email-3-8",      "off");
+$sel->value_is("email-3-9",      "on");
+$sel->value_is("email-3-0",      "off");
 $sel->value_is("neg-email-3-50", "off");
 $sel->value_is("neg-email-3-51", "off");
-$sel->value_is("email-100-100", "on");
-$sel->value_is("email-100-101", "off");
-$sel->click_ok("update", undef, "Submit modified normal user email preferences");
+$sel->value_is("email-100-100",  "on");
+$sel->value_is("email-100-101",  "off");
+$sel->click_ok("update", undef,
+  "Submit modified normal user email preferences");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
-$sel->is_text_present_ok("The changes to your email notifications have been saved.");
+$sel->is_text_present_ok(
+  "The changes to your email notifications have been saved.");
 
 # Create a test bug (bugmail to both normal user and admin)
 file_bug_in_product($sel, "Another Product");
 $sel->select_ok("component", "label=c1");
 my $bug_summary = "Selenium Email Preference test bug";
 $sel->type_ok("short_desc", $bug_summary, "Enter bug summary");
-$sel->type_ok("comment", "Created by Selenium to test Email Notifications", "Enter bug description");
+$sel->type_ok(
+  "comment",
+  "Created by Selenium to test Email Notifications",
+  "Enter bug description"
+);
 $sel->type_ok("assigned_to", $config->{editbugs_user_login});
-$sel->type_ok("qa_contact", $config->{admin_user_login});
-$sel->type_ok("cc", $config->{admin_user_login});
+$sel->type_ok("qa_contact",  $config->{admin_user_login});
+$sel->type_ok("cc",          $config->{admin_user_login});
 my $bug1_id = create_bug($sel, $bug_summary);
 verify_bugmail_recipients($sel, RCPT_BOTH);
 
 # Make normal user changes (first pass)
 #
 go_to_bug($sel, $bug1_id);
+
 # Severity change (bugmail to normal user but not admin)
 $sel->select_ok("bug_severity", "label=blocker");
 $sel->selected_label_is("bug_severity", "blocker");
 edit_bug($sel, $bug1_id, $bug_summary);
 verify_bugmail_recipients($sel, RCPT_NORMAL);
+
 # Add a comment (bugmail to no one)
-$sel->type_ok("comment", "This is a Selenium generated normal user test comment 1 of 2. (No bugmail should be generated for this.)");
-$sel->value_is("comment", "This is a Selenium generated normal user test comment 1 of 2. (No bugmail should be generated for this.)");
+$sel->type_ok("comment",
+  "This is a Selenium generated normal user test comment 1 of 2. (No bugmail should be generated for this.)"
+);
+$sel->value_is("comment",
+  "This is a Selenium generated normal user test comment 1 of 2. (No bugmail should be generated for this.)"
+);
 edit_bug($sel, $bug1_id, $bug_summary);
 verify_bugmail_recipients($sel, RCPT_NONE);
+
 # Add normal user to CC list (bugmail to admin but not normal user)
 $sel->type_ok("newcc", $config->{editbugs_user_login});
 $sel->value_is("newcc", $config->{editbugs_user_login});
 edit_bug($sel, $bug1_id, $bug_summary);
 verify_bugmail_recipients($sel, RCPT_ADMIN);
+
 # Request a flag from admin (bugmail to no one, request mail to no one)
 $sel->select_ok("flag_type-1", "label=?");
 $sel->type_ok("requestee_type-1", $config->{admin_user_login});
@@ -270,16 +306,23 @@ verify_bugmail_recipients($sel, RCPT_NONE);
 logout($sel);
 log_in($sel, $config, 'admin');
 go_to_bug($sel, $bug1_id);
+
 # Severity change (bugmail to normal user but not admin)
 $sel->select_ok("bug_severity", "label=trivial");
 $sel->selected_label_is("bug_severity", "trivial");
 edit_bug($sel, $bug1_id, $bug_summary);
 verify_bugmail_recipients($sel, RCPT_NORMAL);
+
 # Add a comment (bugmail to normal user but not admin)
-$sel->type_ok("comment", "This is a Selenium generated admin user test comment. (Only normal user should get bugmail for this.)");
-$sel->value_is("comment", "This is a Selenium generated admin user test comment. (Only normal user should get bugmail for this.)");
+$sel->type_ok("comment",
+  "This is a Selenium generated admin user test comment. (Only normal user should get bugmail for this.)"
+);
+$sel->value_is("comment",
+  "This is a Selenium generated admin user test comment. (Only normal user should get bugmail for this.)"
+);
 edit_bug($sel, $bug1_id, $bug_summary);
 verify_bugmail_recipients($sel, RCPT_NORMAL);
+
 # Remove normal user from CC list (bugmail to both normal user and admin)
 $sel->click_ok("removecc");
 $sel->add_selection_ok("cc", "label=$config->{editbugs_user_login}");
@@ -287,17 +330,20 @@ $sel->value_is("removecc", "on");
 $sel->selected_label_is("cc", $config->{editbugs_user_login});
 edit_bug($sel, $bug1_id, $bug_summary);
 verify_bugmail_recipients($sel, RCPT_BOTH);
+
 # Reassign bug to admin user (bugmail to both normal user and admin)
 $sel->type_ok("assigned_to", $config->{admin_user_login});
 $sel->value_is("assigned_to", $config->{admin_user_login});
 edit_bug($sel, $bug1_id, $bug_summary);
 verify_bugmail_recipients($sel, RCPT_BOTH);
+
 # Request a flag from normal user (bugmail to admin but not normal user and request mail to admin)
 $sel->select_ok("flag_type-1", "label=?");
 $sel->type_ok("requestee_type-1", $config->{editbugs_user_login});
 $sel->value_is("requestee_type-1", $config->{editbugs_user_login});
 edit_bug($sel, $bug1_id, $bug_summary);
 verify_bugmail_recipients($sel, RCPT_ADMIN);
+
 # Grant a normal user flag request (bugmail to admin but not normal user and request mail to no one)
 my $flag1_id = set_flag($sel, $config->{admin_user_login}, "?", "+");
 edit_bug($sel, $bug1_id, $bug_summary);
@@ -308,25 +354,34 @@ verify_bugmail_recipients($sel, RCPT_ADMIN);
 logout($sel);
 log_in($sel, $config, 'editbugs');
 go_to_bug($sel, $bug1_id);
+
 # Severity change (bugmail to both admin and normal user)
 $sel->select_ok("bug_severity", "label=normal");
 $sel->selected_label_is("bug_severity", "normal");
 edit_bug($sel, $bug1_id, $bug_summary);
 verify_bugmail_recipients($sel, RCPT_BOTH);
+
 # Add a comment (bugmail to admin but not normal user)
-$sel->type_ok("comment", "This is a Selenium generated normal user test comment 2 of 2. (Only admin should get bugmail for this.)");
-$sel->value_is("comment", "This is a Selenium generated normal user test comment 2 of 2. (Only admin should get bugmail for this.)");
+$sel->type_ok("comment",
+  "This is a Selenium generated normal user test comment 2 of 2. (Only admin should get bugmail for this.)"
+);
+$sel->value_is("comment",
+  "This is a Selenium generated normal user test comment 2 of 2. (Only admin should get bugmail for this.)"
+);
 edit_bug($sel, $bug1_id, $bug_summary);
 verify_bugmail_recipients($sel, RCPT_ADMIN);
+
 # Reassign to normal user (bugmail to admin but not normal user)
 $sel->type_ok("assigned_to", $config->{editbugs_user_login});
 $sel->value_is("assigned_to", $config->{editbugs_user_login});
 edit_bug($sel, $bug1_id, $bug_summary);
 verify_bugmail_recipients($sel, RCPT_ADMIN);
+
 # Deny a flag requested by admin (bugmail to no one and request mail to admin)
 my $flag2_id = set_flag($sel, $config->{editbugs_user_login}, "?", "-");
 edit_bug($sel, $bug1_id, $bug_summary);
 verify_bugmail_recipients($sel, RCPT_NONE);
+
 # Cancel both flags (bugmail and request mail to no one)
 set_flag($sel, undef, "+", "X", $flag1_id);
 set_flag($sel, undef, "-", "X", $flag2_id);
@@ -348,61 +403,73 @@ logout($sel);
 
 # Help functions
 sub verify_bugmail_recipients {
-    my ($sel, $rcpt_sentto) = @_;
-    my $wanted_sentto;
-    my $err = 0;
-
-    my ($package, $filename, $line) = caller;
-    my $prefix = "verify_bugmail_recipients:$line";
-
-    # Verify sentto field
-    my @email_sentto
-        = sort split(/, /, $sel->get_text("//dt[text()='Email sent to:']/following-sibling::dd"));
-    if ($rcpt_sentto == RCPT_BOTH) {
-      $wanted_sentto = \@email_both;
-      is_deeply(\@email_sentto, $wanted_sentto, "$prefix: Bugmail sent to both")
-          or $err = 1;
-    }
-    elsif ($rcpt_sentto == RCPT_ADMIN) {
-      $wanted_sentto = \@email_admin;
-      is_deeply(\@email_sentto, $wanted_sentto, "$prefix: Bugmail sent to admin")
-          or $err = 1;
-    }
-    elsif ($rcpt_sentto == RCPT_NORMAL) {
-      $wanted_sentto = \@email_normal;
-      is_deeply(\@email_sentto, $wanted_sentto, "$prefix: Bugmail sent to normal user")
-          or $err = 1;
-    } else {
-      $wanted_sentto = \@email_none;
-      is_deeply(\@email_sentto, $wanted_sentto, "$prefix: Bugmail sent to no one")
-          or $err = 1;
-    }
-
-    # In case of an error, retrieve and show diagnostics info
-    if ($err) {
-        diag("Sent, actual     : " . join(', ', @email_sentto));
-        diag("Sent, wanted     : " . join(', ', @$wanted_sentto));
-        diag("Changer          : " . trim($sel->get_text('//a[contains(@href, "logout")]/../text()[3]')));
-        diag("Reporter         : " . $sel->get_attribute('//th[contains(text(), "Reported:")]/following-sibling::td//a@title'));
-        diag("Assignee         : " . $sel->get_value('assigned_to'));
-        diag("QA contact       : " . $sel->get_value('qa_contact'));
-        diag("CC List          : " . join(', ', $sel->get_select_options('cc')));
-    }
+  my ($sel, $rcpt_sentto) = @_;
+  my $wanted_sentto;
+  my $err = 0;
+
+  my ($package, $filename, $line) = caller;
+  my $prefix = "verify_bugmail_recipients:$line";
+
+  # Verify sentto field
+  my @email_sentto = sort split(/, /,
+    $sel->get_text("//dt[text()='Email sent to:']/following-sibling::dd"));
+  if ($rcpt_sentto == RCPT_BOTH) {
+    $wanted_sentto = \@email_both;
+    is_deeply(\@email_sentto, $wanted_sentto, "$prefix: Bugmail sent to both")
+      or $err = 1;
+  }
+  elsif ($rcpt_sentto == RCPT_ADMIN) {
+    $wanted_sentto = \@email_admin;
+    is_deeply(\@email_sentto, $wanted_sentto, "$prefix: Bugmail sent to admin")
+      or $err = 1;
+  }
+  elsif ($rcpt_sentto == RCPT_NORMAL) {
+    $wanted_sentto = \@email_normal;
+    is_deeply(\@email_sentto, $wanted_sentto,
+      "$prefix: Bugmail sent to normal user")
+      or $err = 1;
+  }
+  else {
+    $wanted_sentto = \@email_none;
+    is_deeply(\@email_sentto, $wanted_sentto, "$prefix: Bugmail sent to no one")
+      or $err = 1;
+  }
+
+  # In case of an error, retrieve and show diagnostics info
+  if ($err) {
+    diag("Sent, actual     : " . join(', ', @email_sentto));
+    diag("Sent, wanted     : " . join(', ', @$wanted_sentto));
+    diag("Changer          : "
+        . trim($sel->get_text('//a[contains(@href, "logout")]/../text()[3]')));
+    diag(
+      "Reporter         : "
+        . $sel->get_attribute(
+        '//th[contains(text(), "Reported:")]/following-sibling::td//a@title')
+    );
+    diag("Assignee         : " . $sel->get_value('assigned_to'));
+    diag("QA contact       : " . $sel->get_value('qa_contact'));
+    diag("CC List          : " . join(', ', $sel->get_select_options('cc')));
+  }
 }
 
 sub set_flag {
-    my ($sel, $login, $curval, $newval, $prev_id) = @_;
+  my ($sel, $login, $curval, $newval, $prev_id) = @_;
 
-    # Retrieve flag id for the flag to be set
-    my $flag_id = $prev_id;
-    if (defined $login) {
-        my $flag_name = $sel->get_attribute("//table[\@id='flags']//input[\@value='$login']\@name");
-        $flag_name =~ /^requestee-(\d+)$/;
-        $flag_id = $1;
-    }
+  # Retrieve flag id for the flag to be set
+  my $flag_id = $prev_id;
+  if (defined $login) {
+    my $flag_name
+      = $sel->get_attribute("//table[\@id='flags']//input[\@value='$login']\@name");
+    $flag_name =~ /^requestee-(\d+)$/;
+    $flag_id = $1;
+  }
 
-    # Set new value for the flag (verifies current value)
-    $sel->select_ok("//select[\@id=\"flag-$flag_id\"]/option[\@value=\"$curval\" and \@selected]/..", "value=$newval", "Set flag ID $flag_id to $newval from $curval");
+  # Set new value for the flag (verifies current value)
+  $sel->select_ok(
+    "//select[\@id=\"flag-$flag_id\"]/option[\@value=\"$curval\" and \@selected]/..",
+    "value=$newval",
+    "Set flag ID $flag_id to $newval from $curval"
+  );
 
-   return $flag_id;
+  return $flag_id;
 }
index 743b931aac5959ccd6a05f10d198820feeacc7d2..4d9f3ddf78003748039ddc141428ddda7a0fa51e 100644 (file)
@@ -26,10 +26,14 @@ my ($sel, $config) = get_selenium();
 
 my $bug_summary = "Bug created by Selenium";
 foreach my $user (qw(admin unprivileged canconfirm)) {
-    log_in($sel, $config, $user);
-    file_bug_in_product($sel, "TestProduct");
-    $sel->type_ok("short_desc", $bug_summary, "Enter bug summary");
-    $sel->type_ok("comment", "--- Bug created by Selenium ---", "Enter bug description");
-    create_bug($sel, $bug_summary);
-    logout($sel);
+  log_in($sel, $config, $user);
+  file_bug_in_product($sel, "TestProduct");
+  $sel->type_ok("short_desc", $bug_summary, "Enter bug summary");
+  $sel->type_ok(
+    "comment",
+    "--- Bug created by Selenium ---",
+    "Enter bug description"
+  );
+  create_bug($sel, $bug_summary);
+  logout($sel);
 }
index 1b1f59bcf1c4247a20357965a2371e63a9a9249a..f7ba9d75a6175f7089798e3267ace407f40dc8b8 100644 (file)
@@ -30,7 +30,7 @@ $sel->title_is("Administer Flag Types");
 $sel->click_ok("link=Create Flag Type for Bugs");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Create Flag Type for Bugs");
-$sel->type_ok("name", "SeleniumBugFlag1Test");
+$sel->type_ok("name",        "SeleniumBugFlag1Test");
 $sel->type_ok("description", "bugflag1");
 $sel->select_ok("product", "label=TestProduct");
 $sel->click_ok("categoryAction-include");
@@ -51,23 +51,33 @@ $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Create Flag Type for Bugs");
 my @inclusion = $sel->get_select_options("inclusion_to_remove");
 ok(scalar @inclusion == 2, "The inclusion list contains 2 elements");
-ok(grep($_ eq "QA-Selenium-TEST:__Any__", @inclusion), "QA-Selenium-TEST:__Any__ is in the inclusion list");
-ok(grep($_ eq "TestProduct:__Any__", @inclusion), "TestProduct:__Any__ is in the inclusion list");
+ok(
+  grep($_ eq "QA-Selenium-TEST:__Any__", @inclusion),
+  "QA-Selenium-TEST:__Any__ is in the inclusion list"
+);
+ok(
+  grep($_ eq "TestProduct:__Any__", @inclusion),
+  "TestProduct:__Any__ is in the inclusion list"
+);
 my @exclusion = $sel->get_select_options("exclusion_to_remove");
 ok(scalar @exclusion == 1, "The exclusion list contains 1 element");
-ok($exclusion[0] eq "QA-Selenium-TEST:__Any__", "QA-Selenium-TEST:__Any__ is in the exclusion list");
+ok(
+  $exclusion[0] eq "QA-Selenium-TEST:__Any__",
+  "QA-Selenium-TEST:__Any__ is in the exclusion list"
+);
 $sel->type_ok("sortkey", "900");
-$sel->value_is("cc_list", "");
-$sel->value_is("is_active", "on");
-$sel->value_is("is_requestable", "on");
-$sel->value_is("is_requesteeble", "on");
+$sel->value_is("cc_list",          "");
+$sel->value_is("is_active",        "on");
+$sel->value_is("is_requestable",   "on");
+$sel->value_is("is_requesteeble",  "on");
 $sel->value_is("is_multiplicable", "on");
-$sel->select_ok("grant_group", "label=admin");
+$sel->select_ok("grant_group",   "label=admin");
 $sel->select_ok("request_group", "label=(no group)");
 $sel->click_ok("save");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Flag Type 'SeleniumBugFlag1Test' Created");
-$sel->is_text_present_ok("The flag type SeleniumBugFlag1Test has been created.");
+$sel->is_text_present_ok(
+  "The flag type SeleniumBugFlag1Test has been created.");
 my $flagtype_url = $sel->get_attribute('link=SeleniumBugFlag1Test@href');
 $flagtype_url =~ /id=(\d+)$/;
 my $flagtype1_id = $1;
@@ -77,19 +87,28 @@ my $flagtype1_id = $1;
 $sel->click_ok("//a[\@href='editflagtypes.cgi?action=copy&id=$flagtype1_id']");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Create Flag Type for Bugs Based on SeleniumBugFlag1Test");
-$sel->type_ok("name", "SeleniumBugFlag2Test");
+$sel->type_ok("name",        "SeleniumBugFlag2Test");
 $sel->type_ok("description", "bugflag2");
 @inclusion = $sel->get_select_options("inclusion_to_remove");
 ok(scalar @inclusion == 2, "The inclusion list contains 2 elements");
-ok(grep($_ eq "QA-Selenium-TEST:__Any__", @inclusion), "QA-Selenium-TEST:__Any__ is in the inclusion list");
-ok(grep($_ eq "TestProduct:__Any__", @inclusion), "TestProduct:__Any__ is in the inclusion list");
+ok(
+  grep($_ eq "QA-Selenium-TEST:__Any__", @inclusion),
+  "QA-Selenium-TEST:__Any__ is in the inclusion list"
+);
+ok(
+  grep($_ eq "TestProduct:__Any__", @inclusion),
+  "TestProduct:__Any__ is in the inclusion list"
+);
 @exclusion = $sel->get_select_options("exclusion_to_remove");
 ok(scalar @exclusion == 1, "The exclusion list contains 1 element");
-ok($exclusion[0] eq "QA-Selenium-TEST:__Any__", "QA-Selenium-TEST:__Any__ is in the exclusion list");
+ok(
+  $exclusion[0] eq "QA-Selenium-TEST:__Any__",
+  "QA-Selenium-TEST:__Any__ is in the exclusion list"
+);
 $sel->type_ok("sortkey", "950");
-$sel->value_is("is_active", "on");
-$sel->value_is("is_requestable", "on");
-$sel->value_is("is_requesteeble", "on");
+$sel->value_is("is_active",        "on");
+$sel->value_is("is_requestable",   "on");
+$sel->value_is("is_requesteeble",  "on");
 $sel->value_is("is_multiplicable", "on");
 $sel->type_ok("cc_list", $config->{canconfirm_user_login});
 $sel->selected_label_is("grant_group", "admin");
@@ -97,7 +116,8 @@ $sel->select_ok("request_group", "label=editbugs");
 $sel->click_ok("save");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Flag Type 'SeleniumBugFlag2Test' Created");
-$sel->is_text_present_ok("The flag type SeleniumBugFlag2Test has been created.");
+$sel->is_text_present_ok(
+  "The flag type SeleniumBugFlag2Test has been created.");
 $flagtype_url = $sel->get_attribute('link=SeleniumBugFlag2Test@href');
 $flagtype_url =~ /id=(\d+)$/;
 my $flagtype2_id = $1;
@@ -107,10 +127,10 @@ my $flagtype2_id = $1;
 $sel->click_ok("//a[\@href='editflagtypes.cgi?action=copy&id=$flagtype1_id']");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Create Flag Type for Bugs Based on SeleniumBugFlag1Test");
-$sel->type_ok("name", "SeleniumBugFlag3Test");
+$sel->type_ok("name",        "SeleniumBugFlag3Test");
 $sel->type_ok("description", "bugflag3");
-$sel->type_ok("sortkey", "980");
-$sel->value_is("is_active", "on");
+$sel->type_ok("sortkey",     "980");
+$sel->value_is("is_active",      "on");
 $sel->value_is("is_requestable", "on");
 $sel->uncheck_ok("is_requesteeble");
 $sel->uncheck_ok("is_multiplicable");
@@ -120,7 +140,8 @@ $sel->selected_label_is("request_group", "(no group)");
 $sel->click_ok("save");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Flag Type 'SeleniumBugFlag3Test' Created");
-$sel->is_text_present_ok("The flag type SeleniumBugFlag3Test has been created.");
+$sel->is_text_present_ok(
+  "The flag type SeleniumBugFlag3Test has been created.");
 $flagtype_url = $sel->get_attribute('link=SeleniumBugFlag3Test@href');
 $flagtype_url =~ /id=(\d+)$/;
 my $flagtype3_id = $1;
@@ -130,7 +151,7 @@ my $flagtype3_id = $1;
 $sel->click_ok("link=Create Flag Type For Attachments");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Create Flag Type for Attachments");
-$sel->type_ok("name", "SeleniumAttachmentFlag1Test");
+$sel->type_ok("name",        "SeleniumAttachmentFlag1Test");
 $sel->type_ok("description", "attachmentflag1");
 $sel->select_ok("product", "label=TestProduct");
 $sel->click_ok("categoryAction-include");
@@ -143,15 +164,19 @@ $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Create Flag Type for Attachments");
 @inclusion = $sel->get_select_options("inclusion_to_remove");
 ok(scalar @inclusion == 1, "The inclusion list contains 1 element");
-ok($inclusion[0] eq "TestProduct:__Any__", "TestProduct:__Any__ is in the exclusion list");
+ok(
+  $inclusion[0] eq "TestProduct:__Any__",
+  "TestProduct:__Any__ is in the exclusion list"
+);
 $sel->type_ok("sortkey", "700");
 $sel->value_is("cc_list", "");
-$sel->select_ok("grant_group", "label=editbugs");
+$sel->select_ok("grant_group",   "label=editbugs");
 $sel->select_ok("request_group", "label=canconfirm");
 $sel->click_ok("save");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Flag Type 'SeleniumAttachmentFlag1Test' Created");
-$sel->is_text_present_ok("The flag type SeleniumAttachmentFlag1Test has been created.");
+$sel->is_text_present_ok(
+  "The flag type SeleniumAttachmentFlag1Test has been created.");
 $flagtype_url = $sel->get_attribute('link=SeleniumAttachmentFlag1Test@href');
 $flagtype_url =~ /id=(\d+)$/;
 my $aflagtype1_id = $1;
@@ -160,21 +185,26 @@ my $aflagtype1_id = $1;
 
 $sel->click_ok("//a[\@href='editflagtypes.cgi?action=copy&id=$aflagtype1_id']");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
-$sel->title_is("Create Flag Type for Attachments Based on SeleniumAttachmentFlag1Test");
-$sel->type_ok("name", "SeleniumAttachmentFlag2Test");
+$sel->title_is(
+  "Create Flag Type for Attachments Based on SeleniumAttachmentFlag1Test");
+$sel->type_ok("name",        "SeleniumAttachmentFlag2Test");
 $sel->type_ok("description", "attachmentflag2");
 @inclusion = $sel->get_select_options("inclusion_to_remove");
 ok(scalar @inclusion == 1, "The inclusion list contains 1 element");
-ok($inclusion[0] eq "TestProduct:__Any__", "TestProduct:__Any__ is in the exclusion list");
+ok(
+  $inclusion[0] eq "TestProduct:__Any__",
+  "TestProduct:__Any__ is in the exclusion list"
+);
 $sel->type_ok("sortkey", "750");
 $sel->type_ok("cc_list", $config->{admin_user_login});
 $sel->uncheck_ok("is_multiplicable");
-$sel->select_ok("grant_group", "label=(no group)");
+$sel->select_ok("grant_group",   "label=(no group)");
 $sel->select_ok("request_group", "label=(no group)");
 $sel->click_ok("save");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Flag Type 'SeleniumAttachmentFlag2Test' Created");
-$sel->is_text_present_ok("The flag type SeleniumAttachmentFlag2Test has been created.");
+$sel->is_text_present_ok(
+  "The flag type SeleniumAttachmentFlag2Test has been created.");
 $flagtype_url = $sel->get_attribute('link=SeleniumAttachmentFlag2Test@href');
 $flagtype_url =~ /id=(\d+)$/;
 my $aflagtype2_id = $1;
@@ -183,15 +213,17 @@ my $aflagtype2_id = $1;
 
 $sel->click_ok("//a[\@href='editflagtypes.cgi?action=copy&id=$aflagtype1_id']");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
-$sel->title_is("Create Flag Type for Attachments Based on SeleniumAttachmentFlag1Test");
-$sel->type_ok("name", "SeleniumAttachmentFlag3Test");
+$sel->title_is(
+  "Create Flag Type for Attachments Based on SeleniumAttachmentFlag1Test");
+$sel->type_ok("name",        "SeleniumAttachmentFlag3Test");
 $sel->type_ok("description", "attachmentflag3");
-$sel->type_ok("sortkey", "800");
+$sel->type_ok("sortkey",     "800");
 $sel->uncheck_ok("is_active");
 $sel->click_ok("save");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Flag Type 'SeleniumAttachmentFlag3Test' Created");
-$sel->is_text_present_ok("The flag type SeleniumAttachmentFlag3Test has been created.");
+$sel->is_text_present_ok(
+  "The flag type SeleniumAttachmentFlag3Test has been created.");
 $flagtype_url = $sel->get_attribute('link=SeleniumAttachmentFlag3Test@href');
 $flagtype_url =~ /id=(\d+)$/;
 my $aflagtype3_id = $1;
@@ -201,7 +233,8 @@ my $aflagtype3_id = $1;
 file_bug_in_product($sel, 'TestProduct');
 my $bug_summary = "test flags";
 $sel->type_ok("short_desc", $bug_summary);
-$sel->type_ok("comment", "this bug is used by Selenium to test flags");
+$sel->type_ok("comment",    "this bug is used by Selenium to test flags");
+
 # Restrict the bug to the Master group. That's important for subsequent tests!
 $sel->check_ok('//input[@name="groups" and @value="Master"]');
 my $bug1_id = create_bug($sel, $bug_summary);
@@ -209,16 +242,21 @@ my $bug1_id = create_bug($sel, $bug_summary);
 # All 3 bug flag types must be available; we are in the TestProduct product.
 
 $sel->is_text_present_ok("SeleniumBugFlag1Test");
+
 # We specify //select or //input, just to be sure. This is not required, though.
 $sel->is_element_present_ok("//select[\@id='flag_type-$flagtype1_id']");
 $sel->is_element_present_ok("//input[\@id='requestee_type-$flagtype1_id']");
+
 # If fields are of the correct type above, we assume this is still true below.
 $sel->is_text_present_ok("SeleniumBugFlag2Test");
 $sel->is_element_present_ok("flag_type-$flagtype2_id");
 $sel->is_element_present_ok("requestee_type-$flagtype2_id");
 $sel->is_text_present_ok("SeleniumBugFlag3Test");
 $sel->is_element_present_ok("flag_type-$flagtype3_id");
-ok(!$sel->is_element_present("requestee_type-$flagtype3_id"), "SeleniumBugFlag3Test is not specifically requestable");
+ok(
+  !$sel->is_element_present("requestee_type-$flagtype3_id"),
+  "SeleniumBugFlag3Test is not specifically requestable"
+);
 
 # This is intentional to generate "flagmail". Some flags have a CC list
 # associated with them, some others don't. This is to catch crashes due to
@@ -232,19 +270,25 @@ edit_bug_and_return($sel, $bug1_id, $bug_summary);
 
 # We need to store the new flag IDs.
 
-$sel->is_text_present_ok("$config->{admin_user_username}: SeleniumBugFlag1Test");
+$sel->is_text_present_ok(
+  "$config->{admin_user_username}: SeleniumBugFlag1Test");
 my $flag1_1_id = $sel->get_attribute('//select[@title="bugflag1"]@id');
 $flag1_1_id =~ s/flag-//;
-$sel->is_text_present_ok("$config->{admin_user_username}: SeleniumBugFlag2Test");
+$sel->is_text_present_ok(
+  "$config->{admin_user_username}: SeleniumBugFlag2Test");
 my $flag2_1_id = $sel->get_attribute('//select[@title="bugflag2"]@id');
 $flag2_1_id =~ s/flag-//;
-$sel->is_text_present_ok("$config->{admin_user_username}: SeleniumBugFlag3Test");
+$sel->is_text_present_ok(
+  "$config->{admin_user_username}: SeleniumBugFlag3Test");
 my $flag3_1_id = $sel->get_attribute('//select[@title="bugflag3"]@id');
 $flag3_1_id =~ s/flag-//;
 
 $sel->is_text_present_ok("addl. SeleniumBugFlag1Test");
 $sel->is_text_present_ok("addl. SeleniumBugFlag2Test");
-ok(!$sel->is_text_present("addl. SeleniumBugFlag3Test"), "SeleniumBugFlag3Test is not multiplicable");
+ok(
+  !$sel->is_text_present("addl. SeleniumBugFlag3Test"),
+  "SeleniumBugFlag3Test is not multiplicable"
+);
 $sel->select_ok("flag_type-$flagtype1_id", "label=+");
 $sel->select_ok("flag_type-$flagtype2_id", "label=-");
 edit_bug_and_return($sel, $bug1_id, $bug_summary);
@@ -255,7 +299,8 @@ edit_bug_and_return($sel, $bug1_id, $bug_summary);
 $sel->select_ok("flag_type-$flagtype1_id", "label=?");
 $sel->type_ok("requestee_type-$flagtype1_id", $config->{admin_user_login});
 $sel->select_ok("flag_type-$flagtype2_id", "label=?");
-$sel->type_ok("requestee_type-$flagtype2_id", $config->{unprivileged_user_login});
+$sel->type_ok("requestee_type-$flagtype2_id",
+  $config->{unprivileged_user_login});
 $sel->click_ok("commit");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Flag Requestee Not Authorized");
@@ -277,12 +322,15 @@ edit_bug_and_return($sel, $bug1_id, $bug_summary);
 $sel->click_ok("link=Add an attachment");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Create New Attachment for Bug #$bug1_id");
-$sel->type_ok("data", $config->{attachment_file});
+$sel->type_ok("data",        $config->{attachment_file});
 $sel->type_ok("description", "patch, v1");
 $sel->check_ok("ispatch");
 $sel->is_text_present_ok("SeleniumAttachmentFlag1Test");
 $sel->is_text_present_ok("SeleniumAttachmentFlag2Test");
-ok(!$sel->is_text_present("SeleniumAttachmentFlag3Test"), "Inactive SeleniumAttachmentFlag3Test flag type not displayed");
+ok(
+  !$sel->is_text_present("SeleniumAttachmentFlag3Test"),
+  "Inactive SeleniumAttachmentFlag3Test flag type not displayed"
+);
 
 # Let's generate some "flagmail", first with no requestee.
 
@@ -302,17 +350,20 @@ my $attachment1_id = $1;
 $sel->click_ok("link=Create Another Attachment to Bug $bug1_id");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Create New Attachment for Bug #$bug1_id");
-$sel->type_ok("data", $config->{attachment_file});
+$sel->type_ok("data",        $config->{attachment_file});
 $sel->type_ok("description", "patch, v2");
 $sel->check_ok("ispatch");
+
 # Mark the previous attachment as obsolete.
 $sel->check_ok($attachment1_id);
 $sel->select_ok("flag_type-$aflagtype1_id", "label=?");
 $sel->type_ok("requestee_type-$aflagtype1_id", $config->{admin_user_login});
 $sel->select_ok("flag_type-$aflagtype2_id", "label=?");
+
 # The requestee is not in the Master group, and so he cannot view the bug.
 # He must be silently skipped from the requestee field.
-$sel->type_ok("requestee_type-$aflagtype2_id", $config->{unprivileged_user_login});
+$sel->type_ok("requestee_type-$aflagtype2_id",
+  $config->{unprivileged_user_login});
 $sel->type_ok("comment", "second patch, with requestee");
 edit_bug($sel, $bug1_id, $bug_summary, {id => "create"});
 $alink = $sel->get_attribute('//a[@title="patch, v2"]@href');
@@ -324,10 +375,10 @@ my $attachment2_id = $1;
 $sel->click_ok("link=Create Another Attachment to Bug $bug1_id");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Create New Attachment for Bug #$bug1_id");
-$sel->type_ok("data", $config->{attachment_file});
+$sel->type_ok("data",        $config->{attachment_file});
 $sel->type_ok("description", "patch, v3");
 $sel->click_ok("list");
-$sel->select_ok("contenttypeselection", "label=plain text (text/plain)");
+$sel->select_ok("contenttypeselection",     "label=plain text (text/plain)");
 $sel->select_ok("flag_type-$aflagtype1_id", "label=+");
 $sel->type_ok("comment", "one +, the other one blank");
 edit_bug($sel, $bug1_id, $bug_summary, {id => "create"});
@@ -340,9 +391,14 @@ my $attachment3_id = $1;
 $sel->click_ok("link=bug $bug1_id");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_like(qr/^$bug1_id /);
-$sel->is_text_present_ok("$config->{admin_user_username}: SeleniumAttachmentFlag1Test? ($config->{admin_user_username})");
-$sel->is_text_present_ok("$config->{admin_user_username}: SeleniumAttachmentFlag2Test?");
-$sel->is_text_present_ok("$config->{admin_user_username}: SeleniumAttachmentFlag1Test+");
+$sel->is_text_present_ok(
+  "$config->{admin_user_username}: SeleniumAttachmentFlag1Test? ($config->{admin_user_username})"
+);
+$sel->is_text_present_ok(
+  "$config->{admin_user_username}: SeleniumAttachmentFlag2Test?");
+$sel->is_text_present_ok(
+  "$config->{admin_user_username}: SeleniumAttachmentFlag1Test+");
+
 # We marked the first attachment as obsolete, so it should have no flag on it.
 $sel->is_text_present_ok("no flags");
 
@@ -356,6 +412,7 @@ logout($sel);
 
 log_in($sel, $config, 'unprivileged');
 go_to_bug($sel, $bug1_id);
+
 # No privs are required to clear this flag.
 $sel->select_ok("flag-$flag3_1_id", "value=X");
 edit_bug_and_return($sel, $bug1_id, $bug_summary);
@@ -364,7 +421,8 @@ edit_bug_and_return($sel, $bug1_id, $bug_summary);
 # should be displayed besides the currently set "+".
 
 my @flag_states = $sel->get_select_options("flag-$flag2_1_id");
-ok(scalar(@flag_states) == 1 && $flag_states[0] eq '+', "Single flag state '+' available");
+ok(scalar(@flag_states) == 1 && $flag_states[0] eq '+',
+  "Single flag state '+' available");
 
 # Powerless users cannot set the flag to +, but setting it to ? is allowed.
 
@@ -378,7 +436,7 @@ $sel->click_ok("//a[\@href='attachment.cgi?id=$attachment2_id&action=edit']");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_like(qr/Attachment $attachment2_id Details for Bug $bug1_id/);
 ok($sel->is_element_present('//select[@title="attachmentflag2" and @disabled]'),
-   "Attachment flags are not editable by a powerless user");
+  "Attachment flags are not editable by a powerless user");
 
 # Add an attachment and set flags on it.
 
@@ -388,21 +446,23 @@ $sel->title_like(qr/^$bug1_id/);
 $sel->click_ok("link=Add an attachment");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Create New Attachment for Bug #$bug1_id");
-$sel->type_ok("data", $config->{attachment_file});
+$sel->type_ok("data",        $config->{attachment_file});
 $sel->type_ok("description", "patch, v4");
-$sel->value_is("ispatch", "off");
+$sel->value_is("ispatch",    "off");
 $sel->value_is("autodetect", "on");
 
 # canconfirm/editbugs privs are required to edit this flag.
 
-ok(!$sel->is_element_present("flag_type-$aflagtype1_id"), "Flag type 'SeleniumAttachmentFlag1Test' not displayed to powerless users");
+ok(!$sel->is_element_present("flag_type-$aflagtype1_id"),
+  "Flag type 'SeleniumAttachmentFlag1Test' not displayed to powerless users");
 
 # No privs are required to edit this flag.
 
 $sel->select_ok("flag_type-$aflagtype2_id", "label=+");
 $sel->type_ok("comment", "granting again");
 edit_bug_and_return($sel, $bug1_id, $bug_summary, {id => "create"});
-$sel->is_text_present_ok("$config->{unprivileged_user_username}: SeleniumAttachmentFlag2Test+");
+$sel->is_text_present_ok(
+  "$config->{unprivileged_user_username}: SeleniumAttachmentFlag2Test+");
 logout($sel);
 
 # Final tests as an admin. He has editbugs privs, so he can edit
@@ -423,19 +483,26 @@ $sel->click_ok("link=Flags");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Administer Flag Types");
 
-foreach my $flagtype ([$flagtype1_id, "SeleniumBugFlag1Test"], [$flagtype2_id, "SeleniumBugFlag2Test"],
-                      [$flagtype3_id, "SeleniumBugFlag3Test"], [$aflagtype1_id, "SeleniumAttachmentFlag1Test"],
-                      [$aflagtype2_id, "SeleniumAttachmentFlag2Test"], [$aflagtype3_id, "SeleniumAttachmentFlag3Test"])
+foreach my $flagtype (
+  [$flagtype1_id,  "SeleniumBugFlag1Test"],
+  [$flagtype2_id,  "SeleniumBugFlag2Test"],
+  [$flagtype3_id,  "SeleniumBugFlag3Test"],
+  [$aflagtype1_id, "SeleniumAttachmentFlag1Test"],
+  [$aflagtype2_id, "SeleniumAttachmentFlag2Test"],
+  [$aflagtype3_id, "SeleniumAttachmentFlag3Test"]
+  )
 {
-    my $flag_id = $flagtype->[0];
-    my $flag_name = $flagtype->[1];
-    $sel->click_ok("//a[\@href='editflagtypes.cgi?action=confirmdelete&id=$flag_id']");
-    $sel->wait_for_page_to_load_ok(WAIT_TIME);
-    $sel->title_is("Confirm Deletion of Flag Type '$flag_name'");
-    $sel->click_ok("link=Yes, delete");
-    $sel->wait_for_page_to_load_ok(WAIT_TIME);
-    $sel->title_is("Flag Type '$flag_name' Deleted");
-    my $msg = trim($sel->get_text("message"));
-    ok($msg eq "The flag type $flag_name has been deleted.", "Flag type $flag_name deleted");
+  my $flag_id   = $flagtype->[0];
+  my $flag_name = $flagtype->[1];
+  $sel->click_ok(
+    "//a[\@href='editflagtypes.cgi?action=confirmdelete&id=$flag_id']");
+  $sel->wait_for_page_to_load_ok(WAIT_TIME);
+  $sel->title_is("Confirm Deletion of Flag Type '$flag_name'");
+  $sel->click_ok("link=Yes, delete");
+  $sel->wait_for_page_to_load_ok(WAIT_TIME);
+  $sel->title_is("Flag Type '$flag_name' Deleted");
+  my $msg = trim($sel->get_text("message"));
+  ok($msg eq "The flag type $flag_name has been deleted.",
+    "Flag type $flag_name deleted");
 }
 logout($sel);
index a836f6908519e9085a14e6553e4f4f73f0dbe5a6..cc8ef038d0233378f7b359aef7034de38dfe1c5d 100644 (file)
@@ -35,7 +35,7 @@ $sel->title_is("Administer Flag Types");
 $sel->click_ok("link=Create Flag Type for Bugs");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Create Flag Type for Bugs");
-$sel->type_ok("name", "selenium");
+$sel->type_ok("name",        "selenium");
 $sel->type_ok("description", "Available in TestProduct and Another Product/c1");
 $sel->add_selection_ok("inclusion_to_remove", "label=__Any__:__Any__");
 $sel->click_ok("categoryAction-removeInclusion");
@@ -46,7 +46,7 @@ $sel->selected_label_is("component", "__Any__");
 $sel->click_ok("categoryAction-include");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Create Flag Type for Bugs");
-$sel->select_ok("product", "label=Another Product");
+$sel->select_ok("product",   "label=Another Product");
 $sel->select_ok("component", "label=c1");
 $sel->click_ok("categoryAction-include");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
@@ -58,11 +58,11 @@ $sel->title_is("Create Flag Type for Bugs");
 # catch it, not this one (which will be unique for now, so no worry to find it).
 
 $sel->type_ok("sortkey", 100);
-$sel->value_is("is_active", "on");
+$sel->value_is("is_active",      "on");
 $sel->value_is("is_requestable", "on");
 $sel->click_ok("is_multiplicable");
 $sel->value_is("is_multiplicable", "off");
-$sel->select_ok("grant_group", "label=editbugs");
+$sel->select_ok("grant_group",   "label=editbugs");
 $sel->select_ok("request_group", "label=canconfirm");
 $sel->click_ok("save");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
@@ -84,7 +84,7 @@ $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->click_ok("link=Create Flag Type For Attachments");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Create Flag Type for Attachments");
-$sel->type_ok("name", "selenium_review");
+$sel->type_ok("name",        "selenium_review");
 $sel->type_ok("description", "Review flag used by Selenium");
 $sel->add_selection_ok("inclusion_to_remove", "label=__Any__:__Any__");
 $sel->click_ok("categoryAction-removeInclusion");
@@ -95,11 +95,11 @@ $sel->click_ok("categoryAction-include");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Create Flag Type for Attachments");
 $sel->type_ok("sortkey", 100);
-$sel->value_is("is_active", "on");
+$sel->value_is("is_active",      "on");
 $sel->value_is("is_requestable", "on");
 $sel->click_ok("is_multiplicable");
 $sel->value_is("is_multiplicable", "off");
-$sel->selected_label_is("grant_group", "(no group)");
+$sel->selected_label_is("grant_group",   "(no group)");
 $sel->selected_label_is("request_group", "(no group)");
 $sel->click_ok("save");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
@@ -121,17 +121,17 @@ $sel->go_back_ok();
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->click_ok("link=Create Flag Type For Attachments");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
-$sel->type_ok("name", "selenium_review");
+$sel->type_ok("name",        "selenium_review");
 $sel->type_ok("description", "Another review flag used by Selenium");
 $sel->select_ok("product", "label=Another Product");
 $sel->click_ok("categoryAction-include");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Create Flag Type for Attachments");
 $sel->type_ok("sortkey", 50);
-$sel->value_is("is_active", "on");
-$sel->value_is("is_requestable", "on");
+$sel->value_is("is_active",        "on");
+$sel->value_is("is_requestable",   "on");
 $sel->value_is("is_multiplicable", "on");
-$sel->select_ok("grant_group", "label=editbugs");
+$sel->select_ok("grant_group",   "label=editbugs");
 $sel->select_ok("request_group", "label=canconfirm");
 $sel->click_ok("save");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
@@ -151,18 +151,22 @@ file_bug_in_product($sel, 'TestProduct');
 $sel->select_ok("flag_type-$flagtype1_id", "label=+");
 my $bug_summary = "The selenium flag should be kept on product change";
 $sel->type_ok("short_desc", $bug_summary);
-$sel->type_ok("comment", "pom");
+$sel->type_ok("comment",    "pom");
 $sel->click_ok('//input[@value="Add an attachment"]');
-$sel->type_ok("data", $config->{attachment_file});
+$sel->type_ok("data",        $config->{attachment_file});
 $sel->type_ok("description", "small patch");
 $sel->click_ok("ispatch");
 $sel->value_is("ispatch", "on");
-ok(!$sel->is_element_present("flag_type-$aflagtype1_id"), "Flag type $aflagtype1_id not available in TestProduct");
+ok(
+  !$sel->is_element_present("flag_type-$aflagtype1_id"),
+  "Flag type $aflagtype1_id not available in TestProduct"
+);
 $sel->select_ok("flag_type-$aflagtype2_id", "label=-");
 my $bug1_id = create_bug($sel, $bug_summary);
 
 $sel->is_text_present_ok("$config->{admin_user_username}: selenium");
-my $flag1_id = $sel->get_attribute('//select[@title="Available in TestProduct and Another Product/c1"]@id');
+my $flag1_id = $sel->get_attribute(
+  '//select[@title="Available in TestProduct and Another Product/c1"]@id');
 $flag1_id =~ s/flag-//;
 $sel->selected_label_is("flag-$flag1_id", "+");
 $sel->is_text_present_ok("$config->{admin_user_username}: selenium_review-");
@@ -171,7 +175,8 @@ $sel->is_text_present_ok("$config->{admin_user_username}: selenium_review-");
 # Both the bug and attachment flags should survive.
 
 $sel->select_ok("product", "label=Another Product");
-$sel->type_ok("comment", "Moving to Another Product / c1. The flag should be preserved.");
+$sel->type_ok("comment",
+  "Moving to Another Product / c1. The flag should be preserved.");
 $sel->click_ok("commit");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Verify New Product Details...");
@@ -186,8 +191,12 @@ $sel->is_text_present_ok("$config->{admin_user_username}: selenium_review-");
 $sel->type_ok("comment", "Moving to c2. The selenium flag will be deleted.");
 $sel->select_ok("component", "label=c2");
 edit_bug_and_return($sel, $bug1_id, $bug_summary);
-ok(!$sel->is_element_present("flag-$flag1_id"), "The selenium bug flag didn't survive");
-ok(!$sel->is_element_present("flag_type-$flagtype1_id"), "The selenium flag type doesn't exist");
+ok(
+  !$sel->is_element_present("flag-$flag1_id"),
+  "The selenium bug flag didn't survive"
+);
+ok(!$sel->is_element_present("flag_type-$flagtype1_id"),
+  "The selenium flag type doesn't exist");
 $sel->is_text_present_ok("$config->{admin_user_username}: selenium_review-");
 
 # File a bug in 'Another Product / c2' and assign it
@@ -196,17 +205,20 @@ $sel->is_text_present_ok("$config->{admin_user_username}: selenium_review-");
 file_bug_in_product($sel, 'Another Product');
 $sel->select_ok("component", "label=c2");
 $sel->type_ok("assigned_to", $config->{unprivileged_user_login});
-ok(!$sel->is_editable("flag_type-$flagtype1_id"), "The selenium bug flag type is displayed but not selectable");
+ok(!$sel->is_editable("flag_type-$flagtype1_id"),
+  "The selenium bug flag type is displayed but not selectable");
 $sel->select_ok("component", "label=c1");
-$sel->is_editable_ok("flag_type-$flagtype1_id", "The selenium bug flag type is not selectable");
+$sel->is_editable_ok("flag_type-$flagtype1_id",
+  "The selenium bug flag type is not selectable");
 $sel->select_ok("flag_type-$flagtype1_id", "label=?");
 my $bug_summary2 = "Create a new selenium flag for c2";
 $sel->type_ok("short_desc", $bug_summary2);
-$sel->type_ok("comment", ".");
+$sel->type_ok("comment",    ".");
 my $bug2_id = create_bug($sel, $bug_summary2);
 
 $sel->is_text_present_ok("$config->{admin_user_username}: selenium");
-my $flag2_id = $sel->get_attribute('//select[@title="Available in TestProduct and Another Product/c1"]@id');
+my $flag2_id = $sel->get_attribute(
+  '//select[@title="Available in TestProduct and Another Product/c1"]@id');
 $flag2_id =~ s/flag-//;
 $sel->selected_label_is("flag-$flag2_id", '?');
 
@@ -220,22 +232,22 @@ $sel->title_is("Administer Flag Types");
 $sel->click_ok("link=Create Flag Type for Bugs");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Create Flag Type for Bugs");
-$sel->type_ok("name", "selenium");
+$sel->type_ok("name",        "selenium");
 $sel->type_ok("description", "Another flag with the selenium name");
 $sel->add_selection_ok("inclusion_to_remove", "label=__Any__:__Any__");
 $sel->click_ok("categoryAction-removeInclusion");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Create Flag Type for Bugs");
-$sel->select_ok("product", "label=Another Product");
+$sel->select_ok("product",   "label=Another Product");
 $sel->select_ok("component", "label=c2");
 $sel->click_ok("categoryAction-include");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Create Flag Type for Bugs");
 $sel->type_ok("sortkey", 50);
-$sel->value_is("is_active", "on");
-$sel->value_is("is_requestable", "on");
+$sel->value_is("is_active",        "on");
+$sel->value_is("is_requestable",   "on");
 $sel->value_is("is_multiplicable", "on");
-$sel->selected_label_is("grant_group", "(no group)");
+$sel->selected_label_is("grant_group",   "(no group)");
 $sel->selected_label_is("request_group", "(no group)");
 $sel->click_ok("save");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
@@ -253,11 +265,13 @@ my $flagtype2_id = $1;
 
 go_to_bug($sel, $bug2_id);
 $sel->select_ok("component", "label=c2");
-ok(!$sel->is_checked("set_default_assignee"), "Moving the bug into another component must not change the assignee");
+ok(!$sel->is_checked("set_default_assignee"),
+  "Moving the bug into another component must not change the assignee");
 $sel->type_ok("comment", "The selenium flag should be preserved.");
 edit_bug_and_return($sel, $bug2_id, $bug_summary2);
 $sel->selected_label_is("flag-$flag2_id", '?');
-ok(!$sel->is_element_present("flag_type-$flagtype1_id"), "Flag type not available in component c2");
+ok(!$sel->is_element_present("flag_type-$flagtype1_id"),
+  "Flag type not available in component c2");
 $sel->is_element_present_ok("flag_type-$flagtype2_id");
 logout($sel);
 
@@ -273,14 +287,19 @@ $sel->selected_label_is("flag-$flag2_id", "+");
 # as the flag setter is not in the editbugs group.
 
 $sel->select_ok("product", "label=TestProduct");
-$sel->type_ok("comment", "selenium flag will be lost. I don't have editbugs privs.");
+$sel->type_ok("comment",
+  "selenium flag will be lost. I don't have editbugs privs.");
 $sel->click_ok("commit");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Verify New Product Details...");
 edit_bug_and_return($sel, $bug2_id, $bug_summary2, {id => "change_product"});
 ok(!$sel->is_element_present("flag-$flag2_id"), "Flag $flag2_id deleted");
-ok(!$sel->is_element_present("flag_type-$flagtype1_id"), "Flag type 'selenium' not displayed to powerless users");
-ok(!$sel->is_element_present("flag_type-$flagtype2_id"), "Flag type not available in component c1");
+ok(
+  !$sel->is_element_present("flag_type-$flagtype1_id"),
+  "Flag type 'selenium' not displayed to powerless users"
+);
+ok(!$sel->is_element_present("flag_type-$flagtype2_id"),
+  "Flag type not available in component c1");
 logout($sel);
 
 # Time to delete created flag types.
@@ -291,18 +310,24 @@ $sel->click_ok("link=Flags");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Administer Flag Types");
 
-foreach my $flagtype ([$flagtype1_id, "selenium"], [$flagtype2_id, "selenium"],
-                      [$aflagtype1_id, "selenium_review"], [$aflagtype2_id, "selenium_review"])
+foreach my $flagtype (
+  [$flagtype1_id,  "selenium"],
+  [$flagtype2_id,  "selenium"],
+  [$aflagtype1_id, "selenium_review"],
+  [$aflagtype2_id, "selenium_review"]
+  )
 {
-    my $flag_id = $flagtype->[0];
-    my $flag_name = $flagtype->[1];
-    $sel->click_ok("//a[\@href='editflagtypes.cgi?action=confirmdelete&id=$flag_id']");
-    $sel->wait_for_page_to_load_ok(WAIT_TIME);
-    $sel->title_is("Confirm Deletion of Flag Type '$flag_name'");
-    $sel->click_ok("link=Yes, delete");
-    $sel->wait_for_page_to_load_ok(WAIT_TIME);
-    $sel->title_is("Flag Type '$flag_name' Deleted");
-    my $msg = trim($sel->get_text("message"));
-    ok($msg eq "The flag type $flag_name has been deleted.", "Flag type $flag_name deleted");
+  my $flag_id   = $flagtype->[0];
+  my $flag_name = $flagtype->[1];
+  $sel->click_ok(
+    "//a[\@href='editflagtypes.cgi?action=confirmdelete&id=$flag_id']");
+  $sel->wait_for_page_to_load_ok(WAIT_TIME);
+  $sel->title_is("Confirm Deletion of Flag Type '$flag_name'");
+  $sel->click_ok("link=Yes, delete");
+  $sel->wait_for_page_to_load_ok(WAIT_TIME);
+  $sel->title_is("Flag Type '$flag_name' Deleted");
+  my $msg = trim($sel->get_text("message"));
+  ok($msg eq "The flag type $flag_name has been deleted.",
+    "Flag type $flag_name deleted");
 }
 logout($sel);
index e21e60b0e6cc53c830c6eea8ef3b1a77005816c8..18f53313d61978000408d69b873348ffaa2075d3 100644 (file)
@@ -45,7 +45,7 @@ $sel->wait_for_page_to_load(WAIT_TIME);
 $sel->title_is("Edit Group Controls for TestProduct");
 $sel->is_text_present_ok("Selenium-test");
 $sel->select_ok("membercontrol_${group_id}", "label=Shown");
-$sel->select_ok("othercontrol_${group_id}", "label=Mandatory");
+$sel->select_ok("othercontrol_${group_id}",  "label=Mandatory");
 $sel->click_ok("submit");
 $sel->wait_for_page_to_load(WAIT_TIME);
 $sel->title_is("Update group access controls for TestProduct");
@@ -54,15 +54,15 @@ $sel->title_is("Update group access controls for TestProduct");
 
 file_bug_in_product($sel, "TestProduct");
 $sel->is_text_present_ok("Test group for Selenium");
-$sel->value_is("group_${group_id}", "off"); # Must be OFF (else that's a bug)
+$sel->value_is("group_${group_id}", "off");    # Must be OFF (else that's a bug)
 $sel->check_ok("group_${group_id}");
 my $bug_summary = "bug restricted to the Selenium group";
 $sel->type_ok("short_desc", $bug_summary);
-$sel->type_ok("comment", "should be invisible");
+$sel->type_ok("comment",    "should be invisible");
 $sel->selected_label_is("component", "TestComponent");
 my $bug1_id = create_bug($sel, $bug_summary);
 $sel->is_text_present_ok("Test group for Selenium");
-$sel->value_is("group_${group_id}", "on"); # Must be ON
+$sel->value_is("group_${group_id}", "on");     # Must be ON
 
 # Look for this new bug and add it to the new "Selenium bugs" saved search.
 
@@ -88,7 +88,8 @@ $sel->click_ok("link=Selenium bugs");
 $sel->wait_for_page_to_load(WAIT_TIME);
 $sel->title_is("Bug List: Selenium bugs");
 $sel->is_text_present_ok("One bug found");
-$sel->is_element_present_ok("b$bug1_id", undef, "Bug $bug1_id restricted to the bug group");
+$sel->is_element_present_ok("b$bug1_id", undef,
+  "Bug $bug1_id restricted to the bug group");
 
 # No longer use Selenium-test as a bug group.
 
@@ -112,9 +113,12 @@ file_bug_in_product($sel, "TestProduct");
 $sel->selected_label_is("component", "TestComponent");
 my $bug_summary2 = "bug restricted to the Selenium group";
 $sel->type_ok("short_desc", $bug_summary2);
-$sel->type_ok("comment", "should be *visible* when created (the group is disabled)");
-ok(!$sel->is_text_present("Test group for Selenium"), "Selenium-test group unavailable");
-ok(!$sel->is_element_present("group_${group_id}"), "Selenium-test checkbox not present");
+$sel->type_ok("comment",
+  "should be *visible* when created (the group is disabled)");
+ok(!$sel->is_text_present("Test group for Selenium"),
+  "Selenium-test group unavailable");
+ok(!$sel->is_element_present("group_${group_id}"),
+  "Selenium-test checkbox not present");
 my $bug2_id = create_bug($sel, $bug_summary2);
 
 # Make sure the new bug doesn't appear in the "Selenium bugs" saved search.
@@ -123,8 +127,12 @@ $sel->click_ok("link=Selenium bugs");
 $sel->wait_for_page_to_load(WAIT_TIME);
 $sel->title_is("Bug List: Selenium bugs");
 $sel->is_text_present_ok("One bug found");
-$sel->is_element_present_ok("b$bug1_id", undef, "Bug $bug1_id restricted to the bug group");
-ok(!$sel->is_element_present("b$bug2_id"), "Bug $bug2_id NOT restricted to the bug group");
+$sel->is_element_present_ok("b$bug1_id", undef,
+  "Bug $bug1_id restricted to the bug group");
+ok(
+  !$sel->is_element_present("b$bug2_id"),
+  "Bug $bug2_id NOT restricted to the bug group"
+);
 
 # Re-enable the Selenium-test group as bug group. This doesn't affect
 # already filed bugs as this group is not mandatory.
@@ -149,8 +157,12 @@ $sel->click_ok("link=Selenium bugs");
 $sel->wait_for_page_to_load(WAIT_TIME);
 $sel->title_is("Bug List: Selenium bugs");
 $sel->is_text_present_ok("One bug found");
-$sel->is_element_present_ok("b$bug1_id", undef, "Bug $bug1_id restricted to the bug group");
-ok(!$sel->is_element_present("b$bug2_id"), "Bug $bug2_id NOT restricted to the bug group");
+$sel->is_element_present_ok("b$bug1_id", undef,
+  "Bug $bug1_id restricted to the bug group");
+ok(
+  !$sel->is_element_present("b$bug2_id"),
+  "Bug $bug2_id NOT restricted to the bug group"
+);
 
 # Make the Selenium-test group mandatory for TestProduct.
 
@@ -166,15 +178,19 @@ $sel->is_text_present_ok("this group is mandatory and will be added");
 $sel->click_ok("update");
 $sel->wait_for_page_to_load(WAIT_TIME);
 $sel->title_is("Update group access controls for TestProduct");
-$sel->is_text_present_ok('regexp:Adding bugs to group \'Selenium-test\' which is now mandatory for this product');
+$sel->is_text_present_ok(
+  'regexp:Adding bugs to group \'Selenium-test\' which is now mandatory for this product'
+);
 
 # All bugs being in TestProduct must now be restricted to the bug group.
 
 $sel->click_ok("link=Selenium bugs");
 $sel->wait_for_page_to_load(WAIT_TIME);
 $sel->title_is("Bug List: Selenium bugs");
-$sel->is_element_present_ok("b$bug1_id", undef, "Bug $bug1_id restricted to the bug group");
-$sel->is_element_present_ok("b$bug2_id", undef, "Bug $bug2_id restricted to the bug group");
+$sel->is_element_present_ok("b$bug1_id", undef,
+  "Bug $bug1_id restricted to the bug group");
+$sel->is_element_present_ok("b$bug2_id", undef,
+  "Bug $bug2_id restricted to the bug group");
 
 # File a new bug, which must automatically be restricted to the bug group.
 
@@ -182,9 +198,13 @@ file_bug_in_product($sel, "TestProduct");
 $sel->selected_label_is("component", "TestComponent");
 my $bug_summary3 = "Selenium-test group mandatory";
 $sel->type_ok("short_desc", $bug_summary3);
-$sel->type_ok("comment", "group enabled");
-ok(!$sel->is_text_present("Test group for Selenium"), "Selenium-test group not available");
-ok(!$sel->is_element_present("group_${group_id}"), "Selenium-test checkbox not present (mandatory group)");
+$sel->type_ok("comment",    "group enabled");
+ok(!$sel->is_text_present("Test group for Selenium"),
+  "Selenium-test group not available");
+ok(
+  !$sel->is_element_present("group_${group_id}"),
+  "Selenium-test checkbox not present (mandatory group)"
+);
 my $bug3_id = create_bug($sel, $bug_summary3);
 
 # Make sure all three bugs are listed as being restricted to the bug group.
@@ -192,9 +212,12 @@ my $bug3_id = create_bug($sel, $bug_summary3);
 $sel->click_ok("link=Selenium bugs");
 $sel->wait_for_page_to_load(WAIT_TIME);
 $sel->title_is("Bug List: Selenium bugs");
-$sel->is_element_present_ok("b$bug1_id", undef, "Bug $bug1_id restricted to the bug group");
-$sel->is_element_present_ok("b$bug2_id", undef, "Bug $bug2_id restricted to the bug group");
-$sel->is_element_present_ok("b$bug3_id", undef, "Bug $bug3_id restricted to the bug group");
+$sel->is_element_present_ok("b$bug1_id", undef,
+  "Bug $bug1_id restricted to the bug group");
+$sel->is_element_present_ok("b$bug2_id", undef,
+  "Bug $bug2_id restricted to the bug group");
+$sel->is_element_present_ok("b$bug3_id", undef,
+  "Bug $bug3_id restricted to the bug group");
 
 # Turn off the Selenium-test group again.
 
@@ -218,9 +241,11 @@ file_bug_in_product($sel, "TestProduct");
 $sel->selected_label_is("component", "TestComponent");
 my $bug_summary4 = "bug restricted to the Selenium-test group";
 $sel->type_ok("short_desc", $bug_summary4);
-$sel->type_ok("comment", "group disabled");
-ok(!$sel->is_text_present("Test group for Selenium"), "Selenium-test group not available");
-ok(!$sel->is_element_present("group_${group_id}"), "Selenium-test checkbox not present");
+$sel->type_ok("comment",    "group disabled");
+ok(!$sel->is_text_present("Test group for Selenium"),
+  "Selenium-test group not available");
+ok(!$sel->is_element_present("group_${group_id}"),
+  "Selenium-test checkbox not present");
 my $bug4_id = create_bug($sel, $bug_summary4);
 
 # The last bug must not be in the list.
@@ -228,10 +253,16 @@ my $bug4_id = create_bug($sel, $bug_summary4);
 $sel->click_ok("link=Selenium bugs");
 $sel->wait_for_page_to_load(WAIT_TIME);
 $sel->title_is("Bug List: Selenium bugs");
-$sel->is_element_present_ok("b$bug1_id", undef, "Bug $bug1_id restricted to the bug group");
-$sel->is_element_present_ok("b$bug2_id", undef, "Bug $bug2_id restricted to the bug group");
-$sel->is_element_present_ok("b$bug3_id", undef, "Bug $bug3_id restricted to the bug group");
-ok(!$sel->is_element_present("b$bug4_id"), "Bug $bug4_id NOT restricted to the bug group");
+$sel->is_element_present_ok("b$bug1_id", undef,
+  "Bug $bug1_id restricted to the bug group");
+$sel->is_element_present_ok("b$bug2_id", undef,
+  "Bug $bug2_id restricted to the bug group");
+$sel->is_element_present_ok("b$bug3_id", undef,
+  "Bug $bug3_id restricted to the bug group");
+ok(
+  !$sel->is_element_present("b$bug4_id"),
+  "Bug $bug4_id NOT restricted to the bug group"
+);
 
 # Re-enable the mandatory group. All bugs should be restricted to this bug group automatically.
 
@@ -254,10 +285,14 @@ $sel->is_text_present_ok("The group will now be used for bugs");
 $sel->click_ok("link=Selenium bugs");
 $sel->wait_for_page_to_load(WAIT_TIME);
 $sel->title_is("Bug List: Selenium bugs");
-$sel->is_element_present_ok("b$bug1_id", undef, "Bug $bug1_id restricted to the bug group");
-$sel->is_element_present_ok("b$bug2_id", undef, "Bug $bug2_id restricted to the bug group");
-$sel->is_element_present_ok("b$bug3_id", undef, "Bug $bug3_id restricted to the bug group");
-$sel->is_element_present_ok("b$bug4_id", undef, "Bug $bug4_id restricted to the bug group");
+$sel->is_element_present_ok("b$bug1_id", undef,
+  "Bug $bug1_id restricted to the bug group");
+$sel->is_element_present_ok("b$bug2_id", undef,
+  "Bug $bug2_id restricted to the bug group");
+$sel->is_element_present_ok("b$bug3_id", undef,
+  "Bug $bug3_id restricted to the bug group");
+$sel->is_element_present_ok("b$bug4_id", undef,
+  "Bug $bug4_id restricted to the bug group");
 
 # Try to remove the Selenium-test group from TestProduct, but DON'T do it!
 # We just want to make sure a warning is displayed about this removal.
@@ -269,17 +304,24 @@ $sel->wait_for_page_to_load(WAIT_TIME);
 $sel->title_is("Edit Group Controls for TestProduct");
 $sel->is_text_present_ok("Selenium-test");
 $sel->select_ok("membercontrol_${group_id}", "NA");
-$sel->select_ok("othercontrol_${group_id}", "NA");
+$sel->select_ok("othercontrol_${group_id}",  "NA");
 $sel->click_ok("submit");
 $sel->wait_for_page_to_load(WAIT_TIME);
 $sel->title_is("Confirm Group Control Change for product 'TestProduct'");
-$sel->is_text_present_ok("this group is no longer applicable and will be removed");
+$sel->is_text_present_ok(
+  "this group is no longer applicable and will be removed");
 
 # Make sure that renaming a group which is used as a special group
 # (such as insidergroup or querysharegroup) is correctly propagated
 # and that you cannot delete this group.
 
-set_parameters($sel, { "Group Security" => {"querysharegroup" => {type => "select", value => "Selenium-test"}} });
+set_parameters(
+  $sel,
+  {
+    "Group Security" =>
+      {"querysharegroup" => {type => "select", value => "Selenium-test"}}
+  }
+);
 
 go_to_admin($sel);
 $sel->click_ok("link=Groups");
@@ -305,19 +347,27 @@ $sel->value_is("querysharegroup", "X-Selenium-Y");
 
 # There is no UI to delete this group, so we have to type the URL directly.
 
-$sel->open_ok("/$config->{bugzilla_installation}/editgroups.cgi?action=del&group=$group_id");
+$sel->open_ok(
+  "/$config->{bugzilla_installation}/editgroups.cgi?action=del&group=$group_id");
 $sel->title_is("Group not deletable");
-$sel->is_text_present_ok("The group 'X-Selenium-Y' is used by the 'querysharegroup' parameter");
+$sel->is_text_present_ok(
+  "The group 'X-Selenium-Y' is used by the 'querysharegroup' parameter");
 
-$sel->open_ok("/$config->{bugzilla_installation}/editgroups.cgi?action=delete&group=$group_id");
+$sel->open_ok(
+  "/$config->{bugzilla_installation}/editgroups.cgi?action=delete&group=$group_id"
+);
 $sel->title_is("Suspicious Action");
-$sel->is_text_present_ok("you have no valid token for the delete_group action while processing the 'editgroups.cgi' script");
+$sel->is_text_present_ok(
+  "you have no valid token for the delete_group action while processing the 'editgroups.cgi' script"
+);
 $sel->click_ok("confirm");
 $sel->wait_for_page_to_load(WAIT_TIME);
 $sel->title_is("Group not deletable");
-$sel->is_text_present_ok("The group 'X-Selenium-Y' is used by the 'querysharegroup' parameter");
+$sel->is_text_present_ok(
+  "The group 'X-Selenium-Y' is used by the 'querysharegroup' parameter");
 
-set_parameters($sel, { "Group Security" => {"querysharegroup" => {type => "select", value => ""}} });
+set_parameters($sel,
+  {"Group Security" => {"querysharegroup" => {type => "select", value => ""}}});
 
 # Revert the group name change to not mess with the subsequent tests
 # which expect to see 'Selenium-test'.
@@ -355,7 +405,8 @@ $sel->click_ok("delete");
 $sel->wait_for_page_to_load(WAIT_TIME);
 $sel->title_is("Cannot Delete Group");
 my $error_msg = trim($sel->get_text("error_msg"));
-ok($error_msg =~ /^The Selenium-test group cannot be deleted/, "Group is in use - not deletable");
+ok($error_msg =~ /^The Selenium-test group cannot be deleted/,
+  "Group is in use - not deletable");
 $sel->go_back_ok();
 $sel->wait_for_page_to_load(WAIT_TIME);
 $sel->check("removebugs");
index e3c0a25b701b8422d65c2014f6f8950032fcd1bf..06a318b1d7b9ca30f4618812a7151ebae1361fea 100644 (file)
@@ -28,18 +28,18 @@ $sel->title_is("Select keyword");
 
 # If keywords already exist, delete them to not disturb the test.
 
-my $page = $sel->get_body_text();
+my $page     = $sel->get_body_text();
 my @keywords = $page =~ m/(key-selenium-\w+)/gi;
 
 foreach my $keyword (@keywords) {
-    my $url = $sel->get_attribute("link=$keyword\@href");
-    $url =~ s/action=edit/action=del/;
-    $sel->click_ok("//a[\@href='$url']");
-    $sel->wait_for_page_to_load(WAIT_TIME);
-    $sel->title_is("Delete Keyword");
-    $sel->click_ok("delete");
-    $sel->wait_for_page_to_load(WAIT_TIME);
-    $sel->title_is("Keyword Deleted");
+  my $url = $sel->get_attribute("link=$keyword\@href");
+  $url =~ s/action=edit/action=del/;
+  $sel->click_ok("//a[\@href='$url']");
+  $sel->wait_for_page_to_load(WAIT_TIME);
+  $sel->title_is("Delete Keyword");
+  $sel->click_ok("delete");
+  $sel->wait_for_page_to_load(WAIT_TIME);
+  $sel->title_is("Keyword Deleted");
 }
 
 # Now let's create our first keyword.
@@ -51,7 +51,7 @@ $sel->title_is("Select keyword");
 $sel->click_ok("link=Add a new keyword");
 $sel->wait_for_page_to_load(WAIT_TIME);
 $sel->title_is("Add keyword");
-$sel->type_ok("name", "key-selenium-kone");
+$sel->type_ok("name",        "key-selenium-kone");
 $sel->type_ok("description", "Hopefully an ice cream");
 $sel->click_ok("create");
 $sel->wait_for_page_to_load(WAIT_TIME);
@@ -62,19 +62,20 @@ $sel->title_is("New Keyword Created");
 $sel->click_ok("link=Add a new keyword");
 $sel->wait_for_page_to_load(WAIT_TIME);
 $sel->title_is("Add keyword");
-$sel->type_ok("name", "key-selenium-kone");
+$sel->type_ok("name",        "key-selenium-kone");
 $sel->type_ok("description", "FIX ME!");
 $sel->click_ok("create");
 $sel->wait_for_page_to_load(WAIT_TIME);
 $sel->title_is("Keyword Already Exists");
 my $error_msg = trim($sel->get_text("error_msg"));
-ok($error_msg eq 'A keyword with the name key-selenium-kone already exists.', 'Already created keyword');
+ok($error_msg eq 'A keyword with the name key-selenium-kone already exists.',
+  'Already created keyword');
 $sel->go_back_ok();
 $sel->wait_for_page_to_load(WAIT_TIME);
 
 # Create a second keyword.
 
-$sel->type_ok("name", "key-selenium-ktwo");
+$sel->type_ok("name",        "key-selenium-ktwo");
 $sel->type_ok("description", "FIX ME!");
 $sel->click_ok("create");
 $sel->wait_for_page_to_load(WAIT_TIME);
@@ -85,13 +86,14 @@ $sel->title_is("New Keyword Created");
 $sel->click_ok("link=key-selenium-ktwo");
 $sel->wait_for_page_to_load(WAIT_TIME);
 $sel->title_is("Edit keyword");
-$sel->type_ok("name", "key-selenium-kone");
+$sel->type_ok("name",        "key-selenium-kone");
 $sel->type_ok("description", "the second keyword");
 $sel->click_ok("update");
 $sel->wait_for_page_to_load(WAIT_TIME);
 $sel->title_is("Keyword Already Exists");
 $error_msg = trim($sel->get_text("error_msg"));
-ok($error_msg eq 'A keyword with the name key-selenium-kone already exists.', 'Already created keyword');
+ok($error_msg eq 'A keyword with the name key-selenium-kone already exists.',
+  'Already created keyword');
 $sel->go_back_ok();
 $sel->wait_for_page_to_load(WAIT_TIME);
 $sel->title_is("Edit keyword");
@@ -107,7 +109,7 @@ $sel->select_ok("component", "TestComponent");
 $sel->type_ok("keywords", "key-selenium-kone");
 my $bug_summary = "It's a beautiful day";
 $sel->type_ok("short_desc", $bug_summary);
-$sel->type_ok("comment", "This bug is to test keywords");
+$sel->type_ok("comment",    "This bug is to test keywords");
 my $bug1_id = create_bug($sel, $bug_summary);
 
 file_bug_in_product($sel, "TestProduct");
@@ -115,7 +117,8 @@ $sel->select_ok("component", "TestComponent");
 $sel->type_ok("keywords", "key-selenium-kone, key-selenium-ktwo");
 my $bug_summary2 = "Radio gaga";
 $sel->type_ok("short_desc", $bug_summary2);
-$sel->type_ok("comment", "This bug is also to test keywords, like bug $bug1_id");
+$sel->type_ok("comment",
+  "This bug is also to test keywords, like bug $bug1_id");
 my $bug2_id = create_bug($sel, $bug_summary2);
 
 # Now make sure these bugs correctly appear in buglists.
@@ -134,6 +137,7 @@ $sel->wait_for_page_to_load(WAIT_TIME);
 $sel->title_is("Search for bugs");
 $sel->remove_all_selections("product");
 $sel->remove_all_selections("bug_status");
+
 # Try with a different case than the one in the DB.
 $sel->type_ok("keywords", "key-selenium-ktWO");
 $sel->click_ok("Search");
@@ -146,6 +150,7 @@ $sel->wait_for_page_to_load(WAIT_TIME);
 $sel->title_is("Search for bugs");
 $sel->remove_all_selections("product");
 $sel->remove_all_selections("bug_status");
+
 # Substrings also work for keywords.
 $sel->type_ok("keywords", "selenium");
 $sel->click_ok("Search");
index 4f6f589b6072f2b2c60535a17688bbba0d81366a..73c756855aa3c471373166ebe32fb9733e5abc4a 100644 (file)
@@ -28,8 +28,9 @@ go_to_home($sel, $config);
 $sel->click_ok("link=New");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Log in to Bugzilla");
+
 # The login and password are hardcoded here, because this account doesn't exist.
-$sel->type_ok("Bugzilla_login", 'guest@foo.com');
+$sel->type_ok("Bugzilla_login",    'guest@foo.com');
 $sel->type_ok("Bugzilla_password", 'foo-bar-baz');
 $sel->click_ok("log_in");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
index b4d0f983ccbcdae4304176fb45238396b2134acc..7e714e8af9a58a0e2918d20a2d128831e76f1ee6 100644 (file)
@@ -21,24 +21,30 @@ my ($sel, $config) = get_selenium();
 # 1st step: turn on usetargetmilestone and letsubmitterchoosemilestone.
 
 log_in($sel, $config, 'admin');
-set_parameters($sel, {'Bug Fields'          => {'usetargetmilestone-on'          => undef},
-                      'Bug Change Policies' => {'letsubmitterchoosemilestone-on' => undef},
-                     }
-              );
+set_parameters(
+  $sel,
+  {
+    'Bug Fields'          => {'usetargetmilestone-on'          => undef},
+    'Bug Change Policies' => {'letsubmitterchoosemilestone-on' => undef},
+  }
+);
 
 # 2nd step: Add the milestone "2.0" (with sortkey = 10) to the TestProduct product.
 
 edit_product($sel, "TestProduct");
-$sel->click_ok("link=Edit milestones:", undef, "Go to the Edit milestones page");
+$sel->click_ok("link=Edit milestones:", undef,
+  "Go to the Edit milestones page");
 $sel->wait_for_page_to_load(WAIT_TIME);
-$sel->title_is("Select milestone of product 'TestProduct'", "Display milestones");
+$sel->title_is("Select milestone of product 'TestProduct'",
+  "Display milestones");
 $sel->click_ok("link=Add", undef, "Go add a new milestone");
 $sel->wait_for_page_to_load(WAIT_TIME);
 $sel->title_is("Add Milestone to Product 'TestProduct'", "Enter new milestone");
 $sel->type_ok("milestone", "2.0", "Set its name to 2.0");
-$sel->type_ok("sortkey", "10", "Set its sortkey to 10");
+$sel->type_ok("sortkey",   "10",  "Set its sortkey to 10");
 $sel->click_ok("create", undef, "Submit data");
 $sel->wait_for_page_to_load(WAIT_TIME);
+
 # If the milestone already exists, that's not a big deal. So no special action
 # is required in this case.
 $sel->title_is("Milestone Created", "Milestone Created");
@@ -46,30 +52,38 @@ $sel->title_is("Milestone Created", "Milestone Created");
 # 3rd step: file a new bug, leaving the milestone alone (should fall back to the default one).
 
 file_bug_in_product($sel, "TestProduct");
-$sel->selected_label_is("component", "TestComponent", "Component already selected (no other component defined)");
-$sel->selected_label_is("target_milestone", "---", "Default milestone selected");
-$sel->selected_label_is("version", "unspecified", "Version already selected (no other version defined)");
+$sel->selected_label_is("component", "TestComponent",
+  "Component already selected (no other component defined)");
+$sel->selected_label_is("target_milestone", "---",
+  "Default milestone selected");
+$sel->selected_label_is("version", "unspecified",
+  "Version already selected (no other version defined)");
 my $bug_summary = "Target Milestone left to default";
 $sel->type_ok("short_desc", $bug_summary);
-$sel->type_ok("comment", "Created by Selenium to test 'musthavemilestoneonaccept'");
+$sel->type_ok("comment",
+  "Created by Selenium to test 'musthavemilestoneonaccept'");
 my $bug1_id = create_bug($sel, $bug_summary);
 
 # 4th step: edit the bug
 
 go_to_bug($sel, $bug1_id);
-$sel->select_ok("bug_status", "label=IN_PROGRESS", "Change bug status to IN_PROGRESS");
-$sel->select_ok("target_milestone", "label=2.0", "Select a non-default milestone");
+$sel->select_ok("bug_status", "label=IN_PROGRESS",
+  "Change bug status to IN_PROGRESS");
+$sel->select_ok("target_milestone", "label=2.0",
+  "Select a non-default milestone");
 edit_bug($sel, $bug1_id, $bug_summary);
 
 # 5th step: create another bug.
 
 file_bug_in_product($sel, "TestProduct");
 $sel->select_ok("target_milestone", "label=2.0", "Set the milestone to 2.0");
-$sel->selected_label_is("component", "TestComponent", "Component already selected (no other component defined)");
-$sel->selected_label_is("version", "unspecified", "Version already selected (no other version defined)");
+$sel->selected_label_is("component", "TestComponent",
+  "Component already selected (no other component defined)");
+$sel->selected_label_is("version", "unspecified",
+  "Version already selected (no other version defined)");
 my $bug_summary2 = "Target Milestone set to non-default";
 $sel->type_ok("short_desc", $bug_summary2);
-$sel->type_ok("comment", "Created by Selenium to test milestone support");
+$sel->type_ok("comment",    "Created by Selenium to test milestone support");
 my $bug2_id = create_bug($sel, $bug_summary2);
 
 # 6th step: edit the bug
@@ -105,7 +119,8 @@ $sel->click_ok("create");
 $sel->wait_for_page_to_load(WAIT_TIME);
 $sel->title_is("Invalid Milestone Sortkey");
 my $error_msg = trim($sel->get_text("error_msg"));
-ok($error_msg =~ /^The sortkey '99999999999999999' is not in the range/, "Invalid sortkey");
+ok($error_msg =~ /^The sortkey '99999999999999999' is not in the range/,
+  "Invalid sortkey");
 $sel->go_back_ok();
 $sel->wait_for_page_to_load(WAIT_TIME);
 $sel->type_ok("sortkey", "-polu7A");
@@ -114,7 +129,8 @@ $sel->click_ok("create");
 $sel->wait_for_page_to_load(WAIT_TIME);
 $sel->title_is("Invalid Milestone Sortkey");
 $error_msg = trim($sel->get_text("error_msg"));
-ok($error_msg =~ /^The sortkey '-polu7A' is not in the range/, "Invalid sortkey");
+ok($error_msg =~ /^The sortkey '-polu7A' is not in the range/,
+  "Invalid sortkey");
 $sel->go_back_ok();
 $sel->wait_for_page_to_load(WAIT_TIME);
 $sel->click_ok("link='TestProduct'");
@@ -123,7 +139,8 @@ $sel->title_is("Select milestone of product 'TestProduct'");
 $sel->click_ok("link=Delete");
 $sel->wait_for_page_to_load(WAIT_TIME);
 $sel->title_is("Delete Milestone of Product 'TestProduct'");
-$sel->is_text_present_ok("When you delete this milestone", undef, "Warn the user about bugs being affected");
+$sel->is_text_present_ok("When you delete this milestone",
+  undef, "Warn the user about bugs being affected");
 $sel->click_ok("delete");
 $sel->wait_for_page_to_load(WAIT_TIME);
 $sel->title_is("Milestone Deleted");
@@ -131,16 +148,18 @@ $sel->title_is("Milestone Deleted");
 # 8th step: make sure the (now deleted) milestone of the bug has fallen back to the default milestone.
 
 go_to_bug($sel, $bug1_id);
-$sel->is_text_present_ok('regexp:Target Milestone:\W+---', undef, "Milestone has fallen back to the default milestone");
+$sel->is_text_present_ok('regexp:Target Milestone:\W+---',
+  undef, "Milestone has fallen back to the default milestone");
 
 # 9th step: file another bug.
 
 file_bug_in_product($sel, "TestProduct");
-$sel->selected_label_is("target_milestone", "---", "Default milestone selected");
+$sel->selected_label_is("target_milestone", "---",
+  "Default milestone selected");
 $sel->selected_label_is("component", "TestComponent");
 my $bug_summary3 = "Only one Target Milestone available";
 $sel->type_ok("short_desc", $bug_summary3);
-$sel->type_ok("comment", "Created by Selenium to test milestone support");
+$sel->type_ok("comment",    "Created by Selenium to test milestone support");
 my $bug3_id = create_bug($sel, $bug_summary3);
 
 $sel->select_ok("bug_status", "label=IN_PROGRESS");
index f8ea029886ac1df5f9589d59e5912ad5ad34c568..840611d26cfe5f6ad3a4fc0792b37f66a5b30d02 100644 (file)
@@ -19,104 +19,133 @@ use QA::Util;
 my ($sel, $config) = get_selenium();
 log_in($sel, $config, 'admin');
 
-set_parameters($sel, {"Administrative Policies" => {"allowuserdeletion-on" => undef},
-                      "User Authentication"     => {"createemailregexp" => {type => "text", value => '.*'}} });
+set_parameters(
+  $sel,
+  {
+    "Administrative Policies" => {"allowuserdeletion-on" => undef},
+    "User Authentication" =>
+      {"createemailregexp" => {type => "text", value => '.*'}}
+  }
+);
 
 # Set the password complexity to MIXED LETTERS.
 # Password must contain at least one UPPER and one lowercase letter.
 my @invalid_mixed_letter = qw(lowercase UPPERCASE 1234567890 123lowercase
-                              123UPPERCASE !@%&^lower !@&^UPPER);
+  123UPPERCASE !@%&^lower !@&^UPPER);
 
-check_passwords($sel, 'mixed_letters', \@invalid_mixed_letter, ['PaSSwOrd', '%9rT#j22S']);
+check_passwords($sel, 'mixed_letters', \@invalid_mixed_letter,
+  ['PaSSwOrd', '%9rT#j22S']);
 
 # Set the password complexity to LETTERS AND NUMBERS.
 # Passwords must contain at least one UPPER and one lower case letter and a number.
 my @invalid_letter_number = (@invalid_mixed_letter, qw(lowerUPPER 123!@%^$));
 
-check_passwords($sel, 'letters_numbers', \@invalid_letter_number, ['-UniCode6.3', 'UNO54sun']);
+check_passwords($sel, 'letters_numbers', \@invalid_letter_number,
+  ['-UniCode6.3', 'UNO54sun']);
 
 # Set the password complexity to LETTERS, NUMBERS AND SPECIAL CHARACTERS.
 # Passwords must contain at least one letter, a number and a special character.
-my @invalid_letter_number_splchar = (qw(!@%^&~* lowerUPPER123), @invalid_letter_number);
+my @invalid_letter_number_splchar
+  = (qw(!@%^&~* lowerUPPER123), @invalid_letter_number);
 
-check_passwords($sel, 'letters_numbers_specialchars', \@invalid_letter_number_splchar, ['@gu731', 'HU%m70?']);
+check_passwords(
+  $sel,
+  'letters_numbers_specialchars',
+  \@invalid_letter_number_splchar,
+  ['@gu731', 'HU%m70?']
+);
 
 # Set the password complexity to No Constraints.
-check_passwords($sel, 'no_constraints', ['12xY!', 'aaaaa'], ['aaaaaaaa', '>F12Xy?']);
+check_passwords(
+  $sel, 'no_constraints',
+  ['12xY!',    'aaaaa'],
+  ['aaaaaaaa', '>F12Xy?']
+);
 
 logout($sel);
 
 
 sub check_passwords {
-    my ($sel, $param, $invalid_passwords, $valid_passwords) = @_;
+  my ($sel, $param, $invalid_passwords, $valid_passwords) = @_;
 
-    set_parameters($sel, { "User Authentication" => {"password_complexity" => {type => "select", value => $param}} });
-    my $new_user = 'selenium-' . random_string(10) . '@bugzilla.org';
-
-    go_to_admin($sel);
-    $sel->click_ok("link=Users");
-    $sel->wait_for_page_to_load_ok(WAIT_TIME);
-    $sel->title_is('Search users');
-    $sel->click_ok('link=add a new user');
+  set_parameters(
+    $sel,
+    {
+      "User Authentication" =>
+        {"password_complexity" => {type => "select", value => $param}}
+    }
+  );
+  my $new_user = 'selenium-' . random_string(10) . '@bugzilla.org';
+
+  go_to_admin($sel);
+  $sel->click_ok("link=Users");
+  $sel->wait_for_page_to_load_ok(WAIT_TIME);
+  $sel->title_is('Search users');
+  $sel->click_ok('link=add a new user');
+  $sel->wait_for_page_to_load_ok(WAIT_TIME);
+  $sel->title_is('Add user');
+  $sel->type_ok('email', $new_user);
+
+  foreach my $password (@$invalid_passwords) {
+    $sel->type_ok('password', $password, 'Enter password');
+    $sel->click_ok('add');
     $sel->wait_for_page_to_load_ok(WAIT_TIME);
-    $sel->title_is('Add user');
-    $sel->type_ok('email', $new_user);
-
-    foreach my $password (@$invalid_passwords) {
-        $sel->type_ok('password', $password, 'Enter password');
-        $sel->click_ok('add');
-        $sel->wait_for_page_to_load_ok(WAIT_TIME);
-        if ($param eq 'no_constraints') {
-            $sel->title_is('Password Too Short');
-        }
-        else {
-            $sel->title_is('Password Fails Requirements');
-        }
-
-        my $error_msg = trim($sel->get_text("error_msg"));
-        if ($param eq 'mixed_letters') {
-            ok($error_msg =~ /UPPERCASE letter.*lowercase letter/,
-               "Mixed letter password fails requirement: $password");
-        }
-        elsif ($param eq 'letters_numbers') {
-            ok($error_msg =~ /UPPERCASE letter.*lowercase letter.*digit/,
-               "Letter & Number password fails requirement: $password");
-
-        }
-        elsif ($param eq 'letters_numbers_specialchars') {
-            ok($error_msg =~ /letter.*special character.*digit/,
-               "Letter, Number & Special Character password fails requirement: $password");
-        }
-        else {
-            ok($error_msg =~ /The password must be at least \d+ characters long/,
-               "Password Too Short: $password");
-        }
-        $sel->go_back_ok();
-        $sel->wait_for_page_to_load_ok(WAIT_TIME);
+    if ($param eq 'no_constraints') {
+      $sel->title_is('Password Too Short');
     }
-
-    my $created = 0;
-
-    foreach my $password (@$valid_passwords) {
-        $sel->type_ok('password', $password, 'Enter password');
-        $sel->click_ok($created ? 'update' : 'add');
-        $sel->wait_for_page_to_load_ok(WAIT_TIME);
-        $sel->title_is($created ? "User $new_user updated" : "Edit user $new_user");
-        my $msg = trim($sel->get_text('message'));
-        if ($created++) {
-            ok($msg =~ /A new password has been set/, 'Account updated');
-        }
-        else {
-            ok($msg =~ /The user account $new_user has been created successfully/, 'Account created');
-        }
+    else {
+      $sel->title_is('Password Fails Requirements');
     }
 
-    return unless $created;
+    my $error_msg = trim($sel->get_text("error_msg"));
+    if ($param eq 'mixed_letters') {
+      ok(
+        $error_msg =~ /UPPERCASE letter.*lowercase letter/,
+        "Mixed letter password fails requirement: $password"
+      );
+    }
+    elsif ($param eq 'letters_numbers') {
+      ok(
+        $error_msg =~ /UPPERCASE letter.*lowercase letter.*digit/,
+        "Letter & Number password fails requirement: $password"
+      );
 
-    $sel->click_ok('delete');
+    }
+    elsif ($param eq 'letters_numbers_specialchars') {
+      ok($error_msg =~ /letter.*special character.*digit/,
+        "Letter, Number & Special Character password fails requirement: $password");
+    }
+    else {
+      ok($error_msg =~ /The password must be at least \d+ characters long/,
+        "Password Too Short: $password");
+    }
+    $sel->go_back_ok();
     $sel->wait_for_page_to_load_ok(WAIT_TIME);
-    $sel->title_is("Confirm deletion of user $new_user");
-    $sel->click_ok('delete');
+  }
+
+  my $created = 0;
+
+  foreach my $password (@$valid_passwords) {
+    $sel->type_ok('password', $password, 'Enter password');
+    $sel->click_ok($created ? 'update' : 'add');
     $sel->wait_for_page_to_load_ok(WAIT_TIME);
-    $sel->title_is("User $new_user deleted");
+    $sel->title_is($created ? "User $new_user updated" : "Edit user $new_user");
+    my $msg = trim($sel->get_text('message'));
+    if ($created++) {
+      ok($msg =~ /A new password has been set/, 'Account updated');
+    }
+    else {
+      ok($msg =~ /The user account $new_user has been created successfully/,
+        'Account created');
+    }
+  }
+
+  return unless $created;
+
+  $sel->click_ok('delete');
+  $sel->wait_for_page_to_load_ok(WAIT_TIME);
+  $sel->title_is("Confirm deletion of user $new_user");
+  $sel->click_ok('delete');
+  $sel->wait_for_page_to_load_ok(WAIT_TIME);
+  $sel->title_is("User $new_user deleted");
 }
index 1e9f6ad28b63667f562d948701e0ba356e25be91..203c51f84355eaf64ad79f51babfb1a45501b913 100644 (file)
@@ -24,35 +24,43 @@ my ($sel, $config) = get_selenium(CHROME_MODE);
 # we can view and delete attachments.
 
 log_in($sel, $config, 'admin');
-set_parameters($sel, { "Group Security" => {"insidergroup" => {type => "select", value => "admin"}},
-                       "Attachments"    => {"allow_attachment_display-on" => undef,
-                                            "allow_attachment_deletion-on" => undef}
-                     });
+set_parameters(
+  $sel,
+  {
+    "Group Security" => {"insidergroup" => {type => "select", value => "admin"}},
+    "Attachments"    => {
+      "allow_attachment_display-on"  => undef,
+      "allow_attachment_deletion-on" => undef
+    }
+  }
+);
 
 # First create a new bug with a private attachment.
 
 file_bug_in_product($sel, "TestProduct");
 my $bug_summary = "Some comments are private";
 $sel->type_ok("short_desc", $bug_summary);
-$sel->type_ok("comment", "and some attachments too, like this one.");
+$sel->type_ok("comment",    "and some attachments too, like this one.");
 $sel->check_ok("comment_is_private");
 $sel->click_ok('//input[@value="Add an attachment"]');
-$sel->type_ok("data", $config->{attachment_file});
+$sel->type_ok("data",        $config->{attachment_file});
 $sel->type_ok("description", "private attachment, v1");
 $sel->check_ok("ispatch");
 my $bug1_id = create_bug($sel, $bug_summary);
 $sel->is_text_present_ok("private attachment, v1 (");
 $sel->is_text_present_ok("and some attachments too, like this one.");
-$sel->is_checked_ok('//a[@id="comment_link_0"]/../..//div//input[@type="checkbox"]');
+$sel->is_checked_ok(
+  '//a[@id="comment_link_0"]/../..//div//input[@type="checkbox"]');
 
 # Now attach a public patch to the existing bug.
 
 $sel->click_ok("link=Add an attachment");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Create New Attachment for Bug #$bug1_id");
-$sel->type_ok("data", $config->{attachment_file});
+$sel->type_ok("data",        $config->{attachment_file});
 $sel->type_ok("description", "public attachment, v2");
 $sel->check_ok("ispatch");
+
 # The existing attachment name must be displayed, to mark it as obsolete.
 $sel->is_text_present_ok("private attachment, v1");
 $sel->type_ok("comment", "this patch is public. Everyone can see it.");
@@ -66,24 +74,31 @@ my $alink = $sel->get_attribute('//a[@title="public attachment, v2"]@href');
 $alink =~ /id=(\d+)/;
 my $attachment1_id = $1;
 $sel->is_text_present_ok("this patch is public. Everyone can see it.");
-ok(!$sel->is_checked('//a[@id="comment_link_1"]/../..//div//input[@type="checkbox"]'), "Public attachment is visible");
+ok(
+  !$sel->is_checked(
+    '//a[@id="comment_link_1"]/../..//div//input[@type="checkbox"]'),
+  "Public attachment is visible"
+);
 logout($sel);
 
 # A logged out user cannot see the private attachment, only the public one.
 # Same for a user with no privs.
 
 foreach my $user ('', 'unprivileged') {
-    log_in($sel, $config, $user) if $user;
-    go_to_bug($sel, $bug1_id);
-    ok(!$sel->is_text_present("private attachment, v1"), "Private attachment not visible");
-    $sel->is_text_present_ok("public attachment, v2");
-    ok(!$sel->is_text_present("and some attachments too, like this one"), "Private comment not visible");
-    $sel->is_text_present_ok("this patch is public. Everyone can see it.");
+  log_in($sel, $config, $user) if $user;
+  go_to_bug($sel, $bug1_id);
+  ok(!$sel->is_text_present("private attachment, v1"),
+    "Private attachment not visible");
+  $sel->is_text_present_ok("public attachment, v2");
+  ok(!$sel->is_text_present("and some attachments too, like this one"),
+    "Private comment not visible");
+  $sel->is_text_present_ok("this patch is public. Everyone can see it.");
 }
 
 # A powerless user can comment on attachments he doesn't own.
 
-$sel->click_ok('//a[@href="attachment.cgi?id=' . $attachment1_id . '&action=edit"]');
+$sel->click_ok(
+  '//a[@href="attachment.cgi?id=' . $attachment1_id . '&action=edit"]');
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_like(qr/Attachment $attachment1_id Details for Bug $bug1_id/);
 $sel->is_text_present_ok("created by admin");
@@ -99,13 +114,19 @@ $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Create New Attachment for Bug #$bug1_id");
 $sel->type_ok("data", $config->{attachment_file});
 $sel->check_ok("ispatch");
+
 # The user doesn't have editbugs privs.
-ok(!$sel->is_text_present("Check each existing attachment made obsolete by your new attachment"), "No attachments can be marked as obsolete");
+ok(
+  !$sel->is_text_present(
+    "Check each existing attachment made obsolete by your new attachment"),
+  "No attachments can be marked as obsolete"
+);
 $sel->type_ok("description", "My patch, which I should see, always");
-$sel->type_ok("comment", "This is my patch!");
+$sel->type_ok("comment",     "This is my patch!");
 edit_bug($sel, $bug1_id, $bug_summary, {id => "create"});
 $sel->is_text_present_ok("My patch, which I should see, always (");
-$alink = $sel->get_attribute('//a[@title="My patch, which I should see, always"]@href');
+$alink = $sel->get_attribute(
+  '//a[@title="My patch, which I should see, always"]@href');
 $alink =~ /id=(\d+)/;
 my $attachment2_id = $1;
 $sel->is_text_present_ok("This is my patch!");
@@ -115,24 +136,33 @@ logout($sel);
 
 log_in($sel, $config, 'admin');
 go_to_bug($sel, $bug1_id);
-$sel->click_ok('//a[@href="attachment.cgi?id=' . $attachment2_id . '&action=edit"]');
+$sel->click_ok(
+  '//a[@href="attachment.cgi?id=' . $attachment2_id . '&action=edit"]');
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_like(qr/Attachment $attachment2_id Details for Bug $bug1_id/);
 $sel->check_ok("isprivate");
 $sel->type_ok("comment", "Making the powerless user's patch private.");
 edit_bug($sel, $bug1_id, $bug_summary, {id => "update"});
 $sel->is_text_present_ok("My patch, which I should see, always (");
-$sel->is_checked_ok('//a[@id="comment_link_4"]/../..//div//input[@type="checkbox"]');
+$sel->is_checked_ok(
+  '//a[@id="comment_link_4"]/../..//div//input[@type="checkbox"]');
 $sel->is_text_present_ok("Making the powerless user's patch private.");
 logout($sel);
 
 # A logged out user cannot see private attachments.
 
 go_to_bug($sel, $bug1_id);
-ok(!$sel->is_text_present("private attachment, v1"), "Private attachment not visible to logged out users");
-ok(!$sel->is_text_present("My patch, which I should see, always ("), "Private attachment not visible to logged out users");
+ok(
+  !$sel->is_text_present("private attachment, v1"),
+  "Private attachment not visible to logged out users"
+);
+ok(
+  !$sel->is_text_present("My patch, which I should see, always ("),
+  "Private attachment not visible to logged out users"
+);
 $sel->is_text_present_ok("This is my patch!");
-ok(!$sel->is_text_present("Making the powerless user's patch private"), "Private comment not visible to logged out users");
+ok(!$sel->is_text_present("Making the powerless user's patch private"),
+  "Private comment not visible to logged out users");
 
 # A powerless user can only see private attachments he owns.
 
@@ -141,6 +171,7 @@ go_to_bug($sel, $bug1_id);
 $sel->is_text_present_ok("My patch, which I should see, always (");
 $sel->click_ok("link=My patch, which I should see, always");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
+
 # No title displayed while viewing an attachment.
 $sel->title_is("");
 $sel->go_back_ok();
@@ -151,7 +182,8 @@ logout($sel);
 
 log_in($sel, $config, 'admin');
 go_to_bug($sel, $bug1_id);
-$sel->click_ok('//a[@href="attachment.cgi?id=' . $attachment2_id . '&action=edit"]');
+$sel->click_ok(
+  '//a[@href="attachment.cgi?id=' . $attachment2_id . '&action=edit"]');
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_like(qr/Attachment $attachment2_id Details for Bug $bug1_id/);
 $sel->click_ok("link=Delete");
@@ -164,10 +196,14 @@ $sel->is_text_present_ok("deleted by Selenium");
 $sel->click_ok("link=attachment $attachment2_id");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Attachment Removed");
-$sel->is_text_present_ok("The attachment you are attempting to access has been removed");
-
-set_parameters($sel, {
-    "Group Security" => {"insidergroup" => { type => "select",
-                                             value => "QA-Selenium-TEST" }},
-});
+$sel->is_text_present_ok(
+  "The attachment you are attempting to access has been removed");
+
+set_parameters(
+  $sel,
+  {
+    "Group Security" =>
+      {"insidergroup" => {type => "select", value => "QA-Selenium-TEST"}},
+  }
+);
 logout($sel);
index 7bfc5acd358732a127fb191be049876e5855f145..b5beefa4012fcf7c5fd67dd100ab600ab389d8f9 100644 (file)
@@ -22,25 +22,29 @@ my ($sel, $config) = get_selenium();
 
 log_in($sel, $config, 'admin');
 if ($sel->is_text_present("My QA query")) {
-    $sel->click_ok("link=My QA query");
-    $sel->wait_for_page_to_load_ok(WAIT_TIME);
-    $sel->title_is("Bug List: My QA query");
-    $sel->click_ok("forget_search");
-    $sel->wait_for_page_to_load_ok(WAIT_TIME);
-    $sel->title_is("Search is gone");
-    $sel->is_text_present_ok("OK, the My QA query search is gone.");
+  $sel->click_ok("link=My QA query");
+  $sel->wait_for_page_to_load_ok(WAIT_TIME);
+  $sel->title_is("Bug List: My QA query");
+  $sel->click_ok("forget_search");
+  $sel->wait_for_page_to_load_ok(WAIT_TIME);
+  $sel->title_is("Search is gone");
+  $sel->is_text_present_ok("OK, the My QA query search is gone.");
 }
 
 # Enable the QA contact field and file a new bug restricted to the 'Master' group
 # with a powerless user as the QA contact. He should only be able to access the
 # bug if the QA contact field is enabled, else he looses this privilege.
 
-set_parameters($sel, { "Bug Fields" => {"useqacontact-on" => undef} });
+set_parameters($sel, {"Bug Fields" => {"useqacontact-on" => undef}});
 file_bug_in_product($sel, 'TestProduct');
-$sel->type_ok("qa_contact", $config->{unprivileged_user_login}, "Set the powerless user as QA contact");
+$sel->type_ok(
+  "qa_contact",
+  $config->{unprivileged_user_login},
+  "Set the powerless user as QA contact"
+);
 my $bug_summary = "Test for QA contact";
 $sel->type_ok("short_desc", $bug_summary);
-$sel->type_ok("comment", "This is a test to check QA contact privs.");
+$sel->type_ok("comment",    "This is a test to check QA contact privs.");
 $sel->check_ok('//input[@name="groups" and @value="Master"]');
 my $bug1_id = create_bug($sel, $bug_summary);
 
@@ -53,7 +57,11 @@ $sel->add_selection_ok("product", "TestProduct");
 $sel->remove_all_selections("bug_status");
 $sel->select_ok("f1", "label=QA Contact");
 $sel->select_ok("o1", "label=is equal to");
-$sel->type_ok("v1", $config->{unprivileged_user_login}, "Look for the powerless user as QA contact");
+$sel->type_ok(
+  "v1",
+  $config->{unprivileged_user_login},
+  "Look for the powerless user as QA contact"
+);
 $sel->click_ok("Search");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Bug List");
@@ -64,7 +72,8 @@ $sel->click_ok("remember");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Search created");
 my $text = trim($sel->get_text("message"));
-ok($text =~ /OK, you have a new search named My QA query/, "New saved search 'My QA query'");
+ok($text =~ /OK, you have a new search named My QA query/,
+  "New saved search 'My QA query'");
 $sel->click_ok("link=My QA query");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Bug List: My QA query");
@@ -75,7 +84,7 @@ $sel->is_text_present_ok("Test for QA contact");
 # ("work" doesn't mean you should still see all bugs, depending on your role
 # and privs!)
 
-set_parameters($sel, { "Bug Fields" => {"useqacontact-off" => undef} });
+set_parameters($sel, {"Bug Fields" => {"useqacontact-off" => undef}});
 $sel->click_ok("link=My QA query");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Bug List: My QA query");
@@ -84,6 +93,7 @@ $sel->is_element_present_ok("b$bug1_id", undef, "Bug $bug1_id is on the list");
 $sel->click_ok("link=$bug1_id");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_like(qr/^$bug1_id /);
+
 # The 'QA Contact' label must not be displayed.
 ok(!$sel->is_text_present("QA Contact"), "The QA Contact label is not present");
 logout($sel);
@@ -104,9 +114,18 @@ $sel->is_text_present_ok("You are not authorized to access bug");
 $sel->click_ok("link=log in to an account", undef, "Log in");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Log in to Bugzilla");
-$sel->is_text_present_ok("Bugzilla needs a legitimate login and password to continue");
-$sel->type_ok("Bugzilla_login", $config->{unprivileged_user_login}, "Enter login name");
-$sel->type_ok("Bugzilla_password", $config->{unprivileged_user_passwd}, "Enter password");
+$sel->is_text_present_ok(
+  "Bugzilla needs a legitimate login and password to continue");
+$sel->type_ok(
+  "Bugzilla_login",
+  $config->{unprivileged_user_login},
+  "Enter login name"
+);
+$sel->type_ok(
+  "Bugzilla_password",
+  $config->{unprivileged_user_passwd},
+  "Enter password"
+);
 $sel->click_ok("log_in", undef, "Submit credentials");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Bug Access Denied");
@@ -116,7 +135,7 @@ logout($sel);
 # Re-enable the QA contact field.
 
 log_in($sel, $config, 'admin');
-set_parameters($sel, { "Bug Fields" => {"useqacontact-on" => undef} });
+set_parameters($sel, {"Bug Fields" => {"useqacontact-on" => undef}});
 logout($sel);
 
 # Log in as the powerless user. As the QA contact field is enabled again,
@@ -137,7 +156,11 @@ $sel->add_selection_ok("product", "TestProduct");
 $sel->remove_all_selections_ok("bug_status");
 $sel->select_ok("f1", "label=QA Contact");
 $sel->select_ok("o1", "label=is equal to");
-$sel->type_ok("v1", $config->{unprivileged_user_login}, "Look for the powerless user as QA contact");
+$sel->type_ok(
+  "v1",
+  $config->{unprivileged_user_login},
+  "Look for the powerless user as QA contact"
+);
 $sel->click_ok("Search");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Bug List");
@@ -148,7 +171,11 @@ $sel->click_ok("link=$bug1_id");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_like(qr/$bug1_id /);
 $sel->click_ok("bz_qa_contact_edit_action");
-$sel->value_is("qa_contact", $config->{unprivileged_user_login}, "The powerless user is the current QA contact");
+$sel->value_is(
+  "qa_contact",
+  $config->{unprivileged_user_login},
+  "The powerless user is the current QA contact"
+);
 $sel->check_ok("set_default_qa_contact");
 edit_bug($sel, $bug1_id, $bug_summary);
 
index c6f1c8ea1733c121c508325148a141b7da95f309..fc647f52316caf0708dd8637ea3bf02dbf1df7af 100644 (file)
@@ -21,60 +21,64 @@ my ($sel, $config) = get_selenium();
 # Turn on 'requirelogin'.
 
 log_in($sel, $config, 'admin');
-set_parameters($sel, { "User Authentication" => {"requirelogin-on" => undef} });
+set_parameters($sel, {"User Authentication" => {"requirelogin-on" => undef}});
 logout($sel);
 
 # We try to access each page. None of the ones listed below should
 # let you view it without being logged in.
 
 my @pages = qw(admin attachment buglist chart colchange describecomponents
-               describekeywords duplicates editclassifications editcomponents
-               editfields editflagtypes editgroups editkeywords editmilestones
-               editparams editproducts editsettings editusers editvalues
-               editversions editwhines editworkflow enter_bug page post_bug
-               process_bug query quips report reports request sanitycheck
-               search_plugin show_activity show_bug showdependencygraph
-               showdependencytree summarize_time userprefs votes);
+  describekeywords duplicates editclassifications editcomponents
+  editfields editflagtypes editgroups editkeywords editmilestones
+  editparams editproducts editsettings editusers editvalues
+  editversions editwhines editworkflow enter_bug page post_bug
+  process_bug query quips report reports request sanitycheck
+  search_plugin show_activity show_bug showdependencygraph
+  showdependencytree summarize_time userprefs votes);
 
 foreach my $page (@pages) {
-    $sel->open_ok("/$config->{bugzilla_installation}/${page}.cgi");
-    if ($page ne 'votes' || $config->{test_extensions}) {
-        $sel->title_is("Log in to Bugzilla");
-    }
-    else {
-        $sel->title_is("Extension Disabled");
-    }
+  $sel->open_ok("/$config->{bugzilla_installation}/${page}.cgi");
+  if ($page ne 'votes' || $config->{test_extensions}) {
+    $sel->title_is("Log in to Bugzilla");
+  }
+  else {
+    $sel->title_is("Extension Disabled");
+  }
 }
 
 # Those have parameters passed to the page, so we put them here separately.
 
-@pages = ("query.cgi?format=report-table", "query.cgi?format=report-graph",
-          "votes.cgi?action=show_user", "votes.cgi?action=show_bug");
+@pages = (
+  "query.cgi?format=report-table", "query.cgi?format=report-graph",
+  "votes.cgi?action=show_user",    "votes.cgi?action=show_bug"
+);
 
 foreach my $page (@pages) {
-    $sel->open_ok("/$config->{bugzilla_installation}/$page");
-    if ($page !~ /^votes/ || $config->{test_extensions}) {
-        $sel->title_is("Log in to Bugzilla");
-    }
-    else {
-        $sel->title_is("Extension Disabled");
-    }
+  $sel->open_ok("/$config->{bugzilla_installation}/$page");
+  if ($page !~ /^votes/ || $config->{test_extensions}) {
+    $sel->title_is("Log in to Bugzilla");
+  }
+  else {
+    $sel->title_is("Extension Disabled");
+  }
 }
 
 # These pages should still be accessible.
 
-@pages = ("config.cgi", "createaccount.cgi", "index.cgi", "relogin.cgi",
-          "token.cgi?a=reqpw&loginname=" . $config->{unprivileged_user_login});
+@pages = (
+  "config.cgi", "createaccount.cgi", "index.cgi", "relogin.cgi",
+  "token.cgi?a=reqpw&loginname=" . $config->{unprivileged_user_login}
+);
 
 foreach my $page (@pages) {
-    $sel->open_ok("/$config->{bugzilla_installation}/$page");
-    $sel->title_isnt("Log in to Bugzilla");
+  $sel->open_ok("/$config->{bugzilla_installation}/$page");
+  $sel->title_isnt("Log in to Bugzilla");
 }
 
 # Turn off 'requirelogin'.
 
 log_in($sel, $config, 'admin');
-set_parameters($sel, { "User Authentication" => {"requirelogin-off" => undef} });
+set_parameters($sel, {"User Authentication" => {"requirelogin-off" => undef}});
 logout($sel);
 
 # Make sure we can access random pages again.
index 9fc199bcf4f19d5229041755a0710a5122bca5e5..6b6be8d58044431ef955fe002f54cf8c5b3843ed 100644 (file)
@@ -23,27 +23,33 @@ go_to_admin($sel);
 $sel->click_ok("link=Sanity Check", undef, "Go to Sanity Check (no parameter)");
 $sel->wait_for_page_to_load(WAIT_TIME);
 $sel->title_is("Sanity Check", "Display sanitycheck.cgi");
-$sel->is_text_present_ok("Sanity check completed.", undef, "Page displayed correctly");
+$sel->is_text_present_ok("Sanity check completed.",
+  undef, "Page displayed correctly");
 
-my @args = qw(rebuildvotecache createmissinggroupcontrolmapentries repair_creation_date
-              repair_bugs_fulltext remove_invalid_bug_references repair_bugs_fulltext
-              remove_invalid_attach_references remove_old_whine_targets rescanallBugMail);
+my @args
+  = qw(rebuildvotecache createmissinggroupcontrolmapentries repair_creation_date
+  repair_bugs_fulltext remove_invalid_bug_references repair_bugs_fulltext
+  remove_invalid_attach_references remove_old_whine_targets rescanallBugMail);
 
 foreach my $arg (@args) {
-    $sel->open_ok("/$config->{bugzilla_installation}/sanitycheck.cgi?$arg=1");
-    $sel->title_is("Suspicious Action", "Calling sanitycheck.cgi with no token triggers a confirmation page");
-    $sel->click_ok("confirm", "Confirm the action");
-    $sel->wait_for_page_to_load(WAIT_TIME);
-    $sel->title_is("Sanity Check", "Calling sanitycheck.cgi with $arg=1");
-    if ($arg eq 'rescanallBugMail') {
-        # sanitycheck.cgi always stops after looking for unsent bugmail. So we cannot rely on
-        # "Sanity check completed." to determine if an error has been thrown or not.
-        $sel->is_text_present_ok("found with possibly unsent mail", undef, "Look for unsent bugmail");
-        ok(!$sel->is_text_present("Software error"), "No error thrown");
-    }
-    else {
-        $sel->is_text_present_ok("Sanity check completed.", undef, "Page displayed correctly");
-    }
+  $sel->open_ok("/$config->{bugzilla_installation}/sanitycheck.cgi?$arg=1");
+  $sel->title_is("Suspicious Action",
+    "Calling sanitycheck.cgi with no token triggers a confirmation page");
+  $sel->click_ok("confirm", "Confirm the action");
+  $sel->wait_for_page_to_load(WAIT_TIME);
+  $sel->title_is("Sanity Check", "Calling sanitycheck.cgi with $arg=1");
+  if ($arg eq 'rescanallBugMail') {
+
+# sanitycheck.cgi always stops after looking for unsent bugmail. So we cannot rely on
+# "Sanity check completed." to determine if an error has been thrown or not.
+    $sel->is_text_present_ok("found with possibly unsent mail",
+      undef, "Look for unsent bugmail");
+    ok(!$sel->is_text_present("Software error"), "No error thrown");
+  }
+  else {
+    $sel->is_text_present_ok("Sanity check completed.",
+      undef, "Page displayed correctly");
+  }
 }
 
 logout($sel);
index 047f3bcafb77b1720dc1cbae97a137bdb6502b79..591a63d946590dc8d70fe1daf1eef683cd27eb8f 100644 (file)
@@ -28,12 +28,15 @@ $sel->click_ok("link=Saved Searches");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Saved Searches");
 
-if($sel->is_text_present("SavedSearchTEST1")) {
-    # There is no other way to identify this link (as they are all named "Forget").
-    $sel->click_ok('//a[contains(@href,"buglist.cgi?cmdtype=dorem&remaction=forget&namedcmd=SavedSearchTEST1")]');
-    $sel->wait_for_page_to_load_ok(WAIT_TIME);
-    $sel->title_is("Search is gone");
-    $sel->is_text_present_ok("OK, the SavedSearchTEST1 search is gone.");
+if ($sel->is_text_present("SavedSearchTEST1")) {
+
+  # There is no other way to identify this link (as they are all named "Forget").
+  $sel->click_ok(
+    '//a[contains(@href,"buglist.cgi?cmdtype=dorem&remaction=forget&namedcmd=SavedSearchTEST1")]'
+  );
+  $sel->wait_for_page_to_load_ok(WAIT_TIME);
+  $sel->title_is("Search is gone");
+  $sel->is_text_present_ok("OK, the SavedSearchTEST1 search is gone.");
 }
 
 # Create a new saved search.
@@ -48,7 +51,10 @@ $sel->click_ok("remember");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Search created");
 my $text = trim($sel->get_text("message"));
-ok($text =~ /OK, you have a new search named SavedSearchTEST1./, "New search named SavedSearchTEST1 has been created");
+ok(
+  $text =~ /OK, you have a new search named SavedSearchTEST1./,
+  "New search named SavedSearchTEST1 has been created"
+);
 $sel->click_ok("link=SavedSearchTEST1");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Bug List: SavedSearchTEST1");
@@ -64,12 +70,14 @@ $sel->title_is("Saved Searches");
 
 $sel->is_text_present_ok("SavedSearchTEST1");
 $sel->uncheck_ok('//input[@type="checkbox" and @alt="SavedSearchTEST1"]');
+
 # $sel->value_is("//input[\@type='checkbox' and \@alt='SavedSearchTEST1']", "off");
 $sel->click_ok("update");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Saved Searches");
 $text = trim($sel->get_text("message"));
-ok($text =~ /The changes to your saved searches have been saved./, "Saved searches changes have been saved");
+ok($text =~ /The changes to your saved searches have been saved./,
+  "Saved searches changes have been saved");
 
 # Modify the saved search. Said otherwise, we should still be able to save
 # a new search with exactly the same name.
@@ -79,14 +87,21 @@ $sel->type_ok("short_desc", "bilboa");
 $sel->click_ok("Search");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Bug List");
+
 # As we said, this saved search should no longer be displayed in the page footer.
-ok(!$sel->is_text_present("SavedSearchTEST1"), "SavedSearchTEST1 is not present in the page footer");
+ok(
+  !$sel->is_text_present("SavedSearchTEST1"),
+  "SavedSearchTEST1 is not present in the page footer"
+);
 $sel->type_ok("save_newqueryname", "SavedSearchTEST1");
 $sel->click_ok("remember");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Search updated");
 $text = trim($sel->get_text("message"));
-ok($text =~ /Your search named SavedSearchTEST1 has been updated./, "Saved searche SavedSearchTEST1 has been updated.");
+ok(
+  $text =~ /Your search named SavedSearchTEST1 has been updated./,
+  "Saved searche SavedSearchTEST1 has been updated."
+);
 
 # Make sure our new criteria has been saved (let's edit the saved search).
 # As the saved search is no longer displayed in the footer, we have to go
@@ -100,7 +115,9 @@ $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Saved Searches");
 
 $sel->is_text_present_ok("SavedSearchTEST1");
-$sel->click_ok('//a[@href="buglist.cgi?cmdtype=dorem&remaction=run&namedcmd=SavedSearchTEST1"]');
+$sel->click_ok(
+  '//a[@href="buglist.cgi?cmdtype=dorem&remaction=run&namedcmd=SavedSearchTEST1"]'
+);
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Bug List: SavedSearchTEST1");
 $sel->click_ok("edit_search");
@@ -113,5 +130,6 @@ $sel->click_ok("forget_search");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Search is gone");
 $text = trim($sel->get_text("message"));
-ok($text =~ /OK, the SavedSearchTEST1 search is gone./, "The SavedSearchTEST1 search is gone.");
+ok($text =~ /OK, the SavedSearchTEST1 search is gone./,
+  "The SavedSearchTEST1 search is gone.");
 logout($sel);
index f5b51ee9601f743ae164eeebbd4c06589d7eec5b..636e43d6dbdcc20f3ca596833621d09146684e8c 100644 (file)
@@ -24,7 +24,8 @@ my ($sel, $config) = get_selenium();
 
 go_to_home($sel, $config);
 open_advanced_search_page($sel);
-$sel->type_ok("short_desc", "ois£jdfm#sd%fasd!fm", "Type a non-existent string in the bug summary field");
+$sel->type_ok("short_desc", "ois£jdfm#sd%fasd!fm",
+  "Type a non-existent string in the bug summary field");
 $sel->click_ok("Search");
 $sel->wait_for_page_to_load(WAIT_TIME);
 $sel->title_is("Bug List");
@@ -32,7 +33,9 @@ $sel->is_text_present_ok("Zarro Boogs found");
 
 # Display all available columns. Look for all bugs assigned to a user who doesn't exist.
 
-$sel->open_ok("/$config->{bugzilla_installation}/buglist.cgi?quicksearch=%40xx45ft&columnlist=all");
+$sel->open_ok(
+  "/$config->{bugzilla_installation}/buglist.cgi?quicksearch=%40xx45ft&columnlist=all"
+);
 $sel->title_is("Bug List");
 $sel->is_text_present_ok("Zarro Boogs found");
 
@@ -42,12 +45,12 @@ log_in($sel, $config, 'canconfirm');
 file_bug_in_product($sel, "TestProduct");
 my $bug_summary = "Update this summary with this bug ID";
 $sel->type_ok("short_desc", $bug_summary);
-$sel->type_ok("comment", "I'm supposed to appear in the coming buglist.");
+$sel->type_ok("comment",    "I'm supposed to appear in the coming buglist.");
 my $bug1_id = create_bug($sel, $bug_summary);
 $sel->click_ok("summary_edit_action");
 $bug_summary .= ": my ID is $bug1_id";
 $sel->type_ok("short_desc", $bug_summary);
-$sel->type_ok("comment", "Updating bug summary....");
+$sel->type_ok("comment",    "Updating bug summary....");
 edit_bug($sel, $bug1_id, $bug_summary);
 
 # Test pronoun substitution.
@@ -67,5 +70,6 @@ $sel->click_ok("Search");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Bug List");
 $sel->is_text_present_ok("One bug found");
-$sel->is_text_present_ok("Update this summary with this bug ID: my ID is $bug1_id");
+$sel->is_text_present_ok(
+  "Update this summary with this bug ID: my ID is $bug1_id");
 logout($sel);
index c65b4c07be251659d180933093b155fc8017d050..6e5af096ff987e5bb0917570879e0013945b74d1 100644 (file)
@@ -17,7 +17,7 @@ use Test::More "no_plan";
 use QA::Util;
 
 my ($sel, $config) = get_selenium(CHROME_MODE);
-my $urlbase = $config->{bugzilla_installation};
+my $urlbase    = $config->{bugzilla_installation};
 my $admin_user = $config->{admin_user_login};
 
 # Let's create a bug and attachment to play with.
@@ -25,9 +25,9 @@ my $admin_user = $config->{admin_user_login};
 log_in($sel, $config, 'admin');
 file_bug_in_product($sel, "TestProduct");
 my $bug_summary = "Security checks";
-$sel->type_ok("short_desc", $bug_summary);
-$sel->type_ok("comment", "This bug will be used to test security fixes.");
-$sel->type_ok("data", $config->{attachment_file});
+$sel->type_ok("short_desc",  $bug_summary);
+$sel->type_ok("comment",     "This bug will be used to test security fixes.");
+$sel->type_ok("data",        $config->{attachment_file});
 $sel->type_ok("description", "simple patch, v1");
 $sel->click_ok("ispatch");
 my $bug1_id = create_bug($sel, $bug_summary);
@@ -39,16 +39,25 @@ my $bug1_id = create_bug($sel, $bug_summary);
 
 # No alternate host for attachments; cookies will be accessible.
 
-set_parameters($sel, { "Attachments" => {"allow_attachment_display-on" => undef,
-                                         "reset-attachment_base" => undef} });
+set_parameters(
+  $sel,
+  {
+    "Attachments" =>
+      {"allow_attachment_display-on" => undef, "reset-attachment_base" => undef}
+  }
+);
 
 go_to_bug($sel, $bug1_id);
 $sel->click_ok("link=Add an attachment");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Create New Attachment for Bug #$bug1_id");
-$sel->type_ok("attach_text", "<html>\n<head>\n<title>I want your cookies</title>\n<head>\n" .
-                             "<body>\n<script type='text/javascript'>document.write(document.cookie);</script>\n" .
-                             "</body>\n</html>", "Writing text into the attachment textarea");
+$sel->type_ok(
+  "attach_text",
+  "<html>\n<head>\n<title>I want your cookies</title>\n<head>\n"
+    . "<body>\n<script type='text/javascript'>document.write(document.cookie);</script>\n"
+    . "</body>\n</html>",
+  "Writing text into the attachment textarea"
+);
 $sel->type_ok("description", "show my cookies");
 edit_bug($sel, $bug1_id, $bug_summary, {id => "create"});
 my $alink = $sel->get_attribute('//a[@title="show my cookies"]@href');
@@ -68,29 +77,36 @@ my @cookies = split(/[\s;]+/, $sel->get_body_text());
 my $nb_cookies = scalar @cookies;
 ok($nb_cookies, "Found $nb_cookies cookies:\n" . join("\n", @cookies));
 ok(!$sel->is_cookie_present("Bugzilla_login"), "Bugzilla_login not accessible");
-ok(!$sel->is_cookie_present("Bugzilla_logincookie"), "Bugzilla_logincookie not accessible");
+ok(!$sel->is_cookie_present("Bugzilla_logincookie"),
+  "Bugzilla_logincookie not accessible");
 $sel->go_back_ok();
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_like(qr/^$bug1_id /);
 
 # Alternate host for attachments; no cookie should be accessible.
 
-set_parameters($sel, { "Attachments" => {"attachment_base" => {type  => "text",
-                                                               value => "http://127.0.0.1/$urlbase"}} });
+set_parameters(
+  $sel,
+  {
+    "Attachments" =>
+      {"attachment_base" => {type => "text", value => "http://127.0.0.1/$urlbase"}}
+  }
+);
 go_to_bug($sel, $bug1_id);
 $sel->click_ok("link=show my cookies");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("I want your cookies");
 @cookies = split(/[\s;]+/, $sel->get_body_text());
 $nb_cookies = scalar @cookies;
-ok(!$nb_cookies, "No cookies found");
+ok(!$nb_cookies,                               "No cookies found");
 ok(!$sel->is_cookie_present("Bugzilla_login"), "Bugzilla_login not accessible");
-ok(!$sel->is_cookie_present("Bugzilla_logincookie"), "Bugzilla_logincookie not accessible");
+ok(!$sel->is_cookie_present("Bugzilla_logincookie"),
+  "Bugzilla_logincookie not accessible");
 $sel->go_back_ok();
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_like(qr/^$bug1_id /);
 
-set_parameters($sel, { "Attachments" => {"reset-attachment_base" => undef} });
+set_parameters($sel, {"Attachments" => {"reset-attachment_base" => undef}});
 
 #######################################################################
 # Security bug 472362.
@@ -110,25 +126,28 @@ my $editbugs_cookie = $sel->get_value("token");
 
 # Using our own unused token is fine.
 
-$sel->open_ok("/$urlbase/userprefs.cgi?dosave=1&display_quips=off&token=$editbugs_cookie");
+$sel->open_ok(
+  "/$urlbase/userprefs.cgi?dosave=1&display_quips=off&token=$editbugs_cookie");
 $sel->title_is("General Preferences");
-$sel->is_text_present_ok("The changes to your general preferences have been saved");
+$sel->is_text_present_ok(
+  "The changes to your general preferences have been saved");
 
 # Reusing a token must fail. They must all trigger the Suspicious Action warning.
 
-my @args = ("", "token=", "token=i123x", "token=$admin_cookie", "token=$editbugs_cookie");
+my @args = ("", "token=", "token=i123x", "token=$admin_cookie",
+  "token=$editbugs_cookie");
 
 foreach my $arg (@args) {
-    $sel->open_ok("/$urlbase/userprefs.cgi?dosave=1&display_quips=off&$arg");
-    $sel->title_is("Suspicious Action");
-
-    if ($arg eq "token=$admin_cookie") {
-        $sel->is_text_present_ok("Generated by: admin ($admin_user)");
-        $sel->is_text_present_ok("This token has not been generated by you");
-    }
-    else {
-        $sel->is_text_present_ok("It looks like you didn't come from the right page");
-    }
+  $sel->open_ok("/$urlbase/userprefs.cgi?dosave=1&display_quips=off&$arg");
+  $sel->title_is("Suspicious Action");
+
+  if ($arg eq "token=$admin_cookie") {
+    $sel->is_text_present_ok("Generated by: admin ($admin_user)");
+    $sel->is_text_present_ok("This token has not been generated by you");
+  }
+  else {
+    $sel->is_text_present_ok("It looks like you didn't come from the right page");
+  }
 }
 logout($sel);
 
@@ -141,8 +160,8 @@ file_bug_in_product($sel, "TestProduct");
 $sel->type_ok("alias", "secret_qa_bug_" . ($bug1_id + 1));
 my $bug_summary2 = "Private QA Bug";
 $sel->type_ok("short_desc", $bug_summary2);
-$sel->type_ok("comment", "This private bug is used to test security fixes.");
-$sel->type_ok("dependson", $bug1_id);
+$sel->type_ok("comment",    "This private bug is used to test security fixes.");
+$sel->type_ok("dependson",  $bug1_id);
 $sel->check_ok('//input[@name="groups" and @value="Master"]');
 my $bug2_id = create_bug($sel, $bug_summary2);
 
@@ -152,12 +171,14 @@ logout($sel);
 
 log_in($sel, $config, 'editbugs');
 go_to_bug($sel, $bug1_id);
-ok(!$sel->is_text_present("secret_qa_bug_$bug2_id"), "The alias 'secret_qa_bug_$bug2_id' is not visible for unauthorized users");
+ok(!$sel->is_text_present("secret_qa_bug_$bug2_id"),
+  "The alias 'secret_qa_bug_$bug2_id' is not visible for unauthorized users");
 $sel->is_text_present_ok($bug2_id);
 logout($sel);
 
 go_to_bug($sel, $bug1_id);
-ok(!$sel->is_text_present("secret_qa_bug_$bug2_id"), "The alias 'secret_qa_bug_$bug2_id' is not visible for logged out users");
+ok(!$sel->is_text_present("secret_qa_bug_$bug2_id"),
+  "The alias 'secret_qa_bug_$bug2_id' is not visible for logged out users");
 $sel->is_text_present_ok($bug2_id);
 
 #######################################################################
@@ -168,7 +189,8 @@ $sel->is_text_present_ok($bug2_id);
 #######################################################################
 
 log_in($sel, $config, 'admin');
-set_parameters($sel, { "Attachments" => {"allow_attachment_display-off" => undef} });
+set_parameters($sel,
+  {"Attachments" => {"allow_attachment_display-off" => undef}});
 
 # Attachments are not viewable.
 
@@ -176,8 +198,10 @@ go_to_bug($sel, $bug1_id);
 $sel->click_ok("link=Details");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_like(qr/Attachment \d+ Details for Bug $bug1_id/);
-$sel->is_text_present_ok("The attachment is not viewable in your browser due to security restrictions");
+$sel->is_text_present_ok(
+  "The attachment is not viewable in your browser due to security restrictions");
 $sel->click_ok("link=View");
+
 # Wait 1 second to give the browser a chance to display the attachment.
 # Do not use wait_for_page_to_load_ok() as the File Saver will never go away.
 sleep(1);
@@ -185,7 +209,8 @@ ok(!$sel->is_text_present('@@'), "Patch not displayed");
 
 # Enable viewing attachments.
 
-set_parameters($sel, { "Attachments" => {"allow_attachment_display-on" => undef} });
+set_parameters($sel,
+  {"Attachments" => {"allow_attachment_display-on" => undef}});
 
 go_to_bug($sel, $bug1_id);
 $sel->click_ok('link=simple patch, v1');
index d8712f74b19d3c12d63ebe592c9718413daa3334..a66363f0f1771a1fcd57165d2ff7494cd158232b 100644 (file)
@@ -21,7 +21,13 @@ my ($sel, $config) = get_selenium();
 # Set the querysharegroup param to be the canconfirm group.
 
 log_in($sel, $config, 'admin');
-set_parameters($sel, { "Group Security" => {"querysharegroup" => {type => "select", value => "canconfirm"}} });
+set_parameters(
+  $sel,
+  {
+    "Group Security" =>
+      {"querysharegroup" => {type => "select", value => "canconfirm"}}
+  }
+);
 
 # Create new saved search and call it 'Shared Selenium buglist'.
 
@@ -34,7 +40,10 @@ $sel->click_ok("remember");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Search created");
 my $text = trim($sel->get_text("message"));
-ok($text =~ /OK, you have a new search named Shared Selenium buglist./, "New search named 'Shared Selenium buglist' has been created");
+ok(
+  $text =~ /OK, you have a new search named Shared Selenium buglist./,
+  "New search named 'Shared Selenium buglist' has been created"
+);
 
 # Retrieve the newly created saved search's internal ID and make sure it's displayed
 # in the footer by default.
@@ -45,7 +54,8 @@ $sel->title_is("General Preferences");
 $sel->click_ok("link=Saved Searches");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Saved Searches");
-my $ssname = $sel->get_attribute('//input[@type="checkbox" and @alt="Shared Selenium buglist"]@name');
+my $ssname = $sel->get_attribute(
+  '//input[@type="checkbox" and @alt="Shared Selenium buglist"]@name');
 $ssname =~ /(?:link_in_footer_(\d+))/;
 my $saved_search1_id = $1;
 $sel->is_checked_ok("link_in_footer_$saved_search1_id");
@@ -53,7 +63,8 @@ $sel->is_checked_ok("link_in_footer_$saved_search1_id");
 # As an admin, the "Add to footer" checkbox must be displayed, but unchecked by default.
 
 $sel->select_ok("share_$saved_search1_id", "label=canconfirm");
-ok(!$sel->is_checked("force_$saved_search1_id"), "Shared search not displayed in other users' footer by default");
+ok(!$sel->is_checked("force_$saved_search1_id"),
+  "Shared search not displayed in other users' footer by default");
 $sel->click_ok("force_$saved_search1_id");
 $sel->click_ok("update");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
@@ -68,8 +79,12 @@ $sel->is_text_present_ok("Shared Selenium buglist");
 $sel->click_ok("link=Shared Selenium buglist");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Bug List: Shared Selenium buglist");
+
 # You cannot delete other users' saved searches.
-ok(!$sel->is_element_present("forget_search"), "'Forget...' button not available");
+ok(
+  !$sel->is_element_present("forget_search"),
+  "'Forget...' button not available"
+);
 
 # The name of the sharer must appear in the "Saved Searches" section.
 
@@ -88,11 +103,15 @@ $sel->click_ok("link_in_footer_$saved_search1_id");
 $sel->click_ok("update");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Saved Searches");
+
 # Go to a page where the query name is unlikely to appear in the main page.
 $sel->click_ok("link=Permissions");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Permissions");
-ok(!$sel->is_text_present("Shared Selenium buglist"), "Shared query no longer displayed in the footer");
+ok(
+  !$sel->is_text_present("Shared Selenium buglist"),
+  "Shared query no longer displayed in the footer"
+);
 
 # Create your own saved search, and share it with the canconfirm group.
 
@@ -105,7 +124,10 @@ $sel->click_ok("remember");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Search created");
 $text = trim($sel->get_text("message"));
-ok($text =~ /OK, you have a new search named helpwanted./, "New search named helpwanted has been created");
+ok(
+  $text =~ /OK, you have a new search named helpwanted./,
+  "New search named helpwanted has been created"
+);
 
 $sel->click_ok("link=Preferences");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
@@ -113,9 +135,11 @@ $sel->title_is("General Preferences");
 $sel->click_ok("link=Saved Searches");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Saved Searches");
-$ssname = $sel->get_attribute('//input[@type="checkbox" and @alt="helpwanted"]@name');
+$ssname
+  = $sel->get_attribute('//input[@type="checkbox" and @alt="helpwanted"]@name');
 $ssname =~ /(?:link_in_footer_(\d+))/;
 my $saved_search2_id = $1;
+
 # Our own saved searches are displayed in the footer by default.
 $sel->is_checked_ok("link_in_footer_$saved_search2_id");
 $sel->select_ok("share_$saved_search2_id", "label=canconfirm");
@@ -129,7 +153,10 @@ logout($sel);
 # must still be available and can be added to the footer, if desired.
 
 log_in($sel, $config, 'admin');
-ok(!$sel->is_text_present("helpwanted"), "No 'helpwanted' shared search displayed");
+ok(
+  !$sel->is_text_present("helpwanted"),
+  "No 'helpwanted' shared search displayed"
+);
 $sel->click_ok("link=Preferences");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("General Preferences");
@@ -139,11 +166,15 @@ $sel->title_is("Saved Searches");
 $sel->is_text_present_ok("helpwanted");
 $sel->is_text_present_ok($config->{canconfirm_user_login});
 
-ok(!$sel->is_checked("link_in_footer_$saved_search2_id"), "Shared query available but not displayed");
+ok(
+  !$sel->is_checked("link_in_footer_$saved_search2_id"),
+  "Shared query available but not displayed"
+);
 $sel->click_ok("link_in_footer_$saved_search2_id");
 $sel->click_ok("update");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Saved Searches");
+
 # This query is now available from the footer.
 $sel->click_ok("link=helpwanted");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
@@ -157,27 +188,35 @@ $sel->title_is("General Preferences");
 $sel->click_ok("link=Saved Searches");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Saved Searches");
+
 # There is no better way to identify the link
-$sel->click_ok('//a[contains(@href,"buglist.cgi?cmdtype=dorem&remaction=forget&namedcmd=Shared%20Selenium%20buglist")]',
-               undef, "Deleting the 'Shared Selenium buglist' search");
+$sel->click_ok(
+  '//a[contains(@href,"buglist.cgi?cmdtype=dorem&remaction=forget&namedcmd=Shared%20Selenium%20buglist")]',
+  undef, "Deleting the 'Shared Selenium buglist' search"
+);
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Search is gone");
 $text = trim($sel->get_text("message"));
-ok($text =~ /OK, the Shared Selenium buglist search is gone./, "The 'Shared Selenium buglist' search is gone");
+ok(
+  $text =~ /OK, the Shared Selenium buglist search is gone./,
+  "The 'Shared Selenium buglist' search is gone"
+);
 logout($sel);
 
 # Make sure that the 'helpwanted' query is not shared with the QA_Selenium_TEST
 # user as he doesn't belong to the 'canconfirm' group.
 
 log_in($sel, $config, 'QA_Selenium_TEST');
-ok(!$sel->is_text_present("helpwanted"), "The 'helpwanted' query is not displayed in the footer");
+ok(!$sel->is_text_present("helpwanted"),
+  "The 'helpwanted' query is not displayed in the footer");
 $sel->click_ok("link=Preferences");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("General Preferences");
 $sel->click_ok("link=Saved Searches");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Saved Searches");
-ok(!$sel->is_text_present("helpwanted"), "The 'helpwanted' query is not shared with this user");
+ok(!$sel->is_text_present("helpwanted"),
+  "The 'helpwanted' query is not shared with this user");
 logout($sel);
 
 # Now remove the 'helpwanted' saved search.
@@ -189,11 +228,17 @@ $sel->title_is("General Preferences");
 $sel->click_ok("link=Saved Searches");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Saved Searches");
-ok(!$sel->is_text_present("Shared Selenium buglist"), "The 'Shared Selenium buglist' is no longer available");
-$sel->click_ok('//a[contains(@href,"buglist.cgi?cmdtype=dorem&remaction=forget&namedcmd=helpwanted")]',
-               undef, "Deleting the 'helpwanted' search");
+ok(
+  !$sel->is_text_present("Shared Selenium buglist"),
+  "The 'Shared Selenium buglist' is no longer available"
+);
+$sel->click_ok(
+  '//a[contains(@href,"buglist.cgi?cmdtype=dorem&remaction=forget&namedcmd=helpwanted")]',
+  undef, "Deleting the 'helpwanted' search"
+);
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Search is gone");
 $text = trim($sel->get_text("message"));
-ok($text =~ /OK, the helpwanted search is gone./, "The 'helpwanted' search is gone");
+ok($text =~ /OK, the helpwanted search is gone./,
+  "The 'helpwanted' search is gone");
 logout($sel);
index f2521a4071db7151d227fff6fe641ee059e201ab..cab5194c98eb8cb486504e16a0950ca589c334ae 100644 (file)
@@ -19,7 +19,7 @@ use QA::Util;
 my ($sel, $config) = get_selenium();
 
 log_in($sel, $config, 'admin');
-set_parameters($sel, { "Bug Fields" => {"useclassification-on" => undef} });
+set_parameters($sel, {"Bug Fields" => {"useclassification-on" => undef}});
 
 # Do not use file_bug_in_product() because our goal here is not to file
 # a bug but to check what is present in the UI, and also to make sure
@@ -36,7 +36,10 @@ ok($full_text =~ /All: Show all products/, "The 'All' link is displayed");
 $sel->click_ok("link=All");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->is_text_present_ok("Select Product");
-ok(!$sel->is_text_present("QA-Selenium-TEST"), "The QA-Selenium-TEST product is not displayed");
+ok(
+  !$sel->is_text_present("QA-Selenium-TEST"),
+  "The QA-Selenium-TEST product is not displayed"
+);
 logout($sel);
 
 # Same steps, but for a member of the "QA‑Selenium‑TEST" group.
index 3d9de099ff592ce15455adad575744894db4ac80..066a6b707b3838ff82d54306dbb4dcec76432b77 100644 (file)
@@ -19,35 +19,42 @@ use QA::Util;
 my ($sel, $config) = get_selenium();
 
 log_in($sel, $config, 'admin');
-set_parameters($sel, { "General" => {shutdownhtml => {type  => "text",
-                                                      value => "I'm down (set by test_shutdown.t)" }
-                                    } });
+set_parameters(
+  $sel,
+  {
+    "General" => {
+      shutdownhtml => {type => "text", value => "I'm down (set by test_shutdown.t)"}
+    }
+  }
+);
 
 # None of the following pages should be accessible when Bugzilla is down.
 
 my @pages = qw(admin attachment buglist chart colchange config createaccount
-               describecomponents describekeywords duplicates
-               editclassifications editcomponents editfields editflagtypes
-               editgroups editkeywords editmilestones editproducts editsettings
-               editusers editvalues editversions editwhines editworkflow
-               enter_bug index page post_bug process_bug query quips relogin
-               report reports request sanitycheck search_plugin show_activity
-               show_bug showdependencygraph showdependencytree summarize_time
-               token userprefs votes xmlrpc);
+  describecomponents describekeywords duplicates
+  editclassifications editcomponents editfields editflagtypes
+  editgroups editkeywords editmilestones editproducts editsettings
+  editusers editvalues editversions editwhines editworkflow
+  enter_bug index page post_bug process_bug query quips relogin
+  report reports request sanitycheck search_plugin show_activity
+  show_bug showdependencygraph showdependencytree summarize_time
+  token userprefs votes xmlrpc);
 
 foreach my $page (@pages) {
-    $sel->open_ok("/$config->{bugzilla_installation}/${page}.cgi");
-    $sel->title_is("Bugzilla is Down");
+  $sel->open_ok("/$config->{bugzilla_installation}/${page}.cgi");
+  $sel->title_is("Bugzilla is Down");
 }
 
 # Those have parameters passed to the page, so we put them here separately.
 
-@pages = ("query.cgi?format=report-table", "query.cgi?format=report-graph",
-          "votes.cgi?action=show_user", "votes.cgi?action=show_bug");
+@pages = (
+  "query.cgi?format=report-table", "query.cgi?format=report-graph",
+  "votes.cgi?action=show_user",    "votes.cgi?action=show_bug"
+);
 
 foreach my $page (@pages) {
-    $sel->open_ok("/$config->{bugzilla_installation}/$page");
-    $sel->title_is("Bugzilla is Down");
+  $sel->open_ok("/$config->{bugzilla_installation}/$page");
+  $sel->title_is("Bugzilla is Down");
 }
 
 # Clear 'shutdownhtml', to re-enable Bugzilla.
@@ -56,8 +63,16 @@ foreach my $page (@pages) {
 
 $sel->open_ok("/$config->{bugzilla_installation}/editparams.cgi");
 $sel->title_is("Log in to Bugzilla");
-$sel->type_ok("Bugzilla_login", $config->{admin_user_login}, "Enter admin login name");
-$sel->type_ok("Bugzilla_password", $config->{admin_user_passwd}, "Enter admin password");
+$sel->type_ok(
+  "Bugzilla_login",
+  $config->{admin_user_login},
+  "Enter admin login name"
+);
+$sel->type_ok(
+  "Bugzilla_password",
+  $config->{admin_user_passwd},
+  "Enter admin password"
+);
 $sel->click_ok("log_in");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Configuration: Required Settings");
@@ -65,7 +80,8 @@ $sel->click_ok("link=General");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Configuration: General");
 $sel->type_ok("shutdownhtml", "");
-$sel->click_ok('//input[@type="submit" and @value="Save Changes"]', undef, "Save Changes");
+$sel->click_ok('//input[@type="submit" and @value="Save Changes"]',
+  undef, "Save Changes");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Parameters Updated");
 
index e8b6b7b6329329e8f70eb52b82fa1bb2007a0e4e..3084d7f2e290b2ab0f9a132e4a24b2a55757b47f 100644 (file)
@@ -27,7 +27,7 @@ file_bug_in_product($sel, "TestProduct");
 $sel->select_ok("component", "TestComponent");
 my $bug_summary = "white and black";
 $sel->type_ok("short_desc", $bug_summary);
-$sel->type_ok("comment", "This bug is to test the status whiteboard");
+$sel->type_ok("comment",    "This bug is to test the status whiteboard");
 my $bug1_id = create_bug($sel, $bug_summary);
 $sel->is_text_present_ok("Whiteboard:");
 $sel->type_ok("status_whiteboard", "[msg from test_status_whiteboard.t: x77v]");
@@ -37,7 +37,7 @@ file_bug_in_product($sel, "TestProduct");
 $sel->select_ok("component", "TestComponent");
 my $bug_summary2 = "WTC";
 $sel->type_ok("short_desc", $bug_summary2);
-$sel->type_ok("comment", "bugzillation!");
+$sel->type_ok("comment",    "bugzillation!");
 my $bug2_id = create_bug($sel, $bug_summary2);
 $sel->type_ok("status_whiteboard", "[msg from test_status_whiteboard.t: x77v]");
 edit_bug($sel, $bug2_id, $bug_summary2);
@@ -58,7 +58,8 @@ $sel->click_ok("remember");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Search created");
 my $text = trim($sel->get_text("message"));
-ok($text =~ /you have a new search named sw-x77v/, 'Saved search correctly saved');
+ok($text =~ /you have a new search named sw-x77v/,
+  'Saved search correctly saved');
 
 # Make sure the saved query works.
 
@@ -75,9 +76,11 @@ set_parameters($sel, {'Bug Fields' => {'usestatuswhiteboard-off' => undef}});
 $sel->click_ok("link=Search");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Search for bugs");
-ok(!$sel->is_text_present("Whiteboard:"), "Whiteboard label no longer displayed in the search page");
+ok(!$sel->is_text_present("Whiteboard:"),
+  "Whiteboard label no longer displayed in the search page");
 go_to_bug($sel, $bug1_id);
-ok(!$sel->is_text_present("Whiteboard:"), "Whiteboard label no longer displayed in the bug page");
+ok(!$sel->is_text_present("Whiteboard:"),
+  "Whiteboard label no longer displayed in the bug page");
 
 # Queries based on the status whiteboard should still work when
 # the parameter is off.
index a0400bbacaa13007ccf4b6c2ed922e0377d3b5d1..4fed22cede7627c7314869727b24475db511a1b8 100644 (file)
@@ -17,22 +17,23 @@ use Test::More "no_plan";
 use QA::Util;
 
 my ($sel, $config) = get_selenium();
-my $qa_user = $config->{QA_Selenium_TEST_user_login};
+my $qa_user       = $config->{QA_Selenium_TEST_user_login};
 my $no_privs_user = $config->{unprivileged_user_login};
 
 log_in($sel, $config, 'admin');
-set_parameters($sel, { "Group Security" => {"strict_isolation-on" => undef} });
+set_parameters($sel, {"Group Security" => {"strict_isolation-on" => undef}});
 
 # Restrict the bug to the "Master" group, so that we can check that only
 # allowed people can be CC'ed to the bug.
 
 file_bug_in_product($sel, 'Another Product');
 $sel->select_ok("component", "label=c2");
-$sel->select_ok("version", "label=Another2");
+$sel->select_ok("version",   "label=Another2");
 my $bug_summary = "Test isolation";
 $sel->type_ok("short_desc", $bug_summary);
-$sel->type_ok("comment", "Unallowed users refused");
-my $master_gid = $sel->get_attribute('//input[@type="checkbox" and @name="groups" and @value="Master"]@id');
+$sel->type_ok("comment",    "Unallowed users refused");
+my $master_gid = $sel->get_attribute(
+  '//input[@type="checkbox" and @name="groups" and @value="Master"]@id');
 $sel->check_ok($master_gid);
 $master_gid =~ s/group_//;
 my $bug1_id = create_bug($sel, $bug_summary);
@@ -69,7 +70,8 @@ $sel->type_ok("newcc", $no_privs_user);
 $sel->click_ok("commit");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Invalid User Group");
-$sel->is_text_present_ok("User '$no_privs_user' is not able to edit the 'Another Product' Product");
+$sel->is_text_present_ok(
+  "User '$no_privs_user' is not able to edit the 'Another Product' Product");
 $sel->go_back_ok();
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_like(qr/^$bug1_id /);
@@ -78,7 +80,8 @@ $sel->type_ok("newcc", $qa_user);
 $sel->click_ok("commit");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Invalid User Group");
-$sel->is_text_present_ok("User '$qa_user' is not able to edit the 'Another Product' Product");
+$sel->is_text_present_ok(
+  "User '$qa_user' is not able to edit the 'Another Product' Product");
 
 # Now set QA_Selenium_TEST user as a member of the Master group.
 
@@ -114,11 +117,12 @@ $sel->type_ok("newcc", "$qa_user, $no_privs_user");
 $sel->click_ok("commit");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Invalid User Group");
-$sel->is_text_present_ok("User '$no_privs_user' is not able to edit the 'Another Product' Product");
+$sel->is_text_present_ok(
+  "User '$no_privs_user' is not able to edit the 'Another Product' Product");
 
 # Reset parameters back to defaults.
 
-set_parameters($sel, { "Group Security" => {"strict_isolation-off" => undef} });
+set_parameters($sel, {"Group Security" => {"strict_isolation-off" => undef}});
 
 go_to_admin($sel);
 $sel->click_ok("link=Users");
index 99a6c653abfe620b5e1df30dbcbdf2531f5e3cb4..dfde6403bb787545bc80d309b6f055c6da17104e 100644 (file)
@@ -21,7 +21,7 @@ my ($sel, $config) = get_selenium();
 # Turn on the usevisibilitygroups param so that some users are invisible.
 
 log_in($sel, $config, 'admin');
-set_parameters($sel, { "Group Security" => {"usevisibilitygroups-on" => undef} });
+set_parameters($sel, {"Group Security" => {"usevisibilitygroups-on" => undef}});
 
 # You can see all users from editusers.cgi, but once you leave this page,
 # usual group visibility restrictions apply and the "powerless" user cannot
@@ -47,12 +47,16 @@ $sel->click_ok('//input[@value="Begin Session"]');
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Match Failed");
 my $error_msg = trim($sel->get_text("error_msg"));
-ok($error_msg eq "$config->{unprivileged_user_login} does not exist or you are not allowed to see that user.",
-   "Cannot impersonate users you cannot see");
+ok(
+  $error_msg eq
+    "$config->{unprivileged_user_login} does not exist or you are not allowed to see that user.",
+  "Cannot impersonate users you cannot see"
+);
 
 # Turn off the usevisibilitygroups param so that all users are visible again.
 
-set_parameters($sel, { "Group Security" => {"usevisibilitygroups-off" => undef} });
+set_parameters($sel,
+  {"Group Security" => {"usevisibilitygroups-off" => undef}});
 
 # The "powerless" user can now be sudo'ed.
 
@@ -75,7 +79,10 @@ $sel->click_ok('//input[@value="Begin Session"]');
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Sudo session started");
 my $text = trim($sel->get_text("message"));
-ok($text =~ /The sudo session has been started/, "The sudo session has been started");
+ok(
+  $text =~ /The sudo session has been started/,
+  "The sudo session has been started"
+);
 
 # Make sure this user is not an admin and has no privs at all, and that
 # he cannot access editusers.cgi (despite the sudoer can).
@@ -87,11 +94,13 @@ $sel->click_ok("link=Permissions");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Permissions");
 $sel->is_text_present_ok("There are no permission bits set on your account");
+
 # We access the page directly as there is no link pointing to it.
 $sel->open_ok("/$config->{bugzilla_installation}/editusers.cgi");
 $sel->title_is("Authorization Required");
 $error_msg = trim($sel->get_text("error_msg"));
-ok($error_msg =~ /^Sorry, you aren't a member of the 'editusers' group/, "Not a member of the editusers group");
+ok($error_msg =~ /^Sorry, you aren't a member of the 'editusers' group/,
+  "Not a member of the editusers group");
 $sel->click_ok("link=end session");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Sudo session complete");
@@ -99,23 +108,35 @@ $sel->is_text_present_ok("Your sudo session has ended");
 
 # Try to access the sudo page directly, with no credentials.
 
-$sel->open_ok("/$config->{bugzilla_installation}/relogin.cgi?action=begin-sudo&target_login=$config->{admin_user_login}");
+$sel->open_ok(
+  "/$config->{bugzilla_installation}/relogin.cgi?action=begin-sudo&target_login=$config->{admin_user_login}"
+);
 $sel->title_is("Password Required");
 
 # Now try to start a sudo session directly, with all required credentials.
 
-$sel->open_ok("/$config->{bugzilla_installation}/relogin.cgi?action=begin-sudo&password=$config->{admin_user_passwd}&target_login=$config->{unprivileged_user_login}", undef, "Impersonate a user directly by providing all required data");
+$sel->open_ok(
+  "/$config->{bugzilla_installation}/relogin.cgi?action=begin-sudo&password=$config->{admin_user_passwd}&target_login=$config->{unprivileged_user_login}",
+  undef, "Impersonate a user directly by providing all required data"
+);
+
 # A direct access to the page is supposed to have no Referer header set,
 # which would trigger the "Untrusted Authentication Request" error, but
 # due to the way Selenium works, the Referer header is set and the
 # "Preparation Required" error is thrown instead. In any case, one of
 # those two errors must be thrown.
 my $title = $sel->get_title();
-ok($title eq "Untrusted Authentication Request" || $title eq "Preparation Required", $title);
+ok(
+  $title eq "Untrusted Authentication Request"
+    || $title eq "Preparation Required",
+  $title
+);
 
 # Now try to sudo an admin, which is not allowed.
 
-$sel->open_ok("/$config->{bugzilla_installation}/relogin.cgi?action=prepare-sudo&target_login=$config->{admin_user_login}");
+$sel->open_ok(
+  "/$config->{bugzilla_installation}/relogin.cgi?action=prepare-sudo&target_login=$config->{admin_user_login}"
+);
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Begin sudo session");
 $sel->value_is("target_login", $config->{admin_user_login});
@@ -125,13 +146,18 @@ $sel->click_ok('//input[@value="Begin Session"]');
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("User Protected");
 $error_msg = trim($sel->get_text("error_msg"));
-ok($error_msg =~ /^The user $config->{admin_user_login} may not be impersonated by sudoers/, "Cannot impersonate administrators");
+ok(
+  $error_msg
+    =~ /^The user $config->{admin_user_login} may not be impersonated by sudoers/,
+  "Cannot impersonate administrators"
+);
 
 # Now try to sudo a non-existing user account, with no password.
 
 $sel->go_back_ok();
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Begin sudo session");
+
 # Starting with 5.0, the password field is a type=password and is marked
 # "required". This means that we need to remove the required attribute from
 # the input so that it can still be checked by the backend code.
@@ -146,7 +172,9 @@ $sel->title_is("Password Required");
 
 # Same as above, but with your password.
 
-$sel->open_ok("/$config->{bugzilla_installation}/relogin.cgi?action=prepare-sudo&target_login=foo\@bar.com");
+$sel->open_ok(
+  "/$config->{bugzilla_installation}/relogin.cgi?action=prepare-sudo&target_login=foo\@bar.com"
+);
 $sel->title_is("Begin sudo session");
 $sel->value_is("target_login", 'foo@bar.com');
 $sel->type_ok("password", $config->{admin_user_passwd}, "Enter admin password");
@@ -154,5 +182,9 @@ $sel->click_ok('//input[@value="Begin Session"]');
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Match Failed");
 $error_msg = trim($sel->get_text("error_msg"));
-ok($error_msg eq 'foo@bar.com does not exist or you are not allowed to see that user.', "Cannot impersonate non-existing accounts");
+ok(
+  $error_msg eq
+    'foo@bar.com does not exist or you are not allowed to see that user.',
+  "Cannot impersonate non-existing accounts"
+);
 logout($sel);
index 84254d4cdf1e874d12885e9fa384d79346f5c0fc..cdf68040f298e9cebdd83c07587bd3fa6a8e1464 100644 (file)
@@ -19,7 +19,7 @@ use QA::Util;
 my ($sel, $config) = get_selenium();
 
 log_in($sel, $config, 'admin');
-set_parameters($sel, { "Bug Fields" => {"usetargetmilestone-on" => undef} });
+set_parameters($sel, {"Bug Fields" => {"usetargetmilestone-on" => undef}});
 
 # Create a new milestone to the 'TestProduct' product.
 
@@ -31,7 +31,7 @@ $sel->click_ok("link=Add");
 $sel->wait_for_page_to_load(WAIT_TIME);
 $sel->title_is("Add Milestone to Product 'TestProduct'");
 $sel->type_ok("milestone", "TM1");
-$sel->type_ok("sortkey", "10");
+$sel->type_ok("sortkey",   "10");
 $sel->click_ok("create");
 $sel->wait_for_page_to_load(WAIT_TIME);
 $sel->title_is("Milestone Created");
@@ -42,7 +42,7 @@ file_bug_in_product($sel, "TestProduct");
 $sel->select_ok("component", "TestComponent");
 my $bug_summary = "stone and rock";
 $sel->type_ok("short_desc", $bug_summary);
-$sel->type_ok("comment", "This bug is to test milestones");
+$sel->type_ok("comment",    "This bug is to test milestones");
 my $bug1_id = create_bug($sel, $bug_summary);
 $sel->is_text_present_ok("Target Milestone:");
 $sel->select_ok("target_milestone", "label=TM1");
@@ -53,7 +53,7 @@ edit_bug($sel, $bug1_id, $bug_summary);
 open_advanced_search_page($sel);
 $sel->is_text_present_ok("Target Milestone:");
 $sel->remove_all_selections_ok("product");
-$sel->add_selection_ok("product", "label=TestProduct");
+$sel->add_selection_ok("product",          "label=TestProduct");
 $sel->add_selection_ok("target_milestone", "label=TM1");
 $sel->click_ok("Search");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
@@ -64,19 +64,24 @@ $sel->click_ok("remember");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Search created");
 my $text = trim($sel->get_text("message"));
-ok($text =~ /OK, you have a new search named selenium_m0./, "New search named selenium_m0 has been created");
+ok(
+  $text =~ /OK, you have a new search named selenium_m0./,
+  "New search named selenium_m0 has been created"
+);
 
 # Turn off milestones and check that the milestone field no longer appears in bugs.
 
-set_parameters($sel, { "Bug Fields" => {"usetargetmilestone-off" => undef} });
+set_parameters($sel, {"Bug Fields" => {"usetargetmilestone-off" => undef}});
 
 $sel->click_ok("link=Search");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Search for bugs");
-ok(!$sel->is_text_present("Target:"), "The target milestone field is no longer displayed in the search page");
+ok(!$sel->is_text_present("Target:"),
+  "The target milestone field is no longer displayed in the search page");
 
 go_to_bug($sel, $bug1_id);
-ok(!$sel->is_text_present("Target Milestone:"), "The milestone field is no longer displayed in the bug page");
+ok(!$sel->is_text_present("Target Milestone:"),
+  "The milestone field is no longer displayed in the bug page");
 
 # The existing query must still work despite milestones are off now.
 
@@ -88,23 +93,29 @@ $sel->click_ok("forget_search");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Search is gone");
 $text = trim($sel->get_text("message"));
-ok($text =~ /OK, the selenium_m0 search is gone./, "The selenium_m0 search is gone");
+ok($text =~ /OK, the selenium_m0 search is gone./,
+  "The selenium_m0 search is gone");
 
 # Re-enable the usetargetmilestone parameter and delete the created
 # milestone from the Testproduct product.
 
-set_parameters($sel, { "Bug Fields" => {"usetargetmilestone-on" => undef} });
+set_parameters($sel, {"Bug Fields" => {"usetargetmilestone-on" => undef}});
 
 edit_product($sel, "TestProduct");
 $sel->click_ok("link=Edit milestones:");
 $sel->wait_for_page_to_load(WAIT_TIME);
 $sel->title_is("Select milestone of product 'TestProduct'");
-$sel->click_ok('//a[@href="editmilestones.cgi?action=del&product=TestProduct&milestone=TM1"]',
-               undef, "Deleting the TM1 milestone");
+$sel->click_ok(
+  '//a[@href="editmilestones.cgi?action=del&product=TestProduct&milestone=TM1"]',
+  undef, "Deleting the TM1 milestone"
+);
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Delete Milestone of Product 'TestProduct'");
 $text = trim($sel->get_body_text());
-ok($text =~ /There is 1 bug entered for this milestone/, "Warning displayed about 1 bug targetted to TM1");
+ok(
+  $text =~ /There is 1 bug entered for this milestone/,
+  "Warning displayed about 1 bug targetted to TM1"
+);
 $sel->click_ok("delete");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Milestone Deleted");
index 58cf6e76825991b39d2774938da25975a31dfe28..ca0b1dc1debe1a8f76f3ec092b9203ef9978bfaa 100644 (file)
@@ -21,7 +21,13 @@ my ($sel, $config) = get_selenium();
 # Set the timetracking group to "editbugs", which is the default value for this parameter.
 
 log_in($sel, $config, 'admin');
-set_parameters($sel, { "Group Security" => {"timetrackinggroup" => {type => "select", value => "editbugs"}} });
+set_parameters(
+  $sel,
+  {
+    "Group Security" =>
+      {"timetrackinggroup" => {type => "select", value => "editbugs"}}
+  }
+);
 
 # Add some Hours Worked to a bug so that we are sure at least one bug
 # will be present in our buglist below.
@@ -30,11 +36,11 @@ file_bug_in_product($sel, "TestProduct");
 $sel->select_ok("component", "TestComponent");
 my $bug_summary = "Rocket science";
 $sel->type_ok("short_desc", $bug_summary);
-$sel->type_ok("comment", "Time flies");
+$sel->type_ok("comment",    "Time flies");
 my $bug1_id = create_bug($sel, $bug_summary);
 
 $sel->type_ok("work_time", 2.6);
-$sel->type_ok("comment", "I did some work");
+$sel->type_ok("comment",   "I did some work");
 edit_bug_and_return($sel, $bug1_id, $bug_summary);
 $sel->is_text_present_ok("I did some work");
 $sel->is_text_present_ok("Additional hours worked: 2.6");
@@ -44,7 +50,8 @@ $sel->is_text_present_ok("Additional hours worked: 2.6");
 $sel->open_ok("/$config->{bugzilla_installation}/summarize_time.cgi");
 $sel->title_is("No Bugs Selected");
 my $error_msg = trim($sel->get_text("error_msg"));
-ok($error_msg =~ /You apparently didn't choose any bugs for viewing/, "No data displayed");
+ok($error_msg =~ /You apparently didn't choose any bugs for viewing/,
+  "No data displayed");
 
 # Search for bugs which have some value in the Hours Worked field.
 
@@ -66,7 +73,7 @@ $sel->title_like(qr/^Time Summary \(\d+ bugs selected\)/);
 $sel->check_ok("monthly");
 $sel->check_ok("detailed");
 $sel->type_ok("start_date", "2009-01-01");
-$sel->type_ok("end_date", "2009-04-30");
+$sel->type_ok("end_date",   "2009-04-30");
 $sel->click_ok("summarize");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_like(qr/^Time Summary \(\d+ bugs selected\)/);
index 733f5edc3ea8e03f4c83168e8aa417828cec06bd..068c29b0bd1911e902ab55f471e09594b2f54bf7 100644 (file)
@@ -19,7 +19,8 @@ use QA::Util;
 my ($sel, $config) = get_selenium();
 
 log_in($sel, $config, 'admin');
-set_parameters($sel, { "Administrative Policies" => {"allowuserdeletion-on" => undef} });
+set_parameters($sel,
+  {"Administrative Policies" => {"allowuserdeletion-on" => undef}});
 
 # First delete test users, if not deleted correctly during a previous run.
 
@@ -46,7 +47,8 @@ $sel->click_ok("link=Add Group");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Add group");
 $sel->type_ok("name", "Slave");
-$sel->type_ok("desc", "Members of the Master group are also members of this group");
+$sel->type_ok("desc",
+  "Members of the Master group are also members of this group");
 $sel->uncheck_ok("isactive");
 ok(!$sel->is_checked("insertnew"), "Group not added to products by default");
 $sel->click_ok("create");
@@ -67,9 +69,9 @@ $sel->title_is('Search users');
 $sel->click_ok('link=add a new user');
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is('Add user');
-$sel->type_ok('email', 'master@selenium.bugzilla.org');
-$sel->type_ok('name', 'master-user');
-$sel->type_ok('password', 'selenium', 'Enter password');
+$sel->type_ok('email',        'master@selenium.bugzilla.org');
+$sel->type_ok('name',         'master-user');
+$sel->type_ok('password',     'selenium', 'Enter password');
 $sel->type_ok('disabledtext', 'Not for common usage');
 $sel->click_ok('add');
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
@@ -83,9 +85,9 @@ $sel->is_text_present_ok('The account has been added to the Master group');
 $sel->click_ok("link=add a new user");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is('Add user');
-$sel->type_ok('email', 'slave@selenium.bugzilla.org');
-$sel->type_ok('name', 'slave-user');
-$sel->type_ok('password', 'selenium', 'Enter password');
+$sel->type_ok('email',        'slave@selenium.bugzilla.org');
+$sel->type_ok('name',         'slave-user');
+$sel->type_ok('password',     'selenium', 'Enter password');
 $sel->type_ok('disabledtext', 'Not for common usage');
 $sel->click_ok('add');
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
@@ -99,9 +101,9 @@ $sel->is_text_present_ok('The account has been added to the Slave group');
 $sel->click_ok("link=add a new user");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is('Add user');
-$sel->type_ok('email', 'reg@selenium.bugzilla.org');
-$sel->type_ok('name', 'reg-user');
-$sel->type_ok('password', 'selenium', 'Enter password');
+$sel->type_ok('email',        'reg@selenium.bugzilla.org');
+$sel->type_ok('name',         'reg-user');
+$sel->type_ok('password',     'selenium', 'Enter password');
 $sel->type_ok('disabledtext', 'Not for common usage');
 $sel->click_ok('add');
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
@@ -115,9 +117,18 @@ $sel->title_is('Search users');
 $sel->select_ok('is_enabled', 'label=Enabled');
 $sel->click_ok('search');
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
-ok(!$sel->is_text_present('master@selenium.bugzilla.org'), 'Inactive user account master-user not listed by default');
-ok(!$sel->is_text_present('slave@selenium.bugzilla.org'), 'Inactive user account slave-user not listed by default');
-ok(!$sel->is_text_present('reg@selenium.bugzilla.org'), 'Inactive user account reg-user not displayed by default');
+ok(
+  !$sel->is_text_present('master@selenium.bugzilla.org'),
+  'Inactive user account master-user not listed by default'
+);
+ok(
+  !$sel->is_text_present('slave@selenium.bugzilla.org'),
+  'Inactive user account slave-user not listed by default'
+);
+ok(
+  !$sel->is_text_present('reg@selenium.bugzilla.org'),
+  'Inactive user account reg-user not displayed by default'
+);
 
 # Now make sure group inheritance works correctly.
 
@@ -125,27 +136,33 @@ $sel->click_ok('link=find other users');
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is('Search users');
 $sel->check_ok('grouprestrict');
-$sel->select_ok('groupid', 'label=Master');
-$sel->select_ok('matchtype', 'value=substr');
+$sel->select_ok('groupid',    'label=Master');
+$sel->select_ok('matchtype',  'value=substr');
 $sel->select_ok('is_enabled', 'label=All');
 $sel->click_ok('search');
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
-$sel->is_text_present_ok('master@selenium.bugzilla.org', 'master-user in Master group');
-ok(!$sel->is_text_present('slave@selenium.bugzilla.org'), 'slave-user not in Master group');
-ok(!$sel->is_text_present('reg@selenium.bugzilla.org'), 'reg-user not in Master group');
+$sel->is_text_present_ok('master@selenium.bugzilla.org',
+  'master-user in Master group');
+ok(!$sel->is_text_present('slave@selenium.bugzilla.org'),
+  'slave-user not in Master group');
+ok(!$sel->is_text_present('reg@selenium.bugzilla.org'),
+  'reg-user not in Master group');
 
 $sel->click_ok('link=find other users');
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is('Search users');
 $sel->check_ok('grouprestrict');
-$sel->select_ok('groupid', 'label=Slave');
-$sel->select_ok('matchtype', 'value=substr');
+$sel->select_ok('groupid',    'label=Slave');
+$sel->select_ok('matchtype',  'value=substr');
 $sel->select_ok('is_enabled', 'label=All');
 $sel->click_ok('search');
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
-$sel->is_text_present_ok('master@selenium.bugzilla.org', 'master-user in Slave group');
-$sel->is_text_present_ok('slave@selenium.bugzilla.org', 'slave-user in Slave group');
-ok(!$sel->is_text_present('reg@selenium.bugzilla.org'), 'reg-user not in Slave group');
+$sel->is_text_present_ok('master@selenium.bugzilla.org',
+  'master-user in Slave group');
+$sel->is_text_present_ok('slave@selenium.bugzilla.org',
+  'slave-user in Slave group');
+ok(!$sel->is_text_present('reg@selenium.bugzilla.org'),
+  'reg-user not in Slave group');
 
 # Add a regular expression to the Slave group.
 
@@ -168,27 +185,33 @@ $sel->click_ok("link=Users");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is('Search users');
 $sel->check_ok('grouprestrict');
-$sel->select_ok('groupid', 'label=Master');
-$sel->select_ok('matchtype', 'value=substr');
+$sel->select_ok('groupid',    'label=Master');
+$sel->select_ok('matchtype',  'value=substr');
 $sel->select_ok('is_enabled', 'label=All');
 $sel->click_ok('search');
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
-$sel->is_text_present_ok('master@selenium.bugzilla.org', 'master-user in Master group');
-ok(!$sel->is_text_present('slave@selenium.bugzilla.org'), 'slave-user not in Master group');
-ok(!$sel->is_text_present('reg@selenium.bugzilla.org'), 'reg-user not in Master group');
+$sel->is_text_present_ok('master@selenium.bugzilla.org',
+  'master-user in Master group');
+ok(!$sel->is_text_present('slave@selenium.bugzilla.org'),
+  'slave-user not in Master group');
+ok(!$sel->is_text_present('reg@selenium.bugzilla.org'),
+  'reg-user not in Master group');
 
 $sel->click_ok('link=find other users');
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is('Search users');
 $sel->check_ok('grouprestrict');
-$sel->select_ok('groupid', 'label=Slave');
-$sel->select_ok('matchtype', 'value=substr');
+$sel->select_ok('groupid',    'label=Slave');
+$sel->select_ok('matchtype',  'value=substr');
 $sel->select_ok('is_enabled', 'label=All');
 $sel->click_ok('search');
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
-$sel->is_text_present_ok('master@selenium.bugzilla.org', 'master-user in Slave group');
-$sel->is_text_present_ok('slave@selenium.bugzilla.org', 'slave-user in Slave group');
-$sel->is_text_present_ok('reg@selenium.bugzilla.org', 'reg-user in Slave group');
+$sel->is_text_present_ok('master@selenium.bugzilla.org',
+  'master-user in Slave group');
+$sel->is_text_present_ok('slave@selenium.bugzilla.org',
+  'slave-user in Slave group');
+$sel->is_text_present_ok('reg@selenium.bugzilla.org',
+  'reg-user in Slave group');
 
 # Remove created users and groups.
 
@@ -197,53 +220,54 @@ cleanup_groups($sel, $slave_gid);
 logout($sel);
 
 sub cleanup_users {
-    my $sel = shift;
-
-    go_to_admin($sel);
-    $sel->click_ok("link=Users");
+  my $sel = shift;
+
+  go_to_admin($sel);
+  $sel->click_ok("link=Users");
+  $sel->wait_for_page_to_load_ok(WAIT_TIME);
+  $sel->title_is("Search users");
+  $sel->type_ok('matchstr', '(master|slave|reg)@selenium.bugzilla.org');
+  $sel->select_ok('matchtype',  'value=regexp');
+  $sel->select_ok('is_enabled', 'label=All');
+  $sel->click_ok("search");
+  $sel->wait_for_page_to_load_ok(WAIT_TIME);
+  $sel->title_is("Select user");
+
+  foreach my $user ('master', 'slave', 'reg') {
+    my $login = $user . '@selenium.bugzilla.org';
+    next unless $sel->is_text_present($login);
+
+    $sel->click_ok("link=$login");
+    $sel->wait_for_page_to_load_ok(WAIT_TIME);
+    $sel->title_is("Edit user ${user}-user ($login)");
+    $sel->click_ok("delete");
     $sel->wait_for_page_to_load_ok(WAIT_TIME);
-    $sel->title_is("Search users");
-    $sel->type_ok('matchstr', '(master|slave|reg)@selenium.bugzilla.org');
-    $sel->select_ok('matchtype', 'value=regexp');
-    $sel->select_ok('is_enabled', 'label=All');
-    $sel->click_ok("search");
+    $sel->title_is("Confirm deletion of user $login");
+    ok(!$sel->is_text_present('You cannot delete this user account'),
+      'The user can be safely deleted');
+    $sel->click_ok("delete");
+    $sel->wait_for_page_to_load_ok(WAIT_TIME);
+    $sel->title_is("User $login deleted");
+    $sel->click_ok('link=show the user list again');
     $sel->wait_for_page_to_load_ok(WAIT_TIME);
-    $sel->title_is("Select user");
-
-    foreach my $user ('master', 'slave', 'reg') {
-        my $login = $user . '@selenium.bugzilla.org';
-        next unless $sel->is_text_present($login);
-
-        $sel->click_ok("link=$login");
-        $sel->wait_for_page_to_load_ok(WAIT_TIME);
-        $sel->title_is("Edit user ${user}-user ($login)");
-        $sel->click_ok("delete");
-        $sel->wait_for_page_to_load_ok(WAIT_TIME);
-        $sel->title_is("Confirm deletion of user $login");
-        ok(!$sel->is_text_present('You cannot delete this user account'), 'The user can be safely deleted');
-        $sel->click_ok("delete");
-        $sel->wait_for_page_to_load_ok(WAIT_TIME);
-        $sel->title_is("User $login deleted");
-        $sel->click_ok('link=show the user list again');
-        $sel->wait_for_page_to_load_ok(WAIT_TIME);
-        $sel->title_is('Select user');
-    }
+    $sel->title_is('Select user');
+  }
 }
 
 sub cleanup_groups {
-    my ($sel, $slave_gid) = @_;
-
-    go_to_admin($sel);
-    $sel->click_ok("link=Groups");
-    $sel->wait_for_page_to_load(WAIT_TIME);
-    $sel->title_is("Edit Groups");
-    $sel->click_ok("//a[\@href='editgroups.cgi?action=del&group=$slave_gid']");
-    $sel->wait_for_page_to_load(WAIT_TIME);
-    $sel->title_is("Delete group 'Slave'");
-    $sel->is_text_present_ok("Do you really want to delete this group?");
-    ok(!$sel->is_element_present("removeusers"), 'No direct members in this group');
-    $sel->click_ok("delete");
-    $sel->wait_for_page_to_load(WAIT_TIME);
-    $sel->title_is("Group Deleted");
-    $sel->is_text_present_ok("The group Slave has been deleted.");
+  my ($sel, $slave_gid) = @_;
+
+  go_to_admin($sel);
+  $sel->click_ok("link=Groups");
+  $sel->wait_for_page_to_load(WAIT_TIME);
+  $sel->title_is("Edit Groups");
+  $sel->click_ok("//a[\@href='editgroups.cgi?action=del&group=$slave_gid']");
+  $sel->wait_for_page_to_load(WAIT_TIME);
+  $sel->title_is("Delete group 'Slave'");
+  $sel->is_text_present_ok("Do you really want to delete this group?");
+  ok(!$sel->is_element_present("removeusers"), 'No direct members in this group');
+  $sel->click_ok("delete");
+  $sel->wait_for_page_to_load(WAIT_TIME);
+  $sel->title_is("Group Deleted");
+  $sel->is_text_present_ok("The group Slave has been deleted.");
 }
index 63f57820acd9e8f1d5fd1df8a073bbdcb88cd090..118c387e11af7ff49f4606017718943887fdde55 100644 (file)
@@ -19,17 +19,23 @@ use QA::Util;
 my ($sel, $config) = get_selenium();
 
 log_in($sel, $config, 'tweakparams');
-set_parameters($sel, { "User Matching"  => {"usemenuforusers-off" => undef,
-                                            "maxusermatches"      => {type => 'text', value => '0'},
-                                            "confirmuniqueusermatch-on" => undef},
-                       "Group Security" => {"usevisibilitygroups-off" => undef}
-                     });
+set_parameters(
+  $sel,
+  {
+    "User Matching" => {
+      "usemenuforusers-off"       => undef,
+      "maxusermatches"            => {type => 'text', value => '0'},
+      "confirmuniqueusermatch-on" => undef
+    },
+    "Group Security" => {"usevisibilitygroups-off" => undef}
+  }
+);
 
 file_bug_in_product($sel, "TestProduct");
 $sel->select_ok("component", "TestComponent");
 my $bug_summary = "Today is Tuesday";
 $sel->type_ok("short_desc", $bug_summary);
-$sel->type_ok("comment", "Poker Face");
+$sel->type_ok("comment",    "Poker Face");
 my $bug1_id = create_bug($sel, $bug_summary);
 
 # We enter an incomplete email address. process_bug.cgi must ask
@@ -40,7 +46,8 @@ $sel->type_ok("newcc", $config->{unprivileged_user_login_truncated});
 $sel->click_ok("commit");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Confirm Match");
-$sel->is_text_present_ok("$config->{unprivileged_user_login_truncated} matched");
+$sel->is_text_present_ok(
+  "$config->{unprivileged_user_login_truncated} matched");
 $sel->go_back_ok();
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_like(qr/^$bug1_id/);
@@ -77,7 +84,8 @@ $sel->is_text_present_ok("*$config->{common_email} matched:");
 
 # Now restrict 'maxusermatches'.
 
-set_parameters($sel, { "User Matching" => {"maxusermatches" => {type => 'text', value => '1'}} });
+set_parameters($sel,
+  {"User Matching" => {"maxusermatches" => {type => 'text', value => '1'}}});
 
 go_to_bug($sel, $bug1_id);
 $sel->click_ok("cc_edit_area_showhide");
@@ -103,9 +111,13 @@ edit_bug($sel, $bug1_id, $bug_summary);
 
 # Now turn on group visibility. It involves important security checks.
 
-set_parameters($sel, { "User Matching"  => {"maxusermatches" => {type => 'text', value => '2'}},
-                       "Group Security" => {"usevisibilitygroups-on" => undef}
-                     });
+set_parameters(
+  $sel,
+  {
+    "User Matching" => {"maxusermatches" => {type => 'text', value => '2'}},
+    "Group Security" => {"usevisibilitygroups-on" => undef}
+  }
+);
 
 # By default, groups are not visible to themselves, so we have to enable this.
 # The tweakparams user has not enough privs to do it himself.
@@ -121,11 +133,11 @@ $sel->wait_for_page_to_load(WAIT_TIME);
 $sel->title_is("Change Group: tweakparams");
 
 my @groups = $sel->get_select_options("visible_from_add");
-if (grep {$_ eq 'tweakparams'} @groups) {
-    $sel->add_selection_ok("visible_from_add", "label=tweakparams");
-    $sel->click_ok('//input[@value="Update Group"]');
-    $sel->wait_for_page_to_load_ok(WAIT_TIME);
-    $sel->title_is("Change Group: tweakparams");
+if (grep { $_ eq 'tweakparams' } @groups) {
+  $sel->add_selection_ok("visible_from_add", "label=tweakparams");
+  $sel->click_ok('//input[@value="Update Group"]');
+  $sel->wait_for_page_to_load_ok(WAIT_TIME);
+  $sel->title_is("Change Group: tweakparams");
 }
 logout($sel);
 log_in($sel, $config, 'tweakparams');
@@ -139,7 +151,8 @@ $sel->type_ok("newcc", $config->{unprivileged_user_login_truncated});
 $sel->click_ok("commit");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Match Failed");
-$sel->is_text_present_ok("$config->{unprivileged_user_login_truncated} did not match anything");
+$sel->is_text_present_ok(
+  "$config->{unprivileged_user_login_truncated} did not match anything");
 $sel->go_back_ok();
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_like(qr/^$bug1_id/);
@@ -152,7 +165,8 @@ $sel->type_ok("newcc", $config->{common_email});
 $sel->click_ok("commit");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Confirm Match");
-$sel->is_text_present_ok("$config->{common_email} matched more than the maximum of 2 users");
+$sel->is_text_present_ok(
+  "$config->{common_email} matched more than the maximum of 2 users");
 $sel->go_back_ok();
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_like(qr/^$bug1_id/);
@@ -168,21 +182,39 @@ $sel->is_text_present_ok("($config->{tweakparams_user_login})");
 
 # Now test user menus. It must NOT display users we are not allowed to see.
 
-set_parameters($sel, { "User Matching" => {"usemenuforusers-on" => undef} });
+set_parameters($sel, {"User Matching" => {"usemenuforusers-on" => undef}});
 
 go_to_bug($sel, $bug1_id);
 $sel->click_ok("cc_edit_area_showhide");
 my @cc = $sel->get_select_options("newcc");
-ok(!grep($_ =~ /$config->{unprivileged_user_login}/, @cc), "$config->{unprivileged_user_login} is not visible");
-ok(!grep($_ =~ /$config->{canconfirm_user_login}/, @cc), "$config->{canconfirm_user_login} is not visible");
-ok(grep($_ =~ /$config->{admin_user_login}/, @cc), "$config->{admin_user_login} is visible");
-ok(grep($_ =~ /$config->{tweakparams_user_login}/, @cc), "$config->{tweakparams_user_login} is visible");
+ok(
+  !grep($_ =~ /$config->{unprivileged_user_login}/, @cc),
+  "$config->{unprivileged_user_login} is not visible"
+);
+ok(
+  !grep($_ =~ /$config->{canconfirm_user_login}/, @cc),
+  "$config->{canconfirm_user_login} is not visible"
+);
+ok(
+  grep($_ =~ /$config->{admin_user_login}/, @cc),
+  "$config->{admin_user_login} is visible"
+);
+ok(
+  grep($_ =~ /$config->{tweakparams_user_login}/, @cc),
+  "$config->{tweakparams_user_login} is visible"
+);
 
 # Reset parameters.
 
-set_parameters($sel, { "User Matching"  => {"usemenuforusers-off" => undef,
-                                            "maxusermatches"      => {type => 'text', value => '0'},
-                                            "confirmuniqueusermatch-off" => undef},
-                       "Group Security" => {"usevisibilitygroups-off" => undef}
-                     });
+set_parameters(
+  $sel,
+  {
+    "User Matching" => {
+      "usemenuforusers-off"        => undef,
+      "maxusermatches"             => {type => 'text', value => '0'},
+      "confirmuniqueusermatch-off" => undef
+    },
+    "Group Security" => {"usevisibilitygroups-off" => undef}
+  }
+);
 logout($sel);
index c02edffaf9add0951269c03128df6a7c720383cc..8079461ecdf89c9aee2576181a8f0d0a9cf97ca5 100644 (file)
@@ -44,8 +44,10 @@ $sel->wait_for_page_to_load(WAIT_TIME);
 $sel->title_is("General Preferences");
 ok(!$sel->is_editable("skin"), "The 'skin' user preference is not editable");
 $sel->select_ok("state_addselfcc", "label=Site Default (Never)");
-$sel->select_ok("post_bug_submit_action", "label=Site Default (Show the updated bug)");
-ok(!$sel->is_editable("zoom_textareas"), "The 'zoom_textareas' user preference is not editable");
+$sel->select_ok("post_bug_submit_action",
+  "label=Site Default (Show the updated bug)");
+ok(!$sel->is_editable("zoom_textareas"),
+  "The 'zoom_textareas' user preference is not editable");
 $sel->click_ok("update");
 $sel->wait_for_page_to_load(WAIT_TIME);
 $sel->title_is("General Preferences");
@@ -56,7 +58,7 @@ file_bug_in_product($sel, 'TestProduct');
 $sel->value_is("cc", "");
 my $bug_summary = "First bug created";
 $sel->type_ok("short_desc", $bug_summary);
-$sel->type_ok("comment", "I'm not in the CC list.");
+$sel->type_ok("comment",    "I'm not in the CC list.");
 my $bug1_id = create_bug($sel, $bug_summary);
 
 $sel->value_is("addselfcc", "off");
@@ -65,7 +67,7 @@ $sel->select_ok("bug_status", "label=IN_PROGRESS");
 edit_bug($sel, $bug1_id, $bug_summary);
 $sel->click_ok("summary_edit_action");
 $sel->value_is("short_desc", $bug_summary);
-$sel->value_is("addselfcc", "off");
+$sel->value_is("addselfcc",  "off");
 
 # Create a saved search for the 'sel-tmp' tag.
 
@@ -92,7 +94,7 @@ file_bug_in_product($sel, 'TestProduct');
 $sel->value_is("cc", "");
 my $bug_summary2 = "My second bug";
 $sel->type_ok("short_desc", $bug_summary2);
-$sel->type_ok("comment", "Still not in the CC list");
+$sel->type_ok("comment",    "Still not in the CC list");
 my $bug2_id = create_bug($sel, $bug_summary2);
 $sel->value_is("addselfcc", "off");
 $sel->type_ok("tag", "sel-tmp");
@@ -128,6 +130,7 @@ $sel->click_ok("find_top");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Bug List");
 $sel->is_text_present_ok("2 bugs found");
+
 # We cannot remove tags from several bugs at once (bug 791584).
 go_to_bug($sel, $bug1_id);
 $sel->type_ok("tag", "");
@@ -151,9 +154,10 @@ $sel->click_ok("link=Preferences");
 $sel->wait_for_page_to_load(WAIT_TIME);
 $sel->title_is("General Preferences");
 ok(!$sel->is_editable("skin"), "The 'skin' user preference is not editable");
-$sel->select_ok("state_addselfcc", "label=Always");
+$sel->select_ok("state_addselfcc",        "label=Always");
 $sel->select_ok("post_bug_submit_action", "label=Show next bug in my list");
-ok(!$sel->is_editable("zoom_textareas"), "The 'zoom_textareas' user preference is not editable");
+ok(!$sel->is_editable("zoom_textareas"),
+  "The 'zoom_textareas' user preference is not editable");
 $sel->click_ok("update");
 $sel->wait_for_page_to_load(WAIT_TIME);
 $sel->title_is("General Preferences");
@@ -186,10 +190,13 @@ $sel->click_ok("link=$bug1_id");
 $sel->wait_for_page_to_load(WAIT_TIME);
 $sel->title_like(qr/^$bug1_id /);
 $sel->value_is("addselfcc", "on");
-$sel->type_ok("comment", "I should be CC'ed and then I should see the next bug.");
+$sel->type_ok("comment",
+  "I should be CC'ed and then I should see the next bug.");
 edit_bug($sel, $bug2_id, $bug_summary2);
 $sel->is_text_present_ok("The next bug in your list is bug $bug2_id");
-ok(!$sel->is_text_present("I should see the next bug"), "The updated bug is no longer displayed");
+ok(!$sel->is_text_present("I should see the next bug"),
+  "The updated bug is no longer displayed");
+
 # The user has no privs, so the short_desc field is not present.
 $sel->is_text_present("short_desc", "My second bug");
 $sel->value_is("addselfcc", "on");
index 158d2649f50f3e2c5c5357e19bb3802465d24fd3..05b96bd3a223f33bdcf624f1930bee5f3d8e7e7f 100644 (file)
@@ -26,7 +26,7 @@ file_bug_in_product($sel, 'TestProduct');
 ok(!$sel->is_editable("assigned_to"), "The assignee field is not editable");
 my $bug_summary = "Greetings from a powerless user";
 $sel->type_ok("short_desc", $bug_summary);
-$sel->type_ok("comment", "File a bug with an empty CC list");
+$sel->type_ok("comment",    "File a bug with an empty CC list");
 my $bug1_id = create_bug($sel, $bug_summary);
 logout($sel);
 
@@ -35,8 +35,11 @@ logout($sel);
 go_to_bug($sel, $bug1_id);
 ok(!$sel->is_element_present("commit"), "Button 'Commit' not available");
 my $text = trim($sel->get_text("//fieldset"));
-ok($text =~ /You need to log in before you can comment on or make changes to this bug./,
-   "Addl. comment box not displayed");
+ok(
+  $text
+    =~ /You need to log in before you can comment on or make changes to this bug./,
+  "Addl. comment box not displayed"
+);
 
 # Don't call log_in() here. We explicitly want to use the "log in" link
 # in the addl. comment box.
@@ -44,9 +47,18 @@ ok($text =~ /You need to log in before you can comment on or make changes to thi
 $sel->click_ok("link=log in");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Log in to Bugzilla");
-$sel->is_text_present_ok("Bugzilla needs a legitimate login and password to continue");
-$sel->type_ok("Bugzilla_login", $config->{unprivileged_user_login}, "Enter login name");
-$sel->type_ok("Bugzilla_password", $config->{unprivileged_user_passwd}, "Enter password");
+$sel->is_text_present_ok(
+  "Bugzilla needs a legitimate login and password to continue");
+$sel->type_ok(
+  "Bugzilla_login",
+  $config->{unprivileged_user_login},
+  "Enter login name"
+);
+$sel->type_ok(
+  "Bugzilla_password",
+  $config->{unprivileged_user_passwd},
+  "Enter password"
+);
 $sel->click_ok("log_in", undef, "Submit credentials");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_like(qr/^$bug1_id/, "Display bug $bug1_id");
@@ -54,7 +66,13 @@ $sel->title_like(qr/^$bug1_id/, "Display bug $bug1_id");
 # Neither the (edit) link nor the hidden form must exist, at all.
 # But the 'Commit' button does exist.
 
-ok(!$sel->is_element_present("bz_assignee_edit_action"), "No (edit) link displayed for the assignee");
-ok(!$sel->is_element_present("assigned_to"), "No hidden assignee field available");
+ok(
+  !$sel->is_element_present("bz_assignee_edit_action"),
+  "No (edit) link displayed for the assignee"
+);
+ok(
+  !$sel->is_element_present("assigned_to"),
+  "No hidden assignee field available"
+);
 $sel->is_element_present_ok("commit");
 logout($sel);
index 277a867ac571da0207f9a24740dfbd200f797fe4..4580101063ea084adf13e5cf9d22e578494b8279 100644 (file)
@@ -19,26 +19,35 @@ use QA::Util;
 my ($sel, $config) = get_selenium();
 
 unless ($config->{test_extensions}) {
-    ok(1, "this installation doesn't test extensions. Skipping test_votes.t completely.");
-    exit;
+  ok(1,
+    "this installation doesn't test extensions. Skipping test_votes.t completely.");
+  exit;
 }
 
 log_in($sel, $config, 'admin');
-set_parameters($sel, { "Bug Fields"              => {"useclassification-off" => undef},
-                       "Administrative Policies" => {"allowbugdeletion-on"   => undef}
-                     });
+set_parameters(
+  $sel,
+  {
+    "Bug Fields"              => {"useclassification-off" => undef},
+    "Administrative Policies" => {"allowbugdeletion-on"   => undef}
+  }
+);
 
 # Create a new product, so that we can safely play with vote settings.
 
 add_product($sel);
-$sel->type_ok("product", "Eureka");
-$sel->type_ok("description", "A great new product");
-$sel->type_ok("votesperuser", 10);
+$sel->type_ok("product",        "Eureka");
+$sel->type_ok("description",    "A great new product");
+$sel->type_ok("votesperuser",   10);
 $sel->type_ok("maxvotesperbug", 5);
 $sel->type_ok("votestoconfirm", 3);
-$sel->type_ok("component", "Pegasus");
-$sel->type_ok("comp_desc", "A constellation in the north hemisphere.");
-$sel->type_ok("initialowner", $config->{permanent_user}, "Setting the default owner");
+$sel->type_ok("component",      "Pegasus");
+$sel->type_ok("comp_desc",      "A constellation in the north hemisphere.");
+$sel->type_ok(
+  "initialowner",
+  $config->{permanent_user},
+  "Setting the default owner"
+);
 $sel->click_ok('add-product');
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Product Created");
@@ -46,11 +55,12 @@ $sel->title_is("Product Created");
 # Create a new bug with the CONFIRMED status.
 
 file_bug_in_product($sel, 'Eureka');
+
 # CONFIRMED must be the default bug status for users with editbugs privs.
 $sel->selected_label_is("bug_status", "CONFIRMED");
 my $bug_summary = "Aries";
 $sel->type_ok("short_desc", $bug_summary);
-$sel->type_ok("comment", "1st constellation");
+$sel->type_ok("comment",    "1st constellation");
 my $bug1_id = create_bug($sel, $bug_summary);
 
 # Now vote for this bug.
@@ -58,19 +68,25 @@ my $bug1_id = create_bug($sel, $bug_summary);
 $sel->click_ok("link=vote");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Change Votes");
+
 # No comment :-/
 my $full_text = trim($sel->get_body_text());
+
 # OK, this is not the most robust regexp, but that's better than nothing.
-ok($full_text =~ /only 5 votes allowed per bug in this product/,
-   "Notice about the number of votes allowed per bug displayed");
+ok(
+  $full_text =~ /only 5 votes allowed per bug in this product/,
+  "Notice about the number of votes allowed per bug displayed"
+);
 $sel->type_ok("bug_$bug1_id", 4);
 $sel->click_ok("change");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Change Votes");
 $full_text = trim($sel->get_body_text());
+
 # OK, we may get a false positive if another product has the exact same numbers,
 # but I have no better idea to check this information.
-ok($full_text =~ /4 votes used out of 10 allowed/, "Display the number of votes used");
+ok($full_text =~ /4 votes used out of 10 allowed/,
+  "Display the number of votes used");
 
 # File a new bug, now as UNCONFIRMED. We will confirm it by popular votes.
 
@@ -78,7 +94,7 @@ file_bug_in_product($sel, 'Eureka');
 $sel->select_ok("bug_status", "UNCONFIRMED");
 my $bug_summary2 = "Taurus";
 $sel->type_ok("short_desc", $bug_summary2);
-$sel->type_ok("comment", "2nd constellation");
+$sel->type_ok("comment",    "2nd constellation");
 my $bug2_id = create_bug($sel, $bug_summary2);
 
 # Put enough votes on this bug to confirm it by popular votes.
@@ -99,7 +115,7 @@ file_bug_in_product($sel, 'Eureka');
 $sel->select_ok("bug_status", "UNCONFIRMED");
 my $bug_summary3 = "Gemini";
 $sel->type_ok("short_desc", $bug_summary3);
-$sel->type_ok("comment", "3rd constellation");
+$sel->type_ok("comment",    "3rd constellation");
 my $bug3_id = create_bug($sel, $bug_summary3);
 
 # Vote for this bug, but remain below the threshold required
@@ -114,14 +130,18 @@ $sel->type_ok("bug_$bug3_id", 2);
 $sel->click_ok("change");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Change Votes");
+
 # Illegal change: max is 5 votes per bug!
 $sel->type_ok("bug_$bug2_id", 15);
 $sel->click_ok("change");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Illegal Vote");
 my $text = trim($sel->get_text("error_msg"));
-ok($text =~ /You may only use at most 5 votes for a single bug in the Eureka product, but you are trying to use 15/,
-   "Too many votes per bug");
+ok(
+  $text
+    =~ /You may only use at most 5 votes for a single bug in the Eureka product, but you are trying to use 15/,
+  "Too many votes per bug"
+);
 
 # FIXME - We cannot use go_back_ok() here, because Firefox complains about
 # POST data not being stored in its cache. As a workaround, we go to
@@ -139,8 +159,11 @@ $sel->click_ok("change");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Illegal Vote");
 $text = trim($sel->get_text("error_msg"));
-ok($text =~ /You tried to use 12 votes in the Eureka product, which exceeds the maximum of 10 votes for this product/,
-   "Too many votes for this product");
+ok(
+  $text
+    =~ /You tried to use 12 votes in the Eureka product, which exceeds the maximum of 10 votes for this product/,
+  "Too many votes for this product"
+);
 
 # Decrease the confirmation threshold so that $bug3 becomes confirmed.
 
@@ -151,7 +174,7 @@ $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Updating Product 'Eureka'");
 $full_text = trim($sel->get_body_text());
 ok($full_text =~ /Updated number of votes needed to confirm a bug from 3 to 2/,
-   "Confirming the new number of votes to confirm");
+  "Confirming the new number of votes to confirm");
 $sel->is_text_present_ok("Bug $bug3_id confirmed by number of votes");
 
 # Decrease the number of votes per bug so that $bug2 is updated.
@@ -164,9 +187,11 @@ $sel->click_ok("update-product");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Updating Product 'Eureka'");
 $full_text = trim($sel->get_body_text());
-ok($full_text =~ /Updated maximum votes per bug from 5 to 4/, "Confirming the new number of votes per bug");
-$sel->is_text_present_ok("removed votes for bug $bug2_id from " . $config->{admin_user_login}, undef,
-                         "Removed votes from the admin");
+ok($full_text =~ /Updated maximum votes per bug from 5 to 4/,
+  "Confirming the new number of votes per bug");
+$sel->is_text_present_ok(
+  "removed votes for bug $bug2_id from " . $config->{admin_user_login},
+  undef, "Removed votes from the admin");
 
 # Go check that $bug2 has been correctly updated.
 
@@ -185,7 +210,10 @@ $sel->click_ok("update-product");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Updating Product 'Eureka'");
 $full_text = trim($sel->get_body_text());
-ok($full_text =~ /Updated votes per user from 10 to 5/, "Confirming the new number of votes per user");
+ok(
+  $full_text =~ /Updated votes per user from 10 to 5/,
+  "Confirming the new number of votes per user"
+);
 $sel->is_text_present_ok("removed votes for bug");
 
 # Go check that $bug3 has been correctly updated.
@@ -204,15 +232,17 @@ $sel->click_ok("update-product");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Updating Product 'Eureka'");
 $full_text = trim($sel->get_body_text());
-ok($full_text =~ /The product no longer allows the UNCONFIRMED status/, "Disable UNCONFIRMED");
+ok($full_text =~ /The product no longer allows the UNCONFIRMED status/,
+  "Disable UNCONFIRMED");
 
 # File a new bug. UNCONFIRMED must not be listed as a valid bug status.
 
 file_bug_in_product($sel, "Eureka");
-ok(!scalar(grep {$_ eq "UNCONFIRMED"} $sel->get_select_options("bug_status")), "UNCONFIRMED not listed");
+ok(!scalar(grep { $_ eq "UNCONFIRMED" } $sel->get_select_options("bug_status")),
+  "UNCONFIRMED not listed");
 my $bug_summary4 = "Cancer";
 $sel->type_ok("short_desc", $bug_summary4);
-$sel->type_ok("comment", "4th constellation");
+$sel->type_ok("comment",    "4th constellation");
 my $bug4_id = create_bug($sel, $bug_summary4);
 
 # Now delete the 'Eureka' product.
@@ -225,8 +255,10 @@ $sel->click_ok('//a[@href="editproducts.cgi?action=del&product=Eureka"]');
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Delete Product 'Eureka'");
 $full_text = trim($sel->get_body_text());
-ok($full_text =~ /There are 4 bugs entered for this product/, "Display warning about existing bugs");
-ok($full_text =~ /Pegasus: A constellation in the north hemisphere/, "Display product description");
+ok($full_text =~ /There are 4 bugs entered for this product/,
+  "Display warning about existing bugs");
+ok($full_text =~ /Pegasus: A constellation in the north hemisphere/,
+  "Display product description");
 $sel->click_ok("delete");
 $sel->wait_for_page_to_load_ok(WAIT_TIME);
 $sel->title_is("Product Deleted");
index 3d5148062fe22b1e2fb2134a45c5bc589ca99e04..5a9cb91c5389ebff47c2168b3a093d3175e1a8f5 100644 (file)
@@ -17,215 +17,250 @@ use MIME::Base64 qw(encode_base64 decode_base64);
 use Test::More tests => 187;
 my ($config, $xmlrpc, $jsonrpc, $jsonrpc_get) = get_rpc_clients();
 
-use constant INVALID_BUG_ID => -1;
+use constant INVALID_BUG_ID    => -1;
 use constant INVALID_BUG_ALIAS => random_string(20);
-use constant PRIVS_USER => 'QA_Selenium_TEST';
+use constant PRIVS_USER        => 'QA_Selenium_TEST';
 
 sub attach {
-    my ($id, $override) = @_;
-    my %fields = (
-        ids  => [$id],
-        data => 'data-' . random_string(100),
-        file_name => 'file_name-' . random_string(60),
-        summary => 'summary-' . random_string(100),
-        content_type => 'text/plain',
-        comment => 'comment-' . random_string(100),
-    );
+  my ($id, $override) = @_;
+  my %fields = (
+    ids          => [$id],
+    data         => 'data-' . random_string(100),
+    file_name    => 'file_name-' . random_string(60),
+    summary      => 'summary-' . random_string(100),
+    content_type => 'text/plain',
+    comment      => 'comment-' . random_string(100),
+  );
 
-    foreach my $key (keys %{ $override || {} }) {
-        my $value = $override->{$key};
-        if (defined $value) {
-            $fields{$key} = $value;
-        }
-        else {
-            delete $fields{$key};
-        }
+  foreach my $key (keys %{$override || {}}) {
+    my $value = $override->{$key};
+    if (defined $value) {
+      $fields{$key} = $value;
+    }
+    else {
+      delete $fields{$key};
     }
-    return \%fields;
+  }
+  return \%fields;
 }
 
-my ($public_bug, $private_bug) =
-    $xmlrpc->bz_create_test_bugs('private');
-my $public_id = $public_bug->{id};
+my ($public_bug, $private_bug) = $xmlrpc->bz_create_test_bugs('private');
+my $public_id  = $public_bug->{id};
 my $private_id = $private_bug->{id};
 
 my @tests = (
-    # Permissions
-    { args  => attach($public_id),
-      error => 'You must log in',
-      test  => 'Logged-out user cannot add an attachment to a public bug',
-    },
-    { args  => attach($private_id),
-      error => "You must log in",
-      test  => 'Logged-out user cannot add an attachment to a private bug',
-    },
-    { user  => 'editbugs',
-      args  => attach($private_id),
-      error => "not authorized to access",
-      test  => "Editbugs user can't add an attachment to a private bug",
-    },
-
-    # Test ID parameter
-    { user  => 'unprivileged',
-      args  => attach(undef, { ids => undef }),
-      error => 'a ids argument',
-      test  => 'Failing to pass the "ids" param fails',
-    },
-    { user  => 'unprivileged',
-      args  => attach(INVALID_BUG_ID),
-      error => "not a valid bug number",
-      test  => 'Passing invalid bug id returns error "Invalid Bug ID"',
-    },
-    { user  => 'unprivileged',
-      args  => attach(''),
-      error => "You must enter a valid bug number",
-      test  => 'Passing empty bug id returns error "Invalid Bug ID"',
-    },
-    { user  => 'unprivileged',
-      args  => attach(INVALID_BUG_ALIAS),
-      error => "nor an alias to a bug",
-      test  => 'Passing invalid bug alias returns error "Invalid Bug Alias"',
-    },
-
-    # Test Comment parameter
-    { user  => 'unprivileged',
-      args  => attach($public_id, { data => undef }),
-      error => 'a data argument',
-      test  => 'Failing to pass the "data" parameter fails',
-    },
-    { user  => 'unprivileged',
-      args  => attach($public_id, { data => '' }),
-      error => "The file you are trying to attach is empty",
-      test  => 'Passing empty data fails',
-    },
-    { user  => 'unprivileged',
-      args  => attach($public_id, { data => random_string(300_000) }),
-      error => "Attachments cannot be more than",
-      test  => "Passing an attachment that's too large fails",
-    },
-
-    # Test the private parameter
-    { user  => 'unprivileged',
-      args  => attach($public_id, { is_private => 1 }),
-      error => 'attachments as private',
-      test  => 'Unprivileged user cannot add a private attachment'
-    },
-
-    # Content-type
-    { user  => 'unprivileged',
-      args  => attach($public_id, { content_type => 'foo/bar' }),
-      error => "Valid types must be of the form",
-      test  => "Well-formed but invalid content type fails",
-    },
-    { user  => 'unprivileged',
-      args  => attach($public_id, { content_type => undef }),
-      error => 'Valid types must be of the form',
-      test  => "Failing to pass content_type fails",
-    },
-    { user  => 'unprivileged',
-      args  => attach($public_id, { content_type => '' }),
-      error => 'Valid types must be of the form',
-      test  => "Empty content type fails",
-    },
-
-    # Summary
-    { user  => 'unprivileged',
-      args  => attach($public_id, { summary => undef }),
-      error => 'You must enter a description for the attachment',
-      test  => "Failing to pass summary fails",
-    },
-    { user  => 'unprivileged',
-      args  => attach($public_id, { summary => '' }),
-      error => 'You must enter a description for the attachment',
-      test  => "Empty summary fails",
-    },
-
-    # Filename
-    { user  => 'unprivileged',
-      args  => attach($public_id, { file_name => undef }),
-      error => 'You did not specify a file to attach',
-      test  => "Failing to pass file_name fails",
-    },
-    { user  => 'unprivileged',
-      args  => attach($public_id, { file_name => '' }),
-      error => 'You did not specify a file to attach',
-      test  => "Empty file_name fails",
-    },
-
-    # Success tests
-    { user => 'unprivileged',
-      args => attach($public_id),
-      test => 'Unprivileged user can add an attachment to a public bug',
-    },
-    { user => 'unprivileged',
-      args => attach($public_id, { is_patch => 1, content_type => undef }),
-      test => 'Attaching a patch with no content type works',
-    },
-    { user => 'unprivileged',
-      args => attach($public_id, { is_patch => 1,
-                     content_type => 'application/octet-stream' }),
-      test => 'Attaching a patch with a bad content_type works',
-    },
-    { user => PRIVS_USER,
-      args => attach($private_id),
-      test => 'Privileged user can add an attachment to a private bug',
-    },
-    { user => PRIVS_USER,
-      args => attach($public_id, { is_private => 1 }),
-      test => 'Insidergroup user can add a private attachment',
-    },
+
+  # Permissions
+  {
+    args  => attach($public_id),
+    error => 'You must log in',
+    test  => 'Logged-out user cannot add an attachment to a public bug',
+  },
+  {
+    args  => attach($private_id),
+    error => "You must log in",
+    test  => 'Logged-out user cannot add an attachment to a private bug',
+  },
+  {
+    user  => 'editbugs',
+    args  => attach($private_id),
+    error => "not authorized to access",
+    test  => "Editbugs user can't add an attachment to a private bug",
+  },
+
+  # Test ID parameter
+  {
+    user  => 'unprivileged',
+    args  => attach(undef, {ids => undef}),
+    error => 'a ids argument',
+    test  => 'Failing to pass the "ids" param fails',
+  },
+  {
+    user  => 'unprivileged',
+    args  => attach(INVALID_BUG_ID),
+    error => "not a valid bug number",
+    test  => 'Passing invalid bug id returns error "Invalid Bug ID"',
+  },
+  {
+    user  => 'unprivileged',
+    args  => attach(''),
+    error => "You must enter a valid bug number",
+    test  => 'Passing empty bug id returns error "Invalid Bug ID"',
+  },
+  {
+    user  => 'unprivileged',
+    args  => attach(INVALID_BUG_ALIAS),
+    error => "nor an alias to a bug",
+    test  => 'Passing invalid bug alias returns error "Invalid Bug Alias"',
+  },
+
+  # Test Comment parameter
+  {
+    user  => 'unprivileged',
+    args  => attach($public_id, {data => undef}),
+    error => 'a data argument',
+    test  => 'Failing to pass the "data" parameter fails',
+  },
+  {
+    user  => 'unprivileged',
+    args  => attach($public_id, {data => ''}),
+    error => "The file you are trying to attach is empty",
+    test  => 'Passing empty data fails',
+  },
+  {
+    user  => 'unprivileged',
+    args  => attach($public_id, {data => random_string(300_000)}),
+    error => "Attachments cannot be more than",
+    test  => "Passing an attachment that's too large fails",
+  },
+
+  # Test the private parameter
+  {
+    user  => 'unprivileged',
+    args  => attach($public_id, {is_private => 1}),
+    error => 'attachments as private',
+    test  => 'Unprivileged user cannot add a private attachment'
+  },
+
+  # Content-type
+  {
+    user  => 'unprivileged',
+    args  => attach($public_id, {content_type => 'foo/bar'}),
+    error => "Valid types must be of the form",
+    test  => "Well-formed but invalid content type fails",
+  },
+  {
+    user  => 'unprivileged',
+    args  => attach($public_id, {content_type => undef}),
+    error => 'Valid types must be of the form',
+    test  => "Failing to pass content_type fails",
+  },
+  {
+    user  => 'unprivileged',
+    args  => attach($public_id, {content_type => ''}),
+    error => 'Valid types must be of the form',
+    test  => "Empty content type fails",
+  },
+
+  # Summary
+  {
+    user  => 'unprivileged',
+    args  => attach($public_id, {summary => undef}),
+    error => 'You must enter a description for the attachment',
+    test  => "Failing to pass summary fails",
+  },
+  {
+    user  => 'unprivileged',
+    args  => attach($public_id, {summary => ''}),
+    error => 'You must enter a description for the attachment',
+    test  => "Empty summary fails",
+  },
+
+  # Filename
+  {
+    user  => 'unprivileged',
+    args  => attach($public_id, {file_name => undef}),
+    error => 'You did not specify a file to attach',
+    test  => "Failing to pass file_name fails",
+  },
+  {
+    user  => 'unprivileged',
+    args  => attach($public_id, {file_name => ''}),
+    error => 'You did not specify a file to attach',
+    test  => "Empty file_name fails",
+  },
+
+  # Success tests
+  {
+    user => 'unprivileged',
+    args => attach($public_id),
+    test => 'Unprivileged user can add an attachment to a public bug',
+  },
+  {
+    user => 'unprivileged',
+    args => attach($public_id, {is_patch => 1, content_type => undef}),
+    test => 'Attaching a patch with no content type works',
+  },
+  {
+    user => 'unprivileged',
+    args => attach(
+      $public_id, {is_patch => 1, content_type => 'application/octet-stream'}
+    ),
+    test => 'Attaching a patch with a bad content_type works',
+  },
+  {
+    user => PRIVS_USER,
+    args => attach($private_id),
+    test => 'Privileged user can add an attachment to a private bug',
+  },
+  {
+    user => PRIVS_USER,
+    args => attach($public_id, {is_private => 1}),
+    test => 'Insidergroup user can add a private attachment',
+  },
 );
 
-$jsonrpc_get->bz_call_fail('Bug.add_attachment', attach($public_id),
-    'must use HTTP POST', 'add_attachment fails over GET');
+$jsonrpc_get->bz_call_fail(
+  'Bug.add_attachment', attach($public_id),
+  'must use HTTP POST', 'add_attachment fails over GET'
+);
 
 foreach my $rpc ($jsonrpc, $xmlrpc) {
-    $rpc->bz_run_tests(tests => \@tests, method => 'Bug.add_attachment',
-                       post_success => \&post_success, pre_call => \&pre_call);
+  $rpc->bz_run_tests(
+    tests        => \@tests,
+    method       => 'Bug.add_attachment',
+    post_success => \&post_success,
+    pre_call     => \&pre_call
+  );
 }
 
 # We have to encode data manually when using JSON-RPC, else it fails.
 sub pre_call {
-    my ($t, $rpc) = @_;
-    return if !$rpc->isa('QA::RPC::JSONRPC');
-    return if !defined $t->{args}->{data};
+  my ($t, $rpc) = @_;
+  return if !$rpc->isa('QA::RPC::JSONRPC');
+  return if !defined $t->{args}->{data};
 
-    $t->{args}->{data} = encode_base64($t->{args}->{data}, '');
+  $t->{args}->{data} = encode_base64($t->{args}->{data}, '');
 }
 
 sub post_success {
-    my ($call, $t, $rpc) = @_;
-
-    my $ids = $call->result->{ids};
-    $call = $rpc->bz_call_success("Bug.attachments", {attachment_ids => $ids});
-    my $attachments = $call->result->{attachments};
-
-    foreach my $id (keys %$attachments) {
-        my $attachment = $attachments->{$id};
-        if ($t->{args}->{is_private}) {
-            ok($attachment->{is_private},
-               $rpc->TYPE . ": Attachment $id is private");
-        }
-        else {
-            ok(!$attachment->{is_private},
-               $rpc->TYPE . ": Attachment $id is NOT private");
-        }
-
-        if ($t->{args}->{is_patch}) {
-            is($attachment->{content_type}, 'text/plain',
-               $rpc->TYPE . ": Patch $id content type is text/plain");
-        }
-        else {
-            is($attachment->{content_type}, $t->{args}->{content_type},
-               $rpc->TYPE . ": Attachment $id content type is correct");
-        }
-
-        if ($rpc->isa('QA::RPC::JSONRPC')) {
-            # We encoded data in pre_call(), so we have to restore it to its original content.
-            $t->{args}->{data} = decode_base64($t->{args}->{data});
-            $attachment->{data} = decode_base64($attachment->{data});
-        }
-        is($attachment->{data}, $t->{args}->{data},
-           $rpc->TYPE . ": Attachment $id data is correct");
+  my ($call, $t, $rpc) = @_;
+
+  my $ids = $call->result->{ids};
+  $call = $rpc->bz_call_success("Bug.attachments", {attachment_ids => $ids});
+  my $attachments = $call->result->{attachments};
+
+  foreach my $id (keys %$attachments) {
+    my $attachment = $attachments->{$id};
+    if ($t->{args}->{is_private}) {
+      ok($attachment->{is_private}, $rpc->TYPE . ": Attachment $id is private");
+    }
+    else {
+      ok(!$attachment->{is_private}, $rpc->TYPE . ": Attachment $id is NOT private");
+    }
+
+    if ($t->{args}->{is_patch}) {
+      is($attachment->{content_type},
+        'text/plain', $rpc->TYPE . ": Patch $id content type is text/plain");
     }
+    else {
+      is(
+        $attachment->{content_type},
+        $t->{args}->{content_type},
+        $rpc->TYPE . ": Attachment $id content type is correct"
+      );
+    }
+
+    if ($rpc->isa('QA::RPC::JSONRPC')) {
+
+    # We encoded data in pre_call(), so we have to restore it to its original content.
+      $t->{args}->{data} = decode_base64($t->{args}->{data});
+      $attachment->{data} = decode_base64($attachment->{data});
+    }
+    is(
+      $attachment->{data},
+      $t->{args}->{data},
+      $rpc->TYPE . ": Attachment $id data is correct"
+    );
+  }
 }
index e3cf4c0460666a15195842e2d2ea76b1185ec7b8..44c1ea5b5acc87b31ecf1aac1569373bfbfe7e2f 100644 (file)
@@ -21,153 +21,182 @@ use Test::More tests => 141;
 
 my ($config, $xmlrpc, $jsonrpc, $jsonrpc_get) = get_rpc_clients();
 
-use constant INVALID_BUG_ID => -1;
+use constant INVALID_BUG_ID    => -1;
 use constant INVALID_BUG_ALIAS => 'aaaaaaa12345';
-use constant PRIVS_USER => 'QA_Selenium_TEST';
+use constant PRIVS_USER        => 'QA_Selenium_TEST';
 use constant TIMETRACKING_USER => 'admin';
 
-use constant TEST_COMMENT => '--- Test Comment From QA Tests ---';
+use constant TEST_COMMENT     => '--- Test Comment From QA Tests ---';
 use constant TOO_LONG_COMMENT => 'a' x 100000;
 
 my @tests = (
-    # Permissions
-    { args  => { id => 'public_bug', comment => TEST_COMMENT },
-      error => 'You must log in',
-      test  => 'Logged-out user cannot comment on a public bug',
-    },
-    { args  => { id => 'private_bug', comment => TEST_COMMENT },
-      error => "You must log in",
-      test  => 'Logged-out user cannot comment on a private bug',
-    },
-    { user  => 'unprivileged',
-      args  => { id => 'private_bug', comment => TEST_COMMENT },
-      error => "not authorized to access",
-      test  => "Unprivileged user can't comment on a private bug",
-    },
-
-    # Test ID parameter
-    { user  => 'unprivileged',
-      args  => { comment => TEST_COMMENT },
-      error => 'a id argument',
-      test  => 'Failing to pass the "id" param fails',
-    },
-    { user  => 'unprivileged',
-      args  => { id => INVALID_BUG_ID, comment => TEST_COMMENT },
-      error => "not a valid bug number",
-      test  => 'Passing invalid bug id returns error "Invalid Bug ID"',
-    },
-    { user  => 'unprivileged',
-      args  => { id => '', comment => TEST_COMMENT },
-      error => "You must enter a valid bug number",
-      test  => 'Passing empty bug id param returns error "Invalid Bug ID"',
-    },
-    { user  => 'unprivileged',
-      args  => { id => INVALID_BUG_ALIAS, comment => TEST_COMMENT },
-      error => "nor an alias to a bug",
-      test  => 'Passing invalid bug alias returns error "Invalid Bug Alias"',
-    },
-
-    # Test Comment parameter
-    { user  => 'unprivileged',
-      args  => { id => 'public_bug' },
-      error => 'a comment argument',
-      test  => 'Failing to pass the "comment" parameter fails',
-    },
-    { user  => 'unprivileged',
-      args  => { id => 'public_bug', comment => '' },
-      error => "a comment argument",
-      test  => 'Passing an empty comment fails',
-    },
-    { user  => 'unprivileged',
-      args  => { id => 'public_bug', comment => ' ' },
-      error => 'a comment argument',
-      test  => 'Passing only a space for comment fails',
-    },
-    { user  => 'unprivileged',
-      args  => { id => 'public_bug', comment => " \t\n\n\r\n\r\n\r " },
-      error => 'a comment argument',
-      test  => 'Passing only whitespace (including newlines) fails',
-    },
-    { user  => 'unprivileged',
-      args  => { id => 'public_bug', comment => TOO_LONG_COMMENT },
-      error => "cannot be longer than",
-      test  => "Passing a comment that's too long fails",
-    },
-
-    # Testing the "private" parameter happens in the tests for Bug.comments
-
-    # Test work_time parameter
-    # FIXME Should be testing permissions on the work_time parameter,
-    # but we currently have no way to verify whether or not time was
-    # added to the bug, and there's no error thrown if you lack perms.
-    { user  => 'admin',
-      args  => { id => 'public_bug', comment => TEST_COMMENT,
-                 work_time => 'aaa' },
-      error => "is not a numeric value",
-      test  => "Passing a non-numeric work_time fails",
-    },
-    { user  => 'admin',
-      args  => { id => 'public_bug', comment => TEST_COMMENT,
-                 work_time => '1234567890' },
-      error => 'more than the maximum',
-      test  => 'Passing too large of a work_time fails',
-    },
-    { user  => 'admin',
-      args  => { id => 'public_bug', comment => '',
-                 work_time => '1.0' },
-      error => 'a comment argument',
-      test  => 'Passing a work_time with an empty comment fails',
-    },
-
-    # Success tests
-    { user => 'unprivileged',
-      args => { id => 'public_bug', comment => TEST_COMMENT },
-      test => 'Unprivileged user can add a comment to a public bug',
-    },
-    { user => 'unprivileged',
-      args => { id => 'public_bug', comment => " \n" . TEST_COMMENT },
-      test => 'Can add a comment to a bug where the first line is whitespace',
-    },
-    { user => 'QA_Selenium_TEST',
-      args => { id => 'private_bug', comment => TEST_COMMENT },
-      test => 'Privileged user can add a comment to a private bug',
-      check_privacy => 1,
-    },
-    { user => 'QA_Selenium_TEST',
-      args => { id => 'public_bug', comment => TEST_COMMENT,
-                is_private => 1 },
-      test => 'Insidergroup user can add a private comment',
-      check_privacy => 1,
-    },
-    { user => 'admin',
-      args => { id => 'public_bug', comment => TEST_COMMENT,
-                work_time => '1.5' },
-      test => 'Timetracking user can add work_time to a bug',
-    },
-    # FIXME Need to verify that the comment added actually has work_time.
+
+  # Permissions
+  {
+    args  => {id => 'public_bug', comment => TEST_COMMENT},
+    error => 'You must log in',
+    test  => 'Logged-out user cannot comment on a public bug',
+  },
+  {
+    args  => {id => 'private_bug', comment => TEST_COMMENT},
+    error => "You must log in",
+    test  => 'Logged-out user cannot comment on a private bug',
+  },
+  {
+    user  => 'unprivileged',
+    args  => {id => 'private_bug', comment => TEST_COMMENT},
+    error => "not authorized to access",
+    test  => "Unprivileged user can't comment on a private bug",
+  },
+
+  # Test ID parameter
+  {
+    user  => 'unprivileged',
+    args  => {comment => TEST_COMMENT},
+    error => 'a id argument',
+    test  => 'Failing to pass the "id" param fails',
+  },
+  {
+    user  => 'unprivileged',
+    args  => {id => INVALID_BUG_ID, comment => TEST_COMMENT},
+    error => "not a valid bug number",
+    test  => 'Passing invalid bug id returns error "Invalid Bug ID"',
+  },
+  {
+    user  => 'unprivileged',
+    args  => {id => '', comment => TEST_COMMENT},
+    error => "You must enter a valid bug number",
+    test  => 'Passing empty bug id param returns error "Invalid Bug ID"',
+  },
+  {
+    user  => 'unprivileged',
+    args  => {id => INVALID_BUG_ALIAS, comment => TEST_COMMENT},
+    error => "nor an alias to a bug",
+    test  => 'Passing invalid bug alias returns error "Invalid Bug Alias"',
+  },
+
+  # Test Comment parameter
+  {
+    user  => 'unprivileged',
+    args  => {id => 'public_bug'},
+    error => 'a comment argument',
+    test  => 'Failing to pass the "comment" parameter fails',
+  },
+  {
+    user  => 'unprivileged',
+    args  => {id => 'public_bug', comment => ''},
+    error => "a comment argument",
+    test  => 'Passing an empty comment fails',
+  },
+  {
+    user  => 'unprivileged',
+    args  => {id => 'public_bug', comment => ' '},
+    error => 'a comment argument',
+    test  => 'Passing only a space for comment fails',
+  },
+  {
+    user  => 'unprivileged',
+    args  => {id => 'public_bug', comment => " \t\n\n\r\n\r\n\r "},
+    error => 'a comment argument',
+    test  => 'Passing only whitespace (including newlines) fails',
+  },
+  {
+    user  => 'unprivileged',
+    args  => {id => 'public_bug', comment => TOO_LONG_COMMENT},
+    error => "cannot be longer than",
+    test  => "Passing a comment that's too long fails",
+  },
+
+  # Testing the "private" parameter happens in the tests for Bug.comments
+
+  # Test work_time parameter
+  # FIXME Should be testing permissions on the work_time parameter,
+  # but we currently have no way to verify whether or not time was
+  # added to the bug, and there's no error thrown if you lack perms.
+  {
+    user  => 'admin',
+    args  => {id => 'public_bug', comment => TEST_COMMENT, work_time => 'aaa'},
+    error => "is not a numeric value",
+    test  => "Passing a non-numeric work_time fails",
+  },
+  {
+    user => 'admin',
+    args =>
+      {id => 'public_bug', comment => TEST_COMMENT, work_time => '1234567890'},
+    error => 'more than the maximum',
+    test  => 'Passing too large of a work_time fails',
+  },
+  {
+    user  => 'admin',
+    args  => {id => 'public_bug', comment => '', work_time => '1.0'},
+    error => 'a comment argument',
+    test  => 'Passing a work_time with an empty comment fails',
+  },
+
+  # Success tests
+  {
+    user => 'unprivileged',
+    args => {id => 'public_bug', comment => TEST_COMMENT},
+    test => 'Unprivileged user can add a comment to a public bug',
+  },
+  {
+    user => 'unprivileged',
+    args => {id => 'public_bug', comment => " \n" . TEST_COMMENT},
+    test => 'Can add a comment to a bug where the first line is whitespace',
+  },
+  {
+    user          => 'QA_Selenium_TEST',
+    args          => {id => 'private_bug', comment => TEST_COMMENT},
+    test          => 'Privileged user can add a comment to a private bug',
+    check_privacy => 1,
+  },
+  {
+    user          => 'QA_Selenium_TEST',
+    args          => {id => 'public_bug', comment => TEST_COMMENT, is_private => 1},
+    test          => 'Insidergroup user can add a private comment',
+    check_privacy => 1,
+  },
+  {
+    user => 'admin',
+    args => {id => 'public_bug', comment => TEST_COMMENT, work_time => '1.5'},
+    test => 'Timetracking user can add work_time to a bug',
+  },
+
+  # FIXME Need to verify that the comment added actually has work_time.
 );
 
-$jsonrpc_get->bz_call_fail('Bug.add_comment',
-    { id => 'public_bug', comment => TEST_COMMENT },
-    'must use HTTP POST', 'add_comment fails over GET');
+$jsonrpc_get->bz_call_fail(
+  'Bug.add_comment',
+  {id => 'public_bug', comment => TEST_COMMENT},
+  'must use HTTP POST',
+  'add_comment fails over GET'
+);
 
 foreach my $rpc ($jsonrpc, $xmlrpc) {
-    $rpc->bz_run_tests(tests => \@tests, method => 'Bug.add_comment',
-                       post_success => \&post_success);
+  $rpc->bz_run_tests(
+    tests        => \@tests,
+    method       => 'Bug.add_comment',
+    post_success => \&post_success
+  );
 }
 
 sub post_success {
-    my ($call, $t, $rpc) = @_;
-    return unless $t->{check_privacy};
-
-    my $comment_id = $call->result->{id};
-    my $result = $rpc->bz_call_success('Bug.comments', {comment_ids => [$comment_id]});
-    if ($t->{args}->{is_private}) {
-        ok($result->result->{comments}->{$comment_id}->{is_private},
-           $rpc->TYPE . ": Comment $comment_id is private");
-    }
-    else {
-        ok(!$result->result->{comments}->{$comment_id}->{is_private},
-           $rpc->TYPE . ": Comment $comment_id is NOT private");
-    }
+  my ($call, $t, $rpc) = @_;
+  return unless $t->{check_privacy};
+
+  my $comment_id = $call->result->{id};
+  my $result
+    = $rpc->bz_call_success('Bug.comments', {comment_ids => [$comment_id]});
+  if ($t->{args}->{is_private}) {
+    ok(
+      $result->result->{comments}->{$comment_id}->{is_private},
+      $rpc->TYPE . ": Comment $comment_id is private"
+    );
+  }
+  else {
+    ok(
+      !$result->result->{comments}->{$comment_id}->{is_private},
+      $rpc->TYPE . ": Comment $comment_id is NOT private"
+    );
+  }
 }
index cb70c3f4952ea9b8bc29590dcd8f77bd7a9cd52f..d24df60773bd7b3629c8a999ff6516d1b5713d2c 100644 (file)
@@ -27,43 +27,49 @@ my ($config, @clients) = get_rpc_clients();
 our %attachments;
 
 sub post_bug_success {
-    my ($call, $t) = @_;
-
-    my $bugs = $call->result->{bugs};
-    is(scalar keys %$bugs, 1, "Got exactly one bug")
-        or diag(Dumper($call->result));
-
-    my $bug_attachments = (values %$bugs)[0];
-    # Collect attachment ids
-    foreach my $alias (qw(public_bug private_bug)) {
-        foreach my $is_private (0, 1) {
-            my $find_desc = "${alias}_${is_private}";
-            my $attachment = first { $_->{summary} eq $find_desc }
-                                   reverse @$bug_attachments;
-            if ($attachment) {
-                $attachments{$find_desc} = $attachment->{id};
-            }
-        }
+  my ($call, $t) = @_;
+
+  my $bugs = $call->result->{bugs};
+  is(scalar keys %$bugs, 1, "Got exactly one bug") or diag(Dumper($call->result));
+
+  my $bug_attachments = (values %$bugs)[0];
+
+  # Collect attachment ids
+  foreach my $alias (qw(public_bug private_bug)) {
+    foreach my $is_private (0, 1) {
+      my $find_desc = "${alias}_${is_private}";
+      my $attachment = first { $_->{summary} eq $find_desc }
+      reverse @$bug_attachments;
+      if ($attachment) {
+        $attachments{$find_desc} = $attachment->{id};
+      }
     }
+  }
 }
 
 foreach my $rpc (@clients) {
-    $rpc->bz_run_tests(tests => STANDARD_BUG_TESTS, method => 'Bug.attachments',
-                       post_success => \&post_bug_success);
+  $rpc->bz_run_tests(
+    tests        => STANDARD_BUG_TESTS,
+    method       => 'Bug.attachments',
+    post_success => \&post_bug_success
+  );
 }
 
 foreach my $alias (qw(public_bug private_bug)) {
-    foreach my $is_private (0, 1) {
-        ok($attachments{"${alias}_${is_private}"},
-           "Found attachment id for ${alias}_${is_private}");
-    }
+  foreach my $is_private (0, 1) {
+    ok(
+      $attachments{"${alias}_${is_private}"},
+      "Found attachment id for ${alias}_${is_private}"
+    );
+  }
 }
 
 ####################
 # Attachment Tests #
 ####################
 
-my $content_file = $config->{bugzilla_path} . '/xt/config/generate_test_data.pl';
+my $content_file
+  = $config->{bugzilla_path} . '/xt/config/generate_test_data.pl';
 open(my $fh, '<', $content_file) or die "$content_file: $!";
 my $content;
 { local $/; $content = <$fh>; }
@@ -73,83 +79,103 @@ close($fh);
 # format of each return value is correct.
 
 my @tests = (
-    # Logged-out user
-    { args => { attachment_ids => [$attachments{'public_bug_0'}] },
-      test => 'Logged-out user can access public attachment on public'
-              . '  bug by id',
-    },
-    { args  => { attachment_ids => [$attachments{'public_bug_1'}] },
-      test  => 'Logged-out user cannot access private attachment on public bug',
-      error => 'Sorry, you are not authorized',
-    },
-    { args  => { attachment_ids => [$attachments{'private_bug_0'}] },
-      test  => 'Logged-out user cannot access attachments by id on private bug',
-      error => 'You are not authorized to access',
-    },
-    { args  => { attachment_ids => [$attachments{'private_bug_1'}] },
-      test  => 'Logged-out user cannot access private attachment on '
-               . ' private bug',
-      error => 'You are not authorized to access',
-    },
-
-    # Logged-in, unprivileged user.
-    { user => 'unprivileged',
-      args => { attachment_ids => [$attachments{'public_bug_0'}] },
-      test => 'Logged-in user can see a public attachment on a public bug by id',
-    },
-    { user  => 'unprivileged',
-      args  => { attachment_ids => [$attachments{'public_bug_1'}] },
-      test  => 'Logged-in user cannot access private attachment on public bug',
-      error => 'Sorry, you are not authorized',
-    },
-    { user  => 'unprivileged',
-      args  => { attachment_ids => [$attachments{'private_bug_0'}] },
-      test  => 'Logged-in user cannot access attachments by id on private bug',
-      error => "You are not authorized to access",
-    },
-    { user  => 'unprivileged',
-      args  => { attachment_ids => [$attachments{'private_bug_1'}] },
-      test  => 'Logged-in user cannot access private attachment on private bug',
-      error => "You are not authorized to access",
-    },
-
-    # User who can see private bugs and private attachments
-    { user => PRIVATE_BUG_USER,
-      args => { attachment_ids => [$attachments{'public_bug_1'}] },
-      test => PRIVATE_BUG_USER . ' can see private attachment on public bug',
-    },
-    { user  => PRIVATE_BUG_USER,
-      args  => { attachment_ids => [$attachments{'private_bug_1'}] },
-      test  => PRIVATE_BUG_USER . ' can see private attachment on private bug',
-    },
+
+  # Logged-out user
+  {
+    args => {attachment_ids => [$attachments{'public_bug_0'}]},
+    test => 'Logged-out user can access public attachment on public'
+      . '  bug by id',
+  },
+  {
+    args  => {attachment_ids => [$attachments{'public_bug_1'}]},
+    test  => 'Logged-out user cannot access private attachment on public bug',
+    error => 'Sorry, you are not authorized',
+  },
+  {
+    args  => {attachment_ids => [$attachments{'private_bug_0'}]},
+    test  => 'Logged-out user cannot access attachments by id on private bug',
+    error => 'You are not authorized to access',
+  },
+  {
+    args => {attachment_ids => [$attachments{'private_bug_1'}]},
+    test => 'Logged-out user cannot access private attachment on ' . ' private bug',
+    error => 'You are not authorized to access',
+  },
+
+  # Logged-in, unprivileged user.
+  {
+    user => 'unprivileged',
+    args => {attachment_ids => [$attachments{'public_bug_0'}]},
+    test => 'Logged-in user can see a public attachment on a public bug by id',
+  },
+  {
+    user  => 'unprivileged',
+    args  => {attachment_ids => [$attachments{'public_bug_1'}]},
+    test  => 'Logged-in user cannot access private attachment on public bug',
+    error => 'Sorry, you are not authorized',
+  },
+  {
+    user  => 'unprivileged',
+    args  => {attachment_ids => [$attachments{'private_bug_0'}]},
+    test  => 'Logged-in user cannot access attachments by id on private bug',
+    error => "You are not authorized to access",
+  },
+  {
+    user  => 'unprivileged',
+    args  => {attachment_ids => [$attachments{'private_bug_1'}]},
+    test  => 'Logged-in user cannot access private attachment on private bug',
+    error => "You are not authorized to access",
+  },
+
+  # User who can see private bugs and private attachments
+  {
+    user => PRIVATE_BUG_USER,
+    args => {attachment_ids => [$attachments{'public_bug_1'}]},
+    test => PRIVATE_BUG_USER . ' can see private attachment on public bug',
+  },
+  {
+    user => PRIVATE_BUG_USER,
+    args => {attachment_ids => [$attachments{'private_bug_1'}]},
+    test => PRIVATE_BUG_USER . ' can see private attachment on private bug',
+  },
 );
 
 sub post_success {
-    my ($call, $t, $rpc) = @_;
-    is(scalar keys %{ $call->result->{attachments} }, 1,
-       "Got exactly one attachment");
-    my $attachment = (values %{ $call->result->{attachments} })[0];
-
-    cmp_ok($attachment->{last_change_time}, '=~', $rpc->DATETIME_REGEX,
-           "last_change_time is in the right format");
-    cmp_ok($attachment->{creation_time}, '=~', $rpc->DATETIME_REGEX,
-           "creation_time is in the right format");
-    is($attachment->{is_obsolete}, 0, 'is_obsolete is 0');
-    cmp_ok($attachment->{bug_id}, '=~', qr/^\d+$/, "bug_id is an integer");
-    cmp_ok($attachment->{id}, '=~', qr/^\d+$/, "id is an integer");
-    is($attachment->{content_type}, 'application/x-perl',
-       "content_type is correct");
-    cmp_ok($attachment->{file_name}, '=~', qr/^\w+\.pl$/,
-           "filename is in the expected format");
-    is($attachment->{creator}, ($t->{user} ? $config->{QA_Selenium_TEST_user_login} : email_filter($config->{QA_Selenium_TEST_user_login})),
-       "creator is the correct user");
-    my $data = $attachment->{data};
-    $data = decode_base64($data) if $rpc->isa('QA::RPC::JSONRPC');
-    is($data, $content, 'data is correct');
-    is($attachment->{size}, length($data), "size matches data's size");
+  my ($call, $t, $rpc) = @_;
+  is(scalar keys %{$call->result->{attachments}}, 1,
+    "Got exactly one attachment");
+  my $attachment = (values %{$call->result->{attachments}})[0];
+
+  cmp_ok($attachment->{last_change_time},
+    '=~', $rpc->DATETIME_REGEX, "last_change_time is in the right format");
+  cmp_ok($attachment->{creation_time},
+    '=~', $rpc->DATETIME_REGEX, "creation_time is in the right format");
+  is($attachment->{is_obsolete}, 0, 'is_obsolete is 0');
+  cmp_ok($attachment->{bug_id}, '=~', qr/^\d+$/, "bug_id is an integer");
+  cmp_ok($attachment->{id},     '=~', qr/^\d+$/, "id is an integer");
+  is($attachment->{content_type}, 'application/x-perl',
+    "content_type is correct");
+  cmp_ok($attachment->{file_name},
+    '=~', qr/^\w+\.pl$/, "filename is in the expected format");
+  is(
+    $attachment->{creator},
+    (
+        $t->{user}
+      ? $config->{QA_Selenium_TEST_user_login}
+      : email_filter($config->{QA_Selenium_TEST_user_login})
+    ),
+    "creator is the correct user"
+  );
+  my $data = $attachment->{data};
+  $data = decode_base64($data) if $rpc->isa('QA::RPC::JSONRPC');
+  is($data,               $content,      'data is correct');
+  is($attachment->{size}, length($data), "size matches data's size");
 }
 
 foreach my $rpc (@clients) {
-    $rpc->bz_run_tests(method => 'Bug.attachments', tests => \@tests,
-                       post_success => \&post_success);
+  $rpc->bz_run_tests(
+    method       => 'Bug.attachments',
+    tests        => \@tests,
+    post_success => \&post_success
+  );
 }
index 107f866c365a0118517a5130920988eea68db172..60c178459db73642dcc129b22047fdc313fca786 100644 (file)
@@ -25,46 +25,50 @@ my ($config, @clients) = get_rpc_clients();
 # These gets populated when we call Bug.add_comment.
 our $creation_time;
 our %comments = (
-    public_comment_public_bug  => 0,
-    public_comment_private_bug  => 0,
-    private_comment_public_bug  => 0,
-    private_comment_private_bug => 0,
+  public_comment_public_bug   => 0,
+  public_comment_private_bug  => 0,
+  private_comment_public_bug  => 0,
+  private_comment_private_bug => 0,
 );
 
 sub test_comments {
-    my ($comments_returned, $call, $t, $rpc) = @_;
-
-    my $comment = $comments_returned->[0];
-    ok($comment->{bug_id}, "bug_id exists");
-    # FIXME At some point we should test attachment_id here.
-
-    if ($t->{args}->{comment_ids}) {
-        my $expected_id = $t->{args}->{comment_ids}->[0];
-        is($comment->{id}, $expected_id, "comment id is correct");
-
-        my %reverse_map = reverse %comments;
-        my $expected_text = $reverse_map{$expected_id};
-        is($comment->{text}, $expected_text, "comment has the correct text");
-
-        my $priv_login = $rpc->bz_config->{PRIVATE_BUG_USER . '_user_login'};
-        $priv_login = $t->{user} ? $priv_login : email_filter($priv_login);
-        is($comment->{creator}, $priv_login, "comment creator is correct");
-
-        my $creation_day;
-        if ($rpc->isa('QA::RPC::XMLRPC')) {
-            $creation_day = $creation_time->ymd('');
-        }
-        else {
-            $creation_day = $creation_time->ymd;
-        }
-        like($comment->{time}, qr/^\Q${creation_day}\ET\d\d:\d\d:\d\d/,
-             "comment time has the right format");
+  my ($comments_returned, $call, $t, $rpc) = @_;
+
+  my $comment = $comments_returned->[0];
+  ok($comment->{bug_id}, "bug_id exists");
+
+  # FIXME At some point we should test attachment_id here.
+
+  if ($t->{args}->{comment_ids}) {
+    my $expected_id = $t->{args}->{comment_ids}->[0];
+    is($comment->{id}, $expected_id, "comment id is correct");
+
+    my %reverse_map   = reverse %comments;
+    my $expected_text = $reverse_map{$expected_id};
+    is($comment->{text}, $expected_text, "comment has the correct text");
+
+    my $priv_login = $rpc->bz_config->{PRIVATE_BUG_USER . '_user_login'};
+    $priv_login = $t->{user} ? $priv_login : email_filter($priv_login);
+    is($comment->{creator}, $priv_login, "comment creator is correct");
+
+    my $creation_day;
+    if ($rpc->isa('QA::RPC::XMLRPC')) {
+      $creation_day = $creation_time->ymd('');
     }
     else {
-        foreach my $field (qw(id text creator time)) {
-            ok(defined $comment->{$field}, "$field is defined");
-        }
+      $creation_day = $creation_time->ymd;
     }
+    like(
+      $comment->{time},
+      qr/^\Q${creation_day}\ET\d\d:\d\d:\d\d/,
+      "comment time has the right format"
+    );
+  }
+  else {
+    foreach my $field (qw(id text creator time)) {
+      ok(defined $comment->{$field}, "$field is defined");
+    }
+  }
 }
 
 ################
@@ -72,16 +76,19 @@ sub test_comments {
 ################
 
 sub post_bug_success {
-    my ($call, $t) = @_;
-    my @bugs = values %{ $call->result->{bugs} };
-    is(scalar @bugs, 1, "Got exactly one bug");
-    my @comments = map { @{ $_->{comments} } } @bugs;
-    test_comments(\@comments, @_);
+  my ($call, $t) = @_;
+  my @bugs = values %{$call->result->{bugs}};
+  is(scalar @bugs, 1, "Got exactly one bug");
+  my @comments = map { @{$_->{comments}} } @bugs;
+  test_comments(\@comments, @_);
 }
 
 foreach my $rpc (@clients) {
-    $rpc->bz_run_tests(tests => STANDARD_BUG_TESTS, method => 'Bug.comments',
-                       post_success => \&post_bug_success);
+  $rpc->bz_run_tests(
+    tests        => STANDARD_BUG_TESTS,
+    method       => 'Bug.comments',
+    post_success => \&post_bug_success
+  );
 }
 
 ####################
@@ -91,89 +98,110 @@ foreach my $rpc (@clients) {
 # First, create comments using add_comment.
 my @add_comment_tests;
 foreach my $key (keys %comments) {
-    $key =~ /^([a-z]+)_comment_(\w+)$/;
-    my $is_private = ($1 eq 'private' ? 1 : 0);
-    my $bug_alias = $2;
-    push(@add_comment_tests, { args => { id => $bug_alias, comment => $key,
-                                         private => $is_private },
-                               test => "Add comment: $key",
-                               user => PRIVATE_BUG_USER });
+  $key =~ /^([a-z]+)_comment_(\w+)$/;
+  my $is_private = ($1 eq 'private' ? 1 : 0);
+  my $bug_alias = $2;
+  push(
+    @add_comment_tests,
+    {
+      args => {id => $bug_alias, comment => $key, private => $is_private},
+      test => "Add comment: $key",
+      user => PRIVATE_BUG_USER
+    }
+  );
 }
 
 # Set the comment id for each comment that we add, so we can test getting
 # them back, later.
 sub post_add {
-    my ($call, $t) = @_;
-    my $key = $t->{args}->{comment};
-    $comments{$key} = $call->result->{id};
+  my ($call, $t) = @_;
+  my $key = $t->{args}->{comment};
+  $comments{$key} = $call->result->{id};
 }
 
 $creation_time = DateTime->now();
+
 # We only need to create these comments once, with one of the interfaces.
 $clients[0]->bz_run_tests(
-    tests => \@add_comment_tests, method => 'Bug.add_comment',
-    post_success => \&post_add);
+  tests        => \@add_comment_tests,
+  method       => 'Bug.add_comment',
+  post_success => \&post_add
+);
 
 # Now check access on each private and public comment
 
 my @comment_tests = (
-    # Logged-out user
-    { args => { comment_ids => [$comments{'public_comment_public_bug'}] },
-      test => 'Logged-out user can access public comment on public bug by id',
-    },
-    { args  => { comment_ids => [$comments{'private_comment_public_bug'}] },
-      test  => 'Logged-out user cannot access private comment on public bug',
-      error => 'is private',
-    },
-    { args  => { comment_ids => [$comments{'public_comment_private_bug'}] },
-      test  => 'Logged-out user cannot access comments by id on private bug',
-      error => 'You are not authorized to access',
-    },
-    { args  => { comment_ids => [$comments{'private_comment_private_bug'}] },
-      test  => 'Logged-out user cannot access private comment on private bug',
-      error => 'You are not authorized to access',
-    },
-
-    # Logged-in, unprivileged user.
-    { user => 'unprivileged',
-      args => { comment_ids => [$comments{'public_comment_public_bug'}] },
-      test => 'Logged-in user can see a public comment on a public bug by id',
-    },
-    { user  => 'unprivileged',
-      args  => { comment_ids => [$comments{'private_comment_public_bug'}] },
-      test  => 'Logged-in user cannot access private comment on public bug',
-      error => 'is private',
-    },
-    { user  => 'unprivileged',
-      args  => { comment_ids => [$comments{'public_comment_private_bug'}] },
-      test  => 'Logged-in user cannot access comments by id on private bug',
-      error => "You are not authorized to access",
-    },
-    { user  => 'unprivileged',
-      args  => { comment_ids => [$comments{'private_comment_private_bug'}] },
-      test  => 'Logged-in user cannot access private comment on private bug',
-      error => "You are not authorized to access",
-    },
-
-    # User who can see private bugs and private comments
-    { user => PRIVATE_BUG_USER,
-      args => { comment_ids => [$comments{'private_comment_public_bug'}] },
-      test => PRIVATE_BUG_USER . ' can see private comment on public bug',
-    },
-    { user  => PRIVATE_BUG_USER,
-      args  => { comment_ids => [$comments{'private_comment_private_bug'}] },
-      test  => PRIVATE_BUG_USER . ' can see private comment on private bug',
-    },
+
+  # Logged-out user
+  {
+    args => {comment_ids => [$comments{'public_comment_public_bug'}]},
+    test => 'Logged-out user can access public comment on public bug by id',
+  },
+  {
+    args  => {comment_ids => [$comments{'private_comment_public_bug'}]},
+    test  => 'Logged-out user cannot access private comment on public bug',
+    error => 'is private',
+  },
+  {
+    args  => {comment_ids => [$comments{'public_comment_private_bug'}]},
+    test  => 'Logged-out user cannot access comments by id on private bug',
+    error => 'You are not authorized to access',
+  },
+  {
+    args  => {comment_ids => [$comments{'private_comment_private_bug'}]},
+    test  => 'Logged-out user cannot access private comment on private bug',
+    error => 'You are not authorized to access',
+  },
+
+  # Logged-in, unprivileged user.
+  {
+    user => 'unprivileged',
+    args => {comment_ids => [$comments{'public_comment_public_bug'}]},
+    test => 'Logged-in user can see a public comment on a public bug by id',
+  },
+  {
+    user  => 'unprivileged',
+    args  => {comment_ids => [$comments{'private_comment_public_bug'}]},
+    test  => 'Logged-in user cannot access private comment on public bug',
+    error => 'is private',
+  },
+  {
+    user  => 'unprivileged',
+    args  => {comment_ids => [$comments{'public_comment_private_bug'}]},
+    test  => 'Logged-in user cannot access comments by id on private bug',
+    error => "You are not authorized to access",
+  },
+  {
+    user  => 'unprivileged',
+    args  => {comment_ids => [$comments{'private_comment_private_bug'}]},
+    test  => 'Logged-in user cannot access private comment on private bug',
+    error => "You are not authorized to access",
+  },
+
+  # User who can see private bugs and private comments
+  {
+    user => PRIVATE_BUG_USER,
+    args => {comment_ids => [$comments{'private_comment_public_bug'}]},
+    test => PRIVATE_BUG_USER . ' can see private comment on public bug',
+  },
+  {
+    user => PRIVATE_BUG_USER,
+    args => {comment_ids => [$comments{'private_comment_private_bug'}]},
+    test => PRIVATE_BUG_USER . ' can see private comment on private bug',
+  },
 );
 
 sub post_comments {
-    my ($call) = @_;
-    my @comments = values %{ $call->result->{comments} };
-    is(scalar @comments, 1, "Got exactly one comment");
-    test_comments(\@comments, @_);
+  my ($call) = @_;
+  my @comments = values %{$call->result->{comments}};
+  is(scalar @comments, 1, "Got exactly one comment");
+  test_comments(\@comments, @_);
 }
 
 foreach my $rpc (@clients) {
-    $rpc->bz_run_tests(tests => \@comment_tests, method => 'Bug.comments',
-                       post_success => \&post_comments);
+  $rpc->bz_run_tests(
+    tests        => \@comment_tests,
+    method       => 'Bug.comments',
+    post_success => \&post_comments
+  );
 }
index f532a78c160abe2d8d46b07ff392f4288fae8700..77c1973136abc982550d02aede2e919c17e7410b 100644 (file)
@@ -32,212 +32,219 @@ my $bug_fields = create_bug_fields($config);
 # hash to contain all the possible $bug_fields values that
 # can be passed to createBug()
 my $fields = {
-    summary => {
-        undefined => {
-            faultstring => 'You must enter a summary for this bug',
-            value       => undef
-        },
-    },
-
-    product => {
-        undefined => { faultstring => 'You must select/enter a product.', value => undef },
-        invalid =>
-            { faultstring => 'does not exist', value => 'does-not-exist' },
-    },
-
-    component => {
-        undefined => {
-            faultstring => 'you must first choose a component',
-            value       => undef
-        },
-        invalid => {
-            faultstring => "There is no component named 'does-not-exist'",
-            value => 'does-not-exist'
-        },
-    },
-
-    version => {
-        undefined =>
-            { faultstring => 'You must select/enter a version.', value => undef },
-        invalid => {
-            faultstring => "There is no version named 'does-not-exist' in the",
-            value       => 'does-not-exist'
-        },
-    },
-    platform => {
-        undefined =>
-            { faultstring => 'You must select/enter a Hardware.',
-              value => '' },
-        invalid => {
-            faultstring => "There is no Hardware named 'does-not-exist'.",
-            value       => 'does-not-exist'
-        },
-    },
-
-    status => {
-        invalid => {
-            faultstring => "There is no status named 'does-not-exist'",
-            value       => 'does-not-exist'
-        },
-    },
-
-    severity => {
-        undefined =>
-            { faultstring => 'You must select/enter a Severity.',
-              value => '' },
-        invalid => {
-            faultstring => "There is no Severity named 'does-not-exist'.",
-            value       => 'does-not-exist'
-        },
-    },
-
-    priority => {
-        undefined =>
-            { faultstring => 'You must select/enter a Priority.',
-              value => '' },
-        invalid => {
-            faultstring => "There is no Priority named 'does-not-exist'.",
-            value       => 'does-not-exist'
-        },
-    },
-
-    op_sys => {
-        undefined => {
-            faultstring => 'You must select/enter a OS.',
-            value       => ''
-        },
-        invalid => {
-            faultstring => "There is no OS named 'does-not-exist'.",
-            value       => 'does-not-exist'
-        },
-    },
-
-    cc => {
-        invalid => {
-            faultstring => 'not a valid username',
-            value       => ['nonuserATbugillaDOTorg']
-        },
-    },
-
-    assigned_to => {
-        invalid => {
-            faultstring => "There is no user named 'does-not-exist'",
-            value       => 'does-not-exist'
-        },
-    },
-    qa_contact => {
-        invalid => {
-            faultstring => "There is no user named 'does-not-exist'",
-            value       => 'does-not-exist'
-        },
-    },
-    alias => {
-        long => {
-            faultstring => 'Bug aliases cannot be longer than 40 characters',
-            value       => 'MyyyyyyyyyyyyyyyyyyBugggggggggggggggggggggg'
-        },
-        existing => {
-            faultstring => 'Please choose another alias or visit that bug and change its alias.',
-            value       => 'public_bug'
-        },
-        numeric => {
-            faultstring => 'aliases cannot be merely numbers',
-            value       => '12345'
-        },
-        commma_or_space_separated => {
-            faultstring => 'contains one or more commas or spaces',
-            value       => ['Bug 12345']
-        },
-
-    },
-    groups => {
-        non_existent => {
-            faultstring => 'either this group does not exist, or you are not allowed to restrict bugs to this group',
-            value => [random_string(20)],
-        },
-    },
-    comment_is_private => {
-        invalid => {
-             faultstring => 'you are not allowed to.+comments.+private',
-             value => 1,
-        }
-    },
+  summary => {
+    undefined =>
+      {faultstring => 'You must enter a summary for this bug', value => undef},
+  },
+
+  product => {
+    undefined =>
+      {faultstring => 'You must select/enter a product.', value => undef},
+    invalid => {faultstring => 'does not exist', value => 'does-not-exist'},
+  },
+
+  component => {
+    undefined =>
+      {faultstring => 'you must first choose a component', value => undef},
+    invalid => {
+      faultstring => "There is no component named 'does-not-exist'",
+      value       => 'does-not-exist'
+    },
+  },
+
+  version => {
+    undefined =>
+      {faultstring => 'You must select/enter a version.', value => undef},
+    invalid => {
+      faultstring => "There is no version named 'does-not-exist' in the",
+      value       => 'does-not-exist'
+    },
+  },
+  platform => {
+    undefined => {faultstring => 'You must select/enter a Hardware.', value => ''},
+    invalid   => {
+      faultstring => "There is no Hardware named 'does-not-exist'.",
+      value       => 'does-not-exist'
+    },
+  },
+
+  status => {
+    invalid => {
+      faultstring => "There is no status named 'does-not-exist'",
+      value       => 'does-not-exist'
+    },
+  },
+
+  severity => {
+    undefined => {faultstring => 'You must select/enter a Severity.', value => ''},
+    invalid   => {
+      faultstring => "There is no Severity named 'does-not-exist'.",
+      value       => 'does-not-exist'
+    },
+  },
+
+  priority => {
+    undefined => {faultstring => 'You must select/enter a Priority.', value => ''},
+    invalid   => {
+      faultstring => "There is no Priority named 'does-not-exist'.",
+      value       => 'does-not-exist'
+    },
+  },
+
+  op_sys => {
+    undefined => {faultstring => 'You must select/enter a OS.', value => ''},
+    invalid   => {
+      faultstring => "There is no OS named 'does-not-exist'.",
+      value       => 'does-not-exist'
+    },
+  },
+
+  cc => {
+    invalid =>
+      {faultstring => 'not a valid username', value => ['nonuserATbugillaDOTorg']},
+  },
+
+  assigned_to => {
+    invalid => {
+      faultstring => "There is no user named 'does-not-exist'",
+      value       => 'does-not-exist'
+    },
+  },
+  qa_contact => {
+    invalid => {
+      faultstring => "There is no user named 'does-not-exist'",
+      value       => 'does-not-exist'
+    },
+  },
+  alias => {
+    long => {
+      faultstring => 'Bug aliases cannot be longer than 40 characters',
+      value       => 'MyyyyyyyyyyyyyyyyyyBugggggggggggggggggggggg'
+    },
+    existing => {
+      faultstring =>
+        'Please choose another alias or visit that bug and change its alias.',
+      value => 'public_bug'
+    },
+    numeric =>
+      {faultstring => 'aliases cannot be merely numbers', value => '12345'},
+    commma_or_space_separated => {
+      faultstring => 'contains one or more commas or spaces',
+      value       => ['Bug 12345']
+    },
+
+  },
+  groups => {
+    non_existent => {
+      faultstring =>
+        'either this group does not exist, or you are not allowed to restrict bugs to this group',
+      value => [random_string(20)],
+    },
+  },
+  comment_is_private => {
+    invalid =>
+      {faultstring => 'you are not allowed to.+comments.+private', value => 1,}
+  },
 };
 
-$jsonrpc_get->bz_call_fail('Bug.create', $bug_fields,
-    'must use HTTP POST', 'create fails over GET');
+$jsonrpc_get->bz_call_fail(
+  'Bug.create', $bug_fields,
+  'must use HTTP POST',
+  'create fails over GET'
+);
 
 my @tests = (
-    { args  => $bug_fields,
-      error => "You must log in",
-      test  => "Cannot file bugs as a logged-out user",
-    },
-    { user => PRIVATE_BUG_USER,
-      args => { %$bug_fields, product => 'QA-Selenium-TEST',
-                component => 'QA-Selenium-TEST',
-                target_milestone => 'QAMilestone',
-                version => 'QAVersion',
-                groups => ['QA-Selenium-TEST'],
-                # These are set here because we can't actually set them,
-                # and we need the values to be correct for post_success.
-                qa_contact => $config->{PRIVATE_BUG_USER . '_user_login'},
-                status => 'UNCONFIRMED' },
-      test => "Authorized user can file a bug against a group",
-    },
-    { user => PRIVATE_BUG_USER,
-      args => { %$bug_fields, comment_is_private => 1,
-                # These are here because PRIVATE_BUG_USER can't set them
-                # and we need their values to be correct for post_success.
-                assigned_to => $config->{'permanent_user'},
-                qa_contact => '',
-                status => 'UNCONFIRMED' },
-      test => "Insider can create a private description"
-    },
-    { user => 'editbugs',
-      args => $bug_fields,
-      test => "Creating a bug with standard values succeeds",
-    },
+  {
+    args  => $bug_fields,
+    error => "You must log in",
+    test  => "Cannot file bugs as a logged-out user",
+  },
+  {
+    user => PRIVATE_BUG_USER,
+    args => {
+      %$bug_fields,
+      product          => 'QA-Selenium-TEST',
+      component        => 'QA-Selenium-TEST',
+      target_milestone => 'QAMilestone',
+      version          => 'QAVersion',
+      groups           => ['QA-Selenium-TEST'],
+
+      # These are set here because we can't actually set them,
+      # and we need the values to be correct for post_success.
+      qa_contact => $config->{PRIVATE_BUG_USER . '_user_login'},
+      status     => 'UNCONFIRMED'
+    },
+    test => "Authorized user can file a bug against a group",
+  },
+  {
+    user => PRIVATE_BUG_USER,
+    args => {
+      %$bug_fields,
+      comment_is_private => 1,
+
+      # These are here because PRIVATE_BUG_USER can't set them
+      # and we need their values to be correct for post_success.
+      assigned_to => $config->{'permanent_user'},
+      qa_contact  => '',
+      status      => 'UNCONFIRMED'
+    },
+    test => "Insider can create a private description"
+  },
+  {
+    user => 'editbugs',
+    args => $bug_fields,
+    test => "Creating a bug with standard values succeeds",
+  },
 );
 
 # Convert the $fields tests into standard bz_run_tests format.
 foreach my $field (sort keys %$fields) {
-    my $test_values = $fields->{$field};
-    foreach my $test_name (sort keys %$test_values) {
-        my $input_fields = dclone($bug_fields);
-        my $check_value = $test_values->{$test_name}->{value};
-        my $error       = $test_values->{$test_name}->{faultstring};
-        $input_fields->{$field} = $check_value;
-        my $test = { user => 'editbugs', args => $input_fields,
-                     error => $error,
-                     test => "$field $test_name: fails as expected" };
-        push(@tests, $test);
-    }
+  my $test_values = $fields->{$field};
+  foreach my $test_name (sort keys %$test_values) {
+    my $input_fields = dclone($bug_fields);
+    my $check_value  = $test_values->{$test_name}->{value};
+    my $error        = $test_values->{$test_name}->{faultstring};
+    $input_fields->{$field} = $check_value;
+    my $test = {
+      user  => 'editbugs',
+      args  => $input_fields,
+      error => $error,
+      test  => "$field $test_name: fails as expected"
+    };
+    push(@tests, $test);
+  }
 }
 
 sub post_success {
-    my ($call, $t, $rpc) = @_;
+  my ($call, $t, $rpc) = @_;
 
-    my $id = $call->result->{id};
-    ok($id, $rpc->TYPE . ": Result has an id: $id");
+  my $id = $call->result->{id};
+  ok($id, $rpc->TYPE . ": Result has an id: $id");
 
-    my $get_call = $rpc->bz_call_success('Bug.get', { ids => [$id] });
-    my $bug = $get_call->result->{bugs}->[0];
+  my $get_call = $rpc->bz_call_success('Bug.get', {ids => [$id]});
+  my $bug = $get_call->result->{bugs}->[0];
 
-    my $expect = dclone $t->{args};
+  my $expect = dclone $t->{args};
 
-    my $comment_is_private = delete $expect->{comment_is_private};
-    $expect->{creator} = $rpc->bz_config->{$t->{user} . '_user_login'};
+  my $comment_is_private = delete $expect->{comment_is_private};
+  $expect->{creator} = $rpc->bz_config->{$t->{user} . '_user_login'};
 
-    my @fields = keys %$expect;
-    $rpc->bz_test_bug(\@fields, $bug, $expect, $t);
+  my @fields = keys %$expect;
+  $rpc->bz_test_bug(\@fields, $bug, $expect, $t);
 
-    my $comment_call = $rpc->bz_call_success('Bug.comments', { ids => [$id] });
-    my $comment = $comment_call->result->{bugs}->{$id}->{comments}->[0];
-    is($comment->{is_private} ? 1 : 0, $comment_is_private ? 1 : 0,
-       $rpc->TYPE . ": comment privacy is correct");
+  my $comment_call = $rpc->bz_call_success('Bug.comments', {ids => [$id]});
+  my $comment = $comment_call->result->{bugs}->{$id}->{comments}->[0];
+  is(
+    $comment->{is_private} ? 1 : 0,
+    $comment_is_private    ? 1 : 0,
+    $rpc->TYPE . ": comment privacy is correct"
+  );
 }
 
 foreach my $rpc ($jsonrpc, $xmlrpc) {
-    $rpc->bz_run_tests(tests => \@tests, method => 'Bug.create',
-                       post_success => \&post_success);
+  $rpc->bz_run_tests(
+    tests        => \@tests,
+    method       => 'Bug.create',
+    post_success => \&post_success
+  );
 }
index dfc41fab00be84ba64242cf1bda29f58c314f304..18157b5340b5259d98895eda70318291b64141dd 100644 (file)
@@ -21,203 +21,218 @@ my ($config, @clients) = get_rpc_clients();
 plan tests => ($config->{test_extensions} ? 1338 : 1320);
 
 use constant INVALID_FIELD_NAME => 'invalid_field';
-use constant INVALID_FIELD_ID => -1;
-sub GLOBAL_GENERAL_FIELDS {
-    my @fields = qw(
-        attach_data.thedata
-        attachments.description
-        attachments.filename
-        attachments.isobsolete
-        attachments.ispatch
-        attachments.isprivate
-        attachments.mimetype
-        attachments.submitter
-
-        flagtypes.name
-        requestees.login_name
-        setters.login_name
-
-        alias
-        assigned_to
-        blocked
-        bug_file_loc
-        bug_group
-        bug_id
-        cc
-        cclist_accessible
-        classification
-        commenter
-        content
-        creation_ts
-        days_elapsed
-        delta_ts
-        dependson
-        everconfirmed
-        keywords
-        longdesc
-        longdescs.isprivate
-        owner_idle_time
-        product
-        qa_contact
-        reporter
-        reporter_accessible
-        see_also
-        short_desc
-        status_whiteboard
-
-        deadline
-        estimated_time
-        percentage_complete
-        remaining_time
-        work_time
-    );
-    push(@fields, 'votes') if QA::Util::get_config()->{test_extensions};
+use constant INVALID_FIELD_ID   => -1;
 
-    return @fields;
+sub GLOBAL_GENERAL_FIELDS {
+  my @fields = qw(
+    attach_data.thedata
+    attachments.description
+    attachments.filename
+    attachments.isobsolete
+    attachments.ispatch
+    attachments.isprivate
+    attachments.mimetype
+    attachments.submitter
+
+    flagtypes.name
+    requestees.login_name
+    setters.login_name
+
+    alias
+    assigned_to
+    blocked
+    bug_file_loc
+    bug_group
+    bug_id
+    cc
+    cclist_accessible
+    classification
+    commenter
+    content
+    creation_ts
+    days_elapsed
+    delta_ts
+    dependson
+    everconfirmed
+    keywords
+    longdesc
+    longdescs.isprivate
+    owner_idle_time
+    product
+    qa_contact
+    reporter
+    reporter_accessible
+    see_also
+    short_desc
+    status_whiteboard
+
+    deadline
+    estimated_time
+    percentage_complete
+    remaining_time
+    work_time
+  );
+  push(@fields, 'votes') if QA::Util::get_config()->{test_extensions};
+
+  return @fields;
 }
 
 use constant STANDARD_SELECT_FIELDS =>
-    qw(bug_severity bug_status op_sys priority rep_platform resolution);
+  qw(bug_severity bug_status op_sys priority rep_platform resolution);
 
-use constant ALL_SELECT_FIELDS => (STANDARD_SELECT_FIELDS,
-    qw(cf_qa_status cf_single_select));
+use constant ALL_SELECT_FIELDS =>
+  (STANDARD_SELECT_FIELDS, qw(cf_qa_status cf_single_select));
 use constant PRODUCT_FIELDS => qw(version target_milestone component);
-use constant ALL_FIELDS => (GLOBAL_GENERAL_FIELDS, ALL_SELECT_FIELDS,
-                            PRODUCT_FIELDS);
+use constant ALL_FIELDS =>
+  (GLOBAL_GENERAL_FIELDS, ALL_SELECT_FIELDS, PRODUCT_FIELDS);
 use constant MANDATORY_FIELDS => qw(short_desc product version component);
 
 use constant PUBLIC_PRODUCT  => 'Another Product';
 use constant PRIVATE_PRODUCT => 'QA-Selenium-TEST';
 
 sub get_field {
-    my ($fields, $field) = @_;
-    return first { $_->{name} eq $field } @$fields;
+  my ($fields, $field) = @_;
+  return first { $_->{name} eq $field } @$fields;
 }
 
 sub get_products_from_field {
-    my $field = shift;
-    my %products;
-    foreach my $value (@{ $field->{values} }) {
-        foreach my $vis_value (@{ $value->{visibility_values} }) {
-            $products{$vis_value} = 1;
-        }
+  my $field = shift;
+  my %products;
+  foreach my $value (@{$field->{values}}) {
+    foreach my $vis_value (@{$value->{visibility_values}}) {
+      $products{$vis_value} = 1;
     }
-    return \%products;
+  }
+  return \%products;
 }
 
 our %field_ids;
 foreach my $rpc (@clients) {
-    my $call = $rpc->bz_call_success('Bug.fields');
-    my $fields = $call->result->{fields};
-    foreach my $field (ALL_FIELDS) {
-        my $field_data = get_field($fields, $field);
-        ok($field_data, "$field is in the returned result")
-            or diag(Dumper($fields));
-        $field_ids{$field} = $field_data->{id};
-
-        if (grep($_ eq $field, MANDATORY_FIELDS)) {
-            ok($field_data->{is_mandatory}, "$field is mandatory");
-        }
-        else {
-            ok(!$field_data->{is_mandatory}, "$field is not mandatory");
-        }
+  my $call   = $rpc->bz_call_success('Bug.fields');
+  my $fields = $call->result->{fields};
+  foreach my $field (ALL_FIELDS) {
+    my $field_data = get_field($fields, $field);
+    ok($field_data, "$field is in the returned result") or diag(Dumper($fields));
+    $field_ids{$field} = $field_data->{id};
+
+    if (grep($_ eq $field, MANDATORY_FIELDS)) {
+      ok($field_data->{is_mandatory}, "$field is mandatory");
     }
-
-    foreach my $field (ALL_SELECT_FIELDS, PRODUCT_FIELDS) {
-        my $field_data = get_field($fields, $field);
-        ok(defined $field_data->{visibility_values},
-           "$field has visibility_values defined");
-        my $field_vis_undefs = grep { !defined $_ }
-                                    @{ $field_data->{visibility_values} };
-        is($field_vis_undefs, 0, "$field.visibility_values has no undefs")
-          or diag(Dumper($field_data->{visibility_values}));
-
-        ok(defined $field_data->{values},
-           "$field has 'values' defined");
-        my $num_values = scalar @{ $field_data->{values} };
-        ok($num_values, "$field has $num_values values");
-        # The first bug status is a fake one and has no name, so we choose the 2nd item.
-        my $first_value = $field_data->{values}->[1];
-        ok(defined $first_value->{name}, 'The first value has a name')
-            or diag(Dumper($field_data->{values}));
-        # The sortkey for milestones can be negative.
-        cmp_ok($first_value->{sortkey}, '=~', qr/^-?\d+$/,
-               "The first value has a numeric sortkey");
-
-        ok(defined $first_value->{visibility_values},
-           "$field has visibilty_values defined on its first value")
-            or diag(Dumper($field_data->{values}));
-        my @value_visibility_values = map { @{ $_->{visibility_values} } }
-                                      @{ $field_data->{values} };
-        my $undefs = grep { !defined $_ } @value_visibility_values;
-        is($undefs, 0,
-           "$field.values.visibility_values has no undefs");
+    else {
+      ok(!$field_data->{is_mandatory}, "$field is not mandatory");
     }
+  }
 
-    foreach my $field (PRODUCT_FIELDS) {
-        my $field_data = get_field($fields, $field);
-        is($field_data->{value_field}, 'product',
-           "The value_field for $field is 'product'");
-        my $products = get_products_from_field($field_data);
-        ok($products->{+PUBLIC_PRODUCT},
-           "$field values are returned for the public product");
-        ok(!$products->{+PRIVATE_PRODUCT},
-           "No $field values are returned for the private product");
-    }
+  foreach my $field (ALL_SELECT_FIELDS, PRODUCT_FIELDS) {
+    my $field_data = get_field($fields, $field);
+    ok(
+      defined $field_data->{visibility_values},
+      "$field has visibility_values defined"
+    );
+    my $field_vis_undefs = grep { !defined $_ } @{$field_data->{visibility_values}};
+    is($field_vis_undefs, 0, "$field.visibility_values has no undefs")
+      or diag(Dumper($field_data->{visibility_values}));
+
+    ok(defined $field_data->{values}, "$field has 'values' defined");
+    my $num_values = scalar @{$field_data->{values}};
+    ok($num_values, "$field has $num_values values");
+
+    # The first bug status is a fake one and has no name, so we choose the 2nd item.
+    my $first_value = $field_data->{values}->[1];
+    ok(defined $first_value->{name}, 'The first value has a name')
+      or diag(Dumper($field_data->{values}));
+
+    # The sortkey for milestones can be negative.
+    cmp_ok($first_value->{sortkey},
+      '=~', qr/^-?\d+$/, "The first value has a numeric sortkey");
+
+    ok(defined $first_value->{visibility_values},
+      "$field has visibilty_values defined on its first value")
+      or diag(Dumper($field_data->{values}));
+    my @value_visibility_values
+      = map { @{$_->{visibility_values}} } @{$field_data->{values}};
+    my $undefs = grep { !defined $_ } @value_visibility_values;
+    is($undefs, 0, "$field.values.visibility_values has no undefs");
+  }
+
+  foreach my $field (PRODUCT_FIELDS) {
+    my $field_data = get_field($fields, $field);
+    is($field_data->{value_field},
+      'product', "The value_field for $field is 'product'");
+    my $products = get_products_from_field($field_data);
+    ok($products->{+PUBLIC_PRODUCT},
+      "$field values are returned for the public product");
+    ok(!$products->{+PRIVATE_PRODUCT},
+      "No $field values are returned for the private product");
+  }
 }
 
 my @all_tests = (
-    { args => { ids   => [values %field_ids],
-                names => [ALL_FIELDS] },
-                test => 'Getting all fields by name and id simultaneously',
-                count => scalar ALL_FIELDS
-    },
-    { args  => { names => [INVALID_FIELD_NAME] },
-      error => "There is no field named",
-      test  => 'Invalid field name'
-    },
-    { args  => { ids => [INVALID_FIELD_ID] },
-      error => 'must be numeric',
-      test  => 'Invalid field id'
-    },
-    { user  => 'QA_Selenium_TEST',
-      args  => { names => [PRODUCT_FIELDS] },
-      test  => 'Getting product-specific fields as a privileged user',
-      count => scalar PRODUCT_FIELDS,
-      product_private_values => 1
-    },
+  {
+    args  => {ids => [values %field_ids], names => [ALL_FIELDS]},
+    test  => 'Getting all fields by name and id simultaneously',
+    count => scalar ALL_FIELDS
+  },
+  {
+    args  => {names => [INVALID_FIELD_NAME]},
+    error => "There is no field named",
+    test  => 'Invalid field name'
+  },
+  {
+    args  => {ids => [INVALID_FIELD_ID]},
+    error => 'must be numeric',
+    test  => 'Invalid field id'
+  },
+  {
+    user  => 'QA_Selenium_TEST',
+    args  => {names => [PRODUCT_FIELDS]},
+    test  => 'Getting product-specific fields as a privileged user',
+    count => scalar PRODUCT_FIELDS,
+    product_private_values => 1
+  },
 );
 
 foreach my $field (ALL_FIELDS) {
-    push(@all_tests,
-         { args => { names => [$field] },
-           test => "Logged-out users can get the $field field by name" });
-    push(@all_tests,
-         { args => { ids => [$field_ids{$field}] },
-           test => "Logged-out users can get the $field by id" });
+  push(
+    @all_tests,
+    {
+      args => {names => [$field]},
+      test => "Logged-out users can get the $field field by name"
+    }
+  );
+  push(
+    @all_tests,
+    {
+      args => {ids => [$field_ids{$field}]},
+      test => "Logged-out users can get the $field by id"
+    }
+  );
 }
 
 sub post_success {
-    my ($call, $t) = @_;
-    my $fields = $call->result->{fields};
-    my $count = $t->{count};
-    $count = 1 if !defined $count;
-    is(scalar @$fields, $count, "Exactly $count field(s) returned");
-
-    if ($t->{product_private_values}) {
-        foreach my $field (@$fields) {
-            my $name = $field->{name};
-            my $field_data = get_field($fields, $name);
-            my $products = get_products_from_field($field_data);
-            ok($products->{+PUBLIC_PRODUCT},
-               "$name values are returned for the public product");
-            ok($products->{+PRIVATE_PRODUCT},
-               "$name values are returned for the private product");
-        }
+  my ($call, $t) = @_;
+  my $fields = $call->result->{fields};
+  my $count  = $t->{count};
+  $count = 1 if !defined $count;
+  is(scalar @$fields, $count, "Exactly $count field(s) returned");
+
+  if ($t->{product_private_values}) {
+    foreach my $field (@$fields) {
+      my $name       = $field->{name};
+      my $field_data = get_field($fields, $name);
+      my $products   = get_products_from_field($field_data);
+      ok($products->{+PUBLIC_PRODUCT},
+        "$name values are returned for the public product");
+      ok($products->{+PRIVATE_PRODUCT},
+        "$name values are returned for the private product");
     }
+  }
 }
 
 foreach my $rpc (@clients) {
-    $rpc->bz_run_tests(tests => \@all_tests,  method => 'Bug.fields',
-                       post_success => \&post_success);
+  $rpc->bz_run_tests(
+    tests        => \@all_tests,
+    method       => 'Bug.fields',
+    post_success => \&post_success
+  );
 }
index 71c2bb394ad3bdd26f6b9ad6fb31c34cd8943ec5..f36bd209d6d743265b8aacbf309b72b1f150864c 100644 (file)
@@ -27,124 +27,147 @@ my $xmlrpc = $clients[0];
 our $creation_time = DateTime->now();
 our ($public_bug, $private_bug) = $xmlrpc->bz_create_test_bugs('private');
 my $private_id = $private_bug->{id};
-my $public_id = $public_bug->{id};
+my $public_id  = $public_bug->{id};
 
-my $base_url = $config->{browser_url} . "/"
-              . $config->{bugzilla_installation} . '/';
+my $base_url
+  = $config->{browser_url} . "/" . $config->{bugzilla_installation} . '/';
 
 # Set a few fields on the private bug, including setting up
 # a dependency relationship.
 $xmlrpc->bz_log_in(PRIVATE_BUG_USER);
-$xmlrpc->bz_call_success('Bug.update', {
-    ids => [$private_id],
-    blocks => { set => [$public_id] },
-    dupe_of => $public_id,
+$xmlrpc->bz_call_success(
+  'Bug.update',
+  {
+    ids                   => [$private_id],
+    blocks                => {set => [$public_id]},
+    dupe_of               => $public_id,
     is_creator_accessible => 0,
-    keywords => { set => ['test-keyword-1', 'test-keyword-2'] },
-    see_also => { add => ["${base_url}show_bug.cgi?id=$public_id",
-                          "http://landfill.bugzilla.org/show_bug.cgi?id=123456"] },
-    cf_qa_status => ['in progress', 'verified'],
+    keywords              => {set => ['test-keyword-1', 'test-keyword-2']},
+    see_also              => {
+      add => [
+        "${base_url}show_bug.cgi?id=$public_id",
+        "http://landfill.bugzilla.org/show_bug.cgi?id=123456"
+      ]
+    },
+    cf_qa_status     => ['in progress', 'verified'],
     cf_single_select => 'two',
-}, 'Update the private bug');
+  },
+  'Update the private bug'
+);
 $xmlrpc->bz_call_success('User.logout');
 
-$private_bug->{blocks} = [$public_id];
-$private_bug->{dupe_of} = $public_id;
-$private_bug->{status} = 'RESOLVED';
-$private_bug->{is_open} = 0;
-$private_bug->{resolution} = 'DUPLICATE';
+$private_bug->{blocks}                = [$public_id];
+$private_bug->{dupe_of}               = $public_id;
+$private_bug->{status}                = 'RESOLVED';
+$private_bug->{is_open}               = 0;
+$private_bug->{resolution}            = 'DUPLICATE';
 $private_bug->{is_creator_accessible} = 0;
-$private_bug->{is_cc_accessible} = 1;
-$private_bug->{keywords} = ['test-keyword-1', 'test-keyword-2'];
-$private_bug->{see_also} = ["${base_url}show_bug.cgi?id=$public_id",
-                            "http://landfill.bugzilla.org/show_bug.cgi?id=123456"];
+$private_bug->{is_cc_accessible}      = 1;
+$private_bug->{keywords}              = ['test-keyword-1', 'test-keyword-2'];
+$private_bug->{see_also}              = [
+  "${base_url}show_bug.cgi?id=$public_id",
+  "http://landfill.bugzilla.org/show_bug.cgi?id=123456"
+];
 $private_bug->{cf_qa_status} = ['in progress', 'verified'];
 $private_bug->{cf_single_select} = 'two';
 
-$public_bug->{depends_on} = [$private_id];
-$public_bug->{dupe_of} = undef;
-$public_bug->{resolution} = '';
-$public_bug->{is_open} = 1;
+$public_bug->{depends_on}            = [$private_id];
+$public_bug->{dupe_of}               = undef;
+$public_bug->{resolution}            = '';
+$public_bug->{is_open}               = 1;
 $public_bug->{is_creator_accessible} = 1;
-$public_bug->{is_cc_accessible} = 1;
-$public_bug->{keywords} = [];
+$public_bug->{is_cc_accessible}      = 1;
+$public_bug->{keywords}              = [];
+
 # Local Bugzilla bugs are automatically updated.
-$public_bug->{see_also} = ["${base_url}show_bug.cgi?id=$private_id"];
-$public_bug->{cf_qa_status} = [];
+$public_bug->{see_also}         = ["${base_url}show_bug.cgi?id=$private_id"];
+$public_bug->{cf_qa_status}     = [];
 $public_bug->{cf_single_select} = '---';
 
 # Fill in the timetracking fields on the public bug.
 $xmlrpc->bz_log_in('admin');
-$xmlrpc->bz_call_success('Bug.update', {
-    ids => [$public_id],
-    deadline => '2038-01-01',
+$xmlrpc->bz_call_success(
+  'Bug.update',
+  {
+    ids            => [$public_id],
+    deadline       => '2038-01-01',
     estimated_time => '10.0',
     remaining_time => '5.0',
-});
+  }
+);
 $xmlrpc->bz_call_success('User.logout');
 
 # Populate other fields.
-$public_bug->{classification} = 'Unclassified';
+$public_bug->{classification}  = 'Unclassified';
 $private_bug->{classification} = 'Unclassified';
-$private_bug->{groups} = ['QA-Selenium-TEST'];
-$public_bug->{groups} = [];
+$private_bug->{groups}         = ['QA-Selenium-TEST'];
+$public_bug->{groups}          = [];
 
 # The user filing $private_bug doesn't have permission to set the status
 # or qa_contact, so they differ from normal $public_bug values.
 $private_bug->{qa_contact} = $config->{PRIVATE_BUG_USER . '_user_login'};
 
 sub post_success {
-    my ($call, $t, $rpc) = @_;
-
-    is(scalar @{ $call->result->{bugs} }, 1, "Got exactly one bug");
-    my $bug = $call->result->{bugs}->[0];
-
-    if ($t->{user} && $t->{user} eq 'admin') {
-        ok(exists $bug->{estimated_time} && exists $bug->{remaining_time},
-           'Admin correctly gets time-tracking fields');
-        is($bug->{deadline}, '2038-01-01', 'deadline is correct');
-        cmp_ok($bug->{estimated_time}, '==', '10.0',
-               'estimated_time is correct');
-        cmp_ok($bug->{remaining_time}, '==', '5.0',
-               'remaining_time is correct');
-    }
-    else {
-        ok(!exists $bug->{estimated_time} && !exists $bug->{remaining_time},
-           'Time-tracking fields are not returned to non-privileged users');
-    }
-
-    if ($t->{user}) {
-        ok($bug->{update_token}, 'Update token returned for logged-in user');
-    }
-    else {
-        ok(!exists $bug->{update_token},
-           'Update token not returned for logged-out users');
-    }
-
-    my $expect = $bug->{id} == $private_bug->{id} ? $private_bug : $public_bug;
-
-    my @fields = sort keys %$expect;
-    push(@fields, 'creation_time', 'last_change_time');
-
-    $rpc->bz_test_bug(\@fields, $bug, $expect, $t, $creation_time);
+  my ($call, $t, $rpc) = @_;
+
+  is(scalar @{$call->result->{bugs}}, 1, "Got exactly one bug");
+  my $bug = $call->result->{bugs}->[0];
+
+  if ($t->{user} && $t->{user} eq 'admin') {
+    ok(exists $bug->{estimated_time} && exists $bug->{remaining_time},
+      'Admin correctly gets time-tracking fields');
+    is($bug->{deadline}, '2038-01-01', 'deadline is correct');
+    cmp_ok($bug->{estimated_time}, '==', '10.0', 'estimated_time is correct');
+    cmp_ok($bug->{remaining_time}, '==', '5.0',  'remaining_time is correct');
+  }
+  else {
+    ok(
+      !exists $bug->{estimated_time} && !exists $bug->{remaining_time},
+      'Time-tracking fields are not returned to non-privileged users'
+    );
+  }
+
+  if ($t->{user}) {
+    ok($bug->{update_token}, 'Update token returned for logged-in user');
+  }
+  else {
+    ok(!exists $bug->{update_token},
+      'Update token not returned for logged-out users');
+  }
+
+  my $expect = $bug->{id} == $private_bug->{id} ? $private_bug : $public_bug;
+
+  my @fields = sort keys %$expect;
+  push(@fields, 'creation_time', 'last_change_time');
+
+  $rpc->bz_test_bug(\@fields, $bug, $expect, $t, $creation_time);
 }
 
 my @tests = (
-    @{ bug_tests($public_id, $private_id) },
-    { args => { ids => [$public_id],
-                include_fields => ['id', 'summary', 'groups'] },
-      test => 'include_fields',
+  @{bug_tests($public_id, $private_id)},
+  {
+    args => {ids => [$public_id], include_fields => ['id', 'summary', 'groups']},
+    test => 'include_fields',
+  },
+  {
+    args =>
+      {ids => [$public_id], exclude_fields => ['assigned_to', 'cf_qa_status']},
+    test => 'exclude_fields'
+  },
+  {
+    args => {
+      ids            => [$public_id],
+      include_fields => ['id', 'summary', 'groups'],
+      exclude_fields => ['summary']
     },
-    { args => { ids => [$public_id],
-                exclude_fields => ['assigned_to', 'cf_qa_status'] },
-      test => 'exclude_fields' },
-    { args => { ids => [$public_id],
-                include_fields => ['id', 'summary', 'groups'],
-                exclude_fields => ['summary'] },
-      test => 'exclude_fields overrides include_fields' },
+    test => 'exclude_fields overrides include_fields'
+  },
 );
 
 foreach my $rpc (@clients) {
-    $rpc->bz_run_tests(tests => \@tests,  method => 'Bug.get',
-                       post_success => \&post_success);
+  $rpc->bz_run_tests(
+    tests        => \@tests,
+    method       => 'Bug.get',
+    post_success => \&post_success
+  );
 }
index 73142ed3add5d25b3019f177c531bfae4452ec96..1aea9d76085508f80e22a4d65400cc66cdf0ed82 100644 (file)
@@ -22,12 +22,15 @@ use Test::More tests => 114;
 my ($config, @clients) = get_rpc_clients();
 
 sub post_success {
-    my ($call, $t) = @_;
-    is(scalar @{ $call->result->{bugs} }, 1, "Got exactly one bug");
-    isa_ok($call->result->{bugs}->[0]->{history}, 'ARRAY', "Bug's history");
+  my ($call, $t) = @_;
+  is(scalar @{$call->result->{bugs}}, 1, "Got exactly one bug");
+  isa_ok($call->result->{bugs}->[0]->{history}, 'ARRAY', "Bug's history");
 }
 
 foreach my $rpc (@clients) {
-    $rpc->bz_run_tests(tests => STANDARD_BUG_TESTS,
-                       method => 'Bug.history', post_success => \&post_success);
+  $rpc->bz_run_tests(
+    tests        => STANDARD_BUG_TESTS,
+    method       => 'Bug.history',
+    post_success => \&post_success
+  );
 }
index 3c3f655be03d79080092dce7abbb3ab263da4d4f..3d3e6a16f11a75bb8f967bc6147cddbd6ced85e5 100644 (file)
@@ -23,82 +23,92 @@ my ($config, @clients) = get_rpc_clients();
 use constant INVALID_PRODUCT_ID => -1;
 use constant INVALID_FIELD_NAME => 'invalid_field';
 use constant GLOBAL_FIELDS =>
-    qw(bug_severity bug_status op_sys priority rep_platform resolution
-       cf_qa_status cf_single_select);
+  qw(bug_severity bug_status op_sys priority rep_platform resolution
+  cf_qa_status cf_single_select);
 use constant PRODUCT_FIELDS => qw(version target_milestone component);
 
 
-my $products = $clients[0]->bz_get_products();
-my $public_product = $products->{'Another Product'};
+my $products        = $clients[0]->bz_get_products();
+my $public_product  = $products->{'Another Product'};
 my $private_product = $products->{'QA-Selenium-TEST'};
 
 my @all_tests;
 
 for my $field (GLOBAL_FIELDS) {
-    push(@all_tests,
-         { args => { field => $field },
-           test => "Logged-out user can get $field values" });
+  push(@all_tests,
+    {args => {field => $field}, test => "Logged-out user can get $field values"});
 }
 
 for my $field (PRODUCT_FIELDS) {
-    my @tests = (
-        { args  => { field => $field },
-          error => "argument was not set",
-          test  => "$field can't be accessed without a value for 'product'",
-        },
-        { args  => { product_id => INVALID_PRODUCT_ID, field => $field },
-          error => "does not exist",
-          test  => "$field cannot be accessed with an invalid product id",
-        },
-
-        { args  => { product_id => $private_product, field => $field },
-          error => "you don't have access",
-          test => "Logged-out user cannot access $field in private product"
-        },
-        { args  => { product_id => $public_product, field => $field },
-          test  => "Logged-out user can access $field in a public product",
-        },
-
-        { user  => 'unprivileged',
-          args  => { product_id => $private_product, field => $field },
-          error => "you don't have access",
-          test  => "Unprivileged user cannot access $field in private product",
-        },
-        { user => 'unprivileged',
-          args => { product_id => $public_product, field => $field },
-          test => "Logged-in user can access $field in public product",
-        },
-
-        { user => 'QA_Selenium_TEST',
-          args => { product_id => $private_product, field  => $field },
-          test => "Privileged user can access $field in a private product",
-        },
-    );
-
-    push(@all_tests, @tests);
-}
+  my @tests = (
+    {
+      args  => {field => $field},
+      error => "argument was not set",
+      test  => "$field can't be accessed without a value for 'product'",
+    },
+    {
+      args  => {product_id => INVALID_PRODUCT_ID, field => $field},
+      error => "does not exist",
+      test  => "$field cannot be accessed with an invalid product id",
+    },
 
-my @extra_tests = (
-    { args  => { product_id => $private_product, },
-      error => "requires a field argument",
-      test  =>  "Passing product_id without 'field' throws an error",
+    {
+      args  => {product_id => $private_product, field => $field},
+      error => "you don't have access",
+      test  => "Logged-out user cannot access $field in private product"
+    },
+    {
+      args => {product_id => $public_product, field => $field},
+      test => "Logged-out user can access $field in a public product",
+    },
+
+    {
+      user  => 'unprivileged',
+      args  => {product_id => $private_product, field => $field},
+      error => "you don't have access",
+      test  => "Unprivileged user cannot access $field in private product",
     },
-    { args  => { field => INVALID_FIELD_NAME },
-      error => "Can't use \"" . INVALID_FIELD_NAME . "\" as a field name",
-      test  => 'Invalid field name'
+    {
+      user => 'unprivileged',
+      args => {product_id => $public_product, field => $field},
+      test => "Logged-in user can access $field in public product",
     },
+
+    {
+      user => 'QA_Selenium_TEST',
+      args => {product_id => $private_product, field => $field},
+      test => "Privileged user can access $field in a private product",
+    },
+  );
+
+  push(@all_tests, @tests);
+}
+
+my @extra_tests = (
+  {
+    args  => {product_id => $private_product,},
+    error => "requires a field argument",
+    test  => "Passing product_id without 'field' throws an error",
+  },
+  {
+    args  => {field => INVALID_FIELD_NAME},
+    error => "Can't use \"" . INVALID_FIELD_NAME . "\" as a field name",
+    test  => 'Invalid field name'
+  },
 );
 
 push(@all_tests, @extra_tests);
 
 sub post_success {
-    my ($call) = @_;
+  my ($call) = @_;
 
-    cmp_ok(scalar @{ $call->result->{'values'} }, '>', 0,
-           'Got one or more values');
+  cmp_ok(scalar @{$call->result->{'values'}}, '>', 0, 'Got one or more values');
 }
 
 foreach my $rpc (@clients) {
-    $rpc->bz_run_tests(tests => \@all_tests,  method => 'Bug.legal_values',
-                       post_success => \&post_success);
+  $rpc->bz_run_tests(
+    tests        => \@all_tests,
+    method       => 'Bug.legal_values',
+    post_success => \&post_success
+  );
 }
index d37e9c70dca3b4453616e876413b101eefc3605d..3328cece8e9887e504cd5ca4d514226144385ccf 100644 (file)
@@ -32,180 +32,238 @@ my ($public_bug, $private_bug) = $clients[0]->bz_create_test_bugs('private');
 $public_bug->{alias}  = random_string(40);
 $private_bug->{alias} = random_string(40);
 my $alias_tests = [
-    { user => 'editbugs',
-      args => { ids => [ $public_bug->{id} ], alias => $public_bug->{alias} },
-      test => 'Add alias to public bug' },
-    { user => PRIVATE_BUG_USER,
-      args => { ids => [ $private_bug->{id} ],
-                cc  => { add => [ $config->{'editbugs_user_login'} ] } },
-      test => 'Add editusers to cc of private bug' },
-    { user => 'editbugs',
-      args => { ids => [ $private_bug->{id} ], alias => $private_bug->{alias} },
-      test => 'Add alias to private bug' },
-    { user => PRIVATE_BUG_USER,
-      args => { ids => [ $private_bug->{id} ],
-                cc  => { remove => [ $config->{'editbugs_user_login'} ] } },
-      test => 'Remove editusers from cc of private bug' },
+  {
+    user => 'editbugs',
+    args => {ids => [$public_bug->{id}], alias => $public_bug->{alias}},
+    test => 'Add alias to public bug'
+  },
+  {
+    user => PRIVATE_BUG_USER,
+    args => {
+      ids => [$private_bug->{id}],
+      cc  => {add => [$config->{'editbugs_user_login'}]}
+    },
+    test => 'Add editusers to cc of private bug'
+  },
+  {
+    user => 'editbugs',
+    args => {ids => [$private_bug->{id}], alias => $private_bug->{alias}},
+    test => 'Add alias to private bug'
+  },
+  {
+    user => PRIVATE_BUG_USER,
+    args => {
+      ids => [$private_bug->{id}],
+      cc  => {remove => [$config->{'editbugs_user_login'}]}
+    },
+    test => 'Remove editusers from cc of private bug'
+  },
 ];
 $clients[0]->bz_run_tests(tests => $alias_tests, method => 'Bug.update');
 
 my @tests;
 foreach my $field (keys %$public_bug) {
-    next if ($field eq 'cc' or $field eq 'description');
-    my $test = { args => { $field => $public_bug->{$field} },
-                 test => "Search by $field" };
-    if ( grep($_ eq $field, qw(alias whiteboard summary)) ) {
-        $test->{exactly} = 1; $test->{bugs} = 1;
-    }
-    push(@tests, $test);
+  next if ($field eq 'cc' or $field eq 'description');
+  my $test
+    = {args => {$field => $public_bug->{$field}}, test => "Search by $field"};
+  if (grep($_ eq $field, qw(alias whiteboard summary))) {
+    $test->{exactly} = 1;
+    $test->{bugs}    = 1;
+  }
+  push(@tests, $test);
 }
 
-push(@tests, (
-    { args  => { offset => 1 },
+push(
+  @tests,
+  (
+    {
+      args  => {offset => 1},
       test  => "Offset without limit fails",
       error => 'requires a limit argument',
     },
 
-    { args => { alias => $private_bug->{alias} },
+    {
+      args => {alias => $private_bug->{alias}},
       test => 'Logged-out cannot find a private_bug by alias',
       bugs => 0,
     },
 
-    { args => { creation_time => '19700101T00:00:00' },
+    {
+      args => {creation_time => '19700101T00:00:00'},
       test => 'Get all bugs by creation time',
     },
-    { args => { creation_time => '20380101T00:00:00' },
+    {
+      args => {creation_time => '20380101T00:00:00'},
       test => 'Get no bugs, by creation time',
       bugs => 0,
     },
-    { args => { last_change_time => '19700101T00:00:00' },
+    {
+      args => {last_change_time => '19700101T00:00:00'},
       test => 'Get all bugs by last_change_time',
     },
-    { args => { last_change_time => '20380101T00:00:00' },
+    {
+      args => {last_change_time => '20380101T00:00:00'},
       test => 'Get no bugs by last_change_time',
       bugs => 0,
     },
 
-    { args => { reporter => $config->{editbugs_user_login} },
+    {
+      args => {reporter => $config->{editbugs_user_login}},
       test => 'Search by reporter',
     },
-    { args => { resolution => '' },
-      test => 'Search for empty resolution',
-    },
-    { args => { resolution => 'NO_SUCH_RESOLUTION' },
+    {args => {resolution => ''}, test => 'Search for empty resolution',},
+    {
+      args => {resolution => 'NO_SUCH_RESOLUTION'},
       test => 'Search for invalid resolution',
       bugs => 0,
     },
-    { args => { summary => substr($public_bug->{summary}, 0, 50) },
-      test => 'Search by partial summary',
-      bugs => 1, exactly => 1
+    {
+      args    => {summary => substr($public_bug->{summary}, 0, 50)},
+      test    => 'Search by partial summary',
+      bugs    => 1,
+      exactly => 1
     },
-    { args => { summary => random_string() . ' ' . random_string() },
+    {
+      args => {summary => random_string() . ' ' . random_string()},
       test => 'Summary search that returns no results',
       bugs => 0,
     },
-    { args => { summary => [split(/\s/, $public_bug->{summary})] },
+    {
+      args => {summary => [split(/\s/, $public_bug->{summary})]},
       test => 'Summary search using multiple terms',
     },
 
-    { args => { whiteboard => substr($public_bug->{whiteboard}, 0, 50) },
-      test => 'Search by partial whiteboard',
-      bugs => 1, exactly => 1,
+    {
+      args    => {whiteboard => substr($public_bug->{whiteboard}, 0, 50)},
+      test    => 'Search by partial whiteboard',
+      bugs    => 1,
+      exactly => 1,
     },
-    { args => { whiteboard => random_string(100) },
+    {
+      args => {whiteboard => random_string(100)},
       test => 'Whiteboard search that returns no results',
       bugs => 0,
     },
-    { args => { whiteboard => [split(/\s/, $public_bug->{whiteboard})] },
-      test => 'Whiteboard search using multiple terms',
-      bugs => 1, exactly => 1,
+    {
+      args    => {whiteboard => [split(/\s/, $public_bug->{whiteboard})]},
+      test    => 'Whiteboard search using multiple terms',
+      bugs    => 1,
+      exactly => 1,
     },
 
-    { args => { product => $public_bug->{product},
-                component => $public_bug->{component},
-                last_change_time => '19700101T00:00:00' },
+    {
+      args => {
+        product          => $public_bug->{product},
+        component        => $public_bug->{component},
+        last_change_time => '19700101T00:00:00'
+      },
       test => 'Search by multiple arguments',
     },
 
     # Logged-in user who can see private bugs
-    { user => PRIVATE_BUG_USER,
-      args => { alias => [$public_bug->{alias}, $private_bug->{alias}] },
-      test => 'Search using two aliases (including one private)',
-      bugs => 2, exactly => 1,
+    {
+      user    => PRIVATE_BUG_USER,
+      args    => {alias => [$public_bug->{alias}, $private_bug->{alias}]},
+      test    => 'Search using two aliases (including one private)',
+      bugs    => 2,
+      exactly => 1,
     },
-    { user => PRIVATE_BUG_USER,
-      args => { product => [$public_bug->{product}, $private_bug->{product}],
-                limit => 1 },
-      test => 'Limit 1',
-      bugs => 1, exactly => 1,
+    {
+      user => PRIVATE_BUG_USER,
+      args =>
+        {product => [$public_bug->{product}, $private_bug->{product}], limit => 1},
+      test    => 'Limit 1',
+      bugs    => 1,
+      exactly => 1,
     },
-    { user => PRIVATE_BUG_USER,
-      args => { product => [$public_bug->{product}, $private_bug->{product}],
-                limit => 1, offset => 1 },
-      test => 'Limit 1 Offset 1',
-      bugs => 1, exactly => 1,
+    {
+      user => PRIVATE_BUG_USER,
+      args => {
+        product => [$public_bug->{product}, $private_bug->{product}],
+        limit   => 1,
+        offset  => 1
+      },
+      test    => 'Limit 1 Offset 1',
+      bugs    => 1,
+      exactly => 1,
     },
 
     # include_fields ane exclude_fields
-    { args => { id => $public_bug->{id},
-                include_fields => ['id', 'alias', 'summary', 'groups'] },
+    {
+      args => {
+        id             => $public_bug->{id},
+        include_fields => ['id', 'alias', 'summary', 'groups']
+      },
       test => 'include_fields',
     },
-    { args => { id => $public_bug->{id},
-                exclude_fields => ['assigned_to', 'cf_qa_status'] },
-      test => 'exclude_fields' },
-    { args => { id => $public_bug->{id},
-                include_fields => ['id', 'alias', 'summary', 'groups'],
-                exclude_fields => ['summary'] },
-      test => 'exclude_fields overrides include_fields' },
-));
-
-push(@tests,
-    { args => { votes => 1 },
-      test => 'Search by votes',
-      bugs => -1, # We don't care how many it returns, for now.
-    }) if $config->{test_extensions};
+    {
+      args =>
+        {id => $public_bug->{id}, exclude_fields => ['assigned_to', 'cf_qa_status']},
+      test => 'exclude_fields'
+    },
+    {
+      args => {
+        id             => $public_bug->{id},
+        include_fields => ['id', 'alias', 'summary', 'groups'],
+        exclude_fields => ['summary']
+      },
+      test => 'exclude_fields overrides include_fields'
+    },
+  )
+);
 
-sub post_success {
-    my ($call, $t) = @_;
-    my $bugs = $call->result->{bugs};
-
-    my $expected_count = $t->{bugs};
-    $expected_count = 1 if !defined $expected_count;
-    if ($expected_count) {
-        my $operator = $t->{exactly} ? '==' : '>=';
-        cmp_ok(scalar @$bugs, $operator, $expected_count,
-               'The right number of bugs are returned');
-        unless ($t->{user} and $t->{user} eq PRIVATE_BUG_USER) {
-            ok(!grep($_->{alias} && $_->{alias} eq $private_bug->{alias}, @$bugs),
-               'Result does not contain the private bug');
-        }
+push(
+  @tests,
+  {
+    args => {votes => 1},
+    test => 'Search by votes',
+    bugs => -1,                  # We don't care how many it returns, for now.
+  }
+) if $config->{test_extensions};
 
-        my @include = @{ $t->{args}->{include_fields} || [] };
-        my @exclude = @{ $t->{args}->{exclude_fields} || [] };
-        if (@include or @exclude) {
-            my @check_fields = uniq (keys %$public_bug, @include);
-            foreach my $field (sort @check_fields) {
-                next if $field eq 'description';
-                if ((@include and !grep { $_ eq $field } @include )
-                    or (@exclude and grep { $_ eq $field } @exclude))
-                {
-                    ok(!exists $bugs->[0]->{$field}, "$field is not included")
-                      or diag Dumper($bugs);
-                }
-                else {
-                    ok(exists $bugs->[0]->{$field}, "$field is included");
-                }
-            }
-        }
+sub post_success {
+  my ($call, $t) = @_;
+  my $bugs = $call->result->{bugs};
 
+  my $expected_count = $t->{bugs};
+  $expected_count = 1 if !defined $expected_count;
+  if ($expected_count) {
+    my $operator = $t->{exactly} ? '==' : '>=';
+    cmp_ok(scalar @$bugs,
+      $operator, $expected_count, 'The right number of bugs are returned');
+    unless ($t->{user} and $t->{user} eq PRIVATE_BUG_USER) {
+      ok(!grep($_->{alias} && $_->{alias} eq $private_bug->{alias}, @$bugs),
+        'Result does not contain the private bug');
     }
-    else {
-        is(scalar @$bugs, 0, 'No bugs returned');
+
+    my @include = @{$t->{args}->{include_fields} || []};
+    my @exclude = @{$t->{args}->{exclude_fields} || []};
+    if (@include or @exclude) {
+      my @check_fields = uniq(keys %$public_bug, @include);
+      foreach my $field (sort @check_fields) {
+        next if $field eq 'description';
+        if ( (@include and !grep { $_ eq $field } @include)
+          or (@exclude and grep { $_ eq $field } @exclude))
+        {
+          ok(!exists $bugs->[0]->{$field}, "$field is not included")
+            or diag Dumper($bugs);
+        }
+        else {
+          ok(exists $bugs->[0]->{$field}, "$field is included");
+        }
+      }
     }
+
+  }
+  else {
+    is(scalar @$bugs, 0, 'No bugs returned');
+  }
 }
 
 foreach my $rpc (@clients) {
-    $rpc->bz_run_tests(tests => \@tests,
-                       method => 'Bug.search', post_success => \&post_success);
+  $rpc->bz_run_tests(
+    tests        => \@tests,
+    method       => 'Bug.search',
+    post_success => \&post_success
+  );
 }
index 32cac40eac671a4b3c68fb803dde7c3c2db60057..f12d287b10aa8b7097326905b41941fc10ad6100 100644 (file)
@@ -27,603 +27,788 @@ use constant NONEXISTENT_BUG => 12_000_000;
 # We have to generate different values for each RPC client, so we
 # have a function to generate the tests for each client.
 sub get_tests {
-    my ($config, $rpc) = @_;
-
-    # update doesn't support logged-out users.
-    my @tests = grep { $_->{user} } @{ STANDARD_BUG_TESTS() };
-
-    my ($public_bug, $second_bug) = $rpc->bz_create_test_bugs();
-    my ($public_id, $second_id) = ($public_bug->{id}, $second_bug->{id});
-
-    # Add aliases to both bugs
-    $public_bug->{alias} = random_string(40);
-    $second_bug->{alias} = random_string(40);
-    my $alias_tests = [
-        { user => 'editbugs',
-          args => { ids => [ $public_id ], alias => $public_bug->{alias} },
-          test => 'Add alias to public bug' },
-        { user => 'editbugs',
-          args => { ids => [ $second_id ], alias => $second_bug->{alias} },
-          test => 'Add alias to second bug' },
-    ];
-    $rpc->bz_run_tests(tests => $alias_tests, method => 'Bug.update');
-
-    my $comment_call = $rpc->bz_call_success(
-        'Bug.comments', { ids => [$public_id, $second_id] });
-    $public_bug->{comment} =
-        $comment_call->result->{bugs}->{$public_id}->{comments}->[0];
-    $second_bug->{comment} =
-        $comment_call->result->{bugs}->{$second_id}->{comments}->[0];
-
-    push(@tests, (
-        { args  => { ids => [$public_id] },
-          error => 'You must log in',
-          test  => 'Logged-out users cannot call update' },
-
-        # FIXME We need a permissions test for canedit, but it's so uncommonly
-        # used that it's not a high priority.
-    ));
-
-    my %valid = valid_values($config, $public_bug, $second_bug);
-    my $valid_value_tests = valid_values_to_tests(\%valid, $public_bug);
-    push(@tests, @$valid_value_tests);
-
-    my %invalid = invalid_values($public_bug, $second_bug);
-    my $invalid_value_tests = invalid_values_to_tests(\%invalid, $public_bug);
-    push(@tests, @$invalid_value_tests);
-
-    return \@tests;
+  my ($config, $rpc) = @_;
+
+  # update doesn't support logged-out users.
+  my @tests = grep { $_->{user} } @{STANDARD_BUG_TESTS()};
+
+  my ($public_bug, $second_bug) = $rpc->bz_create_test_bugs();
+  my ($public_id, $second_id) = ($public_bug->{id}, $second_bug->{id});
+
+  # Add aliases to both bugs
+  $public_bug->{alias} = random_string(40);
+  $second_bug->{alias} = random_string(40);
+  my $alias_tests = [
+    {
+      user => 'editbugs',
+      args => {ids => [$public_id], alias => $public_bug->{alias}},
+      test => 'Add alias to public bug'
+    },
+    {
+      user => 'editbugs',
+      args => {ids => [$second_id], alias => $second_bug->{alias}},
+      test => 'Add alias to second bug'
+    },
+  ];
+  $rpc->bz_run_tests(tests => $alias_tests, method => 'Bug.update');
+
+  my $comment_call
+    = $rpc->bz_call_success('Bug.comments', {ids => [$public_id, $second_id]});
+  $public_bug->{comment}
+    = $comment_call->result->{bugs}->{$public_id}->{comments}->[0];
+  $second_bug->{comment}
+    = $comment_call->result->{bugs}->{$second_id}->{comments}->[0];
+
+  push(
+    @tests,
+    (
+      {
+        args  => {ids => [$public_id]},
+        error => 'You must log in',
+        test  => 'Logged-out users cannot call update'
+      },
+
+      # FIXME We need a permissions test for canedit, but it's so uncommonly
+      # used that it's not a high priority.
+    )
+  );
+
+  my %valid = valid_values($config, $public_bug, $second_bug);
+  my $valid_value_tests = valid_values_to_tests(\%valid, $public_bug);
+  push(@tests, @$valid_value_tests);
+
+  my %invalid = invalid_values($public_bug, $second_bug);
+  my $invalid_value_tests = invalid_values_to_tests(\%invalid, $public_bug);
+  push(@tests, @$invalid_value_tests);
+
+  return \@tests;
 }
 
 sub valid_values {
-    my ($config, $public_bug, $second_bug) = @_;
-
-    my $admin = $config->{'admin_user_login'};
-    my $second_id = $second_bug->{id};
-    my $comment_id = $public_bug->{comment}->{id};
-    my $bug_uri = $config->{browser_url} . '/'
-                  . $config->{bugzilla_installation} . '/show_bug.cgi?id=';
-
-    my %values = (
-        alias => [
-            { value => random_string(20) },
-        ],
-        assigned_to => [
-            { value => $config->{'unprivileged_user_login'} }
-        ],
-        blocks => [
-            { value => { set => [$second_id] },
-              added => $second_id,
-              test  => 'set to second bug' },
-            { value => { remove => [$second_id] },
-              added => '', removed => $second_id,
-              test  =>  'remove second bug' },
-            { value => { add => [$second_id] },
-              added => $second_id, removed => '',
-              test  => 'add second bug' },
-            { value => { set => [] },
-              added => '', removed => $second_id,
-              test  => 'set to nothing' },
-        ],
-
-        cc => [
-            { value => { add => [$admin] },
-              added => $admin, removed => '',
-              test  => 'add admin' },
-            { value => { remove => [$admin] },
-              added => '', removed => $admin,
-              test  =>  'remove admin' },
-            { value => { remove => [$admin] },
-              test  => "removing user who isn't on the list works",
-              no_changes => 1 },
-        ],
-
-        is_cc_accessible => [
-            { value => 0, test => 'set to 0' },
-            { value => 1, test => 'set to 1' },
-        ],
-
-        comment => [
-            { value => { body => random_string(100) }, test => 'public' },
-            { value => { body => random_string(100), is_private => 1 },
-              user  => PRIVATE_BUG_USER, test => 'private' },
-        ],
-
-        comment_is_private => [
-            { value => { $comment_id => 1 },
-              user  => PRIVATE_BUG_USER, test => 'make description private' },
-            { value => { $comment_id => 0 },
-              user  => PRIVATE_BUG_USER, test => 'make description public' },
-        ],
-
-        component => [
-            { value => 'c2' }
-        ],
-
-        deadline => [
-            { value => '2037-01-01' },
-            { value => '', removed => '2037-01-01', test => 'remove' },
-        ],
-
-        dupe_of => [
-            { value => $second_id },
-        ],
-
-        estimated_time => [
-            { value => '10.0' },
-            { value => '0.0', removed => '10.0', test => 'set to zero' },
-        ],
-
-        groups => [
-            { value => { add => ['Master'] },
-              user => 'admin', added => 'Master', test => 'add Master' },
-            { value => { remove => ['Master'] },
-              user => 'admin', added => '', removed => 'Master',
-              test => 'remove Master' },
-        ],
-
-        keywords => [
-            { value => { add => ['test-keyword-1'] },
-              test => 'add one', added => 'test-keyword-1' },
-            { value => { set => ['test-keyword-1', 'test-keyword-2'] },
-              test  => 'set two', added => 'test-keyword-2' },
-            { value => { remove => ['test-keyword-1'] },
-              removed => 'test-keyword-1', added => '',
-              test  => 'remove one' },
-            { value => { set => [] },
-              removed => 'test-keyword-2', added => '',
-              test  => 'set to empty' },
-            { value => { remove => ['test-keyword-2'] },
-              test  => 'removing removed keyword does nothing',
-              no_changes => 1 },
-        ],
-
-        op_sys => [
-            { value => 'All' },
-        ],
-
-        platform => [
-            { value => 'All' },
-        ],
-
-        priority => [
-            { value => 'Normal' },
-        ],
-
-        product => [
-            { value => 'C2 Forever',
-              extra => {
-                component => 'Helium', version => 'unspecified',
-                target_milestone => '---',
-              },
-              test  => 'move to C2 Forever'
-            },
-            # This also tests that the extra fields transfer over properly
-            # when they have identical names in both products.
-            { value => $public_bug->{product},
-              extra => { component => $public_bug->{component} },
-              test  => 'move back to original product' },
-        ],
-
-        qa_contact => [
-            { value => $admin },
-            { value => '', test => 'set blank', removed => $admin },
-            # Reset to the original so that reset_qa_contact can also be tested.
-            { value => $public_bug->{qa_contact} },
-        ],
-
-        remaining_time => [
-            { value => '1000.50' },
-            { value => 0 },
-        ],
-
-        reset_assigned_to => [
-            { value => 1, field => 'assigned_to',
-              added => $config->{permanent_user} },
-        ],
-
-        reset_qa_contact => [
-            { value => 1, field => 'qa_contact', added => '' },
-        ],
-
-        resolution => [
-            { value => 'FIXED', extra => { status => 'RESOLVED' },
-              test => 'to RESOLVED FIXED' },
-            { value => 'INVALID', test => 'just resolution' },
-        ],
-
-        see_also => [
-            { value => { add => [$bug_uri . $second_id] },
-              added => $bug_uri . $second_id, removed => '',
-              test => 'add local bug URI' },
-            { value => { remove => [$bug_uri . $second_id] },
-              removed => $bug_uri . $second_id, added => '',
-              test => 'remove local bug URI' },
-            { value => { remove => ['http://landfill.bugzilla.org/bugzilla-tip/show_bug.cgi?id=1'] },
-              no_changes => 1,
-              test => 'removing non-existent URI works' },
-            { value => { add => [''] },
-              no_changes => 1,
-              test  => 'adding an empty string to see_also does nothing' },
-            { value => { add => [undef] },
-              no_changes => 1,
-              test  => 'adding a null to see_also does nothing' },
-        ],
-
-        status => [
-            # At this point, due to previous tests, the status is RESOLVED,
-            # so changing to CONFIRMED is our only real option if we want to
-            # test a simple open status.
-            { value => 'CONFIRMED' },
-        ],
-
-        severity => [
-            { value => 'critical' },
-        ],
-
-        summary => [
-            { value => random_string(100) },
-        ],
-
-        target_milestone => [
-            { value => 'AnotherMS2' },
-        ],
-
-        url => [
-            { value => 'http://' . random_string(20) . '/' },
-        ],
-
-        version => [
-            { value => 'Another2' },
-        ],
-
-        whiteboard => [
-            { value => random_string(1000) },
-        ],
-
-        work_time => [
-            # FIXME: work_time really needs to start showing up in the changes
-            # hash.
-            { value => '1.2', no_changes => 1 },
-            { value => '-1.2', test => 'negative value', no_changes => 1 },
-        ],
-    );
-
-    $values{depends_on} = $values{blocks};
-    $values{is_creator_accessible} = $values{is_cc_accessible};
-
-    return %values;
-};
+  my ($config, $public_bug, $second_bug) = @_;
+
+  my $admin      = $config->{'admin_user_login'};
+  my $second_id  = $second_bug->{id};
+  my $comment_id = $public_bug->{comment}->{id};
+  my $bug_uri
+    = $config->{browser_url} . '/'
+    . $config->{bugzilla_installation}
+    . '/show_bug.cgi?id=';
+
+  my %values = (
+    alias       => [{value => random_string(20)},],
+    assigned_to => [{value => $config->{'unprivileged_user_login'}}],
+    blocks      => [
+      {
+        value => {set => [$second_id]},
+        added => $second_id,
+        test  => 'set to second bug'
+      },
+      {
+        value   => {remove => [$second_id]},
+        added   => '',
+        removed => $second_id,
+        test    => 'remove second bug'
+      },
+      {
+        value   => {add => [$second_id]},
+        added   => $second_id,
+        removed => '',
+        test    => 'add second bug'
+      },
+      {
+        value   => {set => []},
+        added   => '',
+        removed => $second_id,
+        test    => 'set to nothing'
+      },
+    ],
+
+    cc => [
+      {
+        value   => {add => [$admin]},
+        added   => $admin,
+        removed => '',
+        test    => 'add admin'
+      },
+      {
+        value   => {remove => [$admin]},
+        added   => '',
+        removed => $admin,
+        test    => 'remove admin'
+      },
+      {
+        value      => {remove => [$admin]},
+        test       => "removing user who isn't on the list works",
+        no_changes => 1
+      },
+    ],
+
+    is_cc_accessible =>
+      [{value => 0, test => 'set to 0'}, {value => 1, test => 'set to 1'},],
+
+    comment => [
+      {value => {body => random_string(100)}, test => 'public'},
+      {
+        value => {body => random_string(100), is_private => 1},
+        user  => PRIVATE_BUG_USER,
+        test  => 'private'
+      },
+    ],
+
+    comment_is_private => [
+      {
+        value => {$comment_id => 1},
+        user  => PRIVATE_BUG_USER,
+        test  => 'make description private'
+      },
+      {
+        value => {$comment_id => 0},
+        user  => PRIVATE_BUG_USER,
+        test  => 'make description public'
+      },
+    ],
+
+    component => [{value => 'c2'}],
+
+    deadline => [
+      {value => '2037-01-01'},
+      {value => '', removed => '2037-01-01', test => 'remove'},
+    ],
+
+    dupe_of => [{value => $second_id},],
+
+    estimated_time => [
+      {value => '10.0'},
+      {value => '0.0', removed => '10.0', test => 'set to zero'},
+    ],
+
+    groups => [
+      {
+        value => {add => ['Master']},
+        user  => 'admin',
+        added => 'Master',
+        test  => 'add Master'
+      },
+      {
+        value   => {remove => ['Master']},
+        user    => 'admin',
+        added   => '',
+        removed => 'Master',
+        test    => 'remove Master'
+      },
+    ],
+
+    keywords => [
+      {
+        value => {add => ['test-keyword-1']},
+        test  => 'add one',
+        added => 'test-keyword-1'
+      },
+      {
+        value => {set => ['test-keyword-1', 'test-keyword-2']},
+        test  => 'set two',
+        added => 'test-keyword-2'
+      },
+      {
+        value   => {remove => ['test-keyword-1']},
+        removed => 'test-keyword-1',
+        added   => '',
+        test    => 'remove one'
+      },
+      {
+        value   => {set => []},
+        removed => 'test-keyword-2',
+        added   => '',
+        test    => 'set to empty'
+      },
+      {
+        value      => {remove => ['test-keyword-2']},
+        test       => 'removing removed keyword does nothing',
+        no_changes => 1
+      },
+    ],
+
+    op_sys => [{value => 'All'},],
+
+    platform => [{value => 'All'},],
+
+    priority => [{value => 'Normal'},],
+
+    product => [
+      {
+        value => 'C2 Forever',
+        extra =>
+          {component => 'Helium', version => 'unspecified', target_milestone => '---',},
+        test => 'move to C2 Forever'
+      },
+
+      # This also tests that the extra fields transfer over properly
+      # when they have identical names in both products.
+      {
+        value => $public_bug->{product},
+        extra => {component => $public_bug->{component}},
+        test  => 'move back to original product'
+      },
+    ],
+
+    qa_contact => [
+      {value => $admin}, {value => '', test => 'set blank', removed => $admin},
+
+      # Reset to the original so that reset_qa_contact can also be tested.
+      {value => $public_bug->{qa_contact}},
+    ],
+
+    remaining_time => [{value => '1000.50'}, {value => 0},],
+
+    reset_assigned_to =>
+      [{value => 1, field => 'assigned_to', added => $config->{permanent_user}},],
+
+    reset_qa_contact => [{value => 1, field => 'qa_contact', added => ''},],
+
+    resolution => [
+      {
+        value => 'FIXED',
+        extra => {status => 'RESOLVED'},
+        test  => 'to RESOLVED FIXED'
+      },
+      {value => 'INVALID', test => 'just resolution'},
+    ],
+
+    see_also => [
+      {
+        value   => {add => [$bug_uri . $second_id]},
+        added   => $bug_uri . $second_id,
+        removed => '',
+        test    => 'add local bug URI'
+      },
+      {
+        value   => {remove => [$bug_uri . $second_id]},
+        removed => $bug_uri . $second_id,
+        added   => '',
+        test    => 'remove local bug URI'
+      },
+      {
+        value =>
+          {remove => ['http://landfill.bugzilla.org/bugzilla-tip/show_bug.cgi?id=1']},
+        no_changes => 1,
+        test       => 'removing non-existent URI works'
+      },
+      {
+        value      => {add => ['']},
+        no_changes => 1,
+        test       => 'adding an empty string to see_also does nothing'
+      },
+      {
+        value      => {add => [undef]},
+        no_changes => 1,
+        test       => 'adding a null to see_also does nothing'
+      },
+    ],
+
+    status => [
+
+      # At this point, due to previous tests, the status is RESOLVED,
+      # so changing to CONFIRMED is our only real option if we want to
+      # test a simple open status.
+      {value => 'CONFIRMED'},
+    ],
+
+    severity => [{value => 'critical'},],
+
+    summary => [{value => random_string(100)},],
+
+    target_milestone => [{value => 'AnotherMS2'},],
+
+    url => [{value => 'http://' . random_string(20) . '/'},],
+
+    version => [{value => 'Another2'},],
+
+    whiteboard => [{value => random_string(1000)},],
+
+    work_time => [
+
+      # FIXME: work_time really needs to start showing up in the changes
+      # hash.
+      {value => '1.2', no_changes => 1},
+      {value => '-1.2', test => 'negative value', no_changes => 1},
+    ],
+  );
+
+  $values{depends_on}            = $values{blocks};
+  $values{is_creator_accessible} = $values{is_cc_accessible};
+
+  return %values;
+}
 
 sub valid_values_to_tests {
-    my ($valid_values, $public_bug) = @_;
-
-    my @tests;
-    foreach my $field (sort keys %$valid_values) {
-        my @tests_valid = @{ $valid_values->{$field} };
-        foreach my $item (@tests_valid) {
-            my $desc = $item->{test} || 'valid value';
-            my %args = (
-                ids => [$public_bug->{id}],
-                $field => $item->{value},
-                %{ $item->{extra} || {} },
-            );
-            my %test = ( user => 'editbugs', args => \%args, field => $field,
-                         test => "$field: $desc" );
-            foreach my $item_field (qw(no_changes added removed field user)) {
-                next if !exists $item->{$item_field};
-                $test{$item_field} = $item->{$item_field};
-            }
-            push(@tests, \%test);
-        }
+  my ($valid_values, $public_bug) = @_;
+
+  my @tests;
+  foreach my $field (sort keys %$valid_values) {
+    my @tests_valid = @{$valid_values->{$field}};
+    foreach my $item (@tests_valid) {
+      my $desc = $item->{test} || 'valid value';
+      my %args = (
+        ids    => [$public_bug->{id}],
+        $field => $item->{value},
+        %{$item->{extra} || {}},
+      );
+      my %test = (
+        user  => 'editbugs',
+        args  => \%args,
+        field => $field,
+        test  => "$field: $desc"
+      );
+      foreach my $item_field (qw(no_changes added removed field user)) {
+        next if !exists $item->{$item_field};
+        $test{$item_field} = $item->{$item_field};
+      }
+      push(@tests, \%test);
     }
+  }
 
-    return \@tests;
+  return \@tests;
 }
 
 sub invalid_values {
-    my ($public_bug, $second_bug) = @_;
-
-    my $public_id = $public_bug->{id};
-    my $second_id = $second_bug->{id};
-
-    my $comment_id = $public_bug->{comment}->{id};
-    my $second_comment_id = $second_bug->{comment}->{id};
-
-    my %values = (
-        alias => [
-            { value => random_string(41),
-              error => 'aliases cannot be longer than',
-              test  => 'alias cannot be too long' },
-            { value => $second_bug->{alias},
-              error => 'Please choose another alias or visit that bug and change its alias',
-              test  => 'duplicate alias fails' },
-            { value => 123456,
-              error => 'at least one letter',
-              test  => 'numeric alias fails' },
-            { value => random_string(20), ids => [$public_id, $second_id],
-              error => 'aliases when modifying multiple',
-              test  => 'setting alias on multiple bugs fails' },
-        ],
-
-        assigned_to => [
-            { value => random_string(20),
-              error => 'There is no user named',
-              test  => 'changing assigned_to to invalid user fails' },
-            { value => '',
-              error => 'you must provide an address for the new assignee',
-              test  => 'empty assigned_to fails' },
-            # FIXME Also check strict_isolation at some point in the future,
-            # perhaps.
-        ],
-
-        blocks => [
-            { value => { add => [NONEXISTENT_BUG] },
-              error => 'does not exist',
-              test  => 'Non-existent bug number fails in deps' },
-            { value => { add => [$public_id] },
-              error => 'block itself or depend on itself',
-              test  => "can't add this bug itself in a dep field" },
-            # FIXME Could use strict_isolation checks at some point.
-            # FIXME Could use a dependency_loop_multi test.
-        ],
-
-        cc => [
-            { value => { add => [random_string(20)] },
-              error => 'There is no user named',
-              test  => 'adding invalid user to cc fails' },
-            { value => { remove => [random_string(20)] },
-              error => 'There is no user named',
-              test  => 'removing invalid user from cc fails' },
-        ],
-
-        comment => [
-            { value => { body => random_string(100_000) },
-              error => 'cannot be longer',
-              test  => 'comment too long' },
-            { value => { body => random_string(100), is_private => 1 },
-              error => 'comments or attachments as private',
-              test  => 'normal user cannot add private comments' },
-        ],
-
-        comment_is_private => [
-            { value => { $comment_id => 1 },
-              error => 'comments or attachments as private',
-              test  => 'normal user cannot make a comment private' },
-            { value => { $second_comment_id => 1 },
-              error => 'You tried to modify the privacy of comment',
-              user  => PRIVATE_BUG_USER,
-              test  => 'cannot change privacy on a comment on another bug' },
-        ],
-
-        component => [
-            { value => '',
-              error => 'you must first choose a component',
-              test  => 'empty component fails' },
-            { value => random_string(20),
-              error => 'There is no component named',
-              test  => 'invalid component fails' },
-        ],
-
-        deadline => [
-            { value => random_string(20),
-              error => 'is not a legal date',
-              test  => 'Non-date fails in deadline' },
-            { value => '2037',
-              error => 'is not a legal date',
-              test  => 'year alone fails in deadline' },
-        ],
-
-        dupe_of => [
-            { value => undef,
-              error => 'dup_id was not defined',
-              test  => 'undefined dupe_of fails' },
-            { value => NONEXISTENT_BUG,
-              error => 'does not exist',
-              test  => 'Cannot dup to a nonexistent bug' },
-            { value => $public_id,
-              error => 'as a duplicate of itself',
-              test  => 'Cannot dup bug to itself' },
-        ],
-
-        estimated_time => [
-            { value => -1,
-              error => 'less than the minimum allowable value',
-              test  => 'negative estimated_time fails' },
-            { value => 100_000_000,
-              error => 'more than the maximum allowable value',
-              test  => 'too-large estimated_time fails' },
-            { value => random_string(20),
-              error => 'is not a numeric value',
-              test  => 'non-numeric estimated_time fails' },
-            # We use PRIVATE_BUG_USER because he can modify the bug, but
-            # can't change time-tracking fields.
-            { value => '100', user => PRIVATE_BUG_USER,
-              error => 'only a user with the required permissions',
-              test  => 'non-timetracker can not set estimated_time' },
-        ],
-
-        groups => [
-            { value => { add => ['Master'] },
-              error => 'either this group does not exist, or you are not allowed to restrict bugs to this group',
-              test  => "adding group we don't have access to but is valid fails" },
-            { value => { add => ['QA-Selenium-TEST'] },
-              error => 'either this group does not exist, or you are not allowed to restrict bugs to this group',
-              test  => 'adding valid group that is not in this product fails' },
-            { value => { add => [random_string(20)] },
-              error => 'either this group does not exist, or you are not allowed to restrict bugs to this group',
-              test  => 'adding non-existent group fails' },
-            { value => { remove => [random_string(20)] },
-              error => 'either this group does not exist, or you are not allowed to remove bugs from this group',
-              test => 'removing non-existent group fails' },
-        ],
-
-        keywords => [
-            { value => { add => [random_string(20)] },
-              error => 'See the list of available keywords',
-              test  => 'adding invalid keyword fails' },
-            { value => { remove => [random_string(20)] },
-              error => 'See the list of available keywords',
-              test  => 'removing invalid keyword fails' },
-            { value => { set => [random_string(20)] },
-              error => 'See the list of available keywords',
-              test  => 'setting invalid keyword fails' },
-        ],
-
-        op_sys => [
-            { value => random_string(20),
-              error => 'There is no',
-              test  => 'invalid op_sys fails' },
-            { value => '',
-              error => 'You must select/enter',
-              test => 'blank op_sys fails' },
-        ],
-
-        product => [
-            { value => random_string(60),
-              error => "does not exist or you aren't authorized",
-              test  => 'invalid product fails' },
-            { value => '',
-              error => 'You must select/enter a product',
-              test  => 'moving to blank product fails' },
-            { value => 'TestProduct',
-              error => 'There is no component named',
-              test  => 'moving products without other fields fails' },
-            { value => 'QA-Selenium-TEST',
-              extra => { component => 'QA-Selenium-TEST' },
-              error => "does not exist or you aren't authorized",
-              test  => 'moving to inaccessible product fails' },
-            { value => 'QA Entry Only',
-              error => "does not exist or you aren't authorized",
-              test  => 'moving to product where ENTRY is denied fails' },
-        ],
-
-        qa_contact => [
-            { value => random_string(20),
-              error => 'There is no user named',
-              test  => 'changing qa_contact to invalid user fails' },
-        ],
-
-        remaining_time => [
-            { value => -1,
-              error => 'less than the minimum allowable value',
-              test  => 'negative remaining_time fails' },
-            { value => 100_000_000,
-              error => 'more than the maximum allowable value',
-              test  => 'too-large remaining_time fails' },
-            { value => random_string(20),
-              error => 'is not a numeric value',
-              test  => 'non-numeric remaining_time fails' },
-            # We use PRIVATE_BUG_USER because he can modify the bug, but
-            # can't change time-tracking fields.
-            { value => '100', user => PRIVATE_BUG_USER,
-              error => 'only a user with the required permissions',
-              test  => 'non-timetracker can not set remaining_time' },
-        ],
-
-        # We do all the failing resolution tests on the second bug,
-        # because we want to be sure that we're starting from an open
-        # status.
-        resolution => [
-            { value => random_string(20), ids => [$second_id],
-              extra => { status => 'RESOLVED' },
-              error => 'There is no Resolution named',
-              test  => 'invalid resolution fails' },
-            { value => 'FIXED', ids => [$second_id],
-              error => 'You cannot set a resolution for open bugs',
-              test  => 'setting resolution on open bug fails' },
-            { value => 'DUPLICATE', ids => [$second_id],
-              extra => { status => 'RESOLVED' },
-              error => 'id to mark this bug as a duplicate',
-              test  => 'setting DUPLICATE without dup_id fails' },
-            { value => '', ids => [$second_id],
-              extra => { status => 'RESOLVED' },
-              error => 'A valid resolution is required',
-              test => 'blank resolution fails with closed status' },
-        ],
-
-        see_also => [
-            { value => { add => [random_string(20)] },
-              error => 'is not a valid bug number nor an alias',
-              test  => 'random string fails in see_also' },
-            { value => { add => ['http://landfill.bugzilla.org/'] },
-              error => 'See Also URLs should point to one of',
-              test  => 'no show_bug.cgi in see_also URI' },
-        ],
-
-        status => [
-            { value => random_string(20),
-              error => 'There is no status named',
-              test  => 'invalid status fails' },
-            { value => '',
-              error => 'You must select/enter a status',
-              test => 'blank status fails' },
-            # We use the second bug for this because we can guarantee that
-            # it is open.
-            { value => 'VERIFIED', ids => [$second_id],
-              extra => { resolution => 'FIXED' },
-              error => 'You are not allowed to change the bug status from',
-              test  => 'invalid transition fails' },
-        ],
-
-        summary => [
-            { value => random_string(300),
-              error => 'The text you entered in the Summary field is too long',
-              test  => 'too-long summary fails' },
-            { value => '',
-              error => 'You must enter a summary for this bug',
-              test  => 'blank summary fails' },
-        ],
-
-        work_time => [
-            { value => 100_000_000,
-              error => 'more than the maximum allowable value',
-              test  => 'too-large work_time fails' },
-            { value => random_string(20),
-              error => 'is not a numeric value',
-              test  => 'non-numeric work_time fails' },
-            # We use PRIVATE_BUG_USER because he can modify the bug, but
-            # can't change time-tracking fields.
-            { value => '10', user => PRIVATE_BUG_USER,
-              error => 'only a user with the required permissions',
-              test  => 'non-timetracker can not set work_time' },
-        ],
-    );
-
-    $values{depends_on} = $values{blocks};
-
-    foreach my $field (qw(platform priority severity target_milestone version))
-    {
-        my $tests = dclone($values{op_sys});
-        foreach my $test (@$tests) {
-            $test->{test} =~ s/op_sys/$field/g;
-        }
-        $values{$field} = $tests;
+  my ($public_bug, $second_bug) = @_;
+
+  my $public_id = $public_bug->{id};
+  my $second_id = $second_bug->{id};
+
+  my $comment_id        = $public_bug->{comment}->{id};
+  my $second_comment_id = $second_bug->{comment}->{id};
+
+  my %values = (
+    alias => [
+      {
+        value => random_string(41),
+        error => 'aliases cannot be longer than',
+        test  => 'alias cannot be too long'
+      },
+      {
+        value => $second_bug->{alias},
+        error => 'Please choose another alias or visit that bug and change its alias',
+        test  => 'duplicate alias fails'
+      },
+      {
+        value => 123456,
+        error => 'at least one letter',
+        test  => 'numeric alias fails'
+      },
+      {
+        value => random_string(20),
+        ids   => [$public_id, $second_id],
+        error => 'aliases when modifying multiple',
+        test  => 'setting alias on multiple bugs fails'
+      },
+    ],
+
+    assigned_to => [
+      {
+        value => random_string(20),
+        error => 'There is no user named',
+        test  => 'changing assigned_to to invalid user fails'
+      },
+      {
+        value => '',
+        error => 'you must provide an address for the new assignee',
+        test  => 'empty assigned_to fails'
+      },
+
+      # FIXME Also check strict_isolation at some point in the future,
+      # perhaps.
+    ],
+
+    blocks => [
+      {
+        value => {add => [NONEXISTENT_BUG]},
+        error => 'does not exist',
+        test  => 'Non-existent bug number fails in deps'
+      },
+      {
+        value => {add => [$public_id]},
+        error => 'block itself or depend on itself',
+        test  => "can't add this bug itself in a dep field"
+      },
+
+      # FIXME Could use strict_isolation checks at some point.
+      # FIXME Could use a dependency_loop_multi test.
+    ],
+
+    cc => [
+      {
+        value => {add => [random_string(20)]},
+        error => 'There is no user named',
+        test  => 'adding invalid user to cc fails'
+      },
+      {
+        value => {remove => [random_string(20)]},
+        error => 'There is no user named',
+        test  => 'removing invalid user from cc fails'
+      },
+    ],
+
+    comment => [
+      {
+        value => {body => random_string(100_000)},
+        error => 'cannot be longer',
+        test  => 'comment too long'
+      },
+      {
+        value => {body => random_string(100), is_private => 1},
+        error => 'comments or attachments as private',
+        test  => 'normal user cannot add private comments'
+      },
+    ],
+
+    comment_is_private => [
+      {
+        value => {$comment_id => 1},
+        error => 'comments or attachments as private',
+        test  => 'normal user cannot make a comment private'
+      },
+      {
+        value => {$second_comment_id => 1},
+        error => 'You tried to modify the privacy of comment',
+        user  => PRIVATE_BUG_USER,
+        test  => 'cannot change privacy on a comment on another bug'
+      },
+    ],
+
+    component => [
+      {
+        value => '',
+        error => 'you must first choose a component',
+        test  => 'empty component fails'
+      },
+      {
+        value => random_string(20),
+        error => 'There is no component named',
+        test  => 'invalid component fails'
+      },
+    ],
+
+    deadline => [
+      {
+        value => random_string(20),
+        error => 'is not a legal date',
+        test  => 'Non-date fails in deadline'
+      },
+      {
+        value => '2037',
+        error => 'is not a legal date',
+        test  => 'year alone fails in deadline'
+      },
+    ],
+
+    dupe_of => [
+      {
+        value => undef,
+        error => 'dup_id was not defined',
+        test  => 'undefined dupe_of fails'
+      },
+      {
+        value => NONEXISTENT_BUG,
+        error => 'does not exist',
+        test  => 'Cannot dup to a nonexistent bug'
+      },
+      {
+        value => $public_id,
+        error => 'as a duplicate of itself',
+        test  => 'Cannot dup bug to itself'
+      },
+    ],
+
+    estimated_time => [
+      {
+        value => -1,
+        error => 'less than the minimum allowable value',
+        test  => 'negative estimated_time fails'
+      },
+      {
+        value => 100_000_000,
+        error => 'more than the maximum allowable value',
+        test  => 'too-large estimated_time fails'
+      },
+      {
+        value => random_string(20),
+        error => 'is not a numeric value',
+        test  => 'non-numeric estimated_time fails'
+      },
+
+      # We use PRIVATE_BUG_USER because he can modify the bug, but
+      # can't change time-tracking fields.
+      {
+        value => '100',
+        user  => PRIVATE_BUG_USER,
+        error => 'only a user with the required permissions',
+        test  => 'non-timetracker can not set estimated_time'
+      },
+    ],
+
+    groups => [
+      {
+        value => {add => ['Master']},
+        error =>
+          'either this group does not exist, or you are not allowed to restrict bugs to this group',
+        test => "adding group we don't have access to but is valid fails"
+      },
+      {
+        value => {add => ['QA-Selenium-TEST']},
+        error =>
+          'either this group does not exist, or you are not allowed to restrict bugs to this group',
+        test => 'adding valid group that is not in this product fails'
+      },
+      {
+        value => {add => [random_string(20)]},
+        error =>
+          'either this group does not exist, or you are not allowed to restrict bugs to this group',
+        test => 'adding non-existent group fails'
+      },
+      {
+        value => {remove => [random_string(20)]},
+        error =>
+          'either this group does not exist, or you are not allowed to remove bugs from this group',
+        test => 'removing non-existent group fails'
+      },
+    ],
+
+    keywords => [
+      {
+        value => {add => [random_string(20)]},
+        error => 'See the list of available keywords',
+        test  => 'adding invalid keyword fails'
+      },
+      {
+        value => {remove => [random_string(20)]},
+        error => 'See the list of available keywords',
+        test  => 'removing invalid keyword fails'
+      },
+      {
+        value => {set => [random_string(20)]},
+        error => 'See the list of available keywords',
+        test  => 'setting invalid keyword fails'
+      },
+    ],
+
+    op_sys => [
+      {
+        value => random_string(20),
+        error => 'There is no',
+        test  => 'invalid op_sys fails'
+      },
+      {value => '', error => 'You must select/enter', test => 'blank op_sys fails'},
+    ],
+
+    product => [
+      {
+        value => random_string(60),
+        error => "does not exist or you aren't authorized",
+        test  => 'invalid product fails'
+      },
+      {
+        value => '',
+        error => 'You must select/enter a product',
+        test  => 'moving to blank product fails'
+      },
+      {
+        value => 'TestProduct',
+        error => 'There is no component named',
+        test  => 'moving products without other fields fails'
+      },
+      {
+        value => 'QA-Selenium-TEST',
+        extra => {component => 'QA-Selenium-TEST'},
+        error => "does not exist or you aren't authorized",
+        test  => 'moving to inaccessible product fails'
+      },
+      {
+        value => 'QA Entry Only',
+        error => "does not exist or you aren't authorized",
+        test  => 'moving to product where ENTRY is denied fails'
+      },
+    ],
+
+    qa_contact => [
+      {
+        value => random_string(20),
+        error => 'There is no user named',
+        test  => 'changing qa_contact to invalid user fails'
+      },
+    ],
+
+    remaining_time => [
+      {
+        value => -1,
+        error => 'less than the minimum allowable value',
+        test  => 'negative remaining_time fails'
+      },
+      {
+        value => 100_000_000,
+        error => 'more than the maximum allowable value',
+        test  => 'too-large remaining_time fails'
+      },
+      {
+        value => random_string(20),
+        error => 'is not a numeric value',
+        test  => 'non-numeric remaining_time fails'
+      },
+
+      # We use PRIVATE_BUG_USER because he can modify the bug, but
+      # can't change time-tracking fields.
+      {
+        value => '100',
+        user  => PRIVATE_BUG_USER,
+        error => 'only a user with the required permissions',
+        test  => 'non-timetracker can not set remaining_time'
+      },
+    ],
+
+    # We do all the failing resolution tests on the second bug,
+    # because we want to be sure that we're starting from an open
+    # status.
+    resolution => [
+      {
+        value => random_string(20),
+        ids   => [$second_id],
+        extra => {status => 'RESOLVED'},
+        error => 'There is no Resolution named',
+        test  => 'invalid resolution fails'
+      },
+      {
+        value => 'FIXED',
+        ids   => [$second_id],
+        error => 'You cannot set a resolution for open bugs',
+        test  => 'setting resolution on open bug fails'
+      },
+      {
+        value => 'DUPLICATE',
+        ids   => [$second_id],
+        extra => {status => 'RESOLVED'},
+        error => 'id to mark this bug as a duplicate',
+        test  => 'setting DUPLICATE without dup_id fails'
+      },
+      {
+        value => '',
+        ids   => [$second_id],
+        extra => {status => 'RESOLVED'},
+        error => 'A valid resolution is required',
+        test  => 'blank resolution fails with closed status'
+      },
+    ],
+
+    see_also => [
+      {
+        value => {add => [random_string(20)]},
+        error => 'is not a valid bug number nor an alias',
+        test  => 'random string fails in see_also'
+      },
+      {
+        value => {add => ['http://landfill.bugzilla.org/']},
+        error => 'See Also URLs should point to one of',
+        test  => 'no show_bug.cgi in see_also URI'
+      },
+    ],
+
+    status => [
+      {
+        value => random_string(20),
+        error => 'There is no status named',
+        test  => 'invalid status fails'
+      },
+      {
+        value => '',
+        error => 'You must select/enter a status',
+        test  => 'blank status fails'
+      },
+
+      # We use the second bug for this because we can guarantee that
+      # it is open.
+      {
+        value => 'VERIFIED',
+        ids   => [$second_id],
+        extra => {resolution => 'FIXED'},
+        error => 'You are not allowed to change the bug status from',
+        test  => 'invalid transition fails'
+      },
+    ],
+
+    summary => [
+      {
+        value => random_string(300),
+        error => 'The text you entered in the Summary field is too long',
+        test  => 'too-long summary fails'
+      },
+      {
+        value => '',
+        error => 'You must enter a summary for this bug',
+        test  => 'blank summary fails'
+      },
+    ],
+
+    work_time => [
+      {
+        value => 100_000_000,
+        error => 'more than the maximum allowable value',
+        test  => 'too-large work_time fails'
+      },
+      {
+        value => random_string(20),
+        error => 'is not a numeric value',
+        test  => 'non-numeric work_time fails'
+      },
+
+      # We use PRIVATE_BUG_USER because he can modify the bug, but
+      # can't change time-tracking fields.
+      {
+        value => '10',
+        user  => PRIVATE_BUG_USER,
+        error => 'only a user with the required permissions',
+        test  => 'non-timetracker can not set work_time'
+      },
+    ],
+  );
+
+  $values{depends_on} = $values{blocks};
+
+  foreach my $field (qw(platform priority severity target_milestone version)) {
+    my $tests = dclone($values{op_sys});
+    foreach my $test (@$tests) {
+      $test->{test} =~ s/op_sys/$field/g;
     }
+    $values{$field} = $tests;
+  }
 
-    return %values;
+  return %values;
 }
 
 sub invalid_values_to_tests {
-    my ($invalid_values, $public_bug) = @_;
-
-    my @tests;
-    foreach my $field (sort keys %$invalid_values) {
-        my @tests_invalid = @{ $invalid_values->{$field} };
-        foreach my $item (@tests_invalid) {
-            my %args = (
-                ids => $item->{ids} || [$public_bug->{id}],
-                $field => $item->{value},
-                %{ $item->{extra} || {} },
-            );
-            push(@tests, { user => $item->{user} || 'editbugs',
-                           args => \%args,
-                           error => $item->{error},
-                           test => $item->{test} });
+  my ($invalid_values, $public_bug) = @_;
+
+  my @tests;
+  foreach my $field (sort keys %$invalid_values) {
+    my @tests_invalid = @{$invalid_values->{$field}};
+    foreach my $item (@tests_invalid) {
+      my %args = (
+        ids => $item->{ids} || [$public_bug->{id}],
+        $field => $item->{value},
+        %{$item->{extra} || {}},
+      );
+      push(
+        @tests,
+        {
+          user  => $item->{user} || 'editbugs',
+          args  => \%args,
+          error => $item->{error},
+          test  => $item->{test}
         }
+      );
     }
+  }
 
-    return \@tests;
+  return \@tests;
 }
 
 ###############
@@ -632,74 +817,78 @@ sub invalid_values_to_tests {
 
 my ($config, $xmlrpc, $jsonrpc, $jsonrpc_get) = get_rpc_clients();
 
-$jsonrpc_get->bz_call_fail('Bug.update',
-    { ids => ['public_bug'] },
-    'must use HTTP POST', 'update fails over GET');
+$jsonrpc_get->bz_call_fail(
+  'Bug.update',
+  {ids => ['public_bug']},
+  'must use HTTP POST',
+  'update fails over GET'
+);
 
 sub post_success {
-    my ($call, $t, $rpc) = @_;
-    return if $t->{no_changes};
-    my $field = $t->{field};
-    return if !$field;
-
-    my @bugs = @{ $call->result->{bugs} };
-    foreach my $bug (@bugs) {
-        if ($field =~ /^comment/) {
-            _check_comment($bug, $field, $t, $rpc);
-        }
-        else {
-            _check_changes($bug, $field, $t);
-        }
+  my ($call, $t, $rpc) = @_;
+  return if $t->{no_changes};
+  my $field = $t->{field};
+  return if !$field;
+
+  my @bugs = @{$call->result->{bugs}};
+  foreach my $bug (@bugs) {
+    if ($field =~ /^comment/) {
+      _check_comment($bug, $field, $t, $rpc);
+    }
+    else {
+      _check_changes($bug, $field, $t);
     }
+  }
 }
 
 sub _check_changes {
-    my ($bug, $field, $t) = @_;
+  my ($bug, $field, $t) = @_;
 
-    my $changes = $bug->{changes}->{$field};
-    ok(defined $changes, "$field was changed")
-      or diag Dumper($bug, $t);
+  my $changes = $bug->{changes}->{$field};
+  ok(defined $changes, "$field was changed") or diag Dumper($bug, $t);
 
-    my $new_value = $t->{added};
-    $new_value = $t->{args}->{$field} if !defined $new_value;
-    _test_value($changes->{added}, $new_value, $field, 'added');
+  my $new_value = $t->{added};
+  $new_value = $t->{args}->{$field} if !defined $new_value;
+  _test_value($changes->{added}, $new_value, $field, 'added');
 
-    if (defined $t->{removed}) {
-        _test_value($changes->{removed}, $t->{removed}, $field, 'removed');
-    }
+  if (defined $t->{removed}) {
+    _test_value($changes->{removed}, $t->{removed}, $field, 'removed');
+  }
 }
 
 sub _test_value {
-    my ($got, $expected, $field, $type) = @_;
-    if ($field eq 'estimated_time' or $field eq 'remaining_time') {
-        cmp_ok($got, '==', $expected, "$field: $type is correct");
-    }
-    else {
-        is($got, $expected, "$field: $type is correct");
-    }
+  my ($got, $expected, $field, $type) = @_;
+  if ($field eq 'estimated_time' or $field eq 'remaining_time') {
+    cmp_ok($got, '==', $expected, "$field: $type is correct");
+  }
+  else {
+    is($got, $expected, "$field: $type is correct");
+  }
 }
 
 sub _check_comment {
-    my ($bug, $field, $t, $rpc) = @_;
-    my $bug_id = $bug->{id};
-    my $call = $rpc->bz_call_success('Bug.comments', { ids => [$bug_id] });
-    my $comments = $call->result->{bugs}->{$bug_id}->{comments};
-
-    if ($field eq 'comment_is_private') {
-        my $first_private = $comments->[0]->{is_private};
-        my ($expected) = values %{ $t->{args}->{comment_is_private} };
-        cmp_ok($first_private, '==', $expected,
-               'description privacy is correct');
-    }
-    else {
-        my $last_comment = $comments->[-1];
-        my $expected = $t->{args}->{comment}->{body};
-        is($last_comment->{text}, $expected, 'comment added correctly');
-    }
+  my ($bug, $field, $t, $rpc) = @_;
+  my $bug_id   = $bug->{id};
+  my $call     = $rpc->bz_call_success('Bug.comments', {ids => [$bug_id]});
+  my $comments = $call->result->{bugs}->{$bug_id}->{comments};
+
+  if ($field eq 'comment_is_private') {
+    my $first_private = $comments->[0]->{is_private};
+    my ($expected) = values %{$t->{args}->{comment_is_private}};
+    cmp_ok($first_private, '==', $expected, 'description privacy is correct');
+  }
+  else {
+    my $last_comment = $comments->[-1];
+    my $expected     = $t->{args}->{comment}->{body};
+    is($last_comment->{text}, $expected, 'comment added correctly');
+  }
 
 }
 
 foreach my $rpc ($jsonrpc, $xmlrpc) {
-    $rpc->bz_run_tests(tests => get_tests($config, $rpc),
-        method => 'Bug.update', post_success => \&post_success);
+  $rpc->bz_run_tests(
+    tests        => get_tests($config, $rpc),
+    method       => 'Bug.update',
+    post_success => \&post_success
+  );
 }
index f35c7b8a819946f60fb35a96d30c77993c2169c6..77f7002ac092ac9cdde2511c21858f1b00bb2edd 100644 (file)
@@ -24,63 +24,80 @@ my ($config, $xmlrpc, $jsonrpc, $jsonrpc_get) = get_rpc_clients();
 my $bug_url = 'http://landfill.bugzilla.org/bugzilla-tip/show_bug.cgi?id=100';
 
 # update_see_also doesn't support logged-out users.
-my @tests = grep { $_->{user} } @{ STANDARD_BUG_TESTS() };
+my @tests = grep { $_->{user} } @{STANDARD_BUG_TESTS()};
 foreach my $t (@tests) {
-    $t->{args}->{add} = $t->{args}->{remove} = [];
+  $t->{args}->{add} = $t->{args}->{remove} = [];
 }
 
-push(@tests, (
-    { user  => 'unprivileged',
-      args  => { ids => ['public_bug'], add => [$bug_url] },
+push(
+  @tests,
+  (
+    {
+      user  => 'unprivileged',
+      args  => {ids => ['public_bug'], add => [$bug_url]},
       error => 'only the assignee or reporter of the bug, or a user',
       test  => 'Unprivileged user cannot add a URL to a bug',
     },
 
-    { user  => 'admin',
-      args  => { ids => ['public_bug'], add => ['asdfasdfasdf'] },
+    {
+      user  => 'admin',
+      args  => {ids => ['public_bug'], add => ['asdfasdfasdf']},
       error => 'asdf',
       test  => 'Admin cannot add an invalid URL',
     },
-    { user => 'admin',
-      args => { ids => ['public_bug'], remove => ['asdfasdfasdf'] },
+    {
+      user => 'admin',
+      args => {ids => ['public_bug'], remove => ['asdfasdfasdf']},
       test => 'Invalid URL silently ignored',
     },
 
-    { user => 'admin',
-      args => { ids => ['public_bug'], add => [$bug_url] },
+    {
+      user => 'admin',
+      args => {ids => ['public_bug'], add => [$bug_url]},
       test => 'Admin can add a URL to a public bug',
     },
-    { user  => 'unprivileged',
-      args  => { ids => ['public_bug'], remove => [$bug_url] },
+    {
+      user  => 'unprivileged',
+      args  => {ids => ['public_bug'], remove => [$bug_url]},
       error => 'only the assignee or reporter of the bug, or a user',
       test  => 'Unprivileged user cannot remove a URL from a bug',
     },
-    { user => 'admin',
-      args => { ids => ['public_bug'], remove => [$bug_url] },
+    {
+      user => 'admin',
+      args => {ids => ['public_bug'], remove => [$bug_url]},
       test => 'Admin can remove a URL from a public bug',
     },
 
-    { user => PRIVATE_BUG_USER,
-      args => { ids => ['private_bug'], add => [$bug_url] },
+    {
+      user => PRIVATE_BUG_USER,
+      args => {ids => ['private_bug'], add => [$bug_url]},
       test => PRIVATE_BUG_USER . ' can add a URL to a private bug',
     },
-    { user => PRIVATE_BUG_USER,
-      args => { ids => ['private_bug'], remove => [$bug_url] },
+    {
+      user => PRIVATE_BUG_USER,
+      args => {ids => ['private_bug'], remove => [$bug_url]},
       test => PRIVATE_BUG_USER . ' can remove a URL from a private bug',
     },
 
-));
+  )
+);
 
 sub post_success {
-    my ($call, $t) = @_;
-    isa_ok($call->result->{changes}, 'HASH', "Changes");
+  my ($call, $t) = @_;
+  isa_ok($call->result->{changes}, 'HASH', "Changes");
 }
 
-$jsonrpc_get->bz_call_fail('Bug.update_see_also',
-    { ids => ['public_bug'], add => [$bug_url] },
-    'must use HTTP POST', 'update_see_also fails over GET');
+$jsonrpc_get->bz_call_fail(
+  'Bug.update_see_also',
+  {ids => ['public_bug'], add => [$bug_url]},
+  'must use HTTP POST',
+  'update_see_also fails over GET'
+);
 
 foreach my $rpc ($jsonrpc, $xmlrpc) {
-    $rpc->bz_run_tests(tests => \@tests, method => 'Bug.update_see_also',
-                       post_success => \&post_success);
+  $rpc->bz_run_tests(
+    tests        => \@tests,
+    method       => 'Bug.update_see_also',
+    post_success => \&post_success
+  );
 }
index 00c4fc27ac7ec017491299150f216ea62334df95..2b5bcfa46dfa3471c2e2d6dd734dfebda511e205 100644 (file)
@@ -21,29 +21,30 @@ use QA::Util;
 my ($config, @clients) = get_rpc_clients();
 
 foreach my $rpc (@clients) {
-    my $vers_call = $rpc->bz_call_success('Bugzilla.version');
-    my $version = $vers_call->result->{version};
-    ok($version, "Bugzilla.version returns $version");
-
-    my $tz_call = $rpc->bz_call_success('Bugzilla.timezone');
-    my $tz = $tz_call->result->{timezone};
-    ok($tz, "Bugzilla.timezone returns $tz");
-
-    my $ext_call = $rpc->bz_call_success('Bugzilla.extensions');
-    my $extensions = $ext_call->result->{extensions};
-    isa_ok($extensions, 'HASH', 'extensions');
-
-    # There is always at least the QA extension enabled.
-    my $cmp = $config->{test_extensions} ? '>' : '==';
-    my @ext_names = keys %$extensions;
-    my $desc = scalar(@ext_names) . ' extension(s) returned: ' . join(', ', @ext_names);
-    cmp_ok(scalar(@ext_names), $cmp, 1, $desc);
-    ok(grep($_ eq 'QA', @ext_names), 'The QA extension is enabled');
-
-    my $time_call = $rpc->bz_call_success('Bugzilla.time');
-    my $time_result = $time_call->result;
-    foreach my $type (qw(db_time web_time)) {
-        cmp_ok($time_result->{$type}, '=~', $rpc->DATETIME_REGEX,
-               "Bugzilla.time returns a datetime for $type");
-    }
+  my $vers_call = $rpc->bz_call_success('Bugzilla.version');
+  my $version   = $vers_call->result->{version};
+  ok($version, "Bugzilla.version returns $version");
+
+  my $tz_call = $rpc->bz_call_success('Bugzilla.timezone');
+  my $tz      = $tz_call->result->{timezone};
+  ok($tz, "Bugzilla.timezone returns $tz");
+
+  my $ext_call   = $rpc->bz_call_success('Bugzilla.extensions');
+  my $extensions = $ext_call->result->{extensions};
+  isa_ok($extensions, 'HASH', 'extensions');
+
+  # There is always at least the QA extension enabled.
+  my $cmp = $config->{test_extensions} ? '>' : '==';
+  my @ext_names = keys %$extensions;
+  my $desc
+    = scalar(@ext_names) . ' extension(s) returned: ' . join(', ', @ext_names);
+  cmp_ok(scalar(@ext_names), $cmp, 1, $desc);
+  ok(grep($_ eq 'QA', @ext_names), 'The QA extension is enabled');
+
+  my $time_call   = $rpc->bz_call_success('Bugzilla.time');
+  my $time_result = $time_call->result;
+  foreach my $type (qw(db_time web_time)) {
+    cmp_ok($time_result->{$type}, '=~', $rpc->DATETIME_REGEX,
+      "Bugzilla.time returns a datetime for $type");
+  }
 }
index 97aaff5ed02d2d62615011c54a16783372320872..9634c61873aa68371e303675e4ce605522ea9335 100644 (file)
@@ -22,80 +22,103 @@ use QA::Util;
 use constant DESCRIPTION => 'Group created by Group.create';
 
 sub post_success {
-    my $call = shift;
-    my $gid = $call->result->{id};
-    ok($gid, "Got a non-zero group ID: $gid");
+  my $call = shift;
+  my $gid  = $call->result->{id};
+  ok($gid, "Got a non-zero group ID: $gid");
 }
 
 my ($config, $xmlrpc, $jsonrpc, $jsonrpc_get) = get_rpc_clients();
 
 my @tests = (
-    { args  => { name => random_string(20), description => DESCRIPTION },
-      error => 'You must log in',
-      test  => 'Logged-out user cannot call Group.create',
-    },
-    { user  => 'unprivileged',
-      args  => { name => random_string(20), description => DESCRIPTION },
-      error => 'you are not authorized',
-      test  => 'Unprivileged user cannot call Group.create',
-    },
-    { user  => 'admin',
-      args  => { description => DESCRIPTION },
-      error => 'You must enter a name',
-      test  => 'Missing name to Group.create',
-    },
-    { user  => 'admin',
-      args  => { name => random_string(20) },
-      error => 'You must enter a description',
-      test  => 'Missing description to Group.create',
-    },
-    { user  => 'admin',
-      args  => { name => '', description => DESCRIPTION },
-      error => 'You must enter a name',
-      test  => 'Name to Group.create cannot be empty',
-    },
-    { user  => 'admin',
-      args  => { name => random_string(20), description => '' },
-      error => 'You must enter a description',
-      test  => 'Description to Group.create cannot be empty',
-    },
-    { user  => 'admin',
-      args  => { name => 'canconfirm', description => DESCRIPTION },
-      error => 'already exists',
-      test  => 'Name to Group.create already exists',
-    },
-    { user  => 'admin',
-      args  => { name => 'caNConFIrm', description => DESCRIPTION },
-      error => 'already exists',
-      test  => 'Name to Group.create already exists but with a different case',
-    },
-    { user  => 'admin',
-      args  => { name => random_string(20), description => DESCRIPTION,
-                 user_regexp => '\\'},
-      error => 'The regular expression you entered is invalid',
-      test  => 'The regular expression passed to Group.create is invalid',
-    },
+  {
+    args  => {name => random_string(20), description => DESCRIPTION},
+    error => 'You must log in',
+    test  => 'Logged-out user cannot call Group.create',
+  },
+  {
+    user  => 'unprivileged',
+    args  => {name => random_string(20), description => DESCRIPTION},
+    error => 'you are not authorized',
+    test  => 'Unprivileged user cannot call Group.create',
+  },
+  {
+    user  => 'admin',
+    args  => {description => DESCRIPTION},
+    error => 'You must enter a name',
+    test  => 'Missing name to Group.create',
+  },
+  {
+    user  => 'admin',
+    args  => {name => random_string(20)},
+    error => 'You must enter a description',
+    test  => 'Missing description to Group.create',
+  },
+  {
+    user  => 'admin',
+    args  => {name => '', description => DESCRIPTION},
+    error => 'You must enter a name',
+    test  => 'Name to Group.create cannot be empty',
+  },
+  {
+    user  => 'admin',
+    args  => {name => random_string(20), description => ''},
+    error => 'You must enter a description',
+    test  => 'Description to Group.create cannot be empty',
+  },
+  {
+    user  => 'admin',
+    args  => {name => 'canconfirm', description => DESCRIPTION},
+    error => 'already exists',
+    test  => 'Name to Group.create already exists',
+  },
+  {
+    user  => 'admin',
+    args  => {name => 'caNConFIrm', description => DESCRIPTION},
+    error => 'already exists',
+    test  => 'Name to Group.create already exists but with a different case',
+  },
+  {
+    user => 'admin',
+    args =>
+      {name => random_string(20), description => DESCRIPTION, user_regexp => '\\'},
+    error => 'The regular expression you entered is invalid',
+    test  => 'The regular expression passed to Group.create is invalid',
+  },
 );
 
-$jsonrpc_get->bz_call_fail('Group.create',
-    { name => random_string(20), description => 'Created with JSON-RPC via GET' },
-    'must use HTTP POST', 'Group.create fails over GET');
+$jsonrpc_get->bz_call_fail(
+  'Group.create',
+  {name => random_string(20), description => 'Created with JSON-RPC via GET'},
+  'must use HTTP POST',
+  'Group.create fails over GET'
+);
 
 foreach my $rpc ($xmlrpc, $jsonrpc) {
-    # Tests which work must be called from here,
-    # to avoid creating twice the same group.
-    my @all_tests = (@tests,
-        { user  => 'admin',
-          args  => { name => random_string(20), description => DESCRIPTION },
-          test  => 'Passing the name and description only works',
-        },
-        { user  => 'admin',
-          args  => { name => random_string(20), description => DESCRIPTION,
-                     user_regexp => '\@foo.com$', is_active => 1,
-                     icon_url => 'http://www.bugzilla.org/favicon.ico' },
-          test  => 'Passing all arguments works',
-        },
-    );
-    $rpc->bz_run_tests(tests => \@all_tests, method => 'Group.create',
-                       post_success => \&post_success);
+
+  # Tests which work must be called from here,
+  # to avoid creating twice the same group.
+  my @all_tests = (
+    @tests,
+    {
+      user => 'admin',
+      args => {name => random_string(20), description => DESCRIPTION},
+      test => 'Passing the name and description only works',
+    },
+    {
+      user => 'admin',
+      args => {
+        name        => random_string(20),
+        description => DESCRIPTION,
+        user_regexp => '\@foo.com$',
+        is_active   => 1,
+        icon_url    => 'http://www.bugzilla.org/favicon.ico'
+      },
+      test => 'Passing all arguments works',
+    },
+  );
+  $rpc->bz_run_tests(
+    tests        => \@all_tests,
+    method       => 'Group.create',
+    post_success => \&post_success
+  );
 }
index a89e1259c7b30e1df1967723d50fcfcf5c4dd5f1..d2f8b2deabe62ded3163072795643702000019c5 100644 (file)
@@ -16,19 +16,26 @@ use Test::More tests => 85;
 use QA::Util;
 my $jsonrpc_get = QA::Util::get_jsonrpc_client('GET');
 
-my @chars = (0..9, 'A'..'Z', 'a'..'z', '_[].');
+my @chars = (0 .. 9, 'A' .. 'Z', 'a' .. 'z', '_[].');
 
 our @tests = (
-    { args => { callback => join('', @chars) },
-      test => 'callback accepts all legal characters.' },
+  {
+    args => {callback => join('', @chars)},
+    test => 'callback accepts all legal characters.'
+  },
 );
-foreach my $char (qw(! ~ ` @ $ % ^ & * - + = { } ; : ' " < > / ? |),
-                  '(', ')', '\\', '#', ',')
+foreach
+  my $char (qw(! ~ ` @ $ % ^ & * - + = { } ; : ' " < > / ? |), '(', ')', '\\',
+  '#', ',')
 {
-    push(@tests,
-         { args  => { callback => "a$char" },
-           error => "as your 'callback' parameter",
-           test  => "$char is not valid in callback" });
+  push(
+    @tests,
+    {
+      args  => {callback => "a$char"},
+      error => "as your 'callback' parameter",
+      test  => "$char is not valid in callback"
+    }
+  );
 }
 
 $jsonrpc_get->bz_run_tests(method => 'Bugzilla.version', tests => \@tests);
index 273c817be6d6b022cf6d56d1ae9544347afc1d33..0b9f45cde6bd0e4d00602a720aeacef9821766eb 100644 (file)
@@ -19,149 +19,226 @@ use lib "$RealBin/../lib", "$RealBin/../../local/lib/perl5";
 use Test::More tests => 121;
 use QA::Util;
 
-use constant DESCRIPTION => 'Product created by Product.create';
+use constant DESCRIPTION  => 'Product created by Product.create';
 use constant PROD_VERSION => 'unspecified';
 
 sub post_success {
-    my ($call, $test, $self) = @_;
-    my $args = $test->{args};
-    my $prod_id = $call->result->{id};
-    ok($prod_id, "Got a non-zero product ID: $prod_id");
+  my ($call, $test, $self) = @_;
+  my $args    = $test->{args};
+  my $prod_id = $call->result->{id};
+  ok($prod_id, "Got a non-zero product ID: $prod_id");
 
-    $call = $self->bz_call_success("Product.get", {ids => [$prod_id]});
-    my $product = $call->result->{products}->[0];
-    my $prod_name = $product->{name};
-    my $is_active = defined $args->{is_open} ? $args->{is_open} : 1;
-    ok($product->{is_active} == $is_active,
-       "Product $prod_name has the correct value for is_active/is_open: $is_active");
-    my $has_unco = defined $args->{has_unconfirmed} ? $args->{has_unconfirmed} : 1;
-    ok($product->{has_unconfirmed} == $has_unco,
-       "Product $prod_name has the correct value for has_unconfirmed: $has_unco");
+  $call = $self->bz_call_success("Product.get", {ids => [$prod_id]});
+  my $product   = $call->result->{products}->[0];
+  my $prod_name = $product->{name};
+  my $is_active = defined $args->{is_open} ? $args->{is_open} : 1;
+  ok($product->{is_active} == $is_active,
+    "Product $prod_name has the correct value for is_active/is_open: $is_active");
+  my $has_unco = defined $args->{has_unconfirmed} ? $args->{has_unconfirmed} : 1;
+  ok($product->{has_unconfirmed} == $has_unco,
+    "Product $prod_name has the correct value for has_unconfirmed: $has_unco");
 }
 
 my ($config, $xmlrpc, $jsonrpc, $jsonrpc_get) = get_rpc_clients();
 
 my @tests = (
-    { args  => { name => random_string(20), version => PROD_VERSION,
-                 description => DESCRIPTION },
-      error => 'You must log in',
-      test  => 'Logged-out user cannot call Product.create',
+  {
+    args => {
+      name        => random_string(20),
+      version     => PROD_VERSION,
+      description => DESCRIPTION
     },
-    { user  => 'unprivileged',
-      args  => { name => random_string(20), version => PROD_VERSION,
-                 description => DESCRIPTION },
-      error => 'you are not authorized',
-      test  => 'Unprivileged user cannot call Product.create',
+    error => 'You must log in',
+    test  => 'Logged-out user cannot call Product.create',
+  },
+  {
+    user => 'unprivileged',
+    args => {
+      name        => random_string(20),
+      version     => PROD_VERSION,
+      description => DESCRIPTION
     },
-    { user  => 'admin',
-      args  => { version => PROD_VERSION, description => DESCRIPTION },
-      error => 'You must enter a name',
-      test  => 'Missing name to Product.create',
+    error => 'you are not authorized',
+    test  => 'Unprivileged user cannot call Product.create',
+  },
+  {
+    user  => 'admin',
+    args  => {version => PROD_VERSION, description => DESCRIPTION},
+    error => 'You must enter a name',
+    test  => 'Missing name to Product.create',
+  },
+  {
+    user  => 'admin',
+    args  => {name => random_string(20), version => PROD_VERSION},
+    error => 'You must enter a description',
+    test  => 'Missing description to Product.create',
+  },
+  {
+    user  => 'admin',
+    args  => {name => random_string(20), description => DESCRIPTION},
+    error => 'You must enter a valid version',
+    test  => 'Missing version to Product.create',
+  },
+  {
+    user  => 'admin',
+    args  => {name => '', version => PROD_VERSION, description => DESCRIPTION},
+    error => 'You must enter a name',
+    test  => 'Name to Product.create cannot be empty',
+  },
+  {
+    user => 'admin',
+    args => {name => random_string(20), version => PROD_VERSION, description => ''},
+    error => 'You must enter a description',
+    test  => 'Description to Product.create cannot be empty',
+  },
+  {
+    user  => 'admin',
+    args  => {name => random_string(20), version => '', description => DESCRIPTION},
+    error => 'You must enter a valid version',
+    test  => 'Version to Product.create cannot be empty',
+  },
+  {
+    user => 'admin',
+    args => {
+      name        => random_string(20000),
+      version     => PROD_VERSION,
+      description => DESCRIPTION
     },
-    { user  => 'admin',
-      args  => { name => random_string(20), version => PROD_VERSION },
-      error => 'You must enter a description',
-      test  => 'Missing description to Product.create',
+    error => 'The name of a product is limited',
+    test  => 'Name to Product.create too long',
+  },
+  {
+    user => 'admin',
+    args => {
+      name        => 'Another Product',
+      version     => PROD_VERSION,
+      description => DESCRIPTION
     },
-    { user  => 'admin',
-      args  => { name => random_string(20), description => DESCRIPTION },
-      error => 'You must enter a valid version',
-      test  => 'Missing version to Product.create',
-    },
-    { user  => 'admin',
-      args  => { name => '', version => PROD_VERSION, description => DESCRIPTION },
-      error => 'You must enter a name',
-      test  => 'Name to Product.create cannot be empty',
-    },
-    { user  => 'admin',
-      args  => { name => random_string(20), version => PROD_VERSION, description => '' },
-      error => 'You must enter a description',
-      test  => 'Description to Product.create cannot be empty',
-    },
-    { user  => 'admin',
-      args  => { name => random_string(20), version => '', description => DESCRIPTION },
-      error => 'You must enter a valid version',
-      test  => 'Version to Product.create cannot be empty',
-    },
-    { user  => 'admin',
-      args  => { name => random_string(20000), version => PROD_VERSION,
-                 description => DESCRIPTION },
-      error => 'The name of a product is limited',
-      test  => 'Name to Product.create too long',
-    },
-    { user  => 'admin',
-      args  => { name => 'Another Product', version => PROD_VERSION,
-                 description => DESCRIPTION },
-      error => 'already exists',
-      test  => 'Name to Product.create already exists',
-    },
-    { user  => 'admin',
-      args  => { name => 'aNoThEr Product', version => PROD_VERSION,
-                 description => DESCRIPTION },
-      error => 'differs from existing product',
-      test  => 'Name to Product.create already exists but with a different case',
+    error => 'already exists',
+    test  => 'Name to Product.create already exists',
+  },
+  {
+    user => 'admin',
+    args => {
+      name        => 'aNoThEr Product',
+      version     => PROD_VERSION,
+      description => DESCRIPTION
     },
+    error => 'differs from existing product',
+    test  => 'Name to Product.create already exists but with a different case',
+  },
 );
 
 # FIXME - Should be: if (classifications enabled).
 # But there is currently now way to query the value of a parameter via WS.
 if (0) {
-    push(@tests,
-        { user  => 'admin',
-          args  => { name => random_string(20), version => PROD_VERSION,
-                     description => DESCRIPTION, has_unconfirmed => 1,
-                     classification => '', default_milestone => '2.0',
-                     is_open => 1, create_series => 1 },
-          error => 'You must select/enter a classification',
-          test  => 'Passing an empty classification to Product.create fails',
-        },
-        { user  => 'admin',
-          args  => { name => random_string(20), version => PROD_VERSION,
-                     description => DESCRIPTION, has_unconfirmed => 1,
-                     classification => random_string(10), default_milestone => '2.0',
-                     is_open => 1, create_series => 1 },
-          error => 'You must select/enter a classification',
-          test  => 'Passing an invalid classification to Product.create fails',
-        },
-    )
+  push(
+    @tests,
+    {
+      user => 'admin',
+      args => {
+        name              => random_string(20),
+        version           => PROD_VERSION,
+        description       => DESCRIPTION,
+        has_unconfirmed   => 1,
+        classification    => '',
+        default_milestone => '2.0',
+        is_open           => 1,
+        create_series     => 1
+      },
+      error => 'You must select/enter a classification',
+      test  => 'Passing an empty classification to Product.create fails',
+    },
+    {
+      user => 'admin',
+      args => {
+        name              => random_string(20),
+        version           => PROD_VERSION,
+        description       => DESCRIPTION,
+        has_unconfirmed   => 1,
+        classification    => random_string(10),
+        default_milestone => '2.0',
+        is_open           => 1,
+        create_series     => 1
+      },
+      error => 'You must select/enter a classification',
+      test  => 'Passing an invalid classification to Product.create fails',
+    },
+  );
 }
 
-$jsonrpc_get->bz_call_fail('Product.create',
-    { name => random_string(20), version => PROD_VERSION,
-      description => 'Created with JSON-RPC via GET' },
-    'must use HTTP POST', 'Product.create fails over GET');
+$jsonrpc_get->bz_call_fail(
+  'Product.create',
+  {
+    name        => random_string(20),
+    version     => PROD_VERSION,
+    description => 'Created with JSON-RPC via GET'
+  },
+  'must use HTTP POST',
+  'Product.create fails over GET'
+);
 
 foreach my $rpc ($xmlrpc, $jsonrpc) {
-    # Tests which work must be called from here,
-    # to avoid creating twice the same product.
-    my @all_tests = (@tests,
-        { user  => 'admin',
-          args  => { name => random_string(20), version => PROD_VERSION,
-                     description => DESCRIPTION },
-          test  => 'Passing the name, description and version only works',
-        },
-        { user  => 'admin',
-          args  => { name => random_string(20), version => PROD_VERSION,
-                     description => DESCRIPTION, has_unconfirmed => 1,
-                     classification => 'Class2_QA', default_milestone => '2.0',
-                     is_open => 1, create_series => 1 },
-          test  => 'Passing all arguments works',
-        },
-        { user  => 'admin',
-          args  => { name => random_string(20), version => PROD_VERSION,
-                     description => DESCRIPTION, has_unconfirmed => 0,
-                     classification => 'Class2_QA', default_milestone => '2.0',
-                     is_open => 0, create_series => 0 },
-          test  => 'Passing null values works',
-        },
-        { user  => 'admin',
-          args  => { name => random_string(20), version => PROD_VERSION,
-                     description => DESCRIPTION, has_unconfirmed => 1,
-                     classification => 'Class2_QA', default_milestone => '',
-                     is_open => 1, create_series => 1 },
-          test  => 'Passing an empty default milestone works (falls back to "---")',
-        },
-    );
-    $rpc->bz_run_tests(tests => \@all_tests, method => 'Product.create',
-                       post_success => \&post_success);
+
+  # Tests which work must be called from here,
+  # to avoid creating twice the same product.
+  my @all_tests = (
+    @tests,
+    {
+      user => 'admin',
+      args => {
+        name        => random_string(20),
+        version     => PROD_VERSION,
+        description => DESCRIPTION
+      },
+      test => 'Passing the name, description and version only works',
+    },
+    {
+      user => 'admin',
+      args => {
+        name              => random_string(20),
+        version           => PROD_VERSION,
+        description       => DESCRIPTION,
+        has_unconfirmed   => 1,
+        classification    => 'Class2_QA',
+        default_milestone => '2.0',
+        is_open           => 1,
+        create_series     => 1
+      },
+      test => 'Passing all arguments works',
+    },
+    {
+      user => 'admin',
+      args => {
+        name              => random_string(20),
+        version           => PROD_VERSION,
+        description       => DESCRIPTION,
+        has_unconfirmed   => 0,
+        classification    => 'Class2_QA',
+        default_milestone => '2.0',
+        is_open           => 0,
+        create_series     => 0
+      },
+      test => 'Passing null values works',
+    },
+    {
+      user => 'admin',
+      args => {
+        name              => random_string(20),
+        version           => PROD_VERSION,
+        description       => DESCRIPTION,
+        has_unconfirmed   => 1,
+        classification    => 'Class2_QA',
+        default_milestone => '',
+        is_open           => 1,
+        create_series     => 1
+      },
+      test => 'Passing an empty default milestone works (falls back to "---")',
+    },
+  );
+  $rpc->bz_run_tests(
+    tests        => \@all_tests,
+    method       => 'Product.create',
+    post_success => \&post_success
+  );
 }
index a692a3ceec58adc34feef4c631a0b6336fe44d5c..b4906b395a38005784cf9512418f7bcd8b3a3318 100644 (file)
@@ -24,7 +24,7 @@ use Test::More tests => 134;
 use QA::Util;
 my ($config, @clients) = get_rpc_clients();
 
-my $products = $clients[0]->bz_get_products();
+my $products  = $clients[0]->bz_get_products();
 my $public    = $products->{'Another Product'};
 my $private   = $products->{'QA-Selenium-TEST'};
 my $no_entry  = $products->{'QA Entry Only'};
@@ -33,81 +33,79 @@ my $no_search = $products->{'QA Search Only'};
 my %id_map = reverse %$products;
 
 my $tests = {
-    'QA_Selenium_TEST' => {
-        selectable => [$public, $private, $no_entry, $no_search],
-        enterable  => [$public, $private, $no_entry, $no_search],
-        accessible => [$public, $private, $no_entry, $no_search],
-    },
-    'unprivileged' => {
-        selectable => [$public, $no_entry],
-        not_selectable => $no_search,
-        enterable  => [$public, $no_search],
-        not_enterable => $no_entry,
-        accessible => [$public, $no_entry, $no_search],
-        not_accessible => $private,
-    },
-    '' => {
-        selectable => [$public, $no_entry],
-        not_selectable => $no_search,
-        enterable  => [$public, $no_search],
-        not_enterable => $no_entry,
-        accessible => [$public, $no_entry, $no_search],
-        not_accessible => $private,
-    },
+  'QA_Selenium_TEST' => {
+    selectable => [$public, $private, $no_entry, $no_search],
+    enterable  => [$public, $private, $no_entry, $no_search],
+    accessible => [$public, $private, $no_entry, $no_search],
+  },
+  'unprivileged' => {
+    selectable     => [$public, $no_entry],
+    not_selectable => $no_search,
+    enterable      => [$public, $no_search],
+    not_enterable  => $no_entry,
+    accessible     => [$public, $no_entry, $no_search],
+    not_accessible => $private,
+  },
+  '' => {
+    selectable     => [$public, $no_entry],
+    not_selectable => $no_search,
+    enterable      => [$public, $no_search],
+    not_enterable  => $no_entry,
+    accessible     => [$public, $no_entry, $no_search],
+    not_accessible => $private,
+  },
 };
 
 foreach my $rpc (@clients) {
-    foreach my $user (keys %$tests) {
-        my @selectable = @{ $tests->{$user}->{selectable} };
-        my @enterable  = @{ $tests->{$user}->{enterable} };
-        my @accessible = @{ $tests->{$user}->{accessible} };
-        my $not_selectable = $tests->{$user}->{not_selectable};
-        my $not_enterable  = $tests->{$user}->{not_enterable};
-        my $not_accessible = $tests->{$user}->{not_accessible};
+  foreach my $user (keys %$tests) {
+    my @selectable     = @{$tests->{$user}->{selectable}};
+    my @enterable      = @{$tests->{$user}->{enterable}};
+    my @accessible     = @{$tests->{$user}->{accessible}};
+    my $not_selectable = $tests->{$user}->{not_selectable};
+    my $not_enterable  = $tests->{$user}->{not_enterable};
+    my $not_accessible = $tests->{$user}->{not_accessible};
 
-        $rpc->bz_log_in($user) if $user;
-        $user ||= "Logged-out user";
+    $rpc->bz_log_in($user) if $user;
+    $user ||= "Logged-out user";
 
-        my $select_call =
-            $rpc->bz_call_success('Product.get_selectable_products');
-        my $select_ids = $select_call->result->{ids};
-        foreach my $id (@selectable) {
-            ok(grep($_ == $id, @$select_ids),
-               "$user can select " . $id_map{$id});
-        }
-        if ($not_selectable) {
-            ok(!grep($_ == $not_selectable, @$select_ids),
-               "$user cannot select " . $id_map{$not_selectable});
-        }
-
-        my $enter_call =
-            $rpc->bz_call_success('Product.get_enterable_products');
-        my $enter_ids = $enter_call->result->{ids};
-        foreach my $id (@enterable) {
-            ok(grep($_ == $id, @$enter_ids), "$user can enter " . $id_map{$id});
-        }
-        if ($not_enterable) {
-            ok(!grep($_ == $not_enterable, @$enter_ids),
-               "$user cannot enter " . $id_map{$not_enterable});
-        }
+    my $select_call = $rpc->bz_call_success('Product.get_selectable_products');
+    my $select_ids  = $select_call->result->{ids};
+    foreach my $id (@selectable) {
+      ok(grep($_ == $id, @$select_ids), "$user can select " . $id_map{$id});
+    }
+    if ($not_selectable) {
+      ok(
+        !grep($_ == $not_selectable, @$select_ids),
+        "$user cannot select " . $id_map{$not_selectable}
+      );
+    }
 
-        my $access_call =
-            $rpc->bz_call_success('Product.get_accessible_products');
-        my $get_call = $rpc->bz_call_success('Product.get',
-                                             { ids => \@accessible });
-        my $products = $get_call->result->{products};
-        my $expected_count = scalar @accessible;
-        cmp_ok(scalar @$products, '==', $expected_count,
-           "Product.get gets all $expected_count accessible products"
-           . " for $user.");
-        if ($not_accessible) {
-            my $no_access_call = $rpc->bz_call_success(
-                'Product.get', { ids => [$not_accessible] });
-            ok(!scalar @{ $no_access_call->result->{products} },
-               "$user gets 0 products when asking for "
-               . $id_map{$not_accessible});
-        }
+    my $enter_call = $rpc->bz_call_success('Product.get_enterable_products');
+    my $enter_ids  = $enter_call->result->{ids};
+    foreach my $id (@enterable) {
+      ok(grep($_ == $id, @$enter_ids), "$user can enter " . $id_map{$id});
+    }
+    if ($not_enterable) {
+      ok(
+        !grep($_ == $not_enterable, @$enter_ids),
+        "$user cannot enter " . $id_map{$not_enterable}
+      );
+    }
 
-        $rpc->bz_call_success('User.logout') if $user ne "Logged-out user";
+    my $access_call = $rpc->bz_call_success('Product.get_accessible_products');
+    my $get_call    = $rpc->bz_call_success('Product.get', {ids => \@accessible});
+    my $products    = $get_call->result->{products};
+    my $expected_count = scalar @accessible;
+    cmp_ok(scalar @$products,
+      '==', $expected_count,
+      "Product.get gets all $expected_count accessible products" . " for $user.");
+    if ($not_accessible) {
+      my $no_access_call
+        = $rpc->bz_call_success('Product.get', {ids => [$not_accessible]});
+      ok(!scalar @{$no_access_call->result->{products}},
+        "$user gets 0 products when asking for " . $id_map{$not_accessible});
     }
+
+    $rpc->bz_call_success('User.logout') if $user ne "Logged-out user";
+  }
 }
index f307b939c57eebfdf384593b03aee1e8ff9dd1d8..1ed2674f95f2ba834ebb4053eeb4e478b94b93e7 100644 (file)
@@ -29,90 +29,115 @@ use constant PASSWORD_TOO_SHORT => 'a';
 use constant INVALID_EMAIL => '()[]\;:,<>@webservice.test';
 
 sub new_login {
-    return 'created_' . random_string(@_) . '@webservice.test';
+  return 'created_' . random_string(@_) . '@webservice.test';
 }
 
 sub post_success {
-    my ($call) = @_;
-    ok($call->result->{id}, "Got a non-zero user id");
+  my ($call) = @_;
+  ok($call->result->{id}, "Got a non-zero user id");
 }
 
-$jsonrpc_get->bz_call_fail('User.create',
-    { email => new_login(), full_name => NEW_FULLNAME,
-      password => '*' },
-    'must use HTTP POST', 'User.create fails over GET');
+$jsonrpc_get->bz_call_fail(
+  'User.create',
+  {email => new_login(), full_name => NEW_FULLNAME, password => '*'},
+  'must use HTTP POST',
+  'User.create fails over GET'
+);
 
 # We have to wrap @tests in the foreach, because we want a different
 # login for each user, separately for each RPC client. (You can't create
 # two users with the same username, and XML-RPC would otherwise try to
 # create the same users that JSON-RPC created.)
 foreach my $rpc ($jsonrpc, $xmlrpc) {
-    my @tests = (
-        # Permissions checks
-        { args  => { email    => new_login(), full_name => NEW_FULLNAME,
-                     password => NEW_PASSWORD },
-          error => "you are not authorized",
-          test  => 'Logged-out user cannot call User.create',
-        },
-        { user  => 'unprivileged',
-          args  => { email    => new_login(), full_name => NEW_FULLNAME,
-                     password => NEW_PASSWORD },
-          error => "you are not authorized",
-          test  => 'Unprivileged user cannot call User.create',
-        },
+  my @tests = (
 
-        # Login name checks.
-        { user  => 'admin',
-          args  => { full_name => NEW_FULLNAME, password => NEW_PASSWORD },
-          error => "argument was not set",
-          test  => 'Leaving out email argument fails',
-        },
-        { user  => 'admin',
-          args  => { email    => '', full_name => NEW_FULLNAME,
-                     password => NEW_PASSWORD },
-          error => "argument was not set",
-          test  => "Passing an empty email argument fails",
-        },
-        { user  => 'admin',
-          args  => { email    => INVALID_EMAIL, full_name => NEW_FULLNAME,
-                     password => NEW_PASSWORD },
-          error =>  "didn't pass our syntax checking",
-          test  => 'Invalid email address fails',
-        },
-        { user  => 'admin',
-          args  => { email    => new_login(128), full_name => NEW_FULLNAME,
-            password => NEW_PASSWORD },
-            error =>  "didn't pass our syntax checking",
-            test  => 'Too long (> 127 chars) email address fails',
-        },
-        { user  => 'admin',
-          args  => { email     => $config->{unprivileged_user_login},
-                     full_name => NEW_FULLNAME, password => NEW_PASSWORD },
-          error =>  "There is already an account",
-          test  => 'Trying to use an existing login name fails',
-        },
+    # Permissions checks
+    {
+      args =>
+        {email => new_login(), full_name => NEW_FULLNAME, password => NEW_PASSWORD},
+      error => "you are not authorized",
+      test  => 'Logged-out user cannot call User.create',
+    },
+    {
+      user => 'unprivileged',
+      args =>
+        {email => new_login(), full_name => NEW_FULLNAME, password => NEW_PASSWORD},
+      error => "you are not authorized",
+      test  => 'Unprivileged user cannot call User.create',
+    },
 
-        { user  => 'admin',
-          args  => { email    => new_login(), full_name => NEW_FULLNAME,
-                     password => PASSWORD_TOO_SHORT },
-          error => 'password must be at least',
-          test  => 'Password Too Short fails',
-        },
-        { user => 'admin',
-          args => { email    => new_login(), full_name => NEW_FULLNAME,
-                    password => NEW_PASSWORD },
-          test => 'Creating a user with all arguments and correct privileges',
-        },
-        { user => 'admin',
-          args => { email => new_login(), password => NEW_PASSWORD },
-          test => 'Leaving out fullname works',
-        },
-        { user => 'admin',
-          args => { email => new_login(), full_name => NEW_FULLNAME },
-          test => 'Leaving out password works',
-        },
-    );
+    # Login name checks.
+    {
+      user  => 'admin',
+      args  => {full_name => NEW_FULLNAME, password => NEW_PASSWORD},
+      error => "argument was not set",
+      test  => 'Leaving out email argument fails',
+    },
+    {
+      user  => 'admin',
+      args  => {email => '', full_name => NEW_FULLNAME, password => NEW_PASSWORD},
+      error => "argument was not set",
+      test  => "Passing an empty email argument fails",
+    },
+    {
+      user => 'admin',
+      args =>
+        {email => INVALID_EMAIL, full_name => NEW_FULLNAME, password => NEW_PASSWORD},
+      error => "didn't pass our syntax checking",
+      test  => 'Invalid email address fails',
+    },
+    {
+      user => 'admin',
+      args => {
+        email     => new_login(128),
+        full_name => NEW_FULLNAME,
+        password  => NEW_PASSWORD
+      },
+      error => "didn't pass our syntax checking",
+      test  => 'Too long (> 127 chars) email address fails',
+    },
+    {
+      user => 'admin',
+      args => {
+        email     => $config->{unprivileged_user_login},
+        full_name => NEW_FULLNAME,
+        password  => NEW_PASSWORD
+      },
+      error => "There is already an account",
+      test  => 'Trying to use an existing login name fails',
+    },
 
-    $rpc->bz_run_tests(tests => \@tests, method => 'User.create',
-                       post_success => \&post_success);
+    {
+      user => 'admin',
+      args => {
+        email     => new_login(),
+        full_name => NEW_FULLNAME,
+        password  => PASSWORD_TOO_SHORT
+      },
+      error => 'password must be at least',
+      test  => 'Password Too Short fails',
+    },
+    {
+      user => 'admin',
+      args =>
+        {email => new_login(), full_name => NEW_FULLNAME, password => NEW_PASSWORD},
+      test => 'Creating a user with all arguments and correct privileges',
+    },
+    {
+      user => 'admin',
+      args => {email => new_login(), password => NEW_PASSWORD},
+      test => 'Leaving out fullname works',
+    },
+    {
+      user => 'admin',
+      args => {email => new_login(), full_name => NEW_FULLNAME},
+      test => 'Leaving out password works',
+    },
+  );
+
+  $rpc->bz_run_tests(
+    tests        => \@tests,
+    method       => 'User.create',
+    post_success => \&post_success
+  );
 }
index 835b48153d65d3a9099e71f8436bcbe73f4456b3..3688317b1cef845694f845783ff8612c48449332 100644 (file)
@@ -21,202 +21,254 @@ use QA::Tests qw(PRIVATE_BUG_USER);
 use Test::More tests => 330;
 our ($config, @clients) = get_rpc_clients();
 
-my $get_user = $config->{'unprivileged_user_login'};
+my $get_user        = $config->{'unprivileged_user_login'};
 my $canconfirm_user = $config->{'canconfirm_user_login'};
-my $priv_user = $config->{PRIVATE_BUG_USER . '_user_login'};
-my $disabled = $config->{'disabled_user_login'};
-my $disabled_match = substr($disabled, 0, length($disabled) - 1);
+my $priv_user       = $config->{PRIVATE_BUG_USER . '_user_login'};
+my $disabled        = $config->{'disabled_user_login'};
+my $disabled_match  = substr($disabled, 0, length($disabled) - 1);
 
 # These are the basic tests. There are tests for include_fields
 # and exclude_field below.
 
 my @tests = (
-    { args => { names => [$get_user] },
-      test => "Logged-out user can get unprivileged user by name"
-    },
-    { args  => { match => [$get_user] },
-      test  => 'Logged-out user cannot use the match argument',
-      error => 'Logged-out users cannot use',
-    },
-    { args  => { ids => [1] },
-      test  => 'Logged-out users cannot use the "ids" argument',
-      error => 'Logged-out users cannot use',
-    },
+  {
+    args => {names => [$get_user]},
+    test => "Logged-out user can get unprivileged user by name"
+  },
+  {
+    args  => {match => [$get_user]},
+    test  => 'Logged-out user cannot use the match argument',
+    error => 'Logged-out users cannot use',
+  },
+  {
+    args  => {ids => [1]},
+    test  => 'Logged-out users cannot use the "ids" argument',
+    error => 'Logged-out users cannot use',
+  },
 
-    # match & names
-    { user => 'unprivileged',
-      args => { names => [$get_user] },
-      test => "Unprivileged user can get himself",
-    },
-    { user => 'unprivileged',
-      args => { match => [$get_user] },
-      test => 'Logged-in user can use the match argument',
-    },
-    { user => 'unprivileged',
-      args => { match => [$get_user], names => [$get_user] },
-      test => 'Specifying the same thing in "match" and "names"',
-    },
+  # match & names
+  {
+    user => 'unprivileged',
+    args => {names => [$get_user]},
+    test => "Unprivileged user can get himself",
+  },
+  {
+    user => 'unprivileged',
+    args => {match => [$get_user]},
+    test => 'Logged-in user can use the match argument',
+  },
+  {
+    user => 'unprivileged',
+    args => {match => [$get_user], names => [$get_user]},
+    test => 'Specifying the same thing in "match" and "names"',
+  },
 
-    # include_disabled
-    { user => 'unprivileged',
-      args => { match => [$get_user, $disabled_match] },
-      test => 'Disabled users are not normally returned'
-    },
-    { user => 'unprivileged',
-      args => { match => [$disabled_match], include_disabled => 1 },
-      test => 'Specifying include_disabled returns disabled users'
-    },
-    { user => 'unprivileged',
-      args => { match => [$disabled] },
-      test => 'Full match on a disabled user returns that user',
-    },
+  # include_disabled
+  {
+    user => 'unprivileged',
+    args => {match => [$get_user, $disabled_match]},
+    test => 'Disabled users are not normally returned'
+  },
+  {
+    user => 'unprivileged',
+    args => {match => [$disabled_match], include_disabled => 1},
+    test => 'Specifying include_disabled returns disabled users'
+  },
+  {
+    user => 'unprivileged',
+    args => {match => [$disabled]},
+    test => 'Full match on a disabled user returns that user',
+  },
 
-    # groups and group_ids
-    { args  => { groups => ['QA-Selenium-TEST'] },
-      test  => 'Specifying just groups fails',
-      error => 'one of the following parameters',
-    },
-    { args => { group_ids => [1] },
-      test => 'Specifying just group ids fails',
-      error => 'one of the following parameters',
-    },
-    { args => { names => [$get_user, $priv_user], groups => ['QA-Selenium-TEST'] },
-      test => 'Limiting the return value to a group while being logged out fails',
-      error => 'The group you specified, QA-Selenium-TEST, is not valid here',
-    },
-    { user => 'unprivileged',
-      args => { names => [$get_user, $priv_user], groups => ['missing_group'] },
-      test => 'Limiting the return value to a group which does not exist fails',
-      error => 'The group you specified, missing_group, is not valid here',
-    },
-    { user => 'unprivileged',
-      args => { names => [$get_user, $priv_user], groups => ['QA-Selenium-TEST'] },
-      test => 'Limiting the return value to a group you do not belong to fails',
-      error => 'The group you specified, QA-Selenium-TEST, is not valid here',
-    },
-    { user => 'editbugs',
-      args => { names => [$get_user, $priv_user], groups => ['canconfirm', 'editbugs'] },
-      test => 'Limiting the return value to some groups you do not belong to fails',
-      error => 'The group you specified, canconfirm, is not valid here',
-    },
-    { user => 'admin',
-      args => { names => [$canconfirm_user], groups => ['canconfirm', 'editbugs'] },
-      test => 'Limiting the return value to groups you belong to',
-    },
+  # groups and group_ids
+  {
+    args  => {groups => ['QA-Selenium-TEST']},
+    test  => 'Specifying just groups fails',
+    error => 'one of the following parameters',
+  },
+  {
+    args  => {group_ids => [1]},
+    test  => 'Specifying just group ids fails',
+    error => 'one of the following parameters',
+  },
+  {
+    args  => {names => [$get_user, $priv_user], groups => ['QA-Selenium-TEST']},
+    test  => 'Limiting the return value to a group while being logged out fails',
+    error => 'The group you specified, QA-Selenium-TEST, is not valid here',
+  },
+  {
+    user  => 'unprivileged',
+    args  => {names => [$get_user, $priv_user], groups => ['missing_group']},
+    test  => 'Limiting the return value to a group which does not exist fails',
+    error => 'The group you specified, missing_group, is not valid here',
+  },
+  {
+    user  => 'unprivileged',
+    args  => {names => [$get_user, $priv_user], groups => ['QA-Selenium-TEST']},
+    test  => 'Limiting the return value to a group you do not belong to fails',
+    error => 'The group you specified, QA-Selenium-TEST, is not valid here',
+  },
+  {
+    user => 'editbugs',
+    args =>
+      {names => [$get_user, $priv_user], groups => ['canconfirm', 'editbugs']},
+    test  => 'Limiting the return value to some groups you do not belong to fails',
+    error => 'The group you specified, canconfirm, is not valid here',
+  },
+  {
+    user => 'admin',
+    args => {names => [$canconfirm_user], groups => ['canconfirm', 'editbugs']},
+    test => 'Limiting the return value to groups you belong to',
+  },
 
-    # groups returned
-    { user => 'admin',
-      args => { names => [$get_user] },
-      test => 'Admin can get user',
-    },
-    { user => 'admin',
-      args => { names => [$canconfirm_user] },
-      test => 'Admin can get user',
-    },
-    { user => 'canconfirm',
-      args => { names => [$canconfirm_user] },
-      test => 'Privileged user can get himself',
-    },
-    { user => 'editbugs',
-      args => { names => [$canconfirm_user] },
-      test => 'Privileged user can get another user',
-    },
+  # groups returned
+  {
+    user => 'admin',
+    args => {names => [$get_user]},
+    test => 'Admin can get user',
+  },
+  {
+    user => 'admin',
+    args => {names => [$canconfirm_user]},
+    test => 'Admin can get user',
+  },
+  {
+    user => 'canconfirm',
+    args => {names => [$canconfirm_user]},
+    test => 'Privileged user can get himself',
+  },
+  {
+    user => 'editbugs',
+    args => {names => [$canconfirm_user]},
+    test => 'Privileged user can get another user',
+  },
 );
 
 sub post_success {
-    my ($call, $t) = @_;
+  my ($call, $t) = @_;
 
-    my $result = $call->result;
-    is(scalar @{ $result->{users} }, 1, "Got exactly one user");
-    my $item = $result->{users}->[0];
-    my $user = $t->{user} || '';
+  my $result = $call->result;
+  is(scalar @{$result->{users}}, 1, "Got exactly one user");
+  my $item = $result->{users}->[0];
+  my $user = $t->{user} || '';
 
-    if ($user eq 'admin') {
-        ok(exists $item->{email} && exists $item->{can_login}
-           && exists $item->{email_enabled} && exists $item->{login_denied_text},
-           'Admin correctly gets all user fields');
-    }
-    elsif ($user) {
-        ok(exists $item->{can_login},
-           'Logged-in user correctly gets can_login');
-        ok(!exists $item->{email_enabled}
-           && !exists $item->{login_denied_text},
-           "Non-admin user doesn't get email_enabled and login_denied_text");
-    }
-    else {
-        my @item_keys = sort keys %$item;
-        is_deeply(\@item_keys, ['id', 'name', 'real_name'],
-            'Only id, name, and real_name are returned to logged-out users');
-        return;
-    }
+  if ($user eq 'admin') {
+    ok(
+      exists $item->{email}
+        && exists $item->{can_login}
+        && exists $item->{email_enabled}
+        && exists $item->{login_denied_text},
+      'Admin correctly gets all user fields'
+    );
+  }
+  elsif ($user) {
+    ok(exists $item->{can_login}, 'Logged-in user correctly gets can_login');
+    ok(
+      !exists $item->{email_enabled} && !exists $item->{login_denied_text},
+      "Non-admin user doesn't get email_enabled and login_denied_text"
+    );
+  }
+  else {
+    my @item_keys = sort keys %$item;
+    is_deeply(
+      \@item_keys,
+      ['id', 'name', 'real_name'],
+      'Only id, name, and real_name are returned to logged-out users'
+    );
+    return;
+  }
 
-    my $username = $config->{"${user}_user_login"};
-    # FIXME We have no way to create a saved search or a saved report from
-    # the WebService, so we cannot test that the correct data is returned
-    # if the user is accessing his own account.
-    if ($username eq $item->{name}) {
-        ok(exists $item->{saved_searches} && exists $item->{saved_reports},
-           'Users can get the list of saved searches and reports for their own account');
-    }
-    else {
-        ok(!exists $item->{saved_searches} && !exists $item->{saved_reports},
-           "Users cannot get the list of saved searches and reports from someone else's acccount");
-    }
+  my $username = $config->{"${user}_user_login"};
+
+  # FIXME We have no way to create a saved search or a saved report from
+  # the WebService, so we cannot test that the correct data is returned
+  # if the user is accessing his own account.
+  if ($username eq $item->{name}) {
+    ok(
+      exists $item->{saved_searches} && exists $item->{saved_reports},
+      'Users can get the list of saved searches and reports for their own account'
+    );
+  }
+  else {
+    ok(
+      !exists $item->{saved_searches} && !exists $item->{saved_reports},
+      "Users cannot get the list of saved searches and reports from someone else's acccount"
+    );
+  }
 
-    my @groups = map { $_->{name} } @{$item->{groups}};
-    # Admins can see all groups a user belongs to (assuming they inherited
-    # membership for all groups). Same for a user querying his own account.
-    if ($username eq $item->{name} || $user eq 'admin') {
-        if ($username eq $get_user) {
-            ok(!scalar @groups, "The unprivileged user doesn't belong to any group");
-        }
-        elsif ($username eq $canconfirm_user) {
-            ok(grep($_ eq 'canconfirm', @groups), "Group 'canconfirm' returned");
-        }
+  my @groups = map { $_->{name} } @{$item->{groups}};
+
+  # Admins can see all groups a user belongs to (assuming they inherited
+  # membership for all groups). Same for a user querying his own account.
+  if ($username eq $item->{name} || $user eq 'admin') {
+    if ($username eq $get_user) {
+      ok(!scalar @groups, "The unprivileged user doesn't belong to any group");
     }
-    else {
-        ok(!scalar @groups, "No groups are visible to users without bless privs");
+    elsif ($username eq $canconfirm_user) {
+      ok(grep($_ eq 'canconfirm', @groups), "Group 'canconfirm' returned");
     }
+  }
+  else {
+    ok(!scalar @groups, "No groups are visible to users without bless privs");
+  }
 }
 
 foreach my $rpc (@clients) {
-    $rpc->bz_run_tests(tests => \@tests, method => 'User.get',
-                       post_success => \&post_success);
-
-    #############################
-    # Include and Exclude Tests #
-    #############################
-
-    my $include_nothing = $rpc->bz_call_success('User.get', {
-        names => [$get_user], include_fields => ['asdfasdfsdf'],
-    }, 'User.get including only invalid fields');
-    is(scalar keys %{ $include_nothing->result->{users}->[0] }, 0,
-       'No fields returned for user');
-
-    my $include_one = $rpc->bz_call_success('User.get', {
-        names => [$get_user], include_fields => ['id'],
-    }, 'User.get including only id');
-    is(scalar keys %{ $include_one->result->{users}->[0] }, 1,
-       'Only one field returned for user');
-
-    my $exclude_none = $rpc->bz_call_success('User.get', {
-        names => [$get_user], exclude_fields => ['asdfasdfsdf'],
-    }, 'User.get excluding only invalid fields');
-    is(scalar keys %{ $exclude_none->result->{users}->[0] }, 3,
-       'All fields returned for user');
-
-    my $exclude_one = $rpc->bz_call_success('User.get', {
-        names => [$get_user], exclude_fields => ['id'],
-    }, 'User.get excluding id');
-    is(scalar keys %{ $exclude_one->result->{users}->[0] }, 2,
-       'Only two fields returned for user');
-
-    my $override = $rpc->bz_call_success('User.get', {
-        names => [$get_user], include_fields => ['id', 'name'],
-        exclude_fields => ['id']
-    }, 'User.get with both include and exclude');
-    is(scalar keys %{ $override->result->{users}->[0] }, 1,
-       'Only one field returned');
-    ok(exists $override->result->{users}->[0]->{name},
-       '...and that field is the "name" field');
+  $rpc->bz_run_tests(
+    tests        => \@tests,
+    method       => 'User.get',
+    post_success => \&post_success
+  );
+
+  #############################
+  # Include and Exclude Tests #
+  #############################
+
+  my $include_nothing = $rpc->bz_call_success(
+    'User.get',
+    {names => [$get_user], include_fields => ['asdfasdfsdf'],},
+    'User.get including only invalid fields'
+  );
+  is(scalar keys %{$include_nothing->result->{users}->[0]},
+    0, 'No fields returned for user');
+
+  my $include_one = $rpc->bz_call_success(
+    'User.get',
+    {names => [$get_user], include_fields => ['id'],},
+    'User.get including only id'
+  );
+  is(scalar keys %{$include_one->result->{users}->[0]},
+    1, 'Only one field returned for user');
+
+  my $exclude_none = $rpc->bz_call_success(
+    'User.get',
+    {names => [$get_user], exclude_fields => ['asdfasdfsdf'],},
+    'User.get excluding only invalid fields'
+  );
+  is(scalar keys %{$exclude_none->result->{users}->[0]},
+    3, 'All fields returned for user');
+
+  my $exclude_one = $rpc->bz_call_success(
+    'User.get',
+    {names => [$get_user], exclude_fields => ['id'],},
+    'User.get excluding id'
+  );
+  is(scalar keys %{$exclude_one->result->{users}->[0]},
+    2, 'Only two fields returned for user');
+
+  my $override = $rpc->bz_call_success(
+    'User.get',
+    {
+      names          => [$get_user],
+      include_fields => ['id', 'name'],
+      exclude_fields => ['id']
+    },
+    'User.get with both include and exclude'
+  );
+  is(scalar keys %{$override->result->{users}->[0]}, 1,
+    'Only one field returned');
+  ok(
+    exists $override->result->{users}->[0]->{name},
+    '...and that field is the "name" field'
+  );
 }
index f4b115390b5db89d8250e7c02e37d3201d0f0385..86fbb417fa95b1c735a4c0f22cb62eb247abf433 100644 (file)
@@ -23,106 +23,115 @@ my ($config, @clients) = get_rpc_clients();
 
 use constant INVALID_EMAIL => '@invalid_user@';
 
-my $user = $config->{unprivileged_user_login};
-my $pass = $config->{unprivileged_user_passwd};
+my $user  = $config->{unprivileged_user_login};
+my $pass  = $config->{unprivileged_user_passwd};
 my $error = "The login or password you entered is not valid";
 
 my @tests = (
-    { user => 'unprivileged',
-      test => "Unprivileged user can log in successfully",
-    },
+  {user => 'unprivileged', test => "Unprivileged user can log in successfully",},
 
-    { args  => { login => $user, password => '' },
-      error => $error,
-      test  => "Empty password can't log in",
-    },
-    { args  => { login => '', password => $pass },
-      error => $error,
-      test  => "Empty login can't log in",
-    },
-    { args  => { login => $user },
-      error => "requires a password argument",
-      test  => "Undef password can't log in",
-    },
-    { args  => { password => $pass },
-      error => "requires a login argument",
-      test  => "Undef login can't log in",
-    },
+  {
+    args  => {login => $user, password => ''},
+    error => $error,
+    test  => "Empty password can't log in",
+  },
+  {
+    args  => {login => '', password => $pass},
+    error => $error,
+    test  => "Empty login can't log in",
+  },
+  {
+    args  => {login => $user},
+    error => "requires a password argument",
+    test  => "Undef password can't log in",
+  },
+  {
+    args  => {password => $pass},
+    error => "requires a login argument",
+    test  => "Undef login can't log in",
+  },
 
-    { args  => { login => INVALID_EMAIL, password => $pass },
-      error => $error,
-      test  => "Invalid email can't log in",
-    },
-    { args  => { login => $user, password => '*' },
-      error => $error,
-      test  => "Invalid password can't log in",
-    },
+  {
+    args  => {login => INVALID_EMAIL, password => $pass},
+    error => $error,
+    test  => "Invalid email can't log in",
+  },
+  {
+    args  => {login => $user, password => '*'},
+    error => $error,
+    test  => "Invalid password can't log in",
+  },
 
-    { args  => { login    => $config->{disabled_user_login},
-                 password => $config->{disabled_user_passwd} },
-      error => "!!This is the text!!",
-      test  => "Can't log in with a disabled account",
-    },
-    { args  => { login => $config->{disabled_user_login}, password => '*' },
-      error => $error,
-      test  => "Logging in with invalid password doesn't show disabledtext",
+  {
+    args => {
+      login    => $config->{disabled_user_login},
+      password => $config->{disabled_user_passwd}
     },
+    error => "!!This is the text!!",
+    test  => "Can't log in with a disabled account",
+  },
+  {
+    args  => {login => $config->{disabled_user_login}, password => '*'},
+    error => $error,
+    test  => "Logging in with invalid password doesn't show disabledtext",
+  },
 );
 
 sub _login_args {
-    my $args = shift;
-    my %fixed_args = %$args;
-    $fixed_args{Bugzilla_login} = delete $fixed_args{login};
-    $fixed_args{Bugzilla_password} = delete $fixed_args{password};
-    return \%fixed_args;
+  my $args       = shift;
+  my %fixed_args = %$args;
+  $fixed_args{Bugzilla_login}    = delete $fixed_args{login};
+  $fixed_args{Bugzilla_password} = delete $fixed_args{password};
+  return \%fixed_args;
 }
 
 foreach my $rpc (@clients) {
-    if ($rpc->bz_get_mode) {
-        $rpc->bz_call_fail('User.logout', undef, 'must use HTTP POST',
-                           'User.logout fails when called via GET');
-    }
+  if ($rpc->bz_get_mode) {
+    $rpc->bz_call_fail(
+      'User.logout', undef,
+      'must use HTTP POST',
+      'User.logout fails when called via GET'
+    );
+  }
 
-    foreach my $t (@tests) {
-        if ($t->{user}) {
-            my $username = $config->{$t->{user} . '_user_login'};
-            my $password = $config->{$t->{user} . '_user_passwd'};
+  foreach my $t (@tests) {
+    if ($t->{user}) {
+      my $username = $config->{$t->{user} . '_user_login'};
+      my $password = $config->{$t->{user} . '_user_passwd'};
 
-            if ($rpc->bz_get_mode) {
-                $rpc->bz_call_fail('User.login',
-                    { login => $username, password => $password },
-                    'must use HTTP POST', $t->{test} . ' (fails on GET)');
-            }
-            else {
-                $rpc->bz_log_in($t->{user});
-                ok($rpc->{_bz_credentials}->{token}, 'Login token returned');
-                $rpc->bz_call_success('User.logout');
-            }
+      if ($rpc->bz_get_mode) {
+        $rpc->bz_call_fail(
+          'User.login',
+          {login => $username, password => $password},
+          'must use HTTP POST',
+          $t->{test} . ' (fails on GET)'
+        );
+      }
+      else {
+        $rpc->bz_log_in($t->{user});
+        ok($rpc->{_bz_credentials}->{token}, 'Login token returned');
+        $rpc->bz_call_success('User.logout');
+      }
 
-            if ($t->{error}) {
-                $rpc->bz_call_fail('Bugzilla.version',
-                    { Bugzilla_login => $username,
-                      Bugzilla_password => $password });
-            }
-            else {
-                $rpc->bz_call_success('Bugzilla.version',
-                    { Bugzilla_login => $username,
-                      Bugzilla_password => $password });
-            }
-        }
-        else {
-            # Under GET, there's no reason to have extra failing tests.
-            if (!$rpc->bz_get_mode) {
-                $rpc->bz_call_fail('User.login', $t->{args}, $t->{error},
-                                   $t->{test});
-            }
-            if (defined $t->{args}->{login}
-                and defined $t->{args}->{password})
-            {
-                my $fixed_args = _login_args($t->{args});
-                $rpc->bz_call_fail('Bugzilla.version', $fixed_args,
-                    $t->{error}, "Bugzilla_login: " . $t->{test});
-            }
-        }
+      if ($t->{error}) {
+        $rpc->bz_call_fail('Bugzilla.version',
+          {Bugzilla_login => $username, Bugzilla_password => $password});
+      }
+      else {
+        $rpc->bz_call_success('Bugzilla.version',
+          {Bugzilla_login => $username, Bugzilla_password => $password});
+      }
+    }
+    else {
+      # Under GET, there's no reason to have extra failing tests.
+      if (!$rpc->bz_get_mode) {
+        $rpc->bz_call_fail('User.login', $t->{args}, $t->{error}, $t->{test});
+      }
+      if (defined $t->{args}->{login} and defined $t->{args}->{password}) {
+        my $fixed_args = _login_args($t->{args});
+        $rpc->bz_call_fail('Bugzilla.version', $fixed_args, $t->{error},
+          "Bugzilla_login: " . $t->{test});
+      }
     }
+  }
 }
index 81b3df31f750409363d5c364f7b0729b047091a5..a9cdad542982554c6f16b920c9fe25377eed113b 100644 (file)
@@ -24,40 +24,45 @@ my ($config, $xmlrpc, $jsonrpc, $jsonrpc_get) = get_rpc_clients();
 use constant INVALID_EMAIL => '()[]\;:,<>@webservice.test';
 
 sub new_login {
-    return 'requested_' . random_string() . '@webservice.test';
+  return 'requested_' . random_string() . '@webservice.test';
 }
 
-$jsonrpc_get->bz_call_fail('User.offer_account_by_email',
-    { email => new_login() },
-    'must use HTTP POST', 'offer_account_by_email fails over GET');
+$jsonrpc_get->bz_call_fail(
+  'User.offer_account_by_email',
+  {email => new_login()},
+  'must use HTTP POST',
+  'offer_account_by_email fails over GET'
+);
 
 # Have to wrap @tests in the foreach so that new_login returns something
 # different each time.
 foreach my $rpc ($jsonrpc, $xmlrpc) {
-    my @tests = (
-        # Login name checks.
-        { args  => { },
-          error => "argument was not set",
-          test  => 'Leaving out email argument fails',
-        },
-        { args  => { email => '' },
-          error => "argument was not set",
-          test  => "Passing an empty email argument fails",
-        },
-        { args  => { email => INVALID_EMAIL },
-          error => "didn't pass our syntax checking",
-          test  => 'Invalid email address fails',
-        },
-        { args  => { email => $config->{unprivileged_user_login} },
-          error => "There is already an account",
-          test  => 'Trying to use an existing login name fails',
-        },
-
-        { args => { email => new_login() },
-          test => 'Valid, non-existing email passes.',
-        },
-    );
-
-    $rpc->bz_run_tests(tests => \@tests,
-                       method => 'User.offer_account_by_email');
+  my @tests = (
+
+    # Login name checks.
+    {
+      args  => {},
+      error => "argument was not set",
+      test  => 'Leaving out email argument fails',
+    },
+    {
+      args  => {email => ''},
+      error => "argument was not set",
+      test  => "Passing an empty email argument fails",
+    },
+    {
+      args  => {email => INVALID_EMAIL},
+      error => "didn't pass our syntax checking",
+      test  => 'Invalid email address fails',
+    },
+    {
+      args  => {email => $config->{unprivileged_user_login}},
+      error => "There is already an account",
+      test  => 'Trying to use an existing login name fails',
+    },
+
+    {args => {email => new_login()}, test => 'Valid, non-existing email passes.',},
+  );
+
+  $rpc->bz_run_tests(tests => \@tests, method => 'User.offer_account_by_email');
 }