summaryrefslogtreecommitdiffstats
path: root/Bugzilla
diff options
context:
space:
mode:
Diffstat (limited to 'Bugzilla')
-rw-r--r--Bugzilla/Attachment.pm838
-rw-r--r--Bugzilla/Attachment/Archive.pm154
-rw-r--r--Bugzilla/Attachment/Database.pm52
-rw-r--r--Bugzilla/Attachment/FileSystem.pm52
-rw-r--r--Bugzilla/Attachment/PatchReader.pm536
-rw-r--r--Bugzilla/Attachment/S3.pm56
-rw-r--r--Bugzilla/Auth.pm451
-rw-r--r--Bugzilla/Auth/Login.pm18
-rw-r--r--Bugzilla/Auth/Login/APIKey.pm45
-rw-r--r--Bugzilla/Auth/Login/CGI.pm118
-rw-r--r--Bugzilla/Auth/Login/Cookie.pm221
-rw-r--r--Bugzilla/Auth/Login/Env.pm29
-rw-r--r--Bugzilla/Auth/Login/Stack.pm126
-rw-r--r--Bugzilla/Auth/Persist/Cookie.pm284
-rw-r--r--Bugzilla/Auth/Verify.pm202
-rw-r--r--Bugzilla/Auth/Verify/DB.pm152
-rw-r--r--Bugzilla/Auth/Verify/LDAP.pm253
-rw-r--r--Bugzilla/Auth/Verify/RADIUS.pm58
-rw-r--r--Bugzilla/Auth/Verify/Stack.pm107
-rw-r--r--Bugzilla/Bloomfilter.pm56
-rw-r--r--Bugzilla/Bug.pm7665
-rw-r--r--Bugzilla/BugMail.pm1088
-rw-r--r--Bugzilla/BugUrl.pm228
-rw-r--r--Bugzilla/BugUrl/Aha.pm22
-rw-r--r--Bugzilla/BugUrl/Bugzilla.pm52
-rw-r--r--Bugzilla/BugUrl/Bugzilla/Local.pm121
-rw-r--r--Bugzilla/BugUrl/Chromium.pm41
-rw-r--r--Bugzilla/BugUrl/Debian.pm48
-rw-r--r--Bugzilla/BugUrl/Edge.pm16
-rw-r--r--Bugzilla/BugUrl/GitHub.pm26
-rw-r--r--Bugzilla/BugUrl/Google.pm58
-rw-r--r--Bugzilla/BugUrl/JIRA.pm25
-rw-r--r--Bugzilla/BugUrl/Launchpad.pm44
-rw-r--r--Bugzilla/BugUrl/MantisBT.pm18
-rw-r--r--Bugzilla/BugUrl/MozSupport.pm20
-rw-r--r--Bugzilla/BugUrl/ServiceNow.pm12
-rw-r--r--Bugzilla/BugUrl/SourceForge.pm43
-rw-r--r--Bugzilla/BugUrl/Splat.pm12
-rw-r--r--Bugzilla/BugUrl/Trac.pm25
-rw-r--r--Bugzilla/BugUrl/WebCompat.pm24
-rw-r--r--Bugzilla/BugUserLastVisit.pm22
-rw-r--r--Bugzilla/CGI.pm1351
-rw-r--r--Bugzilla/CGI/ContentSecurityPolicy.pm154
-rw-r--r--Bugzilla/CPAN.pm153
-rw-r--r--Bugzilla/Chart.pm671
-rw-r--r--Bugzilla/Classification.pm153
-rw-r--r--Bugzilla/Comment.pm647
-rw-r--r--Bugzilla/Comment/TagWeights.pm10
-rw-r--r--Bugzilla/Component.pm599
-rw-r--r--Bugzilla/Config.pm488
-rw-r--r--Bugzilla/Config/Admin.pm107
-rw-r--r--Bugzilla/Config/Advanced.pm30
-rw-r--r--Bugzilla/Config/Attachment.pm106
-rw-r--r--Bugzilla/Config/Auth.pm400
-rw-r--r--Bugzilla/Config/BugChange.pm98
-rw-r--r--Bugzilla/Config/BugFields.pm138
-rw-r--r--Bugzilla/Config/Common.pm451
-rw-r--r--Bugzilla/Config/DependencyGraph.pm18
-rw-r--r--Bugzilla/Config/Elastic.pm22
-rw-r--r--Bugzilla/Config/General.pm77
-rw-r--r--Bugzilla/Config/GroupSecurity.pm129
-rw-r--r--Bugzilla/Config/LDAP.pm50
-rw-r--r--Bugzilla/Config/MTA.pm97
-rw-r--r--Bugzilla/Config/PatchViewer.pm38
-rw-r--r--Bugzilla/Config/Query.pm88
-rw-r--r--Bugzilla/Config/RADIUS.pm38
-rw-r--r--Bugzilla/Config/Reports.pm28
-rw-r--r--Bugzilla/Config/ShadowDB.pm49
-rw-r--r--Bugzilla/Config/UserMatch.pm44
-rw-r--r--Bugzilla/Constants.pm753
-rw-r--r--Bugzilla/DB.pm1954
-rw-r--r--Bugzilla/DB/Mysql.pm1515
-rw-r--r--Bugzilla/DB/Oracle.pm992
-rw-r--r--Bugzilla/DB/Pg.pm436
-rw-r--r--Bugzilla/DB/Schema.pm4208
-rw-r--r--Bugzilla/DB/Schema/Mysql.pm603
-rw-r--r--Bugzilla/DB/Schema/Oracle.pm772
-rw-r--r--Bugzilla/DB/Schema/Pg.pm286
-rw-r--r--Bugzilla/DB/Schema/Sqlite.pm414
-rw-r--r--Bugzilla/DB/Sqlite.pm305
-rw-r--r--Bugzilla/DaemonControl.pm386
-rw-r--r--Bugzilla/DuoAPI.pm136
-rw-r--r--Bugzilla/DuoWeb.pm146
-rw-r--r--Bugzilla/Elastic.pm68
-rw-r--r--Bugzilla/Elastic/Indexer.pm277
-rw-r--r--Bugzilla/Elastic/Role/HasClient.pm12
-rw-r--r--Bugzilla/Elastic/Role/Object.pm39
-rw-r--r--Bugzilla/Elastic/Search.pm541
-rw-r--r--Bugzilla/Elastic/Search/FakeCGI.pm33
-rw-r--r--Bugzilla/Error.pm399
-rw-r--r--Bugzilla/Error/Base.pm6
-rw-r--r--Bugzilla/Error/Template.pm12
-rw-r--r--Bugzilla/Extension.pm227
-rw-r--r--Bugzilla/Field.pm1414
-rw-r--r--Bugzilla/Field/Choice.pm304
-rw-r--r--Bugzilla/Field/ChoiceInterface.pm227
-rw-r--r--Bugzilla/Flag.pm1531
-rw-r--r--Bugzilla/FlagType.pm775
-rw-r--r--Bugzilla/Group.pm727
-rw-r--r--Bugzilla/Hook.pm28
-rw-r--r--Bugzilla/Install.pm795
-rw-r--r--Bugzilla/Install/DB.pm6695
-rw-r--r--Bugzilla/Install/Filesystem.pm1279
-rw-r--r--Bugzilla/Install/Localconfig.pm577
-rw-r--r--Bugzilla/Install/Requirements.pm382
-rw-r--r--Bugzilla/Install/Util.pm907
-rw-r--r--Bugzilla/Job/BugMail.pm4
-rw-r--r--Bugzilla/Job/Mailer.pm40
-rw-r--r--Bugzilla/JobQueue.pm163
-rw-r--r--Bugzilla/JobQueue/Runner.pm285
-rw-r--r--Bugzilla/JobQueue/Worker.pm20
-rw-r--r--Bugzilla/Keyword.pm126
-rw-r--r--Bugzilla/Logging.pm166
-rw-r--r--Bugzilla/MFA.pm188
-rw-r--r--Bugzilla/MFA/Dummy.pm10
-rw-r--r--Bugzilla/MFA/Duo.pm71
-rw-r--r--Bugzilla/MFA/TOTP.pm75
-rw-r--r--Bugzilla/Mailer.pm450
-rw-r--r--Bugzilla/Markdown/GFM.pm80
-rw-r--r--Bugzilla/Markdown/GFM/Node.pm34
-rw-r--r--Bugzilla/Markdown/GFM/Parser.pm129
-rw-r--r--Bugzilla/Markdown/GFM/SyntaxExtension.pm36
-rw-r--r--Bugzilla/Markdown/GFM/SyntaxExtensionList.pm20
-rw-r--r--Bugzilla/Memcached.pm495
-rw-r--r--Bugzilla/Migrate.pm1201
-rw-r--r--Bugzilla/Migrate/Gnats.pm1024
-rw-r--r--Bugzilla/Milestone.pm297
-rw-r--r--Bugzilla/Object.pm1374
-rw-r--r--Bugzilla/PSGI.pm38
-rw-r--r--Bugzilla/PatchReader/AddCVSContext.pm81
-rw-r--r--Bugzilla/PatchReader/Base.pm3
-rw-r--r--Bugzilla/PatchReader/CVSClient.pm45
-rw-r--r--Bugzilla/PatchReader/DiffPrinter/raw.pm6
-rw-r--r--Bugzilla/PatchReader/DiffPrinter/template.pm72
-rw-r--r--Bugzilla/PatchReader/FilterPatch.pm2
-rw-r--r--Bugzilla/PatchReader/FixPatchRoot.pm50
-rw-r--r--Bugzilla/PatchReader/NarrowPatch.pm7
-rw-r--r--Bugzilla/PatchReader/PatchInfoGrabber.pm13
-rw-r--r--Bugzilla/PatchReader/Raw.pm86
-rw-r--r--Bugzilla/Product.pm1234
-rw-r--r--Bugzilla/Quantum.pm7
-rw-r--r--Bugzilla/Quantum/CGI.pm27
-rw-r--r--Bugzilla/Quantum/Home.pm3
-rw-r--r--Bugzilla/Quantum/Plugin/BasicAuth.pm40
-rw-r--r--Bugzilla/Quantum/Plugin/Glue.pm3
-rw-r--r--Bugzilla/Quantum/Plugin/Hostage.pm107
-rw-r--r--Bugzilla/Quantum/SES.pm364
-rw-r--r--Bugzilla/RNG.pm245
-rw-r--r--Bugzilla/Report/SecurityRisk.pm409
-rw-r--r--Bugzilla/S3.pm479
-rw-r--r--Bugzilla/S3/Bucket.pm299
-rw-r--r--Bugzilla/Search.pm5289
-rw-r--r--Bugzilla/Search/Clause.pm170
-rw-r--r--Bugzilla/Search/ClauseGroup.pm121
-rw-r--r--Bugzilla/Search/Condition.pm70
-rw-r--r--Bugzilla/Search/Quicksearch.pm1148
-rw-r--r--Bugzilla/Search/Recent.pm116
-rw-r--r--Bugzilla/Search/Saved.pm369
-rw-r--r--Bugzilla/Send/Sendmail.pm148
-rw-r--r--Bugzilla/Series.pm391
-rw-r--r--Bugzilla/Status.pm254
-rw-r--r--Bugzilla/Template.pm1874
-rw-r--r--Bugzilla/Template/Context.pm118
-rw-r--r--Bugzilla/Template/Plugin/Bugzilla.pm14
-rw-r--r--Bugzilla/Template/Plugin/Hook.pm109
-rw-r--r--Bugzilla/Template/Plugin/User.pm14
-rw-r--r--Bugzilla/Template/PreloadProvider.pm144
-rw-r--r--Bugzilla/Test/MockDB.pm179
-rw-r--r--Bugzilla/Test/MockLocalconfig.pm6
-rw-r--r--Bugzilla/Test/MockParams.pm82
-rw-r--r--Bugzilla/Test/Util.pm65
-rw-r--r--Bugzilla/Token.pm764
-rw-r--r--Bugzilla/Types.pm17
-rw-r--r--Bugzilla/Update.pm274
-rw-r--r--Bugzilla/User.pm3948
-rw-r--r--Bugzilla/User/APIKey.pm94
-rw-r--r--Bugzilla/User/Session.pm36
-rw-r--r--Bugzilla/User/Setting.pm437
-rw-r--r--Bugzilla/User/Setting/Lang.pm6
-rw-r--r--Bugzilla/User/Setting/Skin.pm43
-rw-r--r--Bugzilla/User/Setting/Timezone.pm22
-rw-r--r--Bugzilla/UserAgent.pm355
-rw-r--r--Bugzilla/Util.pm1434
-rw-r--r--Bugzilla/Version.pm278
-rw-r--r--Bugzilla/WebService.pm7
-rw-r--r--Bugzilla/WebService/Bug.pm2661
-rw-r--r--Bugzilla/WebService/BugUserLastVisit.pm118
-rw-r--r--Bugzilla/WebService/Bugzilla.pm120
-rw-r--r--Bugzilla/WebService/Classification.pm89
-rw-r--r--Bugzilla/WebService/Constants.pm478
-rw-r--r--Bugzilla/WebService/Elastic.pm32
-rw-r--r--Bugzilla/WebService/Group.pm325
-rw-r--r--Bugzilla/WebService/JSON.pm2
-rw-r--r--Bugzilla/WebService/Product.pm428
-rw-r--r--Bugzilla/WebService/Server.pm147
-rw-r--r--Bugzilla/WebService/Server/JSONRPC.pm714
-rw-r--r--Bugzilla/WebService/Server/REST.pm769
-rw-r--r--Bugzilla/WebService/Server/REST/Resources/Bug.pm331
-rw-r--r--Bugzilla/WebService/Server/REST/Resources/BugUserLastVisit.pm46
-rw-r--r--Bugzilla/WebService/Server/REST/Resources/Bugzilla.pm52
-rw-r--r--Bugzilla/WebService/Server/REST/Resources/Classification.pm27
-rw-r--r--Bugzilla/WebService/Server/REST/Resources/Elastic.pm13
-rw-r--r--Bugzilla/WebService/Server/REST/Resources/Group.pm55
-rw-r--r--Bugzilla/WebService/Server/REST/Resources/Product.pm78
-rw-r--r--Bugzilla/WebService/Server/REST/Resources/User.pm107
-rw-r--r--Bugzilla/WebService/Server/XMLRPC.pm488
-rw-r--r--Bugzilla/WebService/User.pm690
-rw-r--r--Bugzilla/WebService/Util.pm474
-rw-r--r--Bugzilla/Whine.pm22
-rw-r--r--Bugzilla/Whine/Query.pm20
-rw-r--r--Bugzilla/Whine/Schedule.pm76
211 files changed, 44011 insertions, 42468 deletions
diff --git a/Bugzilla/Attachment.pm b/Bugzilla/Attachment.pm
index 9eac3a147..f6b65d368 100644
--- a/Bugzilla/Attachment.pm
+++ b/Bugzilla/Attachment.pm
@@ -58,56 +58,52 @@ use base 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_size
+ attach_id
+ bug_id
+ creation_ts
+ description
+ filename
+ isobsolete
+ ispatch
+ isprivate
+ mimetype
+ modification_time
+ submitter_id
+ attach_size
);
-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 ######
@@ -128,7 +124,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
@@ -142,12 +138,12 @@ the bug object to which the attachment is attached
=cut
sub bug {
- my ($self) = @_;
- require Bugzilla::Bug;
- return $self->{bug} if defined $self->{bug};
- my $bug = $self->{bug} = Bugzilla::Bug->new({ id => $_[0]->bug_id, cache => 1 });
- weaken($self->{bug});
- return $bug;
+ my ($self) = @_;
+ require Bugzilla::Bug;
+ return $self->{bug} if defined $self->{bug};
+ my $bug = $self->{bug} = Bugzilla::Bug->new({id => $_[0]->bug_id, cache => 1});
+ weaken($self->{bug});
+ return $bug;
}
=over
@@ -161,7 +157,7 @@ user-provided text describing the attachment
=cut
sub description {
- return $_[0]->{description};
+ return $_[0]->{description};
}
=over
@@ -175,7 +171,7 @@ the attachment's MIME media type
=cut
sub contenttype {
- return $_[0]->{mimetype};
+ return $_[0]->{mimetype};
}
=over
@@ -189,8 +185,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
@@ -204,7 +200,7 @@ the date and time on which the attacher attached the attachment
=cut
sub attached {
- return $_[0]->{creation_ts};
+ return $_[0]->{creation_ts};
}
=over
@@ -218,7 +214,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
@@ -232,7 +228,7 @@ the name of the file the attacher attached
=cut
sub filename {
- return $_[0]->{filename};
+ return $_[0]->{filename};
}
=over
@@ -246,7 +242,7 @@ whether or not the attachment is a patch
=cut
sub ispatch {
- return $_[0]->{ispatch};
+ return $_[0]->{ispatch};
}
=over
@@ -260,7 +256,7 @@ whether or not the attachment is obsolete
=cut
sub isobsolete {
- return $_[0]->{isobsolete};
+ return $_[0]->{isobsolete};
}
=over
@@ -274,7 +270,7 @@ whether or not the attachment is private
=cut
sub isprivate {
- return $_[0]->{isprivate};
+ return $_[0]->{isprivate};
}
=over
@@ -291,21 +287,21 @@ 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)\//);
- # Modern browsers support PDF as well.
- return 1 if ($contenttype eq 'application/pdf');
+ # Modern browsers support PDF as well.
+ return 1 if ($contenttype eq 'application/pdf');
- # 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
@@ -319,8 +315,8 @@ the content of the attachment
=cut
sub data {
- my $self = shift;
- return $self->{data} //= current_storage()->retrieve($self->id);
+ my $self = shift;
+ return $self->{data} //= current_storage()->retrieve($self->id);
}
=over
@@ -334,7 +330,7 @@ the length (in bytes) of the attachment content
=cut
sub datasize {
- return $_[0]->{attach_size};
+ return $_[0]->{attach_size};
}
=over
@@ -348,8 +344,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
@@ -364,151 +361,163 @@ 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,
- active_or_has_flags => $self->bug_id };
+ my $vars = {
+ target_type => 'attachment',
+ product_id => $self->bug->product_id,
+ component_id => $self->bug->component_id,
+ attach_id => $self->id,
+ active_or_has_flags => $self->bug_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 });
- }
- trick_taint($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);
- return $content_type;
+ return $content_type;
}
sub _check_data {
- my ($invocant, $params) = @_;
+ my ($invocant, $params) = @_;
- my $data = $params->{data};
- $params->{attach_size} = ref $data ? -s $data : length($data);
+ my $data = $params->{data};
+ $params->{attach_size} = 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->{attach_size} || ThrowUserError('zero_length_file');
- # Make sure the attachment does not exceed the maximum permitted size.
- if ($params->{attach_size} > Bugzilla->params->{'maxattachmentsize'} * 1024) {
- ThrowUserError('file_too_large', { filesize => sprintf("%.0f", $params->{attach_size}/1024) });
- }
+ $params->{attach_size} || ThrowUserError('zero_length_file');
+
+ # Make sure the attachment does not exceed the maximum permitted size.
+ if ($params->{attach_size} > Bugzilla->params->{'maxattachmentsize'} * 1024) {
+ ThrowUserError('file_too_large',
+ {filesize => sprintf("%.0f", $params->{attach_size} / 1024)});
+ }
- return $data;
+ 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) = @_;
+ my ($invocant, $filename) = @_;
- $filename = clean_text($filename);
- if (!$filename) {
- if (ref $invocant) {
- ThrowUserError('filename_not_specified');
- }
- else {
- ThrowUserError('file_not_specified');
- }
- }
+ $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/^.*[\/\\]//;
+ # 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 100 characters, counting from the end of the
- # string to make sure we keep the filename extension.
- $filename = substr($filename, -100, 100);
- trick_taint($filename);
+ # Truncate the filename to 100 characters, counting from the end of the
+ # string to make sure we keep the filename extension.
+ $filename = substr($filename, -100, 100);
+ trick_taint($filename);
- return $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
@@ -530,61 +539,66 @@ 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);
+ }
+
+ # BMO - allow loading of just non-obsolete attachments
+ if ($vars->{exclude_obsolete}) {
+ $and_restriction .= ' AND (isobsolete = 0)';
+ }
+
+ 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);
- # BMO - allow loading of just non-obsolete attachments
- if ($vars->{exclude_obsolete}) {
- $and_restriction .= ' AND (isobsolete = 0)';
+ # To avoid $attachment->flags to run SQL queries itself for each
+ # attachment listed here, we collect all the data at once and
+ # populate $attachment->{flags} ourselves.
+ 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);
-
- # To avoid $attachment->flags to run SQL queries itself for each
- # attachment listed here, we collect all the data at once and
- # populate $attachment->{flags} ourselves.
- 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 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}};
}
- return $attachments;
+ }
+ return $attachments;
}
=pod
@@ -603,22 +617,22 @@ Returns: 1 on success, 0 otherwise.
=cut
sub validate_can_edit {
- my $self = shift;
- my $user = Bugzilla->user;
+ my $self = shift;
+ my $user = Bugzilla->user;
- # The submitter can edit their attachments.
- return 1 if $self->attacher->id == $user->id;
+ # The submitter can edit their attachments.
+ return 1 if $self->attacher->id == $user->id;
- # Private attachments
- return 0 if $self->isprivate && !$user->is_insider;
+ # Private attachments
+ return 0 if $self->isprivate && !$user->is_insider;
- # BMO: if you can edit the bug, then you can also edit any of its attachments
- return 1 if $self->bug->user->{canedit};
+ # BMO: if you can edit the bug, then you can also edit any of its attachments
+ return 1 if $self->bug->user->{canedit};
- # If you are in editbugs for this product
- return 1 if $user->in_group('editbugs', $self->bug->product_id);
+ # If you are in editbugs for this product
+ return 1 if $user->in_group('editbugs', $self->bug->product_id);
- return 0;
+ return 0;
}
=item C<validate_obsolete($bug, $attach_ids)>
@@ -637,37 +651,37 @@ Returns: The list of attachment objects to mark as obsolete.
=cut
sub validate_obsolete {
- 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;
-
- detaint_natural($attachid)
- || ThrowCodeError('invalid_attach_id_to_obsolete', $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 });
-
- if ($attachment->bug_id != $bug->bug_id) {
- $vars->{'my_bug_id'} = $bug->bug_id;
- ThrowCodeError('mismatched_bug_ids_on_obsolete', $vars);
- }
+ 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;
+
+ detaint_natural($attachid)
+ || ThrowCodeError('invalid_attach_id_to_obsolete', $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});
+
+ if ($attachment->bug_id != $bug->bug_id) {
+ $vars->{'my_bug_id'} = $bug->bug_id;
+ ThrowCodeError('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;
}
###############################
@@ -702,85 +716,88 @@ 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 $attachment = $class->insert_create_data($params);
- $attachment->{bug} = $bug;
-
- # store attachment data
- if (ref($data)) {
- local $/;
- my $tmp = <$data>;
- close($data);
- $data = $tmp;
- }
- current_storage()->store($attachment->id, $data);
+ my $class = shift;
+ my $dbh = Bugzilla->dbh;
- # Return the new attachment object
- return $attachment;
-}
+ $class->check_required_create_fields(@_);
+ my $params = $class->run_create_validators(@_);
-sub run_create_validators {
- my ($class, $params) = @_;
+ # Extract everything which is not a valid column name.
+ my $bug = delete $params->{bug};
+ $params->{bug_id} = $bug->id;
+ my $data = delete $params->{data};
- # 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);
+ my $attachment = $class->insert_create_data($params);
+ $attachment->{bug} = $bug;
- $params->{creation_ts} ||= Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
- $params->{modification_time} = $params->{creation_ts};
- $params->{submitter_id} = Bugzilla->user->id || ThrowCodeError('invalid_user');
+ # store attachment data
+ if (ref($data)) {
+ local $/;
+ my $tmp = <$data>;
+ close($data);
+ $data = $tmp;
+ }
+ current_storage()->store($attachment->id, $data);
- 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)');
-
- 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];
- }
+sub run_create_validators {
+ my ($class, $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);
- }
+ # 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);
- 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 });
- }
+ $params->{creation_ts}
+ ||= Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
+ $params->{modification_time} = $params->{creation_ts};
+ $params->{submitter_id} = Bugzilla->user->id || ThrowCodeError('invalid_user');
- Bugzilla::Hook::process('attachment_end_of_update',
- { object => $self, old_object => $old_self, changes => $changes });
+ 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});
+ }
+
+ Bugzilla::Hook::process('attachment_end_of_update',
+ {object => $self, old_object => $old_self, changes => $changes});
+
+ return $changes;
}
=pod
@@ -798,25 +815,28 @@ 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('UPDATE attachments SET mimetype = ?, ispatch = ?, isobsolete = ?, attach_size = ?
- WHERE attach_id = ?', undef, ('text/plain', 0, 1, 0, $self->id));
- $dbh->bz_commit_transaction();
- current_storage()->remove($self->id);
-
- # 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(
+ 'UPDATE attachments SET mimetype = ?, ispatch = ?, isobsolete = ?, attach_size = ?
+ WHERE attach_id = ?', undef, ('text/plain', 0, 1, 0, $self->id)
+ );
+ $dbh->bz_commit_transaction();
+ current_storage()->remove($self->id);
+
+ # 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});
+ }
}
###############################
@@ -825,83 +845,87 @@ 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 (!defined $method) {
- ThrowUserError("missing_content_type_method");
- }
- elsif ($method eq 'autodetect') {
- defined $cgi->upload('data') || ThrowUserError('file_not_specified');
- # The user asked us to auto-detect the content type, so use the type
- # specified in the HTTP request headers.
- $content_type =
- $cgi->uploadInfo($cgi->param('data'))->{'Content-Type'};
- $content_type || ThrowUserError("missing_content_type");
-
- # Set the ispatch flag to 1 if the content type
- # is text/x-diff or text/x-patch
- if ($content_type =~ m{text/x-(?:diff|patch)}) {
- $cgi->param('ispatch', 1);
- $content_type = 'text/plain';
- }
-
- # 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';
- }
- }
- elsif ($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');
+ if (!defined $method) {
+ ThrowUserError("missing_content_type_method");
+ }
+ elsif ($method eq 'autodetect') {
+ defined $cgi->upload('data') || ThrowUserError('file_not_specified');
+
+ # The user asked us to auto-detect the content type, so use the type
+ # specified in the HTTP request headers.
+ $content_type = $cgi->uploadInfo($cgi->param('data'))->{'Content-Type'};
+ $content_type || ThrowUserError("missing_content_type");
+
+ # Set the ispatch flag to 1 if the content type
+ # is text/x-diff or text/x-patch
+ if ($content_type =~ m{text/x-(?:diff|patch)}) {
+ $cgi->param('ispatch', 1);
+ $content_type = 'text/plain';
}
- else {
- ThrowCodeError("illegal_content_type_method", { contenttypemethod => $method });
+
+ # 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;
+ }
+ elsif ($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 {
+ ThrowCodeError("illegal_content_type_method", {contenttypemethod => $method});
+ }
+ return $content_type;
}
sub current_storage {
- return state $storage //= get_storage_by_name(Bugzilla->params->{attachment_storage});
+ return state $storage
+ //= get_storage_by_name(Bugzilla->params->{attachment_storage});
}
sub get_storage_names {
- require Bugzilla::Config::Attachment;
- foreach my $param (Bugzilla::Config::Attachment->get_param_list) {
- next unless $param->{name} eq 'attachment_storage';
- return @{ $param->{choices} };
- }
- return [];
+ require Bugzilla::Config::Attachment;
+ foreach my $param (Bugzilla::Config::Attachment->get_param_list) {
+ next unless $param->{name} eq 'attachment_storage';
+ return @{$param->{choices}};
+ }
+ return [];
}
sub get_storage_by_name {
- my ($name) = @_;
- # all options for attachment_storage need to be handled here
- if ($name eq 'database') {
- require Bugzilla::Attachment::Database;
- return Bugzilla::Attachment::Database->new();
- }
- elsif ($name eq 'filesystem') {
- require Bugzilla::Attachment::FileSystem;
- return Bugzilla::Attachment::FileSystem->new();
- }
- elsif ($name eq 's3') {
- require Bugzilla::Attachment::S3;
- return Bugzilla::Attachment::S3->new();
- }
- else {
- return undef;
- }
+ my ($name) = @_;
+
+ # all options for attachment_storage need to be handled here
+ if ($name eq 'database') {
+ require Bugzilla::Attachment::Database;
+ return Bugzilla::Attachment::Database->new();
+ }
+ elsif ($name eq 'filesystem') {
+ require Bugzilla::Attachment::FileSystem;
+ return Bugzilla::Attachment::FileSystem->new();
+ }
+ elsif ($name eq 's3') {
+ require Bugzilla::Attachment::S3;
+ return Bugzilla::Attachment::S3->new();
+ }
+ else {
+ return undef;
+ }
}
1;
diff --git a/Bugzilla/Attachment/Archive.pm b/Bugzilla/Attachment/Archive.pm
index ccedf1da4..1b22fc50e 100644
--- a/Bugzilla/Attachment/Archive.pm
+++ b/Bugzilla/Attachment/Archive.pm
@@ -16,108 +16,110 @@ use IO::File;
use constant HEADER_SIZE => 45;
use constant HEADER_FORMAT => 'ANNNH64';
-has 'file' => ( is => 'ro', required => 1 );
-has 'input_fh' => ( is => 'lazy', predicate => 'has_input_fh' );
-has 'output_fh' => ( is => 'lazy', predicate => 'has_output_fh' );
-has 'checksum' => ( is => 'lazy', clearer => 'reset_checksum' );
+has 'file' => (is => 'ro', required => 1);
+has 'input_fh' => (is => 'lazy', predicate => 'has_input_fh');
+has 'output_fh' => (is => 'lazy', predicate => 'has_output_fh');
+has 'checksum' => (is => 'lazy', clearer => 'reset_checksum');
sub read_member {
- my ($self) = @_;
- my $header = $self->_read_header();
- my ($type, $bug_id, $attach_id, $data_len, $hash) = unpack HEADER_FORMAT, $header;
- if ( $type eq 'D' ) {
- $self->checksum->add($header);
- my $data = $self->_read_data( $data_len, $hash );
- return {
- bug_id => $bug_id,
- attach_id => $attach_id,
- data_len => $data_len,
- hash => $hash,
- data => $data,
- };
- }
- elsif ($type eq 'C') {
- die "bad overall checksum\n" unless $hash eq $self->checksum->hexdigest;
- $self->reset_checksum;
- return undef;
- }
- else {
- die "unknown member type: $type\n";
- }
+ my ($self) = @_;
+ my $header = $self->_read_header();
+ my ($type, $bug_id, $attach_id, $data_len, $hash) = unpack HEADER_FORMAT,
+ $header;
+ if ($type eq 'D') {
+ $self->checksum->add($header);
+ my $data = $self->_read_data($data_len, $hash);
+ return {
+ bug_id => $bug_id,
+ attach_id => $attach_id,
+ data_len => $data_len,
+ hash => $hash,
+ data => $data,
+ };
+ }
+ elsif ($type eq 'C') {
+ die "bad overall checksum\n" unless $hash eq $self->checksum->hexdigest;
+ $self->reset_checksum;
+ return undef;
+ }
+ else {
+ die "unknown member type: $type\n";
+ }
}
sub write_attachment {
- my ( $self, $attachment ) = @_;
- my $data = $attachment->data;
- my $bug_id = $attachment->bug_id;
- my $attach_id = $attachment->id;
-
- if (defined $data && length($data) == $attachment->datasize) {
- my $header = pack HEADER_FORMAT, 'D', $bug_id, $attach_id, length($data), sha256_hex($data);
- $self->checksum->add($header);
- $self->output_fh->print($header, $data);
- }
+ my ($self, $attachment) = @_;
+ my $data = $attachment->data;
+ my $bug_id = $attachment->bug_id;
+ my $attach_id = $attachment->id;
+
+ if (defined $data && length($data) == $attachment->datasize) {
+ my $header = pack HEADER_FORMAT, 'D', $bug_id, $attach_id, length($data),
+ sha256_hex($data);
+ $self->checksum->add($header);
+ $self->output_fh->print($header, $data);
+ }
}
sub write_checksum {
- my ($self) = @_;
- my $header = pack HEADER_FORMAT, 'C', 0, 0, 0, $self->checksum->hexdigest;
- $self->output_fh->print($header);
- $self->reset_checksum;
- $self->output_fh->flush;
+ my ($self) = @_;
+ my $header = pack HEADER_FORMAT, 'C', 0, 0, 0, $self->checksum->hexdigest;
+ $self->output_fh->print($header);
+ $self->reset_checksum;
+ $self->output_fh->flush;
}
sub _build_checksum {
- my ($self) = @_;
- return Digest::SHA->new(256);
+ my ($self) = @_;
+ return Digest::SHA->new(256);
}
sub _build_input_fh {
- my ($self) = @_;
- if ($self->has_output_fh) {
- croak "I will not read and write a file at the same time";
- }
- my $file = $self->file;
- return IO::File->new( $self->file, '<:bytes' ) or die "cannot read $file: $!";
+ my ($self) = @_;
+ if ($self->has_output_fh) {
+ croak "I will not read and write a file at the same time";
+ }
+ my $file = $self->file;
+ return IO::File->new($self->file, '<:bytes') or die "cannot read $file: $!";
}
sub _build_output_fh {
- my ($self) = @_;
- if ($self->has_input_fh) {
- croak "I will not read and write a file at the same time";
- }
- my $file = $self->file;
- if (-e $file) {
- croak "I will not overwrite a file (file $file already exists)";
- }
- return IO::File->new( $file, '>:bytes' ) or die "cannot write $file: $!";
+ my ($self) = @_;
+ if ($self->has_input_fh) {
+ croak "I will not read and write a file at the same time";
+ }
+ my $file = $self->file;
+ if (-e $file) {
+ croak "I will not overwrite a file (file $file already exists)";
+ }
+ return IO::File->new($file, '>:bytes') or die "cannot write $file: $!";
}
sub _read_header {
- my ($self) = @_;
- my $header = '' x HEADER_SIZE;
- my $header_len = $self->input_fh->read($header, HEADER_SIZE);
- if ( !$header_len || $header_len != HEADER_SIZE ) {
- die "bad header\n";
- }
- return $header;
+ my ($self) = @_;
+ my $header = '' x HEADER_SIZE;
+ my $header_len = $self->input_fh->read($header, HEADER_SIZE);
+ if (!$header_len || $header_len != HEADER_SIZE) {
+ die "bad header\n";
+ }
+ return $header;
}
sub _read_data {
- my ($self, $data_len, $hash) = @_;
+ my ($self, $data_len, $hash) = @_;
- my $data = '' x $data_len;
- my $read_data_len = $self->input_fh->read($data, $data_len);
+ my $data = '' x $data_len;
+ my $read_data_len = $self->input_fh->read($data, $data_len);
- unless ( $read_data_len == $data_len ) {
- die "bad data\n";
- }
+ unless ($read_data_len == $data_len) {
+ die "bad data\n";
+ }
- unless ( $hash eq sha256_hex($data) ) {
- die "bad checksum:\n\t$hash\n\t" . sha226_hex($data) . "\n";
- }
+ unless ($hash eq sha256_hex($data)) {
+ die "bad checksum:\n\t$hash\n\t" . sha226_hex($data) . "\n";
+ }
- return $data;
+ return $data;
}
-1; \ No newline at end of file
+1;
diff --git a/Bugzilla/Attachment/Database.pm b/Bugzilla/Attachment/Database.pm
index 42cdd2d6f..661ac9131 100644
--- a/Bugzilla/Attachment/Database.pm
+++ b/Bugzilla/Attachment/Database.pm
@@ -14,48 +14,40 @@ use warnings;
use Bugzilla::Util qw(trick_taint);
sub new {
- return bless({}, shift);
+ return bless({}, shift);
}
sub store {
- my ($self, $attach_id, $data) = @_;
- my $dbh = Bugzilla->dbh;
- my $sth = $dbh->prepare("INSERT INTO attach_data (id, thedata) VALUES ($attach_id, ?)");
- trick_taint($data);
- $sth->bind_param(1, $data, $dbh->BLOB_TYPE);
- $sth->execute();
+ my ($self, $attach_id, $data) = @_;
+ my $dbh = Bugzilla->dbh;
+ my $sth = $dbh->prepare(
+ "INSERT INTO attach_data (id, thedata) VALUES ($attach_id, ?)");
+ trick_taint($data);
+ $sth->bind_param(1, $data, $dbh->BLOB_TYPE);
+ $sth->execute();
}
sub retrieve {
- my ($self, $attach_id) = @_;
- my $dbh = Bugzilla->dbh;
- my ($data) = $dbh->selectrow_array(
- "SELECT thedata FROM attach_data WHERE id = ?",
- undef,
- $attach_id
- );
- return $data;
+ my ($self, $attach_id) = @_;
+ my $dbh = Bugzilla->dbh;
+ my ($data)
+ = $dbh->selectrow_array("SELECT thedata FROM attach_data WHERE id = ?",
+ undef, $attach_id);
+ return $data;
}
sub remove {
- my ($self, $attach_id) = @_;
- my $dbh = Bugzilla->dbh;
- $dbh->do(
- "DELETE FROM attach_data WHERE id = ?",
- undef,
- $attach_id
- );
+ my ($self, $attach_id) = @_;
+ my $dbh = Bugzilla->dbh;
+ $dbh->do("DELETE FROM attach_data WHERE id = ?", undef, $attach_id);
}
sub exists {
- my ($self, $attach_id) = @_;
- my $dbh = Bugzilla->dbh;
- my ($exists) = $dbh->selectrow_array(
- "SELECT 1 FROM attach_data WHERE id = ?",
- undef,
- $attach_id
- );
- return !!$exists;
+ my ($self, $attach_id) = @_;
+ my $dbh = Bugzilla->dbh;
+ my ($exists) = $dbh->selectrow_array("SELECT 1 FROM attach_data WHERE id = ?",
+ undef, $attach_id);
+ return !!$exists;
}
1;
diff --git a/Bugzilla/Attachment/FileSystem.pm b/Bugzilla/Attachment/FileSystem.pm
index a9fdf83b6..f10a994fd 100644
--- a/Bugzilla/Attachment/FileSystem.pm
+++ b/Bugzilla/Attachment/FileSystem.pm
@@ -14,50 +14,50 @@ use warnings;
use Bugzilla::Constants qw(bz_locations);
sub new {
- return bless({}, shift);
+ return bless({}, shift);
}
sub store {
- my ($self, $attach_id, $data) = @_;
- my $path = _local_path($attach_id);
- mkdir($path, 0770) unless -d $path;
- open(my $fh, '>', _local_file($attach_id));
- binmode($fh);
- print $fh $data;
- close($fh);
+ my ($self, $attach_id, $data) = @_;
+ my $path = _local_path($attach_id);
+ mkdir($path, 0770) unless -d $path;
+ open(my $fh, '>', _local_file($attach_id));
+ binmode($fh);
+ print $fh $data;
+ close($fh);
}
sub retrieve {
- my ($self, $attach_id) = @_;
- if (open(my $fh, '<', _local_file($attach_id))) {
- local $/;
- binmode($fh);
- my $data = <$fh>;
- close($fh);
- return $data;
- }
- return undef;
+ my ($self, $attach_id) = @_;
+ if (open(my $fh, '<', _local_file($attach_id))) {
+ local $/;
+ binmode($fh);
+ my $data = <$fh>;
+ close($fh);
+ return $data;
+ }
+ return undef;
}
sub remove {
- my ($self, $attach_id) = @_;
- unlink(_local_file($attach_id));
+ my ($self, $attach_id) = @_;
+ unlink(_local_file($attach_id));
}
sub exists {
- my ($self, $attach_id) = @_;
- return -e _local_file($attach_id);
+ my ($self, $attach_id) = @_;
+ return -e _local_file($attach_id);
}
sub _local_path {
- my ($attach_id) = @_;
- my $hash = sprintf('group.%03d', $attach_id % 1000);
- return bz_locations()->{attachdir} . '/' . $hash;
+ my ($attach_id) = @_;
+ my $hash = sprintf('group.%03d', $attach_id % 1000);
+ return bz_locations()->{attachdir} . '/' . $hash;
}
sub _local_file {
- my ($attach_id) = @_;
- return _local_path($attach_id) . '/attachment.' . $attach_id;
+ my ($attach_id) = @_;
+ return _local_path($attach_id) . '/attachment.' . $attach_id;
}
1;
diff --git a/Bugzilla/Attachment/PatchReader.pm b/Bugzilla/Attachment/PatchReader.pm
index 8025f5b82..0ae70d3f4 100644
--- a/Bugzilla/Attachment/PatchReader.pm
+++ b/Bugzilla/Attachment/PatchReader.pm
@@ -20,148 +20,160 @@ use Bugzilla::Attachment;
use Bugzilla::Util;
sub process_diff {
- my ($attachment, $format, $context) = @_;
- my $dbh = Bugzilla->dbh;
- my $cgi = Bugzilla->cgi;
- my $lc = Bugzilla->localconfig;
- my $vars = {};
-
- my ($reader, $last_reader) = setup_patch_readers(undef, $context);
-
- if ($format eq 'raw') {
- require Bugzilla::PatchReader::DiffPrinter::raw;
- $last_reader->sends_data_to(new Bugzilla::PatchReader::DiffPrinter::raw());
-
- Bugzilla->log_user_request($attachment->bug_id, $attachment->id, "attachment-get")
- if Bugzilla->user->id;
- # Actually print out the patch.
- print $cgi->header(-type => 'text/plain',
- -expires => '+3M');
- 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, $context) = @_;
+ 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($last_reader, $format, $context, $vars);
- # The patch is going to be displayed in a HTML page and if the utf8
- # param is enabled, we have to encode attachment data as utf8.
- if (Bugzilla->params->{'utf8'}) {
- $attachment->data; # Populate ->{data}
- utf8::decode($attachment->{data});
- }
- $reader->iterate_string('Attachment ' . $attachment->id, $attachment->data);
- }
-}
+ my ($reader, $last_reader) = setup_patch_readers(undef, $context);
-sub process_interdiff {
- my ($old_attachment, $new_attachment, $format, $context) = @_;
- my $cgi = Bugzilla->cgi;
- my $lc = Bugzilla->localconfig;
- my $vars = {};
-
- if (Bugzilla->user->id) {
- foreach my $attachment ($old_attachment, $new_attachment) {
- Bugzilla->log_user_request($attachment->bug_id, $attachment->id, "attachment-get");
+ if ($format eq 'raw') {
+ require Bugzilla::PatchReader::DiffPrinter::raw;
+ $last_reader->sends_data_to(new Bugzilla::PatchReader::DiffPrinter::raw());
+
+ Bugzilla->log_user_request($attachment->bug_id, $attachment->id,
+ "attachment-get")
+ if Bugzilla->user->id;
+
+ # Actually print out the patch.
+ print $cgi->header(-type => 'text/plain', -expires => '+3M');
+ 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;
+ }
+ }
}
- # Encode attachment data as utf8 if it's going to be displayed in a HTML
- # page using the UTF-8 encoding.
- if ($format ne 'raw' && Bugzilla->params->{'utf8'}) {
- $old_attachment->data; # Populate ->{data}
- utf8::decode($old_attachment->{data});
- $new_attachment->data; # Populate ->{data}
- utf8::decode($new_attachment->{data});
- }
-
- # Get old patch data.
- my ($old_filename, $old_file_list) = get_unified_diff($old_attachment, $format);
- # Get new patch data.
- my ($new_filename, $new_file_list) = get_unified_diff($new_attachment, $format);
+ $vars->{'bugid'} = $attachment->bug_id;
+ $vars->{'attachid'} = $attachment->id;
+ $vars->{'description'} = $attachment->description;
+ $vars->{'other_patches'} = \@other_patches;
- my $warning = warn_if_interdiff_might_fail($old_file_list, $new_file_list);
+ setup_template_patch_reader($last_reader, $format, $context, $vars);
- # Send through interdiff, send output directly to template.
- # Must hack path so that interdiff will work.
- $ENV{'PATH'} = $lc->{diffpath};
-
- my ($pid, $interdiff_stdout, $interdiff_stderr);
- $interdiff_stderr = gensym;
- $pid = open3(gensym, $interdiff_stdout, $interdiff_stderr,
- $lc->{interdiffbin}, $old_filename, $new_filename);
- binmode $interdiff_stdout;
-
- # Check for errors
- {
- local $/ = undef;
- my $error = <$interdiff_stderr>;
- if ($error) {
- warn($error);
- $warning = 'interdiff3';
- }
+ # The patch is going to be displayed in a HTML page and if the utf8
+ # param is enabled, we have to encode attachment data as utf8.
+ if (Bugzilla->params->{'utf8'}) {
+ $attachment->data; # Populate ->{data}
+ utf8::decode($attachment->{data});
}
+ $reader->iterate_string('Attachment ' . $attachment->id, $attachment->data);
+ }
+}
- my ($reader, $last_reader) = setup_patch_readers("", $context);
-
- if ($format eq 'raw') {
- require Bugzilla::PatchReader::DiffPrinter::raw;
- $last_reader->sends_data_to(new Bugzilla::PatchReader::DiffPrinter::raw());
- # Actually print out the patch.
- print $cgi->header(-type => 'text/plain',
- -expires => '+3M');
- disable_utf8();
+sub process_interdiff {
+ my ($old_attachment, $new_attachment, $format, $context) = @_;
+ my $cgi = Bugzilla->cgi;
+ my $lc = Bugzilla->localconfig;
+ my $vars = {};
+
+ if (Bugzilla->user->id) {
+ foreach my $attachment ($old_attachment, $new_attachment) {
+ Bugzilla->log_user_request($attachment->bug_id, $attachment->id,
+ "attachment-get");
}
- else {
- # In case the HTML page is displayed with the UTF-8 encoding.
- binmode $interdiff_stdout, ':utf8' if Bugzilla->params->{'utf8'};
-
- $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($last_reader, $format, $context, $vars);
+ }
+
+ # Encode attachment data as utf8 if it's going to be displayed in a HTML
+ # page using the UTF-8 encoding.
+ if ($format ne 'raw' && Bugzilla->params->{'utf8'}) {
+ $old_attachment->data; # Populate ->{data}
+ utf8::decode($old_attachment->{data});
+ $new_attachment->data; # Populate ->{data}
+ utf8::decode($new_attachment->{data});
+ }
+
+ # Get old patch data.
+ my ($old_filename, $old_file_list) = get_unified_diff($old_attachment, $format);
+
+ # Get new patch data.
+ my ($new_filename, $new_file_list) = get_unified_diff($new_attachment, $format);
+
+ my $warning = warn_if_interdiff_might_fail($old_file_list, $new_file_list);
+
+ # Send through interdiff, send output directly to template.
+ # Must hack path so that interdiff will work.
+ $ENV{'PATH'} = $lc->{diffpath};
+
+ my ($pid, $interdiff_stdout, $interdiff_stderr);
+ $interdiff_stderr = gensym;
+ $pid = open3(gensym, $interdiff_stdout, $interdiff_stderr, $lc->{interdiffbin},
+ $old_filename, $new_filename);
+ binmode $interdiff_stdout;
+
+ # Check for errors
+ {
+ local $/ = undef;
+ my $error = <$interdiff_stderr>;
+ if ($error) {
+ warn($error);
+ $warning = 'interdiff3';
}
- $reader->iterate_fh($interdiff_stdout, 'interdiff #' . $old_attachment->id .
- ' #' . $new_attachment->id);
- waitpid($pid, 0) if $pid;
- $ENV{'PATH'} = '';
-
- # Delete temporary files.
- unlink($old_filename) or warn "Could not unlink $old_filename: $!";
- unlink($new_filename) or warn "Could not unlink $new_filename: $!";
+ }
+
+ my ($reader, $last_reader) = setup_patch_readers("", $context);
+
+ if ($format eq 'raw') {
+ require Bugzilla::PatchReader::DiffPrinter::raw;
+ $last_reader->sends_data_to(new Bugzilla::PatchReader::DiffPrinter::raw());
+
+ # Actually print out the patch.
+ print $cgi->header(-type => 'text/plain', -expires => '+3M');
+ disable_utf8();
+ }
+ else {
+ # In case the HTML page is displayed with the UTF-8 encoding.
+ binmode $interdiff_stdout, ':utf8' if Bugzilla->params->{'utf8'};
+
+ $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($last_reader, $format, $context, $vars);
+ }
+ $reader->iterate_fh($interdiff_stdout,
+ 'interdiff #' . $old_attachment->id . ' #' . $new_attachment->id);
+ waitpid($pid, 0) if $pid;
+ $ENV{'PATH'} = '';
+
+ # Delete temporary files.
+ unlink($old_filename) or warn "Could not unlink $old_filename: $!";
+ unlink($new_filename) or warn "Could not unlink $new_filename: $!";
}
######################
@@ -169,144 +181,154 @@ sub process_interdiff {
######################
sub get_unified_diff {
- my ($attachment, $format) = @_;
-
- # Bring in the modules we need.
- require Bugzilla::PatchReader::Raw;
- require Bugzilla::PatchReader::FixPatchRoot;
- require Bugzilla::PatchReader::DiffPrinter::raw;
- require Bugzilla::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 Bugzilla::PatchReader::Raw;
- my $last_reader = $reader;
-
- # Fixes patch root (makes canonical if possible).
- if (Bugzilla->params->{'cvsroot'}) {
- my $fix_patch_root =
- new Bugzilla::PatchReader::FixPatchRoot(Bugzilla->params->{'cvsroot'});
- $last_reader->sends_data_to($fix_patch_root);
- $last_reader = $fix_patch_root;
- }
-
- # Grabs the patch file info.
- my $patch_info_grabber = new Bugzilla::PatchReader::PatchInfoGrabber();
- $last_reader->sends_data_to($patch_info_grabber);
- $last_reader = $patch_info_grabber;
-
- # Prints out to temporary file.
- my ($fh, $filename) = File::Temp::tempfile();
- if ($format ne 'raw' && Bugzilla->params->{'utf8'}) {
- # The HTML page will be displayed with the UTF-8 encoding.
- binmode $fh, ':utf8';
- }
- my $raw_printer = new Bugzilla::PatchReader::DiffPrinter::raw($fh);
- $last_reader->sends_data_to($raw_printer);
- $last_reader = $raw_printer;
-
- # Iterate!
- $reader->iterate_string($attachment->id, $attachment->data);
-
- return ($filename, $patch_info_grabber->patch_info()->{files});
+ my ($attachment, $format) = @_;
+
+ # Bring in the modules we need.
+ require Bugzilla::PatchReader::Raw;
+ require Bugzilla::PatchReader::FixPatchRoot;
+ require Bugzilla::PatchReader::DiffPrinter::raw;
+ require Bugzilla::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 Bugzilla::PatchReader::Raw;
+ my $last_reader = $reader;
+
+ # Fixes patch root (makes canonical if possible).
+ if (Bugzilla->params->{'cvsroot'}) {
+ my $fix_patch_root
+ = new Bugzilla::PatchReader::FixPatchRoot(Bugzilla->params->{'cvsroot'});
+ $last_reader->sends_data_to($fix_patch_root);
+ $last_reader = $fix_patch_root;
+ }
+
+ # Grabs the patch file info.
+ my $patch_info_grabber = new Bugzilla::PatchReader::PatchInfoGrabber();
+ $last_reader->sends_data_to($patch_info_grabber);
+ $last_reader = $patch_info_grabber;
+
+ # Prints out to temporary file.
+ my ($fh, $filename) = File::Temp::tempfile();
+ if ($format ne 'raw' && Bugzilla->params->{'utf8'}) {
+
+ # The HTML page will be displayed with the UTF-8 encoding.
+ binmode $fh, ':utf8';
+ }
+ my $raw_printer = new Bugzilla::PatchReader::DiffPrinter::raw($fh);
+ $last_reader->sends_data_to($raw_printer);
+ $last_reader = $raw_printer;
+
+ # Iterate!
+ $reader->iterate_string($attachment->id, $attachment->data);
+
+ return ($filename, $patch_info_grabber->patch_info()->{files});
}
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))
+ 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 (
+ $old_file_list->{$file}{old_revision} ne $new_file_list->{$file}{old_revision})
{
- return 'interdiff1';
- }
-
- # Verify that the revisions in the files are the same.
- foreach my $file (keys %{$old_file_list}) {
- if ($old_file_list->{$file}{old_revision} ne
- $new_file_list->{$file}{old_revision})
- {
- return 'interdiff2';
- }
+ return 'interdiff2';
}
- return undef;
+ }
+ return undef;
}
sub setup_patch_readers {
- my ($diff_root, $context) = @_;
-
- # Parameters:
- # format=raw|html
- # context=patch|file|0-n
- # collapsed=0|1
- # headers=0|1
-
- # Define the patch readers.
- # The reader that reads the patch in (whatever its format).
- require Bugzilla::PatchReader::Raw;
- my $reader = new Bugzilla::PatchReader::Raw;
- my $last_reader = $reader;
- # Fix the patch root if we have a cvs root.
- if (Bugzilla->params->{'cvsroot'}) {
- require Bugzilla::PatchReader::FixPatchRoot;
- $last_reader->sends_data_to(new Bugzilla::PatchReader::FixPatchRoot(Bugzilla->params->{'cvsroot'}));
- $last_reader->sends_data_to->diff_root($diff_root) if defined($diff_root);
- $last_reader = $last_reader->sends_data_to;
- }
-
- # Add in cvs context if we have the necessary info to do it
- if ($context ne 'patch' && Bugzilla->localconfig->{cvsbin}
- && Bugzilla->params->{'cvsroot_get'})
- {
- require Bugzilla::PatchReader::AddCVSContext;
- # We need to set $cvsbin as global, because PatchReader::CVSClient
- # needs it in order to find 'cvs'.
- $main::cvsbin = Bugzilla->localconfig->{cvsbin};
- $last_reader->sends_data_to(
- new Bugzilla::PatchReader::AddCVSContext($context, Bugzilla->params->{'cvsroot_get'}));
- $last_reader = $last_reader->sends_data_to;
- }
-
- return ($reader, $last_reader);
+ my ($diff_root, $context) = @_;
+
+ # Parameters:
+ # format=raw|html
+ # context=patch|file|0-n
+ # collapsed=0|1
+ # headers=0|1
+
+ # Define the patch readers.
+ # The reader that reads the patch in (whatever its format).
+ require Bugzilla::PatchReader::Raw;
+ my $reader = new Bugzilla::PatchReader::Raw;
+ my $last_reader = $reader;
+
+ # Fix the patch root if we have a cvs root.
+ if (Bugzilla->params->{'cvsroot'}) {
+ require Bugzilla::PatchReader::FixPatchRoot;
+ $last_reader->sends_data_to(new Bugzilla::PatchReader::FixPatchRoot(
+ Bugzilla->params->{'cvsroot'}));
+ $last_reader->sends_data_to->diff_root($diff_root) if defined($diff_root);
+ $last_reader = $last_reader->sends_data_to;
+ }
+
+ # Add in cvs context if we have the necessary info to do it
+ if ( $context ne 'patch'
+ && Bugzilla->localconfig->{cvsbin}
+ && Bugzilla->params->{'cvsroot_get'})
+ {
+ require Bugzilla::PatchReader::AddCVSContext;
+
+ # We need to set $cvsbin as global, because PatchReader::CVSClient
+ # needs it in order to find 'cvs'.
+ $main::cvsbin = Bugzilla->localconfig->{cvsbin};
+ $last_reader->sends_data_to(new Bugzilla::PatchReader::AddCVSContext(
+ $context, Bugzilla->params->{'cvsroot_get'}
+ ));
+ $last_reader = $last_reader->sends_data_to;
+ }
+
+ return ($reader, $last_reader);
}
sub setup_template_patch_reader {
- my ($last_reader, $format, $context, $vars) = @_;
- my $cgi = Bugzilla->cgi;
- my $template = Bugzilla->template;
-
- require Bugzilla::PatchReader::DiffPrinter::template;
-
- # Define the vars for templates.
- if (defined $cgi->param('headers')) {
- $vars->{'headers'} = $cgi->param('headers');
- }
- else {
- $vars->{'headers'} = 1;
+ my ($last_reader, $format, $context, $vars) = @_;
+ my $cgi = Bugzilla->cgi;
+ my $template = Bugzilla->template;
+
+ require Bugzilla::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');
+ $vars->{'context'} = $context;
+ $vars->{'do_context'}
+ = Bugzilla->localconfig->{cvsbin}
+ && Bugzilla->params->{'cvsroot_get'}
+ && !$vars->{'newid'};
+
+ # Print everything out.
+ print $cgi->header(-type => 'text/html');
+
+ $last_reader->sends_data_to(new Bugzilla::PatchReader::DiffPrinter::template(
+ $template,
+ "attachment/diff-header.$format.tmpl",
+ "attachment/diff-file.$format.tmpl",
+ "attachment/diff-footer.$format.tmpl",
+ {
+ %{$vars},
+ bonsai_url => Bugzilla->params->{'bonsai_url'},
+ lxr_url => Bugzilla->params->{'lxr_url'},
+ lxr_root => Bugzilla->params->{'lxr_root'},
}
-
- $vars->{'collapsed'} = $cgi->param('collapsed');
- $vars->{'context'} = $context;
- $vars->{'do_context'} = Bugzilla->localconfig->{cvsbin}
- && Bugzilla->params->{'cvsroot_get'} && !$vars->{'newid'};
-
- # Print everything out.
- print $cgi->header(-type => 'text/html');
-
- $last_reader->sends_data_to(new Bugzilla::PatchReader::DiffPrinter::template($template,
- "attachment/diff-header.$format.tmpl",
- "attachment/diff-file.$format.tmpl",
- "attachment/diff-footer.$format.tmpl",
- { %{$vars},
- bonsai_url => Bugzilla->params->{'bonsai_url'},
- lxr_url => Bugzilla->params->{'lxr_url'},
- lxr_root => Bugzilla->params->{'lxr_root'},
- }));
+ ));
}
1;
diff --git a/Bugzilla/Attachment/S3.pm b/Bugzilla/Attachment/S3.pm
index e1c7269a5..7f4755720 100644
--- a/Bugzilla/Attachment/S3.pm
+++ b/Bugzilla/Attachment/S3.pm
@@ -15,44 +15,48 @@ use Bugzilla::Error;
use Bugzilla::S3;
sub new {
- my $s3 = Bugzilla::S3->new({
- aws_access_key_id => Bugzilla->params->{aws_access_key_id},
- aws_secret_access_key => Bugzilla->params->{aws_secret_access_key},
- secure => 1,
- });
- return bless({
- s3 => $s3,
- bucket => $s3->bucket(Bugzilla->params->{s3_bucket}),
- }, shift);
+ my $s3 = Bugzilla::S3->new({
+ aws_access_key_id => Bugzilla->params->{aws_access_key_id},
+ aws_secret_access_key => Bugzilla->params->{aws_secret_access_key},
+ secure => 1,
+ });
+ return
+ bless({s3 => $s3, bucket => $s3->bucket(Bugzilla->params->{s3_bucket}),},
+ shift);
}
sub store {
- my ($self, $attach_id, $data) = @_;
- unless ($self->{bucket}->add_key($attach_id, $data)) {
- warn "Failed to add attachment $attach_id to S3: " . $self->{bucket}->errstr . "\n";
- ThrowCodeError('s3_add_failed', { attach_id => $attach_id, reason => $self->{bucket}->errstr });
- }
+ my ($self, $attach_id, $data) = @_;
+ unless ($self->{bucket}->add_key($attach_id, $data)) {
+ warn "Failed to add attachment $attach_id to S3: "
+ . $self->{bucket}->errstr . "\n";
+ ThrowCodeError('s3_add_failed',
+ {attach_id => $attach_id, reason => $self->{bucket}->errstr});
+ }
}
sub retrieve {
- my ($self, $attach_id) = @_;
- my $response = $self->{bucket}->get_key($attach_id);
- if (!$response) {
- warn "Failed to retrieve attachment $attach_id from S3: " . $self->{bucket}->errstr . "\n";
- ThrowCodeError('s3_get_failed', { attach_id => $attach_id, reason => $self->{bucket}->errstr });
- }
- return $response->{value};
+ my ($self, $attach_id) = @_;
+ my $response = $self->{bucket}->get_key($attach_id);
+ if (!$response) {
+ warn "Failed to retrieve attachment $attach_id from S3: "
+ . $self->{bucket}->errstr . "\n";
+ ThrowCodeError('s3_get_failed',
+ {attach_id => $attach_id, reason => $self->{bucket}->errstr});
+ }
+ return $response->{value};
}
sub remove {
- my ($self, $attach_id) = @_;
- $self->{bucket}->delete_key($attach_id)
- or warn "Failed to remove attachment $attach_id from S3: " . $self->{bucket}->errstr . "\n";
+ my ($self, $attach_id) = @_;
+ $self->{bucket}->delete_key($attach_id)
+ or warn "Failed to remove attachment $attach_id from S3: "
+ . $self->{bucket}->errstr . "\n";
}
sub exists {
- my ($self, $attach_id) = @_;
- return !!$self->{bucket}->head_key($attach_id);
+ my ($self, $attach_id) = @_;
+ return !!$self->{bucket}->head_key($attach_id);
}
1;
diff --git a/Bugzilla/Auth.pm b/Bugzilla/Auth.pm
index 58ac248c5..c40b3582e 100644
--- a/Bugzilla/Auth.pm
+++ b/Bugzilla/Auth.pm
@@ -12,9 +12,9 @@ use strict;
use warnings;
use fields qw(
- _info_getter
- _verifier
- _persister
+ _info_getter
+ _verifier
+ _persister
);
use Bugzilla::Constants;
@@ -30,276 +30,281 @@ use URI;
use URI::QueryParam;
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) = @_;
- my $dbh = Bugzilla->dbh;
-
- # Get login info from the cookie, form, environment variables, etc.
- my $login_info = $self->{_info_getter}->get_login_info();
+ my ($self, $type) = @_;
+ my $dbh = Bugzilla->dbh;
- 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 his 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 his 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);
- }
-
- # 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);
-
- # trigger multi-factor auth
- if ($self->{_info_getter}->{successful}->requires_verification
- && $user->mfa
- && !Bugzilla->sudoer
- && !i_am_webservice()
- ) {
- my $params = Bugzilla->input_params;
- my $cgi = Bugzilla->cgi;
- my $uri = URI->new($cgi->self_url);
- foreach my $param (qw( Bugzilla_remember Bugzilla_restrictlogin GoAheadAndLogIn )) {
- $uri->query_param_delete($param);
- }
- $user->mfa_provider->verify_prompt({
- user => $user,
- type => $type,
- reason => 'Logging in as ' . $user->identity,
- restrictlogin => $params->{Bugzilla_restrictlogin},
- remember => $params->{Bugzilla_remember},
- url => $uri->as_string,
- postback => {
- action => 'token.cgi',
- token_field => 't',
- fields => {
- a => 'mfa_l',
- },
- }
- });
+ 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);
+
+ # trigger multi-factor auth
+ if ( $self->{_info_getter}->{successful}->requires_verification
+ && $user->mfa
+ && !Bugzilla->sudoer
+ && !i_am_webservice())
+ {
+ my $params = Bugzilla->input_params;
+ my $cgi = Bugzilla->cgi;
+ my $uri = URI->new($cgi->self_url);
+ foreach
+ my $param (qw( Bugzilla_remember Bugzilla_restrictlogin GoAheadAndLogIn ))
+ {
+ $uri->query_param_delete($param);
+ }
+ $user->mfa_provider->verify_prompt({
+ user => $user,
+ type => $type,
+ reason => 'Logging in as ' . $user->identity,
+ restrictlogin => $params->{Bugzilla_restrictlogin},
+ remember => $params->{Bugzilla_remember},
+ url => $uri->as_string,
+ postback =>
+ {action => 'token.cgi', token_field => 't', fields => {a => 'mfa_l',},}
+ });
+ }
+
+
+ return $self->_handle_login_result($login_info, $type);
}
sub mfa_verified {
- my ($self, $user, $event) = @_;
- require Bugzilla::Auth::Login::CGI;
+ my ($self, $user, $event) = @_;
+ require Bugzilla::Auth::Login::CGI;
- my $params = Bugzilla->input_params;
- $self->{_info_getter}->{successful} = Bugzilla::Auth::Login::CGI->new();
- $params->{Bugzilla_restrictlogin} = $event->{restrictlogin} if defined $event->{restrictlogin};
- $params->{Bugzilla_remember} = $event->{remember} if defined $event->{remember};
+ my $params = Bugzilla->input_params;
+ $self->{_info_getter}->{successful} = Bugzilla::Auth::Login::CGI->new();
+ $params->{Bugzilla_restrictlogin} = $event->{restrictlogin}
+ if defined $event->{restrictlogin};
+ $params->{Bugzilla_remember} = $event->{remember} if defined $event->{remember};
- $self->_handle_login_result({ user => $user }, $event->{type});
+ $self->_handle_login_result({user => $user}, $event->{type});
}
sub successful_info_getter {
- my ($self) = @_;
+ my ($self) = @_;
- return $self->{_info_getter}->{successful};
+ return $self->{_info_getter}->{successful};
}
sub can_change_password {
- my ($self) = @_;
- my $verifier = $self->{_verifier}->{successful};
- $verifier ||= $self->{_verifier};
- my $getter = $self->{_info_getter}->{successful};
- $getter = $self->{_info_getter}
- if (!$getter || $getter->isa('Bugzilla::Auth::Login::Cookie'));
- return $verifier->can_change_password &&
- $getter->user_can_create_account;
+ my ($self) = @_;
+ my $verifier = $self->{_verifier}->{successful};
+ $verifier ||= $self->{_verifier};
+ my $getter = $self->{_info_getter}->{successful};
+ $getter = $self->{_info_getter}
+ if (!$getter || $getter->isa('Bugzilla::Auth::Login::Cookie'));
+ return $verifier->can_change_password && $getter->user_can_create_account;
}
sub can_login {
- my ($self) = @_;
- my $getter = $self->{_info_getter}->{successful};
- $getter = $self->{_info_getter}
- if (!$getter || $getter->isa('Bugzilla::Auth::Login::Cookie'));
- return $getter->can_login;
+ my ($self) = @_;
+ my $getter = $self->{_info_getter}->{successful};
+ $getter = $self->{_info_getter}
+ if (!$getter || $getter->isa('Bugzilla::Auth::Login::Cookie'));
+ return $getter->can_login;
}
sub can_logout {
- my ($self) = @_;
- my $getter = $self->{_info_getter}->{successful};
- # If there's no successful getter, we're not logged in, so of
- # course we can't log out!
- return 0 unless $getter;
- return $getter->can_logout;
+ my ($self) = @_;
+ my $getter = $self->{_info_getter}->{successful};
+
+ # If there's no successful getter, we're not logged in, so of
+ # course we can't log out!
+ return 0 unless $getter;
+ return $getter->can_logout;
}
sub login_token {
- my ($self) = @_;
- my $getter = $self->{_info_getter}->{successful};
- if ($getter && $getter->isa('Bugzilla::Auth::Login::Cookie')) {
- return $getter->login_token;
- }
- return undef;
+ my ($self) = @_;
+ my $getter = $self->{_info_getter}->{successful};
+ if ($getter && $getter->isa('Bugzilla::Auth::Login::Cookie')) {
+ return $getter->login_token;
+ }
+ return undef;
}
sub user_can_create_account {
- my ($self) = @_;
- my $verifier = $self->{_verifier}->{successful};
- $verifier ||= $self->{_verifier};
- my $getter = $self->{_info_getter}->{successful};
- $getter = $self->{_info_getter}
- if (!$getter || $getter->isa('Bugzilla::Auth::Login::Cookie'));
- return $verifier->user_can_create_account
- && $getter->user_can_create_account;
+ my ($self) = @_;
+ my $verifier = $self->{_verifier}->{successful};
+ $verifier ||= $self->{_verifier};
+ my $getter = $self->{_info_getter}->{successful};
+ $getter = $self->{_info_getter}
+ if (!$getter || $getter->isa('Bugzilla::Auth::Login::Cookie'));
+ return $verifier->user_can_create_account && $getter->user_can_create_account;
}
sub extern_id_used {
- my ($self) = @_;
- return $self->{_info_getter}->extern_id_used
- || $self->{_verifier}->extern_id_used;
+ my ($self) = @_;
+ return $self->{_info_getter}->extern_id_used
+ || $self->{_verifier}->extern_id_used;
}
sub can_change_email {
- return $_[0]->user_can_create_account;
+ return $_[0]->user_can_create_account;
}
sub _handle_login_result {
- my ($self, $result, $login_type) = @_;
- my $dbh = Bugzilla->dbh;
-
- my $user = $result->{user};
- my $fail_code = $result->{failure};
-
- if (!$fail_code) {
- # We don't persist logins over GET requests in the WebService,
- # because the persistance information can't be re-used again.
- # (See Bugzilla::WebService::Server::JSONRPC for more info.)
- if ($self->{_info_getter}->{successful}->requires_persistence
- and !(
- Bugzilla->request_cache->{auth_no_automatic_login}
- || Bugzilla->request_cache->{dont_persist_session}
- )
- ) {
- $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});
- }
+ my ($self, $result, $login_type) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ my $user = $result->{user};
+ my $fail_code = $result->{failure};
+
+ if (!$fail_code) {
+
+ # We don't persist logins over GET requests in the WebService,
+ # because the persistance information can't be re-used again.
+ # (See Bugzilla::WebService::Server::JSONRPC for more info.)
+ if (
+ $self->{_info_getter}->{successful}->requires_persistence
+ and !(
+ Bugzilla->request_cache->{auth_no_automatic_login}
+ || Bugzilla->request_cache->{dont_persist_session}
+ )
+ )
+ {
+ $user->{_login_token} = $self->{_persister}->persist_login($user);
}
- 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;
+ }
+ elsif ($fail_code == AUTH_ERROR) {
+ if ($result->{user_error}) {
+ ThrowUserError($result->{user_error}, $result->{details});
}
- # 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_username_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) {
- my $host = gethostbyaddr($n, AF_INET);
- $address = "$host ($address)" if $host;
- }
- 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);
- Bugzilla->audit(sprintf(
- '<%s> triggered lockout of %s after %s attempts',
- $address, $user->login, scalar(@$attempts)
- ));
- }
-
- $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 });
+ 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_username_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) {
+ my $host = gethostbyaddr($n, AF_INET);
+ $address = "$host ($address)" if $host;
+ }
+ 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);
+ Bugzilla->audit(sprintf(
+ '<%s> triggered lockout of %s after %s attempts',
+ $address, $user->login, scalar(@$attempts)
+ ));
+ }
+
+ $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;
+ return $user;
}
1;
diff --git a/Bugzilla/Auth/Login.pm b/Bugzilla/Auth/Login.pm
index 49d670d93..6b0810035 100644
--- a/Bugzilla/Auth/Login.pm
+++ b/Bugzilla/Auth/Login.pm
@@ -16,18 +16,18 @@ use fields qw();
# Determines whether or not a user can logout. It's really a subroutine,
# but we implement it here as a constant. Override it in subclasses if
# that particular type of login method cannot log out.
-use constant can_logout => 1;
-use constant can_login => 1;
-use constant requires_persistence => 1;
-use constant requires_verification => 1;
+use constant can_logout => 1;
+use constant can_login => 1;
+use constant requires_persistence => 1;
+use constant requires_verification => 1;
use constant user_can_create_account => 0;
-use constant is_automatic => 0;
-use constant extern_id_used => 0;
+use constant is_automatic => 0;
+use constant extern_id_used => 0;
sub new {
- my ($class) = @_;
- my $self = fields::new($class);
- return $self;
+ my ($class) = @_;
+ my $self = fields::new($class);
+ return $self;
}
1;
diff --git a/Bugzilla/Auth/Login/APIKey.pm b/Bugzilla/Auth/Login/APIKey.pm
index 25c2a3555..43032c584 100644
--- a/Bugzilla/Auth/Login/APIKey.pm
+++ b/Bugzilla/Auth/Login/APIKey.pm
@@ -26,41 +26,42 @@ use constant can_logout => 0;
use fields qw(app_id);
sub set_app_id {
- my ($self, $app_id) = @_;
- $self->{app_id} = $app_id;
+ my ($self, $app_id) = @_;
+ $self->{app_id} = $app_id;
}
sub app_id {
- my ($self) = @_;
- return $self->{app_id};
+ my ($self) = @_;
+ return $self->{app_id};
}
# 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();
- $self->set_app_id($api_key->app_id);
+ # 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();
+ $self->set_app_id($api_key->app_id);
+
+ return {user_id => $api_key->user_id};
}
1;
diff --git a/Bugzilla/Auth/Login/CGI.pm b/Bugzilla/Auth/Login/CGI.pm
index a813529d5..1b3b1f69e 100644
--- a/Bugzilla/Auth/Login/CGI.pm
+++ b/Bugzilla/Auth/Login/CGI.pm
@@ -21,65 +21,71 @@ use Bugzilla::Error;
use Bugzilla::Token;
sub get_login_info {
- my ($self) = @_;
- my $params = Bugzilla->input_params;
- my $cgi = Bugzilla->cgi;
-
- my $login = trim(delete $params->{'Bugzilla_login'});
- my $password = delete $params->{'Bugzilla_password'};
- # The token must match the cookie to authenticate the request.
- my $login_token = delete $params->{'Bugzilla_login_token'};
- my $login_cookie = $cgi->cookie('Bugzilla_login_request_cookie');
-
- my $valid = 0;
- # If the web browser accepts cookies, use them.
- if ($login_token && $login_cookie) {
- my ($time, undef) = split(/-/, $login_token);
- # Regenerate the token based on the information we have.
- my $expected_token = issue_hash_token(['login_request', $login_cookie], $time);
- $valid = 1 if $expected_token eq $login_token;
- $cgi->remove_cookie('Bugzilla_login_request_cookie');
- }
- # WebServices and other local scripts can bypass this check.
- # This is safe because we won't store a login cookie in this case.
- elsif (Bugzilla->usage_mode != USAGE_MODE_BROWSER) {
- $valid = 1;
- }
- # Else falls back to the Referer header and accept local URLs.
- # Attachments are served from a separate host (ideally), and so
- # an evil attachment cannot abuse this check with a redirect.
- elsif (my $referer = $cgi->referer) {
- my $urlbase = Bugzilla->localconfig->{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 = Bugzilla->localconfig->{urlbase};
+ $valid = 1 if $referer =~ /^\Q$urlbase\E/;
+ }
+
+ # If the web browser doesn't accept cookies and the Referer header
+ # is missing, we have no way to make sure that the authentication
+ # request comes from the user.
+ elsif ($login && $password) {
+ ThrowUserError('auth_untrusted_request', {login => $login});
+ }
+
+ if (!defined($login) || !defined($password) || !$valid) {
+ return {failure => AUTH_NODATA};
+ }
+
+ return {username => $login, password => $password};
}
sub fail_nodata {
- my ($self) = @_;
- my $cgi = Bugzilla->cgi;
- my $template = Bugzilla->template;
-
- if (Bugzilla->usage_mode != USAGE_MODE_BROWSER) {
- ThrowUserError('login_required');
- }
-
- print $cgi->header();
- $template->process("account/auth/login.html.tmpl",
- { 'target' => $cgi->url(-relative=>1) })
- || ThrowTemplateError($template->error());
- exit;
+ my ($self) = @_;
+ my $cgi = Bugzilla->cgi;
+ my $template = Bugzilla->template;
+
+ if (Bugzilla->usage_mode != USAGE_MODE_BROWSER) {
+ ThrowUserError('login_required');
+ }
+
+ print $cgi->header();
+ $template->process("account/auth/login.html.tmpl",
+ {'target' => $cgi->url(-relative => 1)})
+ || ThrowTemplateError($template->error());
+ exit;
}
1;
diff --git a/Bugzilla/Auth/Login/Cookie.pm b/Bugzilla/Auth/Login/Cookie.pm
index 9a94fe019..79456c383 100644
--- a/Bugzilla/Auth/Login/Cookie.pm
+++ b/Bugzilla/Auth/Login/Cookie.pm
@@ -23,145 +23,148 @@ 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, $is_internal);
-
- 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;
- }
- trick_taint($login_cookie) if $login_cookie;
- $self->cookie($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 });
- }
- $is_internal = 1;
- }
- 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 = '';
- }
+ my ($self) = @_;
+ my $cgi = Bugzilla->cgi;
+ my $dbh = Bugzilla->dbh;
+ my ($user_id, $login_cookie, $is_internal);
+
+ 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;
+ }
+ trick_taint($login_cookie) if $login_cookie;
+ $self->cookie($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});
}
+ $is_internal = 1;
+ }
+ 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 no cookies were provided, we also look for a login token
- # passed in the parameters of a webservice
- my $token = $self->login_token;
- if ($token && (!$login_cookie || !$user_id)) {
- ($user_id, $login_cookie) = ($token->{'user_id'}, $token->{'login_token'});
- }
+ # If 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 (restrict_ipaddr = 0 OR ipaddr = ?)',
- undef, ($login_cookie, $user_id, $ip_addr));
-
- # If the cookie is valid, return a valid username.
- if (defined $db_cookie && $login_cookie eq $db_cookie) {
-
- # forbid logging in with a cookie if only api-keys are allowed
- if (i_am_webservice() && !$is_internal) {
- my $user = Bugzilla::User->new({ id => $user_id, cache => 1 });
- if ($user->settings->{api_key_only}->{value} eq 'on') {
- ThrowUserError('invalid_cookies_or_token');
- }
- }
-
- # 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');
+ undef, ($login_cookie, $user_id, $ip_addr)
+ );
+
+ # If the cookie is valid, return a valid username.
+ if (defined $db_cookie && $login_cookie eq $db_cookie) {
+
+ # forbid logging in with a cookie if only api-keys are allowed
+ if (i_am_webservice() && !$is_internal) {
+ my $user = Bugzilla::User->new({id => $user_id, cache => 1});
+ if ($user->settings->{api_key_only}->{value} eq 'on') {
+ ThrowUserError('invalid_cookies_or_token');
}
+ }
+
+ # 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};
}
sub cookie {
- my ($self, $val) = @_;
- $self->{_cookie} = $val if @_ > 1;
+ my ($self, $val) = @_;
+ $self->{_cookie} = $val if @_ > 1;
- return $self->{_cookie};
+ return $self->{_cookie};
}
1;
diff --git a/Bugzilla/Auth/Login/Env.pm b/Bugzilla/Auth/Login/Env.pm
index a4de8c638..edcc269bb 100644
--- a/Bugzilla/Auth/Login/Env.pm
+++ b/Bugzilla/Auth/Login/Env.pm
@@ -16,29 +16,32 @@ use base 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 $dbh = Bugzilla->dbh;
+ my ($self) = @_;
+ my $dbh = Bugzilla->dbh;
- my $env_id = $ENV{Bugzilla->params->{"auth_env_id"}} || '';
- my $env_email = $ENV{Bugzilla->params->{"auth_env_email"}} || '';
- my $env_realname = $ENV{Bugzilla->params->{"auth_env_realname"}} || '';
+ my $env_id = $ENV{Bugzilla->params->{"auth_env_id"}} || '';
+ my $env_email = $ENV{Bugzilla->params->{"auth_env_email"}} || '';
+ my $env_realname = $ENV{Bugzilla->params->{"auth_env_realname"}} || '';
- return { failure => AUTH_NODATA } if !$env_email;
+ return {failure => AUTH_NODATA} if !$env_email;
- return { username => $env_email, extern_id => $env_id,
- realname => $env_realname };
+ return {
+ username => $env_email,
+ extern_id => $env_id,
+ realname => $env_realname
+ };
}
sub fail_nodata {
- ThrowCodeError('env_no_email');
+ ThrowCodeError('env_no_email');
}
1;
diff --git a/Bugzilla/Auth/Login/Stack.pm b/Bugzilla/Auth/Login/Stack.pm
index d44ebbd46..7786f26c8 100644
--- a/Bugzilla/Auth/Login/Stack.pm
+++ b/Bugzilla/Auth/Login/Stack.pm
@@ -13,8 +13,8 @@ use warnings;
use base qw(Bugzilla::Auth::Login);
use fields qw(
- _stack
- successful
+ _stack
+ successful
);
use Hash::Util qw(lock_keys);
use Bugzilla::Hook;
@@ -22,81 +22,87 @@ use Bugzilla::Constants;
use List::MoreUtils qw(any);
sub new {
- my $class = shift;
- my $self = $class->SUPER::new(@_);
- my $list = shift;
- my %methods = map { $_ => "Bugzilla/Auth/Login/$_.pm" } split(',', $list);
- lock_keys(%methods);
- Bugzilla::Hook::process('auth_login_methods', { modules => \%methods });
-
- $self->{_stack} = [];
- foreach my $login_method (split(',', $list)) {
- my $module = $methods{$login_method};
- require $module;
- $module =~ s|/|::|g;
- $module =~ s/.pm$//;
- push(@{$self->{_stack}}, $module->new(@_));
- }
- return $self;
+ my $class = shift;
+ my $self = $class->SUPER::new(@_);
+ my $list = shift;
+ my %methods = map { $_ => "Bugzilla/Auth/Login/$_.pm" } split(',', $list);
+ lock_keys(%methods);
+ Bugzilla::Hook::process('auth_login_methods', {modules => \%methods});
+
+ $self->{_stack} = [];
+ foreach my $login_method (split(',', $list)) {
+ my $module = $methods{$login_method};
+ require $module;
+ $module =~ s|/|::|g;
+ $module =~ s/.pm$//;
+ push(@{$self->{_stack}}, $module->new(@_));
+ }
+ return $self;
}
sub get_login_info {
- my $self = shift;
- my $result;
- foreach my $object (@{$self->{_stack}}) {
- # See Bugzilla::WebService::Server::JSONRPC for where and why
- # auth_no_automatic_login is used.
- if (Bugzilla->request_cache->{auth_no_automatic_login}) {
- next if $object->is_automatic;
- }
- $result = $object->get_login_info(@_);
- $self->{successful} = $object;
-
- # We only carry on down the stack if this method denied all knowledge.
- last unless ($result->{failure}
- && ($result->{failure} eq AUTH_NODATA
- || $result->{failure} eq AUTH_NO_SUCH_USER));
-
- # If none of the methods succeed, it's undef.
- $self->{successful} = undef;
+ my $self = shift;
+ my $result;
+ foreach my $object (@{$self->{_stack}}) {
+
+ # See Bugzilla::WebService::Server::JSONRPC for where and why
+ # auth_no_automatic_login is used.
+ if (Bugzilla->request_cache->{auth_no_automatic_login}) {
+ next if $object->is_automatic;
}
- return $result;
+ $result = $object->get_login_info(@_);
+ $self->{successful} = $object;
+
+ # We only carry on down the stack if this method denied all knowledge.
+ last
+ unless ($result->{failure}
+ && ( $result->{failure} eq AUTH_NODATA
+ || $result->{failure} eq AUTH_NO_SUCH_USER));
+
+ # If none of the methods succeed, it's undef.
+ $self->{successful} = undef;
+ }
+ return $result;
}
sub fail_nodata {
- my $self = shift;
- # We fail from the bottom of the stack.
- my @reverse_stack = reverse @{$self->{_stack}};
- foreach my $object (@reverse_stack) {
- # We pick the first object that actually has the method
- # implemented.
- if ($object->can('fail_nodata')) {
- $object->fail_nodata(@_);
- }
+ my $self = shift;
+
+ # We fail from the bottom of the stack.
+ my @reverse_stack = reverse @{$self->{_stack}};
+ foreach my $object (@reverse_stack) {
+
+ # We pick the first object that actually has the method
+ # implemented.
+ if ($object->can('fail_nodata')) {
+ $object->fail_nodata(@_);
}
+ }
}
sub can_login {
- my ($self) = @_;
- # We return true if any method can log in.
- foreach my $object (@{$self->{_stack}}) {
- return 1 if $object->can_login;
- }
- return 0;
+ my ($self) = @_;
+
+ # We return true if any method can log in.
+ foreach my $object (@{$self->{_stack}}) {
+ return 1 if $object->can_login;
+ }
+ return 0;
}
sub user_can_create_account {
- my ($self) = @_;
- # We return true if any method allows users to create accounts.
- foreach my $object (@{$self->{_stack}}) {
- return 1 if $object->user_can_create_account;
- }
- return 0;
+ my ($self) = @_;
+
+ # We return true if any method allows users to create accounts.
+ foreach my $object (@{$self->{_stack}}) {
+ return 1 if $object->user_can_create_account;
+ }
+ return 0;
}
sub extern_id_used {
- my ($self) = @_;
- return any { $_->extern_id_used } @{ $self->{_stack} };
+ my ($self) = @_;
+ return any { $_->extern_id_used } @{$self->{_stack}};
}
1;
diff --git a/Bugzilla/Auth/Persist/Cookie.pm b/Bugzilla/Auth/Persist/Cookie.pm
index 57473f551..65e6a1541 100644
--- a/Bugzilla/Auth/Persist/Cookie.pm
+++ b/Bugzilla/Auth/Persist/Cookie.pm
@@ -21,150 +21,166 @@ use List::Util qw(first);
use List::MoreUtils qw(any);
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;
-
- $dbh->bz_start_transaction();
-
- my $login_cookie =
- Bugzilla::Token::GenerateUniqueToken('logincookies', 'cookie');
-
- my $ip_addr = remote_ip();
- trick_taint($ip_addr);
- my $restrict = $input_params->{Bugzilla_restrictlogin} ? 1 : 0;
-
- $dbh->do("INSERT INTO logincookies (cookie, userid, ipaddr, lastused, restrict_ipaddr)
- VALUES (?, ?, ?, NOW(), ?)",
- undef, $login_cookie, $user->id, $ip_addr, $restrict);
-
- # 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();
-
- # 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->remove_cookie('github_secret');
- $cgi->remove_cookie('Bugzilla_login_request_cookie');
- $cgi->send_cookie(-name => 'Bugzilla_login',
- -value => $user->id,
- %cookieargs);
- $cgi->send_cookie(-name => 'Bugzilla_logincookie',
- -value => $login_cookie,
- %cookieargs);
-
- my $securemail_groups = Bugzilla->can('securemail_groups') ? Bugzilla->securemail_groups : [ 'admin' ];
-
- if (any { $user->in_group($_) } 'mozilla-employee-confidential', @$securemail_groups) {
- my $auth_method = eval { ref($user->authorizer->successful_info_getter) } // 'unknown';
-
- Bugzilla->audit(sprintf "successful login of %s from %s using \"%s\", authenticated by %s",
- $user->login, $ip_addr, $cgi->user_agent // '', $auth_method);
- }
-
- return $login_cookie;
+ my ($self, $user) = @_;
+ my $dbh = Bugzilla->dbh;
+ my $cgi = Bugzilla->cgi;
+ my $input_params = Bugzilla->input_params;
+
+ $dbh->bz_start_transaction();
+
+ my $login_cookie
+ = Bugzilla::Token::GenerateUniqueToken('logincookies', 'cookie');
+
+ my $ip_addr = remote_ip();
+ trick_taint($ip_addr);
+ my $restrict = $input_params->{Bugzilla_restrictlogin} ? 1 : 0;
+
+ $dbh->do(
+ "INSERT INTO logincookies (cookie, userid, ipaddr, lastused, restrict_ipaddr)
+ VALUES (?, ?, ?, NOW(), ?)", undef, $login_cookie, $user->id,
+ $ip_addr, $restrict
+ );
+
+ # 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();
+
+ # 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->remove_cookie('github_secret');
+ $cgi->remove_cookie('Bugzilla_login_request_cookie');
+ $cgi->send_cookie(-name => 'Bugzilla_login', -value => $user->id, %cookieargs);
+ $cgi->send_cookie(
+ -name => 'Bugzilla_logincookie',
+ -value => $login_cookie,
+ %cookieargs
+ );
+
+ my $securemail_groups
+ = Bugzilla->can('securemail_groups')
+ ? Bugzilla->securemail_groups
+ : ['admin'];
+
+ if (any { $user->in_group($_) } 'mozilla-employee-confidential',
+ @$securemail_groups)
+ {
+ my $auth_method
+ = eval { ref($user->authorizer->successful_info_getter) } // 'unknown';
+
+ Bugzilla->audit(
+ sprintf "successful login of %s from %s using \"%s\", authenticated by %s",
+ $user->login, $ip_addr, $cgi->user_agent // '', $auth_method);
+ }
+
+ return $login_cookie;
}
sub logout {
- my ($self, $param) = @_;
-
- my $dbh = Bugzilla->dbh;
- my $cgi = Bugzilla->cgi;
- my $input = Bugzilla->input_params;
- $param = {} unless $param;
- my $user = $param->{user} || Bugzilla->user;
- my $type = $param->{type} || LOGOUT_ALL;
-
- if ($type == LOGOUT_ALL) {
- $dbh->do("DELETE FROM logincookies WHERE userid = ?",
- undef, $user->id);
- return;
- }
-
- # The LOGOUT_*_CURRENT options require the current login cookie.
- # If a new cookie has been issued during this run, that's the current one.
- # If not, it's the one we've received.
- my @login_cookies;
- my $cookie = first {$_->name eq 'Bugzilla_logincookie'}
- @{$cgi->{'Bugzilla_cookie_list'}};
- if ($cookie) {
- push(@login_cookies, $cookie->value);
- }
- else {
- push(@login_cookies, $cgi->cookie("Bugzilla_logincookie"));
- }
-
- # If we are a webservice using a token instead of cookie
- # then add that as well to the login cookies to delete
- if (my $login_token = $user->authorizer->login_token) {
- push(@login_cookies, $login_token->{'login_token'});
- }
-
- # Make sure that @login_cookies is not empty to not break SQL statements.
- push(@login_cookies, '') unless @login_cookies;
-
- # These queries use both the cookie ID and the user ID as keys. Even
- # though we know the userid must match, we still check it in the SQL
- # as a sanity check, since there is no locking here, and if the user
- # logged out from two machines simultaneously, while someone else
- # logged in and got the same cookie, we could be logging the other
- # user out here. Yes, this is very very very unlikely, but why take
- # chances? - bbaetz
- map { trick_taint($_) } @login_cookies;
- @login_cookies = map { $dbh->quote($_) } @login_cookies;
- if ($type == LOGOUT_KEEP_CURRENT) {
- $dbh->do("DELETE FROM logincookies WHERE " .
- $dbh->sql_in('cookie', \@login_cookies, 1) .
- " AND userid = ?",
- undef, $user->id);
- } elsif ($type == LOGOUT_CURRENT) {
- $dbh->do("DELETE FROM logincookies WHERE " .
- $dbh->sql_in('cookie', \@login_cookies) .
- " AND userid = ?",
- undef, $user->id);
- } else {
- die("Invalid type $type supplied to logout()");
- }
-
- if ($type != LOGOUT_KEEP_CURRENT) {
- clear_browser_cookies();
- }
+ my ($self, $param) = @_;
+
+ my $dbh = Bugzilla->dbh;
+ my $cgi = Bugzilla->cgi;
+ my $input = Bugzilla->input_params;
+ $param = {} unless $param;
+ my $user = $param->{user} || Bugzilla->user;
+ my $type = $param->{type} || LOGOUT_ALL;
+
+ if ($type == LOGOUT_ALL) {
+ $dbh->do("DELETE FROM logincookies WHERE userid = ?", undef, $user->id);
+ return;
+ }
+
+ # The LOGOUT_*_CURRENT options require the current login cookie.
+ # If a new cookie has been issued during this run, that's the current one.
+ # If not, it's the one we've received.
+ my @login_cookies;
+ my $cookie = first { $_->name eq 'Bugzilla_logincookie' }
+ @{$cgi->{'Bugzilla_cookie_list'}};
+ if ($cookie) {
+ push(@login_cookies, $cookie->value);
+ }
+ else {
+ push(@login_cookies, $cgi->cookie("Bugzilla_logincookie"));
+ }
+
+ # If we are a webservice using a token instead of cookie
+ # then add that as well to the login cookies to delete
+ if (my $login_token = $user->authorizer->login_token) {
+ push(@login_cookies, $login_token->{'login_token'});
+ }
+
+ # Make sure that @login_cookies is not empty to not break SQL statements.
+ push(@login_cookies, '') unless @login_cookies;
+
+ # These queries use both the cookie ID and the user ID as keys. Even
+ # though we know the userid must match, we still check it in the SQL
+ # as a sanity check, since there is no locking here, and if the user
+ # logged out from two machines simultaneously, while someone else
+ # logged in and got the same cookie, we could be logging the other
+ # user out here. Yes, this is very very very unlikely, but why take
+ # chances? - bbaetz
+ map { trick_taint($_) } @login_cookies;
+ @login_cookies = map { $dbh->quote($_) } @login_cookies;
+ if ($type == LOGOUT_KEEP_CURRENT) {
+ $dbh->do(
+ "DELETE FROM logincookies WHERE "
+ . $dbh->sql_in('cookie', \@login_cookies, 1)
+ . " AND userid = ?",
+ undef, $user->id
+ );
+ }
+ elsif ($type == LOGOUT_CURRENT) {
+ $dbh->do(
+ "DELETE FROM logincookies WHERE "
+ . $dbh->sql_in('cookie', \@login_cookies)
+ . " AND userid = ?",
+ undef, $user->id
+ );
+ }
+ else {
+ die("Invalid type $type supplied to logout()");
+ }
+
+ if ($type != LOGOUT_KEEP_CURRENT) {
+ clear_browser_cookies();
+ }
}
sub clear_browser_cookies {
- my $cgi = Bugzilla->cgi;
- $cgi->remove_cookie('Bugzilla_login');
- $cgi->remove_cookie('Bugzilla_logincookie');
- $cgi->remove_cookie('sudo');
+ my $cgi = Bugzilla->cgi;
+ $cgi->remove_cookie('Bugzilla_login');
+ $cgi->remove_cookie('Bugzilla_logincookie');
+ $cgi->remove_cookie('sudo');
}
1;
diff --git a/Bugzilla/Auth/Verify.pm b/Bugzilla/Auth/Verify.pm
index 5895534cd..20782e633 100644
--- a/Bugzilla/Auth/Verify.pm
+++ b/Bugzilla/Auth/Verify.pm
@@ -19,113 +19,127 @@ use Bugzilla::User;
use Bugzilla::Util;
use constant user_can_create_account => 1;
-use constant extern_id_used => 0;
+use constant extern_id_used => 0;
sub new {
- my ($class, $login_type) = @_;
- my $self = fields::new($class);
- return $self;
+ my ($class, $login_type) = @_;
+ my $self = fields::new($class);
+ return $self;
}
sub can_change_password {
- return $_[0]->can('change_password');
+ return $_[0]->can('change_password');
}
sub create_or_update_user {
- my ($self, $params) = @_;
- my $dbh = Bugzilla->dbh;
-
- my $extern_id = $params->{extern_id};
- my $username = $params->{bz_username} || $params->{username};
- my $password = $params->{password} || '*';
- my $real_name = $params->{realname} || '';
- my $user_id = $params->{user_id};
-
- # A passed-in user_id always overrides anything else, for determining
- # what account we should return.
- if (!$user_id) {
- my $username_user_id = login_to_id($username || '');
- my $extern_user_id;
- if ($extern_id) {
- trick_taint($extern_id);
- $extern_user_id = $dbh->selectrow_array('SELECT userid
- FROM profiles WHERE extern_id = ?', undef, $extern_id);
- }
-
- # If we have both a valid extern_id and a valid username, and they are
- # not the same id, then we have a conflict.
- if ($username_user_id && $extern_user_id
- && $username_user_id ne $extern_user_id)
- {
- my $extern_name = Bugzilla::User->new($extern_user_id)->login;
- return { failure => AUTH_ERROR, error => "extern_id_conflict",
- details => {extern_id => $extern_id,
- extern_user => $extern_name,
- username => $username} };
- }
-
- # If we have a valid username, but no valid id,
- # then we have to create the user. This happens when we're
- # passed only a username, and that username doesn't exist already.
- if ($username && !$username_user_id && !$extern_user_id) {
- validate_email_syntax($username)
- || return { failure => AUTH_ERROR,
- error => 'auth_invalid_email',
- details => {addr => $username} };
- # external authentication
- # systems might follow different standards than ours. So in this
- # place here, we call trick_taint without checks.
- trick_taint($password);
-
- # XXX Theoretically this could fail with an error, but the fix for
- # that is too involved to be done right now.
- my $user = Bugzilla::User->create({
- login_name => $username,
- cryptpassword => $password,
- realname => $real_name});
- $username_user_id = $user->id;
- }
-
- # If we have a valid username id and an extern_id, but no valid
- # extern_user_id, then we have to set the user's extern_id.
- if ($extern_id && $username_user_id && !$extern_user_id) {
- $dbh->do('UPDATE profiles SET extern_id = ? WHERE userid = ?',
- undef, $extern_id, $username_user_id);
- Bugzilla->memcached->clear({ table => 'profiles', id => $username_user_id });
- }
-
- # Finally, at this point, one of these will give us a valid user id.
- $user_id = $extern_user_id || $username_user_id;
+ my ($self, $params) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ my $extern_id = $params->{extern_id};
+ my $username = $params->{bz_username} || $params->{username};
+ my $password = $params->{password} || '*';
+ my $real_name = $params->{realname} || '';
+ my $user_id = $params->{user_id};
+
+ # A passed-in user_id always overrides anything else, for determining
+ # what account we should return.
+ if (!$user_id) {
+ my $username_user_id = login_to_id($username || '');
+ my $extern_user_id;
+ if ($extern_id) {
+ trick_taint($extern_id);
+ $extern_user_id = $dbh->selectrow_array(
+ 'SELECT userid
+ FROM profiles WHERE extern_id = ?', undef, $extern_id
+ );
}
- # If we still don't have a valid user_id, then we weren't passed
- # enough information in $params, and we should die right here.
- ThrowCodeError('bad_arg', {argument => 'params', function =>
- 'Bugzilla::Auth::Verify::create_or_update_user'})
- unless $user_id;
-
- my $user = new Bugzilla::User({ id => $user_id, cache => 1 });
-
- # Now that we have a valid User, we need to see if any data has to be
- # updated.
- my $user_updated = 0;
- if ($username && lc($user->login) ne lc($username)) {
- validate_email_syntax($username)
- || return { failure => AUTH_ERROR, error => 'auth_invalid_email',
- details => {addr => $username} };
- $user->set_login($username);
- $user_updated = 1;
+ # If we have both a valid extern_id and a valid username, and they are
+ # not the same id, then we have a conflict.
+ if ( $username_user_id
+ && $extern_user_id
+ && $username_user_id ne $extern_user_id)
+ {
+ my $extern_name = Bugzilla::User->new($extern_user_id)->login;
+ return {
+ failure => AUTH_ERROR,
+ error => "extern_id_conflict",
+ details =>
+ {extern_id => $extern_id, extern_user => $extern_name, username => $username}
+ };
}
- if ($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);
- $user_updated = 1;
+
+ # If we have a valid username, but no valid id,
+ # then we have to create the user. This happens when we're
+ # passed only a username, and that username doesn't exist already.
+ if ($username && !$username_user_id && !$extern_user_id) {
+ validate_email_syntax($username) || return {
+ failure => AUTH_ERROR,
+ error => 'auth_invalid_email',
+ details => {addr => $username}
+ };
+
+ # external authentication
+ # systems might follow different standards than ours. So in this
+ # place here, we call trick_taint without checks.
+ trick_taint($password);
+
+ # XXX Theoretically this could fail with an error, but the fix for
+ # that is too involved to be done right now.
+ my $user
+ = Bugzilla::User->create({
+ login_name => $username, cryptpassword => $password, realname => $real_name
+ });
+ $username_user_id = $user->id;
+ }
+
+ # If we have a valid username id and an extern_id, but no valid
+ # extern_user_id, then we have to set the user's extern_id.
+ if ($extern_id && $username_user_id && !$extern_user_id) {
+ $dbh->do('UPDATE profiles SET extern_id = ? WHERE userid = ?',
+ undef, $extern_id, $username_user_id);
+ Bugzilla->memcached->clear({table => 'profiles', id => $username_user_id});
}
- $user->update() if $user_updated;
- return { user => $user };
+ # Finally, at this point, one of these will give us a valid user id.
+ $user_id = $extern_user_id || $username_user_id;
+ }
+
+ # If we still don't have a valid user_id, then we weren't passed
+ # enough information in $params, and we should die right here.
+ ThrowCodeError(
+ 'bad_arg',
+ {
+ argument => 'params',
+ function => 'Bugzilla::Auth::Verify::create_or_update_user'
+ }
+ ) unless $user_id;
+
+ my $user = new Bugzilla::User({id => $user_id, cache => 1});
+
+ # Now that we have a valid User, we need to see if any data has to be
+ # updated.
+ my $user_updated = 0;
+ if ($username && lc($user->login) ne lc($username)) {
+ validate_email_syntax($username) || return {
+ failure => AUTH_ERROR,
+ error => 'auth_invalid_email',
+ details => {addr => $username}
+ };
+ $user->set_login($username);
+ $user_updated = 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);
+ $user_updated = 1;
+ }
+ $user->update() if $user_updated;
+
+ return {user => $user};
}
1;
diff --git a/Bugzilla/Auth/Verify/DB.pm b/Bugzilla/Auth/Verify/DB.pm
index e46d1cd82..9251fa893 100644
--- a/Bugzilla/Auth/Verify/DB.pm
+++ b/Bugzilla/Auth/Verify/DB.pm
@@ -19,97 +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};
- return { failure => AUTH_NODATA } unless defined $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 ($user->account_is_locked_out) {
+ return {failure => AUTH_LOCKOUT, user => $user};
+ }
- if ($entered_password_crypted ne $real_password_crypted) {
- # Record the login failure
- $user->note_login_failure();
+ my $password = $login_data->{password};
+ return {failure => AUTH_NODATA} unless defined $login_data->{password};
+ my $real_password_crypted = $user->cryptpassword;
- # Immediately check if we are locked out
- if ($user->account_is_locked_out) {
- return { failure => AUTH_LOCKOUT, user => $user,
- just_locked_out => 1 };
- }
+ # Using the internal crypted password as the salt,
+ # crypt the password the user entered.
+ my $entered_password_crypted = bz_crypt($password, $real_password_crypted);
- return { failure => AUTH_LOGINFAILED,
- failure_count => scalar(@{ $user->account_ip_login_failures }),
- };
- }
+ if ($entered_password_crypted ne $real_password_crypted) {
- # 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 $pwqc = Bugzilla->passwdqc;
- unless ($pwqc->validate_password($password)) {
- my $reason = $pwqc->reason;
- Bugzilla->audit(sprintf "%s logged in with a weak password (reason: %s)", $user->login, $reason);
- $user->set_password_change_required(1);
- $user->set_password_change_reason(
- "You must change your password for the following reason: $reason"
- );
- $user->update();
- }
- }
+ # Record the login failure
+ $user->note_login_failure();
- # 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();
-
- # 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;
- if ($real_password_crypted !~ /{\Q$current_algorithm\E}$/) {
- # 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();
+ # Immediately check if we are locked out
+ if ($user->account_is_locked_out) {
+ return {failure => AUTH_LOCKOUT, user => $user, just_locked_out => 1};
}
- if (i_am_webservice() && $user->settings->{api_key_only}->{value} eq 'on') {
- # api-key verification happens in Auth/Login/APIKey
- # token verification happens in Auth/Login/Cookie
- # if we get here from an api call then we must be using user/pass
- return {
- failure => AUTH_ERROR,
- user_error => 'invalid_auth_method',
- };
+ 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 $pwqc = Bugzilla->passwdqc;
+ unless ($pwqc->validate_password($password)) {
+ my $reason = $pwqc->reason;
+ Bugzilla->audit(sprintf "%s logged in with a weak password (reason: %s)",
+ $user->login, $reason);
+ $user->set_password_change_required(1);
+ $user->set_password_change_reason(
+ "You must change your password for the following reason: $reason");
+ $user->update();
}
-
- return $login_data;
+ }
+
+ # 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();
+
+ # 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;
+ if ($real_password_crypted !~ /{\Q$current_algorithm\E}$/) {
+
+ # 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 (i_am_webservice() && $user->settings->{api_key_only}->{value} eq 'on') {
+
+ # api-key verification happens in Auth/Login/APIKey
+ # token verification happens in Auth/Login/Cookie
+ # if we get here from an api call then we must be using user/pass
+ return {failure => AUTH_ERROR, user_error => 'invalid_auth_method',};
+ }
+
+ return $login_data;
}
sub change_password {
- my ($self, $user, $password) = @_;
- my $dbh = Bugzilla->dbh;
- my $cryptpassword = bz_crypt($password);
- $dbh->do("UPDATE profiles SET cryptpassword = ? WHERE userid = ?",
- undef, $cryptpassword, $user->id);
- Bugzilla->memcached->clear({ table => 'profiles', id => $user->id });
+ my ($self, $user, $password) = @_;
+ my $dbh = Bugzilla->dbh;
+ my $cryptpassword = bz_crypt($password);
+ $dbh->do("UPDATE profiles SET cryptpassword = ? WHERE userid = ?",
+ undef, $cryptpassword, $user->id);
+ Bugzilla->memcached->clear({table => 'profiles', id => $user->id});
}
1;
diff --git a/Bugzilla/Auth/Verify/LDAP.pm b/Bugzilla/Auth/Verify/LDAP.pm
index de88f9a43..e9aca17e8 100644
--- a/Bugzilla/Auth/Verify/LDAP.pm
+++ b/Bugzilla/Auth/Verify/LDAP.pm
@@ -13,7 +13,7 @@ use warnings;
use base qw(Bugzilla::Auth::Verify);
use fields qw(
- ldap
+ ldap
);
use Bugzilla::Constants;
@@ -28,126 +28,139 @@ use constant admin_can_create_account => 0;
use constant user_can_create_account => 0;
sub check_credentials {
- my ($self, $params) = @_;
- my $dbh = Bugzilla->dbh;
-
- # We need to bind anonymously to the LDAP server. This is
- # because we need to get the Distinguished Name of the user trying
- # to log in. Some servers (such as iPlanet) allow you to have unique
- # uids spread out over a subtree of an area (such as "People"), so
- # just appending the Base DN to the uid isn't sufficient to get the
- # user's DN. For servers which don't work this way, there will still
- # be no harm done.
+ my ($self, $params) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ # We need to bind anonymously to the LDAP server. This is
+ # because we need to get the Distinguished Name of the user trying
+ # to log in. Some servers (such as iPlanet) allow you to have unique
+ # uids spread out over a subtree of an area (such as "People"), so
+ # just appending the Base DN to the uid isn't sufficient to get the
+ # user's DN. For servers which don't work this way, there will still
+ # be no harm done.
+ $self->_bind_ldap_for_search();
+
+ # Now, we verify that the user exists, and get a LDAP Distinguished
+ # Name for the user.
+ my $username = $params->{username};
+ my $dn_result
+ = $self->ldap->search(_bz_search_params($username), attrs => ['dn']);
+ return {
+ failure => AUTH_ERROR,
+ error => "ldap_search_error",
+ details => {errstr => $dn_result->error, username => $username}
+ }
+ if $dn_result->code;
+
+ return {failure => AUTH_NO_SUCH_USER} if !$dn_result->count;
+
+ my $dn = $dn_result->shift_entry->dn;
+
+ # Check the password.
+ my $pw_result = $self->ldap->bind($dn, password => $params->{password});
+ return {failure => AUTH_LOGINFAILED} if $pw_result->code;
+
+ # And now we fill in the user's details.
+
+ # First try the search as the (already bound) user in question.
+ my $user_entry;
+ my $error_string;
+ my $detail_result = $self->ldap->search(_bz_search_params($username));
+ if ($detail_result->code) {
+
+ # Stash away the original error, just in case
+ $error_string = $detail_result->error;
+ }
+ else {
+ $user_entry = $detail_result->shift_entry;
+ }
+
+ # If that failed (either because the search failed, or returned no
+ # results) then try re-binding as the initial search user, but only
+ # if the LDAPbinddn parameter is set.
+ if (!$user_entry && Bugzilla->params->{"LDAPbinddn"}) {
$self->_bind_ldap_for_search();
- # Now, we verify that the user exists, and get a LDAP Distinguished
- # Name for the user.
- my $username = $params->{username};
- my $dn_result = $self->ldap->search(_bz_search_params($username),
- attrs => ['dn']);
- return { failure => AUTH_ERROR, error => "ldap_search_error",
- details => {errstr => $dn_result->error, username => $username}
- } if $dn_result->code;
-
- return { failure => AUTH_NO_SUCH_USER } if !$dn_result->count;
-
- my $dn = $dn_result->shift_entry->dn;
-
- # Check the password.
- my $pw_result = $self->ldap->bind($dn, password => $params->{password});
- return { failure => AUTH_LOGINFAILED } if $pw_result->code;
-
- # And now we fill in the user's details.
-
- # First try the search as the (already bound) user in question.
- my $user_entry;
- my $error_string;
- my $detail_result = $self->ldap->search(_bz_search_params($username));
- if ($detail_result->code) {
- # Stash away the original error, just in case
- $error_string = $detail_result->error;
- } else {
- $user_entry = $detail_result->shift_entry;
+ $detail_result = $self->ldap->search(_bz_search_params($username));
+ if (!$detail_result->code) {
+ $user_entry = $detail_result->shift_entry;
}
+ }
- # If that failed (either because the search failed, or returned no
- # results) then try re-binding as the initial search user, but only
- # if the LDAPbinddn parameter is set.
- if (!$user_entry && Bugzilla->params->{"LDAPbinddn"}) {
- $self->_bind_ldap_for_search();
-
- $detail_result = $self->ldap->search(_bz_search_params($username));
- if (!$detail_result->code) {
- $user_entry = $detail_result->shift_entry;
- }
+ # If we *still* don't have anything in $user_entry then give up.
+ return {
+ failure => AUTH_ERROR,
+ error => "ldap_search_error",
+ details => {errstr => $error_string, username => $username}
}
+ if !$user_entry;
- # If we *still* don't have anything in $user_entry then give up.
- return { failure => AUTH_ERROR, error => "ldap_search_error",
- details => {errstr => $error_string, username => $username}
- } if !$user_entry;
+ my $mail_attr = Bugzilla->params->{"LDAPmailattribute"};
+ if ($mail_attr) {
+ if (!$user_entry->exists($mail_attr)) {
+ return {
+ failure => AUTH_ERROR,
+ error => "ldap_cannot_retrieve_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_retrieve_attr",
- details => {attr => $mail_attr} };
- }
+ my @emails = $user_entry->get_value($mail_attr);
- my @emails = $user_entry->get_value($mail_attr);
+ # Default to the first email address returned.
+ $params->{bz_username} = $emails[0];
- # Default to the first email address returned.
- $params->{bz_username} = $emails[0];
+ if (@emails > 1) {
- if (@emails > 1) {
- # Cycle through the adresses and check if they're Bugzilla logins.
- # Use the first one that returns a valid id.
- foreach my $email (@emails) {
- if ( login_to_id($email) ) {
- $params->{bz_username} = $email;
- last;
- }
- }
+ # Cycle through the adresses and check if they're Bugzilla logins.
+ # Use the first one that returns a valid id.
+ foreach my $email (@emails) {
+ if (login_to_id($email)) {
+ $params->{bz_username} = $email;
+ last;
}
-
- } else {
- $params->{bz_username} = $username;
+ }
}
- $params->{realname} ||= $user_entry->get_value("displayName");
- $params->{realname} ||= $user_entry->get_value("cn");
+ }
+ else {
+ $params->{bz_username} = $username;
+ }
+
+ $params->{realname} ||= $user_entry->get_value("displayName");
+ $params->{realname} ||= $user_entry->get_value("cn");
- $params->{extern_id} = $username;
+ $params->{extern_id} = $username;
- return $params;
+ return $params;
}
sub _bz_search_params {
- my ($username) = @_;
- $username = escape_filter_value($username);
- return (base => Bugzilla->params->{"LDAPBaseDN"},
- scope => "sub",
- filter => '(&(' . Bugzilla->params->{"LDAPuidattribute"}
- . "=$username)"
- . Bugzilla->params->{"LDAPfilter"} . ')');
+ my ($username) = @_;
+ $username = escape_filter_value($username);
+ return (
+ base => Bugzilla->params->{"LDAPBaseDN"},
+ scope => "sub",
+ filter => '(&('
+ . Bugzilla->params->{"LDAPuidattribute"}
+ . "=$username)"
+ . Bugzilla->params->{"LDAPfilter"} . ')'
+ );
}
sub _bind_ldap_for_search {
- my ($self) = @_;
- my $bind_result;
- if (Bugzilla->params->{"LDAPbinddn"}) {
- my ($LDAPbinddn,$LDAPbindpass) =
- split(":",Bugzilla->params->{"LDAPbinddn"});
- $bind_result =
- $self->ldap->bind($LDAPbinddn, password => $LDAPbindpass);
- }
- else {
- $bind_result = $self->ldap->bind();
- }
- ThrowCodeError("ldap_bind_failed", {errstr => $bind_result->error})
- if $bind_result->code;
+ my ($self) = @_;
+ my $bind_result;
+ if (Bugzilla->params->{"LDAPbinddn"}) {
+ my ($LDAPbinddn, $LDAPbindpass) = split(":", Bugzilla->params->{"LDAPbinddn"});
+ $bind_result = $self->ldap->bind($LDAPbinddn, password => $LDAPbindpass);
+ }
+ else {
+ $bind_result = $self->ldap->bind();
+ }
+ ThrowCodeError("ldap_bind_failed", {errstr => $bind_result->error})
+ if $bind_result->code;
}
# We can't just do this in new(), because we're not allowed to throw any
@@ -156,27 +169,27 @@ sub _bind_ldap_for_search {
# to fix his mistake. (Because Bugzilla->login always calls
# Bugzilla::Auth->new, and almost every page calls Bugzilla->login.)
sub ldap {
- my ($self) = @_;
- return $self->{ldap} if $self->{ldap};
-
- my @servers = split(/[\s,]+/, Bugzilla->params->{"LDAPserver"});
- ThrowCodeError("ldap_server_not_defined") unless @servers;
-
- foreach (@servers) {
- $self->{ldap} = new Net::LDAP(trim($_));
- last if $self->{ldap};
- }
- ThrowCodeError("ldap_connect_failed", { server => join(", ", @servers) })
- unless $self->{ldap};
-
- # try to start TLS if needed
- if (Bugzilla->params->{"LDAPstarttls"}) {
- my $mesg = $self->{ldap}->start_tls();
- ThrowCodeError("ldap_start_tls_failed", { error => $mesg->error() })
- if $mesg->code();
- }
-
- return $self->{ldap};
+ my ($self) = @_;
+ return $self->{ldap} if $self->{ldap};
+
+ my @servers = split(/[\s,]+/, Bugzilla->params->{"LDAPserver"});
+ ThrowCodeError("ldap_server_not_defined") unless @servers;
+
+ foreach (@servers) {
+ $self->{ldap} = new Net::LDAP(trim($_));
+ last if $self->{ldap};
+ }
+ ThrowCodeError("ldap_connect_failed", {server => join(", ", @servers)})
+ unless $self->{ldap};
+
+ # try to start TLS if needed
+ if (Bugzilla->params->{"LDAPstarttls"}) {
+ my $mesg = $self->{ldap}->start_tls();
+ ThrowCodeError("ldap_start_tls_failed", {error => $mesg->error()})
+ if $mesg->code();
+ }
+
+ return $self->{ldap};
}
1;
diff --git a/Bugzilla/Auth/Verify/RADIUS.pm b/Bugzilla/Auth/Verify/RADIUS.pm
index ad0778e39..a2a54b944 100644
--- a/Bugzilla/Auth/Verify/RADIUS.pm
+++ b/Bugzilla/Auth/Verify/RADIUS.pm
@@ -23,33 +23,37 @@ use constant admin_can_create_account => 0;
use constant user_can_create_account => 0;
sub check_credentials {
- my ($self, $params) = @_;
- my $dbh = Bugzilla->dbh;
- my $address_suffix = Bugzilla->params->{'RADIUS_email_suffix'};
- my $username = $params->{username};
-
- # If we're using RADIUS_email_suffix, we may need to cut it off from
- # the login name.
- if ($address_suffix) {
- $username =~ s/\Q$address_suffix\E$//i;
- }
-
- # Create RADIUS object.
- my $radius =
- new Authen::Radius(Host => Bugzilla->params->{'RADIUS_server'},
- Secret => Bugzilla->params->{'RADIUS_secret'})
- || return { failure => AUTH_ERROR, error => 'radius_preparation_error',
- details => {errstr => Authen::Radius::strerror() } };
-
- # Check the password.
- $radius->check_pwd($username, $params->{password},
- Bugzilla->params->{'RADIUS_NAS_IP'} || undef)
- || return { failure => AUTH_LOGINFAILED };
-
- # Build the user account's e-mail address.
- $params->{bz_username} = $username . $address_suffix;
-
- return $params;
+ my ($self, $params) = @_;
+ my $dbh = Bugzilla->dbh;
+ my $address_suffix = Bugzilla->params->{'RADIUS_email_suffix'};
+ my $username = $params->{username};
+
+ # If we're using RADIUS_email_suffix, we may need to cut it off from
+ # the login name.
+ if ($address_suffix) {
+ $username =~ s/\Q$address_suffix\E$//i;
+ }
+
+ # Create RADIUS object.
+ my $radius = new Authen::Radius(
+ Host => Bugzilla->params->{'RADIUS_server'},
+ Secret => Bugzilla->params->{'RADIUS_secret'}
+ )
+ || return {
+ failure => AUTH_ERROR,
+ error => 'radius_preparation_error',
+ details => {errstr => Authen::Radius::strerror()}
+ };
+
+ # Check the password.
+ $radius->check_pwd($username, $params->{password},
+ Bugzilla->params->{'RADIUS_NAS_IP'} || undef)
+ || return {failure => AUTH_LOGINFAILED};
+
+ # Build the user account's e-mail address.
+ $params->{bz_username} = $username . $address_suffix;
+
+ return $params;
}
1;
diff --git a/Bugzilla/Auth/Verify/Stack.pm b/Bugzilla/Auth/Verify/Stack.pm
index 3e5db3cec..9a9412915 100644
--- a/Bugzilla/Auth/Verify/Stack.pm
+++ b/Bugzilla/Auth/Verify/Stack.pm
@@ -13,8 +13,8 @@ use warnings;
use base qw(Bugzilla::Auth::Verify);
use fields qw(
- _stack
- successful
+ _stack
+ successful
);
use Bugzilla::Hook;
@@ -23,70 +23,75 @@ use Hash::Util qw(lock_keys);
use List::MoreUtils qw(any);
sub new {
- my $class = shift;
- my $list = shift;
- my $self = $class->SUPER::new(@_);
- my %methods = map { $_ => "Bugzilla/Auth/Verify/$_.pm" } split(',', $list);
- lock_keys(%methods);
- Bugzilla::Hook::process('auth_verify_methods', { modules => \%methods });
-
- $self->{_stack} = [];
- foreach my $verify_method (split(',', $list)) {
- my $module = $methods{$verify_method};
- require $module;
- $module =~ s|/|::|g;
- $module =~ s/.pm$//;
- push(@{$self->{_stack}}, $module->new(@_));
- }
- return $self;
+ my $class = shift;
+ my $list = shift;
+ my $self = $class->SUPER::new(@_);
+ my %methods = map { $_ => "Bugzilla/Auth/Verify/$_.pm" } split(',', $list);
+ lock_keys(%methods);
+ Bugzilla::Hook::process('auth_verify_methods', {modules => \%methods});
+
+ $self->{_stack} = [];
+ foreach my $verify_method (split(',', $list)) {
+ my $module = $methods{$verify_method};
+ require $module;
+ $module =~ s|/|::|g;
+ $module =~ s/.pm$//;
+ push(@{$self->{_stack}}, $module->new(@_));
+ }
+ return $self;
}
sub can_change_password {
- my ($self) = @_;
- # We return true if any method can change passwords.
- foreach my $object (@{$self->{_stack}}) {
- return 1 if $object->can_change_password;
- }
- return 0;
+ my ($self) = @_;
+
+ # We return true if any method can change passwords.
+ foreach my $object (@{$self->{_stack}}) {
+ return 1 if $object->can_change_password;
+ }
+ return 0;
}
sub check_credentials {
- my $self = shift;
- my $result;
- foreach my $object (@{$self->{_stack}}) {
- $result = $object->check_credentials(@_);
- $self->{successful} = $object;
- last if !$result->{failure};
- # So that if none of them succeed, it's undef.
- $self->{successful} = undef;
- }
- # Returns the result at the bottom of the stack if they all fail.
- return $result;
+ my $self = shift;
+ my $result;
+ foreach my $object (@{$self->{_stack}}) {
+ $result = $object->check_credentials(@_);
+ $self->{successful} = $object;
+ last if !$result->{failure};
+
+ # So that if none of them succeed, it's undef.
+ $self->{successful} = undef;
+ }
+
+ # Returns the result at the bottom of the stack if they all fail.
+ return $result;
}
sub create_or_update_user {
- my $self = shift;
- my $result;
- foreach my $object (@{$self->{_stack}}) {
- $result = $object->create_or_update_user(@_);
- last if !$result->{failure};
- }
- # Returns the result at the bottom of the stack if they all fail.
- return $result;
+ my $self = shift;
+ my $result;
+ foreach my $object (@{$self->{_stack}}) {
+ $result = $object->create_or_update_user(@_);
+ last if !$result->{failure};
+ }
+
+ # Returns the result at the bottom of the stack if they all fail.
+ return $result;
}
sub user_can_create_account {
- my ($self) = @_;
- # We return true if any method allows the user to create an account.
- foreach my $object (@{$self->{_stack}}) {
- return 1 if $object->user_can_create_account;
- }
- return 0;
+ my ($self) = @_;
+
+ # We return true if any method allows the user to create an account.
+ foreach my $object (@{$self->{_stack}}) {
+ return 1 if $object->user_can_create_account;
+ }
+ return 0;
}
sub extern_id_used {
- my ($self) = @_;
- return any { $_->extern_id_used } @{ $self->{_stack} };
+ my ($self) = @_;
+ return any { $_->extern_id_used } @{$self->{_stack}};
}
1;
diff --git a/Bugzilla/Bloomfilter.pm b/Bugzilla/Bloomfilter.pm
index ba1d6d6c3..fb8bcc9ac 100644
--- a/Bugzilla/Bloomfilter.pm
+++ b/Bugzilla/Bloomfilter.pm
@@ -17,45 +17,45 @@ use File::Slurper qw(write_binary read_binary read_lines);
use File::Spec::Functions qw(catfile);
sub _new_bloom_filter {
- my ($n) = @_;
- my $p = 0.01;
- my $m = $n * abs(log $p) / log(2) ** 2;
- my $k = $m / $n * log(2);
- return Algorithm::BloomFilter->new($m, $k);
+ my ($n) = @_;
+ my $p = 0.01;
+ my $m = $n * abs(log $p) / log(2)**2;
+ my $k = $m / $n * log(2);
+ return Algorithm::BloomFilter->new($m, $k);
}
sub _filename {
- my ($name, $type) = @_;
+ my ($name, $type) = @_;
- my $datadir = bz_locations->{datadir};
+ my $datadir = bz_locations->{datadir};
- return catfile($datadir, "$name.$type");
+ return catfile($datadir, "$name.$type");
}
sub populate {
- my ($class, $name) = @_;
- my $memcached = Bugzilla->memcached;
- my @items = read_lines(_filename($name, 'list'));
- my $filter = _new_bloom_filter(@items + 0);
-
- $filter->add($_) foreach @items;
- write_binary(_filename($name, 'bloom'), $filter->serialize);
- $memcached->clear_bloomfilter({name => $name});
+ my ($class, $name) = @_;
+ my $memcached = Bugzilla->memcached;
+ my @items = read_lines(_filename($name, 'list'));
+ my $filter = _new_bloom_filter(@items + 0);
+
+ $filter->add($_) foreach @items;
+ write_binary(_filename($name, 'bloom'), $filter->serialize);
+ $memcached->clear_bloomfilter({name => $name});
}
sub lookup {
- my ($class, $name) = @_;
- my $memcached = Bugzilla->memcached;
- my $filename = _filename($name, 'bloom');
- my $filter_data = $memcached->get_bloomfilter( { name => $name } );
-
- if (!$filter_data && -f $filename) {
- $filter_data = read_binary($filename);
- $memcached->set_bloomfilter({ name => $name, filter => $filter_data });
- }
-
- return Algorithm::BloomFilter->deserialize($filter_data) if $filter_data;
- return undef;
+ my ($class, $name) = @_;
+ my $memcached = Bugzilla->memcached;
+ my $filename = _filename($name, 'bloom');
+ my $filter_data = $memcached->get_bloomfilter({name => $name});
+
+ if (!$filter_data && -f $filename) {
+ $filter_data = read_binary($filename);
+ $memcached->set_bloomfilter({name => $name, filter => $filter_data});
+ }
+
+ return Algorithm::BloomFilter->deserialize($filter_data) if $filter_data;
+ return undef;
}
1;
diff --git a/Bugzilla/Bug.pm b/Bugzilla/Bug.pm
index ee48ed7a2..5673ab6e3 100644
--- a/Bugzilla/Bug.pm
+++ b/Bugzilla/Bug.pm
@@ -41,9 +41,9 @@ use Role::Tiny::With;
use base qw(Bugzilla::Object Exporter);
@Bugzilla::Bug::EXPORT = qw(
- bug_alias_to_id
- LogActivityEntry
- editable_bug_fields
+ bug_alias_to_id
+ LogActivityEntry
+ editable_bug_fields
);
my %CLEANUP;
@@ -56,192 +56,196 @@ use constant DB_TABLE => 'bugs';
use constant ID_FIELD => 'bug_id';
use constant NAME_FIELD => 'alias';
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({skip_extensions => 1});
- my @custom_names = map {$_->name} @custom;
-
- my @columns = (qw(
- alias
- 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({skip_extensions => 1});
+ my @custom_names = map { $_->name } @custom;
+
+ my @columns = (
+ qw(
+ alias
+ 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,
- 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,
- 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_INTEGER) {
- $validator = \&_check_integer_field;
- }
- else {
- $validator = \&_check_default_field;
- }
- $validators->{$field->name} = $validator;
+ my $validators = {
+ alias => \&_check_alias,
+ assigned_to => \&_check_assigned_to,
+ 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,
+ 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_INTEGER) {
+ $validator = \&_check_integer_field;
}
+ else {
+ $validator = \&_check_default_field;
+ }
+ $validators->{$field->name} = $validator;
+ }
- return $validators;
-};
+ return $validators;
+}
sub VALIDATOR_DEPENDENCIES {
- my $cache = Bugzilla->request_cache;
- return $cache->{bug_validator_dependencies}
- if $cache->{bug_validator_dependencies};
-
- my %deps = (
- assigned_to => ['component'],
- bug_status => ['product', 'comment', 'target_milestone'],
- cc => ['component'],
- comment => ['creation_ts'],
- component => ['product'],
- dup_id => ['bug_status', 'resolution'],
- groups => ['product'],
- keywords => ['product'],
- resolution => ['bug_status'],
- qa_contact => ['component'],
- target_milestone => ['product'],
- version => ['product'],
- );
-
- foreach my $field (@{ Bugzilla->fields }) {
- $deps{$field->name} = [ $field->visibility_field->name ]
- if $field->{visibility_field_id};
- }
-
- $cache->{bug_validator_dependencies} = \%deps;
- return \%deps;
-};
+ my $cache = Bugzilla->request_cache;
+ return $cache->{bug_validator_dependencies}
+ if $cache->{bug_validator_dependencies};
+
+ my %deps = (
+ assigned_to => ['component'],
+ bug_status => ['product', 'comment', 'target_milestone'],
+ cc => ['component'],
+ comment => ['creation_ts'],
+ component => ['product'],
+ dup_id => ['bug_status', 'resolution'],
+ groups => ['product'],
+ keywords => ['product'],
+ resolution => ['bug_status'],
+ 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({skip_extensions => 1});
- my @custom_names = map {$_->name} @custom;
- my @columns = qw(
- alias
- 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({skip_extensions => 1});
+ my @custom_names = map { $_->name } @custom;
+ my @columns = qw(
+ alias
+ 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 }) },
- @{ Bugzilla->fields({ type => FIELD_TYPE_DATE }) });
- return map { $_->name } @fields;
+ my @fields = (
+ @{Bugzilla->fields({type => FIELD_TYPE_DATETIME})},
+ @{Bugzilla->fields({type => FIELD_TYPE_DATE})}
+ );
+ return map { $_->name } @fields;
}
# Used in LogActivityEntry(). Gives the max length of lines in the
@@ -253,32 +257,30 @@ 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',
- cc_accessible => 'cclist_accessible',
- 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',
- comment_count => 'longdescs.count',
- platform => 'rep_platform',
- severity => 'bug_severity',
- status => 'bug_status',
- summary => 'short_desc',
- url => 'bug_file_loc',
- whiteboard => 'status_whiteboard',
+ blocks => 'blocked',
+ cc_accessible => 'cclist_accessible',
+ 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',
+ comment_count => 'longdescs.count',
+ 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)
@@ -296,7 +298,8 @@ 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);
with 'Bugzilla::Elastic::Role::Object';
@@ -305,118 +308,97 @@ sub ES_TYPE {'bug'}
sub ES_INDEX { Bugzilla->params->{elasticsearch_index} }
sub ES_SETTINGS {
- return {
- number_of_shards => 2,
- analysis => {
- filter => {
- asciifolding_original => {
- type => "asciifolding",
- preserve_original => \1,
- },
- },
- analyzer => {
- autocomplete => {
- type => 'custom',
- tokenizer => 'keyword',
- filter => [ 'lowercase', 'asciifolding_original' ],
- },
- folding => {
- tokenizer => 'standard',
- filter => [ 'standard', 'lowercase', 'asciifolding_original' ],
- },
- bz_text_analyzer => {
- type => 'standard',
- filter => [ 'lowercase', 'stop' ],
- max_token_length => '20'
- },
- bz_equals_analyzer => {
- type => 'custom',
- filter => ['lowercase'],
- tokenizer => 'keyword',
- },
- whiteboard_words => {
- type => 'custom',
- tokenizer => 'whiteboard_words_pattern',
- filter => ['stop']
- },
- whiteboard_shingle_words => {
- type => 'custom',
- tokenizer => 'whiteboard_words_pattern',
- filter => [ 'stop', 'shingle', 'lowercase' ]
- },
- whiteboard_tokens => {
- type => 'custom',
- tokenizer => 'whiteboard_tokens_pattern',
- filter => [ 'stop', 'lowercase' ]
- },
- whiteboard_shingle_tokens => {
- type => 'custom',
- tokenizer => 'whiteboard_tokens_pattern',
- filter => [ 'stop', 'shingle', 'lowercase' ]
- }
- },
- tokenizer => {
- whiteboard_tokens_pattern => {
- type => 'pattern',
- pattern => '\\s*([,;]*\\[|\\][\\s\\[]*|[;,])\\s*'
- },
- whiteboard_words_pattern => {
- type => 'pattern',
- pattern => '[\\[\\];,\\s]+'
- },
- },
+ return {
+ number_of_shards => 2,
+ analysis => {
+ filter => {
+ asciifolding_original => {type => "asciifolding", preserve_original => \1,},
+ },
+ analyzer => {
+ autocomplete => {
+ type => 'custom',
+ tokenizer => 'keyword',
+ filter => ['lowercase', 'asciifolding_original'],
},
- };
+ folding => {
+ tokenizer => 'standard',
+ filter => ['standard', 'lowercase', 'asciifolding_original'],
+ },
+ bz_text_analyzer => {
+ type => 'standard',
+ filter => ['lowercase', 'stop'],
+ max_token_length => '20'
+ },
+ bz_equals_analyzer =>
+ {type => 'custom', filter => ['lowercase'], tokenizer => 'keyword',},
+ whiteboard_words => {
+ type => 'custom',
+ tokenizer => 'whiteboard_words_pattern',
+ filter => ['stop']
+ },
+ whiteboard_shingle_words => {
+ type => 'custom',
+ tokenizer => 'whiteboard_words_pattern',
+ filter => ['stop', 'shingle', 'lowercase']
+ },
+ whiteboard_tokens => {
+ type => 'custom',
+ tokenizer => 'whiteboard_tokens_pattern',
+ filter => ['stop', 'lowercase']
+ },
+ whiteboard_shingle_tokens => {
+ type => 'custom',
+ tokenizer => 'whiteboard_tokens_pattern',
+ filter => ['stop', 'shingle', 'lowercase']
+ }
+ },
+ tokenizer => {
+ whiteboard_tokens_pattern =>
+ {type => 'pattern', pattern => '\\s*([,;]*\\[|\\][\\s\\[]*|[;,])\\s*'},
+ whiteboard_words_pattern => {type => 'pattern', pattern => '[\\[\\];,\\s]+'},
+ },
+ },
+ };
}
sub _bz_field {
- my ($field, @fields) = @_;
-
- return (
- $field => {
- type => 'string',
- analyzer => 'bz_text_analyzer',
- fields => {
- eq => {
- type => 'string',
- analyzer => 'bz_equals_analyzer',
- },
- @fields,
- },
- },
- );
+ my ($field, @fields) = @_;
+
+ return (
+ $field => {
+ type => 'string',
+ analyzer => 'bz_text_analyzer',
+ fields =>
+ {eq => {type => 'string', analyzer => 'bz_equals_analyzer',}, @fields,},
+ },
+ );
}
sub ES_PROPERTIES {
- return {
- _bz_field('priority'),
- _bz_field('bug_severity'),
- _bz_field('bug_status'),
- _bz_field('resolution'),
- status_whiteboard => { type => 'string', analyzer => 'whiteboard_shingle_tokens' },
- delta_ts => { type => 'string', index => 'not_analyzed' },
- _bz_field('product'),
- _bz_field('component'),
- _bz_field('classification'),
- _bz_field('short_desc'),
- _bz_field('assigned_to'),
- _bz_field('reporter'),
- };
+ return {
+ _bz_field('priority'), _bz_field('bug_severity'), _bz_field('bug_status'),
+ _bz_field('resolution'),
+ status_whiteboard =>
+ {type => 'string', analyzer => 'whiteboard_shingle_tokens'},
+ delta_ts => {type => 'string', index => 'not_analyzed'},
+ _bz_field('product'), _bz_field('component'), _bz_field('classification'),
+ _bz_field('short_desc'), _bz_field('assigned_to'), _bz_field('reporter'),
+ };
}
-sub ES_OBJECTS_AT_ONCE { 4000 }
+sub ES_OBJECTS_AT_ONCE {4000}
sub ES_SELECT_UPDATED_SQL {
- my ($class, $mtime) = @_;
+ my ($class, $mtime) = @_;
- my @fields = (
- 'keywords', 'short_desc', 'product', 'component',
- 'cf_crash_signature', 'alias', 'status_whiteboard',
- 'bug_status', 'resolution', 'priority', 'assigned_to'
- );
- my $fields = join(', ', ("?") x @fields);
+ my @fields = (
+ 'keywords', 'short_desc', 'product', 'component',
+ 'cf_crash_signature', 'alias', 'status_whiteboard', 'bug_status',
+ 'resolution', 'priority', 'assigned_to'
+ );
+ my $fields = join(', ', ("?") x @fields);
- my $sql = qq{
+ my $sql = qq{
SELECT DISTINCT
bug_id
FROM
@@ -468,368 +450,378 @@ sub ES_SELECT_UPDATED_SQL {
AND field = 'name'
AND at_time > FROM_UNIXTIME(?)
};
- return ($sql, [$mtime, @fields, $mtime, $mtime, $mtime, $mtime]);
+ return ($sql, [$mtime, @fields, $mtime, $mtime, $mtime, $mtime]);
}
sub es_document {
- my ($self) = @_;
- return {
- bug_id => $self->id,
- product => $self->product_obj->name,
- alias => $self->alias,
- keywords => [ map { $_->name } @{$self->keyword_objects} ],
- priority => $self->priority,
- bug_status => $self->bug_status,
- resolution => $self->resolution,
- component => $self->component_obj->name,
- classification => $self->product_obj->classification->name,
- status_whiteboard => $self->status_whiteboard,
- short_desc => $self->short_desc,
- assigned_to => $self->assigned_to->login,
- reporter => $self->reporter->login,
- delta_ts => $self->delta_ts,
- bug_severity => $self->bug_severity,
- };
+ my ($self) = @_;
+ return {
+ bug_id => $self->id,
+ product => $self->product_obj->name,
+ alias => $self->alias,
+ keywords => [map { $_->name } @{$self->keyword_objects}],
+ priority => $self->priority,
+ bug_status => $self->bug_status,
+ resolution => $self->resolution,
+ component => $self->component_obj->name,
+ classification => $self->product_obj->classification->name,
+ status_whiteboard => $self->status_whiteboard,
+ short_desc => $self->short_desc,
+ assigned_to => $self->assigned_to->login,
+ reporter => $self->reporter->login,
+ delta_ts => $self->delta_ts,
+ bug_severity => $self->bug_severity,
+ };
}
#####################################################################
sub new {
- my $invocant = shift;
- my $class = ref($invocant) || $invocant;
- my $param = shift;
-
- # Remove leading "#" mark if we've just been passed an id.
- if (!ref $param && $param =~ /^#(\d+)$/) {
- $param = $1;
+ my $invocant = shift;
+ my $class = ref($invocant) || $invocant;
+ my $param = shift;
+
+ # Remove leading "#" mark if we've just been passed an id.
+ if (!ref $param && $param =~ /^#(\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 || $param =~ /\D/))
+ || (ref($param) && (!$param->{id} || $param->{id} =~ /\D/)))
+ {
+ # But only if aliases are enabled.
+ if (Bugzilla->params->{'usebugaliases'} && $param) {
+ $param = {
+ name => ref($param) ? $param->{id} : $param,
+ cache => ref($param) ? $param->{cache} : 0
+ };
}
-
- # If we get something that looks like a word (not a number),
- # make it the "name" param.
- if (!defined $param
- || (!ref($param) && (!$param || $param =~ /\D/))
- || (ref($param) && (!$param->{id} || $param->{id} =~ /\D/)))
- {
- # But only if aliases are enabled.
- if (Bugzilla->params->{'usebugaliases'} && $param) {
- $param = { name => ref($param) ? $param->{id} : $param,
- cache => ref($param) ? $param->{cache} : 0 };
- }
- else {
- # Aliases are off, and 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;
- }
+ else {
+ # Aliases are off, and 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';
}
-
- 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;
+ 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;
}
sub CLEANUP {
- foreach my $bug (values %CLEANUP) {
- next unless $bug;
- delete $bug->{depends_on_obj};
- delete $bug->{blocks_obj};
- }
- %CLEANUP = ();
+ foreach my $bug (values %CLEANUP) {
+ 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;
+ 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});
}
- return $self;
+ }
+
+ unless ($field && $field =~ /^(dependson|blocked|dup_id)$/) {
+ $self->check_is_visible;
+ }
+ return $self;
}
sub check_is_visible {
- my $self = shift;
- 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 ($user->id) {
- ThrowUserError("bug_access_denied", { bug_id => $self->id });
- } else {
- ThrowUserError("bug_access_query", { bug_id => $self->id });
- }
- }
-}
+ my $self = shift;
+ 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 ($user->id) {
+ ThrowUserError("bug_access_denied", {bug_id => $self->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 => $self->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 });
- }
- @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 lists, during get_bug_link in Bugzilla::Template.
- $user->visible_bugs(\@all_dep_ids);
-
- # We preload comments here in order to allow us to compare the time it
- # takes to load comments from the database with the template rendering
- # time.
- foreach my $bug (@$bugs) {
- $bug->comments();
- }
-
- foreach my $bug (@$bugs) {
- $bug->_preload_referenced_bugs();
- }
+ 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});
+ }
+ @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 lists, during get_bug_link in Bugzilla::Template.
+ $user->visible_bugs(\@all_dep_ids);
+
+ # We preload comments here in order to allow us to compare the time it
+ # takes to load comments from the database with the template rendering
+ # time.
+ foreach my $bug (@$bugs) {
+ $bug->comments();
+ }
+
+ foreach my $bug (@$bugs) {
+ $bug->_preload_referenced_bugs();
+ }
}
# 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 @referenced_bug_ids;
-
- # 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
- require Bugzilla::Template;
- foreach my $comment (@{ $self->comments }) {
- if ($comment->type == CMT_HAS_DUPE || $comment->type == CMT_DUPE_OF) {
- # duplicate bugs that aren't currently in $self->duplicates
- push @referenced_bug_ids, $comment->extra_data
- unless Bugzilla::Bug->object_cache_get($comment->extra_data);
- }
- else {
- # bugs referenced in comments
- Bugzilla::Template::quoteUrls($comment->body, undef, undef, undef,
- sub {
- my $bug_id = $_[0];
- push @referenced_bug_ids, $bug_id
- unless Bugzilla::Bug->object_cache_get($bug_id);
- });
+ my $self = shift;
+ my @referenced_bug_ids;
+
+ # 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
+ require Bugzilla::Template;
+ foreach my $comment (@{$self->comments}) {
+ if ($comment->type == CMT_HAS_DUPE || $comment->type == CMT_DUPE_OF) {
+
+ # duplicate bugs that aren't currently in $self->duplicates
+ push @referenced_bug_ids, $comment->extra_data
+ unless Bugzilla::Bug->object_cache_get($comment->extra_data);
+ }
+ else {
+ # bugs referenced in comments
+ Bugzilla::Template::quoteUrls(
+ $comment->body,
+ undef, undef, undef,
+ sub {
+ my $bug_id = $_[0];
+ push @referenced_bug_ids, $bug_id
+ unless Bugzilla::Bug->object_cache_get($bug_id);
}
+ );
}
+ }
- # inject into object-cache
- my $referenced_bugs = Bugzilla::Bug->new_from_list(
- [ uniq @referenced_bug_ids ]);
- foreach my $bug (@$referenced_bugs) {
- $bug->object_cache_set();
- }
+ # inject into object-cache
+ my $referenced_bugs = Bugzilla::Bug->new_from_list([uniq @referenced_bug_ids]);
+ foreach my $bug (@$referenced_bugs) {
+ $bug->object_cache_set();
+ }
- # preload bug visibility
- Bugzilla->user->visible_bugs(\@referenced_bug_ids);
+ # preload bug visibility
+ Bugzilla->user->visible_bugs(\@referenced_bug_ids);
}
sub possible_duplicates {
- my ($class, $params) = @_;
- my $short_desc = $params->{summary};
- my $products = $params->{products} || [];
- my $limit = $params->{limit} || MAX_POSSIBLE_DUPLICATES;
- $limit = MAX_POSSIBLE_DUPLICATES if $limit > MAX_POSSIBLE_DUPLICATES;
- $products = [$products] if !ref($products) eq 'ARRAY';
-
- my $orig_limit = $limit;
- detaint_natural($limit)
- || ThrowCodeError('param_must_be_numeric',
- { function => 'possible_duplicates',
- param => $orig_limit });
-
- my $dbh = Bugzilla->dbh;
- my $user = Bugzilla->user;
- my @words = split(/[\b\s]+/, $short_desc || '');
- # Remove leading/trailing punctuation from words
+ 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) {
- $word =~ s/(?:^\W+|\W+$)//g;
+ my ($term, $rel_term)
+ = $dbh->sql_fulltext_search('bugs_fulltext.short_desc', $word);
+ push(@where, $term);
+ push(@relevance, $rel_term || $term);
}
- # 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);
- }
-
- $where_sql = join(' OR ', @where);
- $relevance_sql = join(' + ', @relevance);
- }
+ $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)" : "";
+ 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
@@ -871,654 +863,699 @@ sub possible_duplicates {
# C<deadline> - For time-tracking. Will be ignored for the same
# reasons as C<estimated_time>.
sub create {
- my ($class, $params) = @_;
- my $dbh = Bugzilla->dbh;
-
- # BMO - allow parameter alteration before creation. also add support for
- # fields which are not bug columns (eg bug_mentors). extensions should move
- # fields from $params to $stash, then use the bug_end_of_create hook to
- # update the database
- my $stash = {};
- Bugzilla::Hook::process('bug_before_create', { params => $params,
- stash => $stash });
-
- $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};
-
- # BMO - per-product hw/os defaults
- if (!defined $params->{rep_platform} || !defined $params->{op_sys}) {
- if (my $product = Bugzilla::Product->new({ name => $params->{product}, cache => 1 })) {
- $params->{rep_platform} //= $product->default_platform;
- $params->{op_sys} //= $product->default_op_sys;
- }
- }
-
- # Make sure a comment is always defined.
- $params->{comment} = '' unless defined $params->{comment};
-
- $class->check_required_create_fields($params);
- $params = $class->run_create_validators($params);
-
- # These are not a fields in the bugs table, so we don't pass them to
- # insert_create_data.
- my $cc_ids = delete $params->{cc};
- my $groups = delete $params->{groups};
- my $depends_on = delete $params->{dependson};
- my $blocked = delete $params->{blocked};
- my $keywords = delete $params->{keywords};
- my $creation_comment = delete $params->{comment};
- my $see_also = delete $params->{see_also};
- my $comment_tags = delete $params->{comment_tags};
-
- # 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 (?, ?)');
-
- 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);
- _update_delta_ts($depends_on_id, $timestamp);
- }
- 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);
- _update_delta_ts($blocked_id, $timestamp);
- }
-
- # Insert the values into the multiselect value tables
- foreach my $field (keys %$ms_values) {
- $dbh->do("DELETE FROM bug_$field where bug_id = ?",
- undef, $bug->bug_id);
- foreach my $value ( @{$ms_values->{$field}} ) {
- $dbh->do("INSERT INTO bug_$field (bug_id, value) VALUES (?,?)",
- undef, $bug->bug_id, $value);
- }
- }
-
- # Insert any see_also values
- if ($see_also) {
- my $see_also_array = $see_also;
- if (!ref $see_also_array) {
- $see_also = trim($see_also);
- $see_also_array = [ split(/[\s,]+/, $see_also) ];
- }
- foreach my $value (@$see_also_array) {
- $bug->add_see_also($value);
- }
- foreach my $see_also (@{ $bug->see_also }) {
- $see_also->insert_create_data($see_also);
- }
- foreach my $ref_bug (@{ $bug->{_update_ref_bugs} || [] }) {
- $ref_bug->update();
- }
- delete $bug->{_update_ref_bugs};
- }
-
- # Comment #0 handling...
-
- # We now have a bug id so we can fill this out
- $creation_comment->{'bug_id'} = $bug->id;
-
- # Insert the comment. We always insert a comment on bug creation,
- # but sometimes it's blank.
- 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();
- }
-
- # BMO - add the stash param from bug_start_of_create
- Bugzilla::Hook::process('bug_end_of_create', { bug => $bug,
- timestamp => $timestamp,
- stash => $stash,
- });
-
-
- $bug->_sync_fulltext( new_bug => 1 );
-
- $dbh->bz_commit_transaction();
-
- # BMO - some work should happen outside of the transaction block
- Bugzilla::Hook::process('bug_after_create', { bug => $bug, timestamp => $timestamp });
-
- return $bug;
-}
-
-sub run_create_validators {
- my $class = shift;
- my $params = $class->SUPER::run_create_validators(@_);
-
- # Add classification for checking mandatory fields which depend on it
- $params->{classification} = $params->{product}->classification->name;
-
- my @mandatory_fields = @{ Bugzilla->fields({ is_mandatory => 1,
- enter_bug => 1,
- obsolete => 0 }) };
- foreach my $field (@mandatory_fields) {
- $class->_check_field_is_mandatory($params->{$field->name}, $field,
- $params);
- }
+ my ($class, $params) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ # BMO - allow parameter alteration before creation. also add support for
+ # fields which are not bug columns (eg bug_mentors). extensions should move
+ # fields from $params to $stash, then use the bug_end_of_create hook to
+ # update the database
+ my $stash = {};
+ Bugzilla::Hook::process('bug_before_create',
+ {params => $params, stash => $stash});
+
+ $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};
+
+ # BMO - per-product hw/os defaults
+ if (!defined $params->{rep_platform} || !defined $params->{op_sys}) {
+ if (my $product
+ = Bugzilla::Product->new({name => $params->{product}, cache => 1}))
+ {
+ $params->{rep_platform} //= $product->default_platform;
+ $params->{op_sys} //= $product->default_op_sys;
+ }
+ }
+
+ # Make sure a comment is always defined.
+ $params->{comment} = '' unless defined $params->{comment};
+
+ $class->check_required_create_fields($params);
+ $params = $class->run_create_validators($params);
+
+ # These are not a fields in the bugs table, so we don't pass them to
+ # insert_create_data.
+ my $cc_ids = delete $params->{cc};
+ my $groups = delete $params->{groups};
+ my $depends_on = delete $params->{dependson};
+ my $blocked = delete $params->{blocked};
+ my $keywords = delete $params->{keywords};
+ my $creation_comment = delete $params->{comment};
+ my $see_also = delete $params->{see_also};
+ my $comment_tags = delete $params->{comment_tags};
+
+ # 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);
- my $product = delete $params->{product};
- $params->{product_id} = $product->id;
- my $component = delete $params->{component};
- $params->{component_id} = $component->id;
+ # Update the bug instance as well
+ $bug->{creation_ts} = $timestamp;
- # Callers cannot set reporter, creation_ts, or delta_ts.
- $params->{reporter} = $class->_check_reporter();
- $params->{delta_ts} = $params->{creation_ts};
+ # 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);
+ }
- if ($params->{estimated_time}) {
- $params->{remaining_time} = $params->{estimated_time};
- }
+ # 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);
+ }
- $class->_check_strict_isolation($params->{cc}, $params->{assigned_to},
- $params->{qa_contact}, $product);
+ # Set up dependencies (blocked/dependson)
+ my $sth_deps = $dbh->prepare(
+ 'INSERT INTO dependencies (blocked, dependson) VALUES (?, ?)');
- ($params->{dependson}, $params->{blocked}) =
- $class->_check_dependencies($params->{dependson}, $params->{blocked},
- $product);
+ foreach my $depends_on_id (@$depends_on) {
+ $sth_deps->execute($bug->bug_id, $depends_on_id);
- # You can't set these fields on bug creation (or sometimes ever).
- delete $params->{resolution};
- delete $params->{lastdiffed};
- delete $params->{bug_id};
- delete $params->{classification};
+ # Log the reverse action on the other bug.
+ LogActivityEntry($depends_on_id, 'blocked', '', $bug->bug_id,
+ $bug->{reporter_id}, $timestamp);
+ _update_delta_ts($depends_on_id, $timestamp);
+ }
+ foreach my $blocked_id (@$blocked) {
+ $sth_deps->execute($blocked_id, $bug->bug_id);
- Bugzilla::Hook::process('bug_end_of_create_validators',
- { params => $params });
+ # Log the reverse action on the other bug.
+ LogActivityEntry($blocked_id, 'dependson', '', $bug->bug_id,
+ $bug->{reporter_id}, $timestamp);
+ _update_delta_ts($blocked_id, $timestamp);
+ }
- 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];
- }
+ # 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);
}
+ }
- # 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];
+ # 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)];
}
-
- # 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 $value (@$see_also_array) {
+ $bug->add_see_also($value);
}
- foreach my $keyword_id (@$added_kw) {
- $dbh->do('INSERT INTO keywords (bug_id, keywordid) VALUES (?,?)',
- undef, $self->id, $keyword_id);
+ foreach my $see_also (@{$bug->see_also}) {
+ $see_also->insert_create_data($see_also);
}
- # 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];
+ foreach my $ref_bug (@{$bug->{_update_ref_bugs} || []}) {
+ $ref_bug->update();
}
+ delete $bug->{_update_ref_bugs};
+ }
- # Dependencies
- foreach my $pair ([qw(dependson blocked)], [qw(blocked dependson)]) {
- my ($type, $other) = @$pair;
- my $old = $old_bug->$type;
- my $new = $self->$type;
+ # Comment #0 handling...
- 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);
+ # We now have a bug id so we can fill this out
+ $creation_comment->{'bug_id'} = $bug->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.
- _update_delta_ts($removed_id, $delta_ts);
- }
- 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.
- _update_delta_ts($added_id, $delta_ts);
- }
-
- if (scalar(@$removed) || scalar(@$added)) {
- $changes->{$type} = [join(', ', @$removed), join(', ', @$added)];
- }
- }
+ # 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);
- # 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)];
-
- # we only audit when bugs protected with a secure-mail enabled group
- # are made public
- if (!scalar @{ $self->groups_in } && any { $old_groups{$_}->secure_mail } @$removed_gr) {
- Bugzilla->audit(sprintf('%s made Bug %s public (%s)', $user->login, $self->id, $self->short_desc));
- }
+ # 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();
+ }
- # 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} || []};
- }
+ # BMO - add the stash param from bug_start_of_create
+ Bugzilla::Hook::process('bug_end_of_create',
+ {bug => $bug, timestamp => $timestamp, stash => $stash,});
- # 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);
- }
+ $bug->_sync_fulltext(new_bug => 1);
- # Clear the cache of comments
- delete $self->{comments};
+ $dbh->bz_commit_transaction();
- # 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)];
+ # BMO - some work should happen outside of the transaction block
+ Bugzilla::Hook::process('bug_after_create',
+ {bug => $bug, timestamp => $timestamp});
- $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);
- }
- }
- }
+ return $bug;
+}
- # See Also
+sub run_create_validators {
+ my $class = shift;
+ my $params = $class->SUPER::run_create_validators(@_);
- my ($removed_see, $added_see) =
- diff_arrays($old_bug->see_also, $self->see_also, 'name');
+ # Add classification for checking mandatory fields which depend on it
+ $params->{classification} = $params->{product}->classification->name;
- $_->remove_from_db foreach @$removed_see;
- $_->insert_create_data($_) foreach @$added_see;
+ 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);
+ }
- # 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)];
- }
+ my $product = delete $params->{product};
+ $params->{product_id} = $product->id;
+ my $component = delete $params->{component};
+ $params->{component_id} = $component->id;
- $_->update foreach @{ $self->{_update_ref_bugs} || [] };
- delete $self->{_update_ref_bugs};
+ # Callers cannot set reporter, creation_ts, or delta_ts.
+ $params->{reporter} = $class->_check_reporter();
+ $params->{delta_ts} = $params->{creation_ts};
- # Flags
- my ($removed, $added) = Bugzilla::Flag->update_flags($self, $old_bug, $delta_ts);
- if ($removed || $added) {
- $changes->{'flagtypes.name'} = [$removed, $added];
- }
+ if ($params->{estimated_time}) {
+ $params->{remaining_time} = $params->{estimated_time};
+ }
- # BMO - allow extensions to alter what is logged into bugs_activity
- Bugzilla::Hook::process('bug_update_before_logging',
- { bug => $self, timestamp => $delta_ts, changes => $changes, old_bug => $old_bug });
+ $class->_check_strict_isolation($params->{cc}, $params->{assigned_to},
+ $params->{qa_contact}, $product);
- # 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);
- }
+ ($params->{dependson}, $params->{blocked})
+ = $class->_check_dependencies($params->{dependson}, $params->{blocked},
+ $product);
- # 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();
- }
- }
+ # You can't set these fields on bug creation (or sometimes ever).
+ delete $params->{resolution};
+ delete $params->{lastdiffed};
+ delete $params->{bug_id};
+ delete $params->{classification};
- $changes->{'dup_id'} = [$old_dup || undef, $cur_dup || undef];
- }
+ Bugzilla::Hook::process('bug_end_of_create_validators', {params => $params});
- Bugzilla::Hook::process('bug_end_of_update',
- { bug => $self, timestamp => $delta_ts, changes => $changes,
- old_bug => $old_bug });
+ return $params;
+}
- # If any change occurred, refresh the timestamp of the bug.
- if (scalar(keys %$changes) || $self->{added_comments}
- || $self->{comment_isprivate})
- {
- _update_delta_ts($self->id, $delta_ts);
- $self->{delta_ts} = $delta_ts;
- }
+sub update {
+ my $self = shift;
+ my $dbh = Bugzilla->dbh;
+ my $user = Bugzilla->user;
- # Update last-visited
- if ($user->is_involved_in_bug($self)) {
- $self->update_user_last_visit($user, $delta_ts);
- }
+ # XXX This is just a temporary hack until all updating happens
+ # inside this function.
+ my $delta_ts = shift || $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
- # 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);
- }
+ $dbh->bz_start_transaction();
- # 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;
+ my ($changes, $old_bug) = $self->SUPER::update(@_);
- }
- 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;
- }
-
- $self->_sync_fulltext(
- update_short_desc => $changes->{short_desc},
- update_comments => $self->{added_comments} || $self->{comment_isprivate}
+ 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];
+ }
+
+ # 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.
+ _update_delta_ts($removed_id, $delta_ts);
+ }
+ 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.
+ _update_delta_ts($added_id, $delta_ts);
+ }
+
+ 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)];
+
+ # we only audit when bugs protected with a secure-mail enabled group
+ # are made public
+ if (
+ !scalar @{$self->groups_in} && any { $old_groups{$_}->secure_mail }
+ @$removed_gr
+ )
+ {
+ Bugzilla->audit(sprintf(
+ '%s made Bug %s public (%s)',
+ $user->login, $self->id, $self->short_desc
+ ));
+ }
+ }
+
+ # 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)
+ ];
+ }
+
+ $_->update foreach @{$self->{_update_ref_bugs} || []};
+ delete $self->{_update_ref_bugs};
+
+ # Flags
+ my ($removed, $added)
+ = Bugzilla::Flag->update_flags($self, $old_bug, $delta_ts);
+ if ($removed || $added) {
+ $changes->{'flagtypes.name'} = [$removed, $added];
+ }
+
+ # BMO - allow extensions to alter what is logged into bugs_activity
+ Bugzilla::Hook::process(
+ 'bug_update_before_logging',
+ {
+ bug => $self,
+ timestamp => $delta_ts,
+ changes => $changes,
+ old_bug => $old_bug
+ }
+ );
+
+ # 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})
+ {
+ _update_delta_ts($self->id, $delta_ts);
+ $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;
+ }
+
+ $self->_sync_fulltext(
+ update_short_desc => $changes->{short_desc},
+ update_comments => $self->{added_comments} || $self->{comment_isprivate}
+ );
+
+ $dbh->bz_commit_transaction();
+
+ # 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};
+
+ # BMO - some work should happen outside of the transaction block
+ Bugzilla::Hook::process(
+ 'bug_after_update',
+ {
+ bug => $self,
+ timestamp => $delta_ts,
+ changes => $changes,
+ old_bug => $old_bug
+ }
+ );
- $dbh->bz_commit_transaction();
-
- # 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};
-
- # BMO - some work should happen outside of the transaction block
- Bugzilla::Hook::process('bug_after_update',
- { bug => $self, timestamp => $delta_ts, changes => $changes, old_bug => $old_bug });
-
- return $changes;
+ 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
+ );
+ }
+ }
}
# Update a bug's delta_ts without requiring the full object to be loaded.
sub _update_delta_ts {
- my ($bug_id, $timestamp) = @_;
- Bugzilla->dbh->do(
- "UPDATE bugs SET delta_ts = ? WHERE bug_id = ?",
- undef,
- $timestamp, $bug_id
- );
- Bugzilla::Hook::process('bug_end_of_update_delta_ts',
- { bug_id => $bug_id, timestamp => $timestamp });
+ my ($bug_id, $timestamp) = @_;
+ Bugzilla->dbh->do("UPDATE bugs SET delta_ts = ? WHERE bug_id = ?",
+ undef, $timestamp, $bug_id);
+ Bugzilla::Hook::process('bug_end_of_update_delta_ts',
+ {bug_id => $bug_id, timestamp => $timestamp});
}
# This is the correct way to delete bugs from the DB.
# No bug should be deleted from anywhere else except from here.
#
sub remove_from_db {
- my ($self) = @_;
- my $dbh = Bugzilla->dbh;
-
- if ($self->{'error'}) {
- ThrowCodeError("bug_error", { bug => $self });
- }
-
- my $bug_id = $self->{'bug_id'};
-
- # tables having 'bugs.bug_id' as a foreign key:
- # - attachments
- # - bug_group_map
- # - bugs
- # - bugs_activity
- # - bugs_fulltext
- # - cc
- # - dependencies
- # - duplicates
- # - flags
- # - keywords
- # - longdescs
-
- # Also, the attach_data table uses attachments.attach_id as a foreign
- # key, and so indirectly depends on a bug deletion too.
-
- $dbh->bz_start_transaction();
-
- $dbh->do("DELETE FROM bug_group_map WHERE bug_id = ?", undef, $bug_id);
- $dbh->do("DELETE FROM bugs_activity WHERE bug_id = ?", undef, $bug_id);
- $dbh->do("DELETE FROM cc WHERE bug_id = ?", undef, $bug_id);
- $dbh->do("DELETE FROM dependencies WHERE blocked = ? OR dependson = ?",
- undef, ($bug_id, $bug_id));
- $dbh->do("DELETE FROM duplicates WHERE dupe = ? OR dupe_of = ?",
- undef, ($bug_id, $bug_id));
- $dbh->do("DELETE FROM flags WHERE bug_id = ?", undef, $bug_id);
- $dbh->do("DELETE FROM keywords WHERE bug_id = ?", undef, $bug_id);
-
- # The attach_data table doesn't depend on bugs.bug_id directly.
- my $attach_ids =
- $dbh->selectcol_arrayref("SELECT attach_id FROM attachments
- WHERE bug_id = ?", undef, $bug_id);
-
- if (scalar(@$attach_ids)) {
- $dbh->do("DELETE FROM attach_data WHERE "
- . $dbh->sql_in('id', $attach_ids));
- }
-
- # Several of the previous tables also depend on attach_id.
- $dbh->do("DELETE FROM attachments WHERE bug_id = ?", undef, $bug_id);
- $dbh->do("DELETE FROM bugs WHERE bug_id = ?", undef, $bug_id);
- $dbh->do("DELETE FROM longdescs WHERE bug_id = ?", undef, $bug_id);
-
- $dbh->bz_commit_transaction();
-
- # The bugs_fulltext table doesn't support transactions.
- $dbh->do("DELETE FROM bugs_fulltext WHERE bug_id = ?", undef, $bug_id);
-
- undef $self;
+ my ($self) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ if ($self->{'error'}) {
+ ThrowCodeError("bug_error", {bug => $self});
+ }
+
+ my $bug_id = $self->{'bug_id'};
+
+ # tables having 'bugs.bug_id' as a foreign key:
+ # - attachments
+ # - bug_group_map
+ # - bugs
+ # - bugs_activity
+ # - bugs_fulltext
+ # - cc
+ # - dependencies
+ # - duplicates
+ # - flags
+ # - keywords
+ # - longdescs
+
+ # Also, the attach_data table uses attachments.attach_id as a foreign
+ # key, and so indirectly depends on a bug deletion too.
+
+ $dbh->bz_start_transaction();
+
+ $dbh->do("DELETE FROM bug_group_map WHERE bug_id = ?", undef, $bug_id);
+ $dbh->do("DELETE FROM bugs_activity WHERE bug_id = ?", undef, $bug_id);
+ $dbh->do("DELETE FROM cc WHERE bug_id = ?", undef, $bug_id);
+ $dbh->do("DELETE FROM dependencies WHERE blocked = ? OR dependson = ?",
+ undef, ($bug_id, $bug_id));
+ $dbh->do("DELETE FROM duplicates WHERE dupe = ? OR dupe_of = ?",
+ undef, ($bug_id, $bug_id));
+ $dbh->do("DELETE FROM flags WHERE bug_id = ?", undef, $bug_id);
+ $dbh->do("DELETE FROM keywords WHERE bug_id = ?", undef, $bug_id);
+
+ # The attach_data table doesn't depend on bugs.bug_id directly.
+ my $attach_ids = $dbh->selectcol_arrayref(
+ "SELECT attach_id FROM attachments
+ WHERE bug_id = ?", undef, $bug_id
+ );
+
+ if (scalar(@$attach_ids)) {
+ $dbh->do("DELETE FROM attach_data WHERE " . $dbh->sql_in('id', $attach_ids));
+ }
+
+ # Several of the previous tables also depend on attach_id.
+ $dbh->do("DELETE FROM attachments WHERE bug_id = ?", undef, $bug_id);
+ $dbh->do("DELETE FROM bugs WHERE bug_id = ?", undef, $bug_id);
+ $dbh->do("DELETE FROM longdescs WHERE bug_id = ?", undef, $bug_id);
+
+ $dbh->bz_commit_transaction();
+
+ # The bugs_fulltext table doesn't support transactions.
+ $dbh->do("DELETE FROM bugs_fulltext WHERE bug_id = ?", undef, $bug_id);
+
+ undef $self;
}
#####################################################################
@@ -1526,91 +1563,91 @@ sub remove_from_db {
#####################################################################
sub send_changes {
- my ($self, $changes) = @_;
- my @results;
- 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,
- );
-
- push @results, _send_bugmail(
- { id => $self->id, type => 'bug', forced => \%forced });
-
- # 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) {
- push @results, _send_bugmail(
- { forced => { changer => $user }, type => "dupe", id => $new_dup_id });
- }
-
- # If there were changes in dependencies, we need to notify those
- # dependencies.
- if ($changes->{'bug_status'}) {
- my ($old_status, $new_status) = @{ $changes->{'bug_status'} };
-
- # If this bug has changed from opened to closed or vice-versa,
- # then all of the bugs we block need to be notified.
- if (is_open_state($old_status) ne is_open_state($new_status)) {
- my $params = { forced => { changer => $user },
- type => 'dep',
- dep_only => 1,
- blocker => $self,
- changes => $changes };
-
- foreach my $id (@{ $self->blocked }) {
- $params->{id} = $id;
- push @results, _send_bugmail($params);
- }
- }
- }
-
- # 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)) {
- push @results, _send_bugmail(
- { forced => { changer => $user }, type => "dep", id => $id });
- }
-
- # Sending emails for the referenced bugs.
- foreach my $ref_bug_id (uniq @{ $self->{see_also_changes} || [] }) {
- push @results, _send_bugmail(
- { forced => { changer => $user }, id => $ref_bug_id });
- }
-
- return \@results;
+ my ($self, $changes) = @_;
+ my @results;
+ 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,
+ );
+
+ push @results,
+ _send_bugmail({id => $self->id, type => 'bug', forced => \%forced});
+
+ # 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) {
+ push @results,
+ _send_bugmail(
+ {forced => {changer => $user}, type => "dupe", id => $new_dup_id});
+ }
+
+ # If there were changes in dependencies, we need to notify those
+ # dependencies.
+ if ($changes->{'bug_status'}) {
+ my ($old_status, $new_status) = @{$changes->{'bug_status'}};
+
+ # If this bug has changed from opened to closed or vice-versa,
+ # then all of the bugs we block need to be notified.
+ if (is_open_state($old_status) ne is_open_state($new_status)) {
+ my $params = {
+ forced => {changer => $user},
+ type => 'dep',
+ dep_only => 1,
+ blocker => $self,
+ changes => $changes
+ };
+
+ foreach my $id (@{$self->blocked}) {
+ $params->{id} = $id;
+ push @results, _send_bugmail($params);
+ }
+ }
+ }
+
+ # 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)) {
+ push @results,
+ _send_bugmail({forced => {changer => $user}, type => "dep", id => $id});
+ }
+
+ # Sending emails for the referenced bugs.
+ foreach my $ref_bug_id (uniq @{$self->{see_also_changes} || []}) {
+ push @results, _send_bugmail({forced => {changer => $user}, id => $ref_bug_id});
+ }
+
+ return \@results;
}
sub _send_bugmail {
- my ($params) = @_;
+ my ($params) = @_;
- require Bugzilla::BugMail;
+ require Bugzilla::BugMail;
- my $sent_bugmail =
- Bugzilla::BugMail::Send($params->{'id'}, $params->{'forced'}, $params);
+ my $sent_bugmail
+ = Bugzilla::BugMail::Send($params->{'id'}, $params->{'forced'}, $params);
- return { params => $params, sent_bugmail => $sent_bugmail };
+ return {params => $params, sent_bugmail => $sent_bugmail};
}
#####################################################################
@@ -1618,902 +1655,925 @@ sub _send_bugmail {
#####################################################################
sub _check_alias {
- my ($invocant, $alias) = @_;
- $alias = trim($alias);
- return undef if (!Bugzilla->params->{'usebugaliases'} || !$alias);
-
- # Make sure the alias isn't too long.
- if (length($alias) > 40) {
- ThrowUserError("alias_too_long");
- }
- # Make sure the alias isn't just a number.
- if ($alias =~ /^\d+$/) {
- ThrowUserError("alias_is_numeric", { alias => $alias });
- }
- # Make sure the alias has no commas or spaces.
- if ($alias =~ /[, ]/) {
- ThrowUserError("alias_has_comma_or_space", { alias => $alias });
- }
- # Make sure the alias is unique, or that it's already our alias.
- my $other_bug = new Bugzilla::Bug($alias);
- if (!$other_bug->{error}
- && (!ref $invocant || $other_bug->id != $invocant->id))
- {
- ThrowUserError("alias_in_use", { alias => $alias,
- bug_id => $other_bug->id });
- }
+ my ($invocant, $alias) = @_;
+ $alias = trim($alias);
+ return undef if (!Bugzilla->params->{'usebugaliases'} || !$alias);
- return $alias;
+ # Make sure the alias isn't too long.
+ if (length($alias) > 40) {
+ ThrowUserError("alias_too_long");
+ }
+
+ # Make sure the alias isn't just a number.
+ if ($alias =~ /^\d+$/) {
+ ThrowUserError("alias_is_numeric", {alias => $alias});
+ }
+
+ # Make sure the alias has no commas or spaces.
+ if ($alias =~ /[, ]/) {
+ ThrowUserError("alias_has_comma_or_space", {alias => $alias});
+ }
+
+ # Make sure the alias is unique, or that it's already our alias.
+ my $other_bug = new Bugzilla::Bug($alias);
+ if (!$other_bug->{error} && (!ref $invocant || $other_bug->id != $invocant->id))
+ {
+ ThrowUserError("alias_in_use", {alias => $alias, bug_id => $other_bug->id});
+ }
+
+ return $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))
- {
- $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;
+ 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);
- # 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);
+
+ # 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::Status->can_change_to()};
- if (!$product->allows_unconfirmed) {
- @valid_statuses = grep {$_->name ne 'UNCONFIRMED'} @valid_statuses;
- }
- }
-
- # 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::Status->can_change_to()};
+ if (!$product->allows_unconfirmed) {
+ @valid_statuses = grep { $_->name ne 'UNCONFIRMED' } @valid_statuses;
+ }
+ }
+
+ # 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];
}
+ }
}
-
- # 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,
- new => $new_status });
-
- }
-
- if (ref $invocant
- && ($new_status->name eq 'IN_PROGRESS'
- # Backwards-compat for the old default workflow.
- or $new_status->name eq 'ASSIGNED')
- && Bugzilla->params->{"usetargetmilestone"}
- && Bugzilla->params->{"musthavemilestoneonaccept"}
- # musthavemilestoneonaccept applies only if at least two
- # target milestones are defined for the product.
- && scalar(@{ $product->milestones }) > 1
- && $invocant->target_milestone eq $product->default_milestone)
- {
- ThrowUserError("milestone_required", { bug => $invocant });
- }
-
- if (!blessed $invocant) {
- $params->{everconfirmed} = $new_status->name eq 'UNCONFIRMED' ? 0 : 1;
- }
-
- return $new_status->name;
+ 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, new => $new_status});
+
+ }
+
+ if (
+ ref $invocant && (
+ $new_status->name eq 'IN_PROGRESS'
+
+ # Backwards-compat for the old default workflow.
+ or $new_status->name eq 'ASSIGNED'
+ )
+ && Bugzilla->params->{"usetargetmilestone"}
+ && Bugzilla->params->{"musthavemilestoneonaccept"}
+
+ # musthavemilestoneonaccept applies only if at least two
+ # target milestones are defined for the product.
+ && scalar(@{$product->milestones}) > 1
+ && $invocant->target_milestone eq $product->default_milestone
+ )
+ {
+ ThrowUserError("milestone_required", {bug => $invocant});
+ }
+
+ if (!blessed $invocant) {
+ $params->{everconfirmed} = $new_status->name eq 'UNCONFIRMED' ? 0 : 1;
+ }
+
+ return $new_status->name;
}
sub _check_cc {
- my ($invocant, $ccs, undef, $params) = @_;
- my $component = blessed($invocant) ? $invocant->component_obj
- : $params->{component};
- return [map {$_->id} @{$component->initial_cc}] unless $ccs;
+ 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_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, $depends_on, $blocks, $product) = @_;
-
- if (!ref $invocant) {
- # Only editbugs users can set dependencies on bug entry.
- return ([], []) unless Bugzilla->user->in_group('editbugs',
- $product->id);
- }
-
- my %deps_in = (dependson => $depends_on || '', blocked => $blocks || '');
-
- 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;
- # We do this up here to make sure all aliases are converted to IDs.
- @bug_ids = map { $invocant->check($_, $type)->id } @bug_ids;
-
- my $user = Bugzilla->user;
- 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);
-
- # If a user has editbugs they are allowed to add dependencies on
- # bugs that they cannot see -- only check access for bugs that are
- # removed.
- if ($user->in_group('editbugs')) {
- @check_access = @$removed;
- }
- else {
- @check_access = (@$added, @$removed);
- }
-
- # Check field permissions if we've changed anything.
- if (@$added || @$removed) {
- my $privs;
- if (!$invocant->check_can_change_field($type, 0, 1, \$privs)) {
- ThrowUserError('illegal_change', { field => $type,
- privs => $privs });
- }
- }
- }
+ my ($invocant, $depends_on, $blocks, $product) = @_;
- foreach my $modified_id (@check_access) {
- # Check that the user has access to the other bug.
- 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});
- }
- }
- }
+ if (!ref $invocant) {
- $deps_in{$type} = \@bug_ids;
- }
+ # Only editbugs users can set dependencies on bug entry.
+ return ([], []) unless Bugzilla->user->in_group('editbugs', $product->id);
+ }
- # 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);
+ my %deps_in = (dependson => $depends_on || '', blocked => $blocks || '');
- return ($deps{'dependson'}, $deps{'blocked'});
-}
+ foreach my $type (qw(dependson blocked)) {
+ my @bug_ids
+ = ref($deps_in{$type})
+ ? @{$deps_in{$type}}
+ : split(/[\s,]+/, $deps_in{$type});
-sub _check_dup_id {
- my ($self, $dupe_of) = @_;
- my $dbh = Bugzilla->dbh;
+ # Eliminate nulls.
+ @bug_ids = grep {$_} @bug_ids;
- $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;
+ # We do this up here to make sure all aliases are converted to IDs.
+ @bug_ids = map { $invocant->check($_, $type)->id } @bug_ids;
- # If the dupe is unchanged, we have nothing more to check.
- return $dupe_of if ($self->dup_id && $self->dup_id == $dupe_of);
+ my $user = Bugzilla->user;
+ my @check_access = @bug_ids;
- # 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;
+ # 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);
+
+ # If a user has editbugs they are allowed to add dependencies on
+ # bugs that they cannot see -- only check access for bugs that are
+ # removed.
+ if ($user->in_group('editbugs')) {
+ @check_access = @$removed;
+ }
+ else {
+ @check_access = (@$added, @$removed);
+ }
+
+ # Check field permissions if we've changed anything.
+ if (@$added || @$removed) {
+ my $privs;
+ if (!$invocant->check_can_change_field($type, 0, 1, \$privs)) {
+ ThrowUserError('illegal_change', {field => $type, privs => $privs});
+ }
+ }
+ }
- # Make sure a loop isn't created when marking this bug
- # as duplicate.
- _resolve_ultimate_dup_id($self->id, $dupe_of, 1);
+ foreach my $modified_id (@check_access) {
- 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 he can see the bug...
- if ($self->reporter->can_see_bug($dupe_of)) {
- # We only add him if he's 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 he user is already the reporter
- # of the dupe_of bug, since we already checked if he 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 he's 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;
+ # Check that the user has access to the other bug.
+ 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});
}
+ }
}
- return $dupe_of;
+ $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);
+
+ return ($deps{'dependson'}, $deps{'blocked'});
}
-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};
- }
+sub _check_dup_id {
+ my ($self, $dupe_of) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ $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;
+
+ # 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 he can see the bug...
+ if ($self->reporter->can_see_bug($dupe_of)) {
+
+ # We only add him if he's 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 {
- # 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 });
-
- # BMO : allow bugs to be always placed into some groups
- if (!$product->group_always_settable($group)
- && !$product->group_is_settable($group)) {
- ThrowUserError('group_restriction_not_allowed', { %args, name => $name });
- }
- $add_groups{$group->id} = $group;
- }
- }
+ # Note that here we don't check if he user is already the reporter
+ # of the dupe_of bug, since we already checked if he 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 he's already the reporter of the
+ # dup_of bug.
+ my $vars = {};
+ my $template = Bugzilla->template;
- # Now enforce mandatory groups.
- $add_groups{$_->id} = $_ foreach @{ $product->groups_mandatory };
+ # 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;
+ }
+ }
- my @add_groups = values %add_groups;
- return \@add_groups;
+ return $dupe_of;
}
-sub _check_keywords {
- my ($invocant, $keywords_in, undef, $params) = @_;
+sub _check_groups {
+ my ($invocant, $group_names, undef, $params) = @_;
- return [] if !defined $keywords_in;
+ my $bug_id = blessed($invocant) ? $invocant->id : undef;
+ my $product = blessed($invocant) ? $invocant->product_obj : $params->{product};
+ my %add_groups;
- my $keyword_array = $keywords_in;
- if (!ref $keyword_array) {
- $keywords_in = trim($keywords_in);
- $keyword_array = [split(/[\s,]+/, $keywords_in)];
+ # 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;
- my %keywords;
- foreach my $keyword (@$keyword_array) {
- next unless $keyword;
- my $obj = Bugzilla::Keyword->check($keyword);
- $keywords{$obj->id} = $obj;
- }
+ # 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});
- my %old_kw_id;
- if (blessed $invocant) {
- my @old_keywords = @{$invocant->keyword_objects};
- %old_kw_id = map { $_->id => 1 } @old_keywords;
+ # BMO : allow bugs to be always placed into some groups
+ if ( !$product->group_always_settable($group)
+ && !$product->group_is_settable($group))
+ {
+ ThrowUserError('group_restriction_not_allowed', {%args, name => $name});
+ }
+ $add_groups{$group->id} = $group;
}
+ }
- 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 });
- }
+ # Now enforce mandatory groups.
+ $add_groups{$_->id} = $_ foreach @{$product->groups_mandatory};
+
+ my @add_groups = values %add_groups;
+ return \@add_groups;
+}
- return [values %keywords];
+sub _check_keywords {
+ my ($invocant, $keywords_in, undef, $params) = @_;
+
+ 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 %old_kw_id;
+ if (blessed $invocant) {
+ my @old_keywords = @{$invocant->keyword_objects};
+ %old_kw_id = map { $_->id => 1 } @old_keywords;
+ }
+
+ 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];
}
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};
- my $id;
- 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)
- {
- $id = $component->default_qa_contact->id;
- }
+ my ($invocant, $qa_contact, undef, $params) = @_;
+ $qa_contact = trim($qa_contact) if !ref $qa_contact;
+ my $component
+ = blessed($invocant) ? $invocant->component_obj : $params->{component};
+ my $id;
+ 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)
+ {
+ $id = $component->default_qa_contact->id;
}
+ }
- # If a QA Contact was specified or if we're updating, check
- # the QA Contact for validity.
- if (!defined $id && $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.
+ if (!defined $id && $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 he 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 he must be logged in first!).
+ Bugzilla->login(LOGIN_REQUIRED);
+ $reporter = Bugzilla->user->id;
+ }
+ return $reporter;
}
sub _check_resolution {
- my ($self, $resolution) = @_;
- $resolution = trim($resolution);
-
- # 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 => $self->status->name })
- if !$resolution && !$self->status->is_open;
-
- # Make sure this is a valid resolution.
- $resolution = $self->_check_select_field($resolution, 'resolution');
-
- # Don't allow open bugs to have resolutions.
- ThrowUserError('resolution_not_allowed') if $self->status->is_open;
-
- # Check noresolveonopenblockers.
- if (Bugzilla->params->{"noresolveonopenblockers"}
- && $resolution eq 'FIXED'
- && (!$self->resolution || $resolution ne $self->resolution)
- && scalar @{$self->dependson})
- {
- my $dep_bugs = Bugzilla::Bug->new_from_list($self->dependson);
- my $count_open = grep { $_->isopened } @$dep_bugs;
- if ($count_open) {
- ThrowUserError("still_unresolved_bugs",
- { bug_id => $self->id, dep_count => $count_open });
- }
- }
-
- # Check if they're changing the resolution and need to comment.
- if (Bugzilla->params->{'commentonchange_resolution'}
- && $self->resolution && $resolution ne $self->resolution
- && !$self->{added_comments})
- {
- ThrowUserError('comment_required');
- }
-
- return $resolution;
+ my ($self, $resolution) = @_;
+ $resolution = trim($resolution);
+
+ # 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 => $self->status->name})
+ if !$resolution && !$self->status->is_open;
+
+ # Make sure this is a valid resolution.
+ $resolution = $self->_check_select_field($resolution, 'resolution');
+
+ # Don't allow open bugs to have resolutions.
+ ThrowUserError('resolution_not_allowed') if $self->status->is_open;
+
+ # Check noresolveonopenblockers.
+ if ( Bugzilla->params->{"noresolveonopenblockers"}
+ && $resolution eq 'FIXED'
+ && (!$self->resolution || $resolution ne $self->resolution)
+ && scalar @{$self->dependson})
+ {
+ my $dep_bugs = Bugzilla::Bug->new_from_list($self->dependson);
+ my $count_open = grep { $_->isopened } @$dep_bugs;
+ if ($count_open) {
+ ThrowUserError("still_unresolved_bugs",
+ {bug_id => $self->id, dep_count => $count_open});
+ }
+ }
+
+ # Check if they're changing the resolution and need to comment.
+ if ( Bugzilla->params->{'commentonchange_resolution'}
+ && $self->resolution
+ && $resolution ne $self->resolution
+ && !$self->{added_comments})
+ {
+ ThrowUserError('comment_required');
+ }
+
+ 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);
- $tag = clean_text($tag);
- $tag || ThrowUserError('no_tag_to_edit');
- ThrowUserError('tag_name_too_long') if length($tag) > MAX_LEN_QUERY_NAME;
- trick_taint($tag);
- # Tags are all lowercase.
- return lc($tag);
+ # Tags are all lowercase.
+ return lc($tag);
}
sub _check_target_milestone {
- my ($invocant, $target, undef, $params) = @_;
- my $product = blessed($invocant) ? $invocant->product_obj
- : $params->{product};
- my $old_target = blessed($invocant) ? $invocant->target_milestone : '';
- $target = trim($target);
- $target = $product->default_milestone if !defined $target;
- my $object = Bugzilla::Milestone->check(
- { product => $product, name => $target });
- if ($old_target && $object->name ne $old_target && !$object->is_active) {
- ThrowUserError('value_inactive', { class => ref($object), value => $target });
- }
- return $object->name;
+ my ($invocant, $target, undef, $params) = @_;
+ my $product = blessed($invocant) ? $invocant->product_obj : $params->{product};
+ my $old_target = blessed($invocant) ? $invocant->target_milestone : '';
+ $target = trim($target);
+ $target = $product->default_milestone if !defined $target;
+ my $object = Bugzilla::Milestone->check({product => $product, name => $target});
+ if ($old_target && $object->name ne $old_target && !$object->is_active) {
+ ThrowUserError('value_inactive', {class => ref($object), value => $target});
+ }
+ return $object->name;
}
sub _check_time_field {
- my ($invocant, $value, $field, $params) = @_;
+ my ($invocant, $value, $field, $params) = @_;
- # When filing bugs, we're forgiving and just return 0 if
- # the user isn't a timetracker. When updating bugs, check_can_change_field
- # controls permissions, so we don't want to check them here.
- if (!ref $invocant and !Bugzilla->user->is_timetracker) {
- return 0;
- }
+ # When filing bugs, we're forgiving and just return 0 if
+ # the user isn't a timetracker. When updating bugs, check_can_change_field
+ # controls permissions, so we don't want to check them here.
+ if (!ref $invocant and !Bugzilla->user->is_timetracker) {
+ return 0;
+ }
- # check_time is in Bugzilla::Object.
- return $invocant->check_time($value, $field, $params);
+ # check_time is in Bugzilla::Object.
+ return $invocant->check_time($value, $field, $params);
}
sub _check_version {
- my ($invocant, $version, undef, $params) = @_;
- $version = trim($version);
- my $product = blessed($invocant) ? $invocant->product_obj
- : $params->{product};
- my $old_vers = blessed($invocant) ? $invocant->version : '';
- my $object = Bugzilla::Version->check({ product => $product, name => $version });
- if ($object->name ne $old_vers && !$object->is_active) {
- ThrowUserError('value_inactive', { class => ref($object), value => $version });
- }
- return $object->name;
+ my ($invocant, $version, undef, $params) = @_;
+ $version = trim($version);
+ my $product = blessed($invocant) ? $invocant->product_obj : $params->{product};
+ my $old_vers = blessed($invocant) ? $invocant->version : '';
+ my $object = Bugzilla::Version->check({product => $product, name => $version});
+ if ($object->name ne $old_vers && !$object->is_active) {
+ ThrowUserError('value_inactive', {class => ref($object), value => $version});
+ }
+ return $object->name;
}
# Custom Field Validators
sub _check_field_is_mandatory {
- my ($invocant, $value, $field, $params) = @_;
+ my ($invocant, $value, $field, $params) = @_;
- if (!blessed($field)) {
- $field = Bugzilla::Field->new({ name => $field });
- return if !$field;
- }
+ if (!blessed($field)) {
+ $field = Bugzilla::Field->new({name => $field});
+ return if !$field;
+ }
- return if !$field->is_mandatory;
+ return if !$field->is_mandatory;
- return if !$field->is_visible_on_bug($params || $invocant);
+ return if !$field->is_visible_on_bug($params || $invocant);
- return if ($field->type == FIELD_TYPE_SINGLE_SELECT
- && scalar @{ get_legal_field_values($field->name) } == 1);
+ return
+ if ($field->type == FIELD_TYPE_SINGLE_SELECT
+ && scalar @{get_legal_field_values($field->name)} == 1);
- return if ($field->type == FIELD_TYPE_MULTI_SELECT
- && !scalar @{ get_legal_field_values($field->name) });
+ return
+ if ($field->type == FIELD_TYPE_MULTI_SELECT
+ && !scalar @{get_legal_field_values($field->name)});
- if (ref($value) eq 'ARRAY') {
- $value = join('', @$value);
- }
+ if (ref($value) eq 'ARRAY') {
+ $value = join('', @$value);
+ }
- $value = trim($value);
- if (!defined($value)
- or $value eq ""
- or ($value eq '---' and $field->type == FIELD_TYPE_SINGLE_SELECT)
- or ($value =~ EMPTY_DATETIME_REGEX
- and $field->type == FIELD_TYPE_DATETIME))
- {
- ThrowUserError('required_field', { field => $field });
- }
+ $value = trim($value);
+ if ( !defined($value)
+ or $value eq ""
+ or ($value eq '---' and $field->type == FIELD_TYPE_SINGLE_SELECT)
+ or ($value =~ EMPTY_DATETIME_REGEX and $field->type == FIELD_TYPE_DATETIME))
+ {
+ ThrowUserError('required_field', {field => $field});
+ }
}
sub _check_date_field {
- my ($invocant, $date) = @_;
- return $invocant->_check_datetime_field($date, undef, {date_only => 1});
+ my ($invocant, $date) = @_;
+ return $invocant->_check_datetime_field($date, undef, {date_only => 1});
}
-sub _check_datetime_field {
- my ($invocant, $date_time, $field, $params) = @_;
-
- # Empty datetimes are empty strings or strings only containing
- # 0's, whitespace, and punctuation.
- if ($date_time =~ /^[\s0[:punct:]]*$/) {
- return undef;
- }
- $date_time = trim($date_time);
- my ($date, $time) = split(' ', $date_time);
- if ($date && !validate_date($date)) {
- ThrowUserError('illegal_date', { date => $date,
- format => 'YYYY-MM-DD' });
- }
- if ($time && $params->{date_only}) {
- ThrowUserError('illegal_date', { date => $date_time,
- format => 'YYYY-MM-DD' });
- }
- if ($time && !validate_time($time)) {
- ThrowUserError('illegal_time', { 'time' => $time,
- format => 'HH:MM:SS' });
- }
- return $date_time
+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;
}
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;
+ 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 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);
- }
+ # 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;
+ return $checked_id;
}
sub _check_integer_field {
- my ($invocant, $value, $field) = @_;
- $value = defined($value) ? trim($value) : '';
+ my ($invocant, $value, $field) = @_;
+ $value = defined($value) ? trim($value) : '';
- # BMO - allow empty values
- if ($value eq '') {
- return undef;
- }
+ # BMO - allow empty values
+ if ($value eq '') {
+ return undef;
+ }
- my $orig_value = $value;
- if (!detaint_signed($value)) {
- ThrowUserError("number_not_integer",
- {field => $field, num => $orig_value});
- }
- elsif ($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 ($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);
+ # 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) = @_;
- # 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});
- }
+ # Don't do anything if this bug doesn't have any dependencies.
+ return unless defined($dep_id);
- # 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 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);
+ _check_relationship_loop($field, $dep_id, $next_dep_id, $ids);
}
#####################################################################
@@ -2521,31 +2581,32 @@ 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} );
+ 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),
- return @fields;
+ # Conditional Fields
+ Bugzilla->params->{'useqacontact'} ? "qa_contact" : (),
+
+ # Custom Fields
+ map { $_->name } Bugzilla->active_custom_fields
+ );
+ Bugzilla::Hook::process('bug_fields', {'fields' => \@fields});
+
+ return @fields;
}
#####################################################################
@@ -2554,30 +2615,29 @@ sub fields {
# 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 = user_id_to_login($value);
- $current = user_id_to_login($current);
- }
- 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 = user_id_to_login($value);
+ $current = user_id_to_login($current);
+ }
+ ThrowUserError('illegal_change',
+ {field => $field, oldvalue => $current, newvalue => $value, privs => $privs});
+ }
+ $self->_check_field_is_mandatory($value, $field);
}
#################
@@ -2587,523 +2647,568 @@ 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;
-
- # BMO - allow extensions to morph params
- Bugzilla::Hook::process('bug_start_of_set_all', { bug => $self, params => $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'});
- }
+ 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;
+
+ # BMO - allow extensions to morph params
+ Bugzilla::Hook::process('bug_start_of_set_all',
+ {bug => $self, params => $params});
+
+ # You cannot mark bugs as duplicate when changing several bugs at once
+ # (because currently there is no way to check for duplicate loops in that
+ # situation). You also cannot set the alias of several bugs at once.
+ if ($params->{other_bugs} and scalar @{$params->{other_bugs}} > 1) {
+ ThrowUserError('dupe_not_allowed') if exists $params->{dup_id};
+ ThrowUserError('multiple_alias_not_allowed') if defined $params->{alias};
+ }
+
+ # For security purposes, and because lots of other checks depend on it,
+ # we set the product first before anything else.
+ my $product_changed; # Used only for strict_isolation checks.
+ if (exists $params->{'product'}) {
+ $product_changed = $self->_set_product($params->{'product'}, $params);
+ }
+
+ # strict_isolation checks mean that we should set the groups
+ # immediately after changing the product.
+ $self->_add_remove($params, 'groups');
+
+ if (exists $params->{'dependson'} or exists $params->{'blocked'}) {
+ my %set_deps;
+ foreach my $name (qw(dependson blocked)) {
+ my @dep_ids = @{$self->$name};
+
+ # If only one of the two fields was passed in, then we need to
+ # retain the current value for the other one.
+ if (!exists $params->{$name}) {
+ $set_deps{$name} = \@dep_ids;
+ next;
+ }
+
+ # Explicitly setting them to a particular value overrides
+ # add/remove.
+ if (exists $params->{$name}->{set}) {
+ $set_deps{$name} = $params->{$name}->{set};
+ next;
+ }
+
+ foreach my $add (@{$params->{$name}->{add} || []}) {
+ push(@dep_ids, $add) if !grep($_ == $add, @dep_ids);
+ }
+ foreach my $remove (@{$params->{$name}->{remove} || []}) {
+ @dep_ids = grep($_ != $remove, @dep_ids);
+ }
+ $set_deps{$name} = \@dep_ids;
+ }
+
+ $self->set_dependencies($set_deps{'dependson'}, $set_deps{'blocked'});
+ }
+
+ if (exists $params->{'keywords'}) {
+
+ # Sorting makes the order "add, remove, set", just like for other
+ # fields.
+ foreach my $action (sort keys %{$params->{'keywords'}}) {
+ $self->modify_keywords($params->{'keywords'}->{$action}, $action);
+ }
+ }
+
+ if (exists $params->{'comment'} or exists $params->{'work_time'}) {
+
+ # Add a comment as needed to each bug. This is done early because
+ # there are lots of things that want to check if we added a comment.
+ $self->add_comment(
+ $params->{'comment'}->{'body'},
+ {
+ isprivate => $params->{'comment'}->{'is_private'},
+ work_time => $params->{'work_time'}
+ }
+ );
+ }
- if (exists $params->{'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 (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->{'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'} });
- }
+ my %normal_set_all;
+ foreach my $name (keys %$params) {
- if (defined $params->{comment_tags} && Bugzilla->user->can_tag_comments()) {
- $self->{added_comment_tags} = ref $params->{comment_tags}
- ? $params->{comment_tags}
- : [ $params->{comment_tags} ];
+ # These are handled separately below.
+ if ($self->can("set_$name")) {
+ $normal_set_all{$name} = $params->{$name};
}
+ }
+ $self->SUPER::set_all(\%normal_set_all);
- 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};
- }
- }
- $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 = grep { $_->type != FIELD_TYPE_EXTENSION }
- 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
+ = grep { $_->type != FIELD_TYPE_EXTENSION } 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, 'cc');
- # 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$//;
- 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$//;
+ my $add_method = "add_$name";
+ my $remove_method = "remove_$name";
+ $self->$add_method($_) foreach @add;
+ $self->$remove_method($_) foreach @remove;
}
sub set_alias { $_[0]->set('alias', $_[1]); }
+
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, $comment_id, $isprivate) = @_;
+ my ($self, $comment_id, $isprivate) = @_;
- # We also allow people to pass in a hash of comment ids to update.
- if (ref $comment_id) {
- while (my ($id, $is) = each %$comment_id) {
- $self->set_comment_is_private($id, $is);
- }
- return;
- }
-
- my ($comment) = grep($comment_id == $_->id, @{ $self->comments });
- ThrowUserError('comment_invalid_isprivate', { id => $comment_id })
- if !$comment;
-
- $isprivate = $isprivate ? 1 : 0;
- if ($isprivate != $comment->is_private) {
- ThrowUserError('user_not_insider') if !Bugzilla->user->is_insider;
- $self->{comment_isprivate} ||= [];
- $comment->set_is_private($isprivate);
- push @{$self->{comment_isprivate}}, $comment;
- }
-}
-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);
- }
+ # We also allow people to pass in a hash of comment ids to update.
+ if (ref $comment_id) {
+ while (my ($id, $is) = each %$comment_id) {
+ $self->set_comment_is_private($id, $is);
}
+ return;
+ }
+
+ my ($comment) = grep($comment_id == $_->id, @{$self->comments});
+ ThrowUserError('comment_invalid_isprivate', {id => $comment_id}) if !$comment;
+
+ $isprivate = $isprivate ? 1 : 0;
+ if ($isprivate != $comment->is_private) {
+ ThrowUserError('user_not_insider') if !Bugzilla->user->is_insider;
+ $self->{comment_isprivate} ||= [];
+ $comment->set_is_private($isprivate);
+ push @{$self->{comment_isprivate}}, $comment;
+ }
}
-sub set_custom_field {
- my ($self, $field, $value) = @_;
- if (ref $value eq 'ARRAY' && $field->type != FIELD_TYPE_MULTI_SELECT) {
- $value = $value->[0];
+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);
}
- ThrowCodeError('field_not_custom', { field => $field }) if !$field->custom;
- $self->set($field->name, $value);
+ }
+}
+
+sub set_custom_field {
+ 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);
}
sub set_deadline { $_[0]->set('deadline', $_[1]); }
+
sub set_dependencies {
- my ($self, $dependson, $blocked) = @_;
- ($dependson, $blocked) = $self->_check_dependencies($dependson, $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) = @_;
+ ($dependson, $blocked) = $self->_check_dependencies($dependson, $blocked);
+
+ # These may already be detainted, but all setters are supposed to
+ # detaint their input if they've run a validator (just as though
+ # we had used Bugzilla::Object::set), so we do that here.
+ detaint_natural($_) foreach (@$dependson, @$blocked);
+ $self->{'dependson'} = $dependson;
+ $self->{'blocked'} = $blocked;
+ delete $self->{depends_on_obj};
+ delete $self->{blocks_obj};
}
sub _clear_dup_id { $_[0]->{dup_id} = undef; }
+
sub set_dup_id {
- my ($self, $dup_id) = @_;
- my $old = $self->dup_id || 0;
- $self->set('dup_id', $dup_id);
- my $new = $self->dup_id;
- return if $old == $new;
-
- # Make sure that we have the DUPLICATE resolution. This is needed
- # if somebody calls set_dup_id without calling set_bug_status or
- # set_resolution.
- if ($self->resolution ne 'DUPLICATE') {
- # Even if the current status is VERIFIED, we change it back to
- # RESOLVED (or whatever the duplicate_or_move_bug_status is) here,
- # because that's the same thing the UI does when you click on the
- # "Mark as Duplicate" link. If people really want to retain their
- # current status, they can use set_bug_status and set the DUPLICATE
- # resolution before getting here.
- $self->set_bug_status(
- Bugzilla->params->{'duplicate_or_move_bug_status'},
- { resolution => 'DUPLICATE' });
- }
-
- # Update the other bug.
- my $dupe_of = new Bugzilla::Bug($self->dup_id);
- if (delete $self->{_add_dup_cc}) {
- $dupe_of->add_cc($self->reporter);
- }
- $dupe_of->add_comment("", { type => CMT_HAS_DUPE,
- extra_data => $self->id });
- $self->{_dup_for_update} = $dupe_of;
-
- # Now make sure that we add a duplicate comment on *this* bug.
- # (Change an existing comment into a dup comment, if there is one,
- # or add an empty dup comment.)
- if ($self->{added_comments}) {
- my @normal = grep { !defined $_->{type} || $_->{type} == CMT_NORMAL }
- @{ $self->{added_comments} };
- # Turn the last one into a dup comment.
- $normal[-1]->{type} = CMT_DUPE_OF;
- $normal[-1]->{extra_data} = $self->dup_id;
- }
- else {
- $self->add_comment('', { type => CMT_DUPE_OF,
- extra_data => $self->dup_id });
- }
+ my ($self, $dup_id) = @_;
+ my $old = $self->dup_id || 0;
+ $self->set('dup_id', $dup_id);
+ my $new = $self->dup_id;
+ return if $old == $new;
+
+ # Make sure that we have the DUPLICATE resolution. This is needed
+ # if somebody calls set_dup_id without calling set_bug_status or
+ # set_resolution.
+ if ($self->resolution ne 'DUPLICATE') {
+
+ # Even if the current status is VERIFIED, we change it back to
+ # RESOLVED (or whatever the duplicate_or_move_bug_status is) here,
+ # because that's the same thing the UI does when you click on the
+ # "Mark as Duplicate" link. If people really want to retain their
+ # current status, they can use set_bug_status and set the DUPLICATE
+ # resolution before getting here.
+ $self->set_bug_status(Bugzilla->params->{'duplicate_or_move_bug_status'},
+ {resolution => 'DUPLICATE'});
+ }
+
+ # Update the other bug.
+ my $dupe_of = new Bugzilla::Bug($self->dup_id);
+ if (delete $self->{_add_dup_cc}) {
+ $dupe_of->add_cc($self->reporter);
+ }
+ $dupe_of->add_comment("", {type => CMT_HAS_DUPE, extra_data => $self->id});
+ $self->{_dup_for_update} = $dupe_of;
+
+ # Now make sure that we add a duplicate comment on *this* bug.
+ # (Change an existing comment into a dup comment, if there is one,
+ # or add an empty dup comment.)
+ if ($self->{added_comments}) {
+ my @normal = grep { !defined $_->{type} || $_->{type} == CMT_NORMAL }
+ @{$self->{added_comments}};
+
+ # Turn the last one into a dup comment.
+ $normal[-1]->{type} = CMT_DUPE_OF;
+ $normal[-1]->{extra_data} = $self->dup_id;
+ }
+ else {
+ $self->add_comment('', {type => CMT_DUPE_OF, extra_data => $self->dup_id});
+ }
}
sub set_estimated_time { $_[0]->set('estimated_time', $_[1]); }
-sub _set_everconfirmed { $_[0]->set('everconfirmed', $_[1]); }
+sub _set_everconfirmed { $_[0]->set('everconfirmed', $_[1]); }
+
sub set_flags {
- my ($self, $flags, $new_flags) = @_;
- Bugzilla::Hook::process('bug_set_flags', { bug => $self, flags => $flags, new_flags => $new_flags });
- Bugzilla::Flag->set_flag($self, $_) foreach (@$flags, @$new_flags);
+ my ($self, $flags, $new_flags) = @_;
+ Bugzilla::Hook::process('bug_set_flags',
+ {bug => $self, flags => $flags, new_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 $invalid_groups;
- my @idlist = ($self->id);
- push(@idlist, map { $_->id } @{ $params->{other_bugs} })
- if $params->{other_bugs};
- @idlist = uniq @idlist;
-
- my $verified = $params->{product_change_confirmed};
-
- # BMO - if everything is ok then we can skip the verfication page when using bug_modal
- if (Bugzilla->input_params->{format} // '' eq 'modal'
- && !$verified
- && $component_ok
- && $version_ok
- && $milestone_ok
- ) {
- $invalid_groups = $self->get_invalid_groups({ bug_ids => \@idlist, product => $product });
- my $has_invalid_group = 0;
- foreach my $group (@$invalid_groups) {
- if (any { $_ eq $group->name } @{ $params->{groups}->{add} }) {
- $has_invalid_group = 1;
- last;
- }
- }
- $verified =
- # always check for invalid groups
- !$has_invalid_group
- # never skip verification when changing multiple bugs
- && scalar(@idlist) == 1
- # ensure the user has seen the group ui for private bugs
- && (!@{ $self->groups_in } || Bugzilla->input_params->{group_verified});
- }
+ my ($self, $name, $params) = @_;
+ my $old_product = $self->product_obj;
+ my $product = $self->_check_product($name);
- 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 $product_changed = 0;
+ if ($old_product->id != $product->id) {
+ $self->{product_id} = $product->id;
+ $self->{product} = $product->name;
+ $self->{product_obj} = $product;
- if (!$verified) {
- $vars{verify_bug_groups} = 1;
- $vars{old_groups} = $invalid_groups || $self->get_invalid_groups({ bug_ids => \@idlist, product => $product });
- }
+ # For update()
+ $self->{_old_product_name} = $old_product->name;
- if (%vars) {
- $vars{product} = $product;
- $vars{bug} = $self;
- require Bugzilla::Error::Template;
- die Bugzilla::Error::Template->new(
- file => "bug/process/verify-new-product.html.tmpl",
- vars => \%vars
- );
- }
+ # 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 {
- # 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);
- }
+ # Have to set this directly to bypass the validators.
+ $self->{target_milestone} = $product->default_milestone;
}
- 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 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 $invalid_groups;
+ my @idlist = ($self->id);
+ push(@idlist, map { $_->id } @{$params->{other_bugs}}) if $params->{other_bugs};
+ @idlist = uniq @idlist;
+
+ my $verified = $params->{product_change_confirmed};
- # Make sure the bug is in all the mandatory groups for the new product.
- foreach my $group (@{$product->groups_mandatory}) {
- $self->add_group($group);
+# BMO - if everything is ok then we can skip the verfication page when using bug_modal
+ if (Bugzilla->input_params->{format}
+ // '' eq 'modal' && !$verified && $component_ok && $version_ok && $milestone_ok)
+ {
+ $invalid_groups
+ = $self->get_invalid_groups({bug_ids => \@idlist, product => $product});
+ my $has_invalid_group = 0;
+ foreach my $group (@$invalid_groups) {
+ if (any { $_ eq $group->name } @{$params->{groups}->{add}}) {
+ $has_invalid_group = 1;
+ last;
}
+ }
+ $verified =
+
+ # always check for invalid groups
+ !$has_invalid_group
+
+ # never skip verification when changing multiple bugs
+ && scalar(@idlist) == 1
+
+ # ensure the user has seen the group ui for private bugs
+ && (!@{$self->groups_in} || Bugzilla->input_params->{group_verified});
+ }
+
+ 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;
+ $vars{old_groups} = $invalid_groups
+ || $self->get_invalid_groups({bug_ids => \@idlist, product => $product});
+ }
+
+ if (%vars) {
+ $vars{product} = $product;
+ $vars{bug} = $self;
+ require Bugzilla::Error::Template;
+ die Bugzilla::Error::Template->new(
+ file => "bug/process/verify-new-product.html.tmpl",
+ vars => \%vars
+ );
+ }
+ }
+ 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);
+ }
+ }
+
+ 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);
+ }
+ }
+
+ # 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;
+ return $product_changed;
}
sub set_qa_contact {
- my ($self, $value) = @_;
- $self->set('qa_contact', $value);
- # Store the old QA contact. check_can_change_field() needs it.
- if ($self->{'qa_contact_obj'}) {
- $self->{'_old_qa_contact'} = $self->{'qa_contact_obj'}->id;
- }
- delete $self->{'qa_contact_obj'};
+ my ($self, $value) = @_;
+ $self->set('qa_contact', $value);
+
+ # Store the old QA contact. check_can_change_field() needs it.
+ if ($self->{'qa_contact_obj'}) {
+ $self->{'_old_qa_contact'} = $self->{'qa_contact_obj'}->id;
+ }
+ delete $self->{'qa_contact_obj'};
}
+
sub reset_qa_contact {
- my $self = shift;
- my $comp = $self->component_obj;
- $self->set_qa_contact($comp->default_qa_contact);
+ my $self = shift;
+ my $comp = $self->component_obj;
+ $self->set_qa_contact($comp->default_qa_contact);
}
sub set_remaining_time { $_[0]->set('remaining_time', $_[1]); }
+
# Used only when closing a bug or moving between closed states.
sub _zero_remaining_time { $_[0]->{'remaining_time'} = 0; }
sub set_reporter_accessible { $_[0]->set('reporter_accessible', $_[1]); }
+
sub set_resolution {
- my ($self, $value, $params) = @_;
+ my ($self, $value, $params) = @_;
- my $old_res = $self->resolution;
- $self->set('resolution', $value);
- delete $self->{choices};
- my $new_res = $self->resolution;
+ 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();
- }
- # Duplicates should have no remaining time left.
- elsif ($new_res eq 'DUPLICATE' && $self->remaining_time != 0) {
- $self->_zero_remaining_time();
- }
+ 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]); }
@@ -3120,355 +3225,374 @@ 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;
}
# $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);
}
# There was a lot of duplicate code when I wrote this as three separate
# functions, so I just combined them all into one. This is also easier for
# process_bug to use.
sub modify_keywords {
- my ($self, $keywords, $action) = @_;
-
- $action ||= 'set';
- if (!grep($action eq $_, qw(add remove set))) {
- $action = 'set';
- }
-
- $keywords = $self->_check_keywords($keywords);
-
- my (@result, $any_changes);
- if ($action eq 'set') {
- @result = @$keywords;
- # Check if anything was added or removed.
- my @old_ids = map { $_->id } @{$self->keyword_objects};
- my @new_ids = map { $_->id } @result;
- my ($removed, $added) = diff_arrays(\@old_ids, \@new_ids);
- $any_changes = scalar @$removed || scalar @$added;
+ my ($self, $keywords, $action) = @_;
+
+ $action ||= 'set';
+ if (!grep($action eq $_, qw(add remove set))) {
+ $action = 'set';
+ }
+
+ $keywords = $self->_check_keywords($keywords);
+
+ my (@result, $any_changes);
+ if ($action eq 'set') {
+ @result = @$keywords;
+
+ # Check if anything was added or removed.
+ my @old_ids = map { $_->id } @{$self->keyword_objects};
+ my @new_ids = map { $_->id } @result;
+ my ($removed, $added) = diff_arrays(\@old_ids, \@new_ids);
+ $any_changes = scalar @$removed || scalar @$added;
+ }
+ else {
+ # We're adding or deleting specific keywords.
+ my %keys = map { $_->id => $_ } @{$self->keyword_objects};
+ if ($action eq 'add') {
+ $keys{$_->id} = $_ foreach @$keywords;
}
else {
- # We're adding or deleting specific keywords.
- my %keys = map {$_->id => $_} @{$self->keyword_objects};
- if ($action eq 'add') {
- $keys{$_->id} = $_ foreach @$keywords;
- }
- else {
- delete $keys{$_->id} foreach @$keywords;
- }
- @result = values %keys;
- $any_changes = scalar @$keywords;
+ delete $keys{$_->id} foreach @$keywords;
}
- # Make sure we retain the sort order.
- @result = sort {lc($a->name) cmp lc($b->name)} @result;
+ @result = values %keys;
+ $any_changes = scalar @$keywords;
+ }
- 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 });
- }
+ # Make sure we retain the sort order.
+ @result = sort { lc($a->name) cmp lc($b->name) } @result;
- $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) = @_;
-
- # 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;
-
- # If the bug is already in this group, then there is nothing to do.
- return if $self->in_group($group);
-
- # BMO : allow bugs to be always placed into some groups by the bug's
- # reporter, or by users with editbugs
- my $user = Bugzilla->user;
- if (!$self->product_obj->group_always_settable($group)
- || ($self->{reporter_id} != $user->id && !$user->in_group('editbugs')))
- {
- # 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 (!$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 ($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'
+ };
+
+ $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);
+
+ # BMO : allow bugs to be always placed into some groups by the bug's
+ # reporter, or by users with editbugs
+ my $user = Bugzilla->user;
+ if (!$self->product_obj->group_always_settable($group)
+ || ($self->{reporter_id} != $user->id && !$user->in_group('editbugs')))
+ {
+ # 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 (!$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);
}
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);
-
- $input = trim($input);
- return if !$input;
-
- my ($class, $uri) = Bugzilla::BugUrl->class_for($input);
-
- my $params = { value => $uri, bug_id => $self, class => $class };
- $class->check_required_create_fields($params);
+ my ($self, $input, $skip_recursion) = @_;
- my $field_values = $class->run_create_validators($params);
- my $value = $field_values->{value}->as_string;
- trick_taint($value);
- $field_values->{value} = $value;
+ # This is needed by xt/search.t.
+ $input = $input->name if blessed($input);
- # We only add the new URI if it hasn't been added yet. URIs are
- # case-sensitive, but most of our DBs are case-insensitive, so we do
- # this check case-insensitively.
- if (!grep { lc($_->name) eq lc($value) } @{ $self->see_also }) {
- my $privs;
- my $can = $self->check_can_change_field('see_also', '', $value, \$privs);
- if (!$can) {
- ThrowUserError('illegal_change', { field => 'see_also',
- newvalue => $value,
- privs => $privs });
- }
- # If this is a link to a local bug then save the
- # ref bug id for sending changes email.
- my $ref_bug = delete $field_values->{ref_bug};
- if ($class->isa('Bugzilla::BugUrl::Bugzilla::Local')
- and !$skip_recursion
- and $ref_bug->check_can_change_field('see_also', '', $self->id, \$privs))
- {
- $ref_bug->add_see_also($self->id, 'skip_recursion');
- push @{ $self->{_update_ref_bugs} }, $ref_bug;
- push @{ $self->{see_also_changes} }, $ref_bug->id;
- }
- push @{ $self->{see_also} }, bless ($field_values, $class);
- }
-}
+ $input = trim($input);
+ return if !$input;
-sub remove_see_also {
- my ($self, $url, $skip_recursion) = @_;
- my $see_also = $self->see_also;
+ my ($class, $uri) = Bugzilla::BugUrl->class_for($input);
- # This is needed by xt/search.t.
- $url = $url->name if blessed($url);
+ my $params = {value => $uri, bug_id => $self, class => $class};
+ $class->check_required_create_fields($params);
- my ($removed_bug_url, $new_see_also) =
- part { lc($_->name) ne lc($url) } @$see_also;
+ my $field_values = $class->run_create_validators($params);
+ my $value = $field_values->{value}->as_string;
+ trick_taint($value);
+ $field_values->{value} = $value;
+ # We only add the new URI if it hasn't been added yet. URIs are
+ # case-sensitive, but most of our DBs are case-insensitive, so we do
+ # this check case-insensitively.
+ if (!grep { lc($_->name) eq lc($value) } @{$self->see_also}) {
my $privs;
- my $can = $self->check_can_change_field('see_also', $see_also, $new_see_also, \$privs);
+ my $can = $self->check_can_change_field('see_also', '', $value, \$privs);
if (!$can) {
- ThrowUserError('illegal_change', { field => 'see_also',
- oldvalue => $url,
- privs => $privs });
+ ThrowUserError('illegal_change',
+ {field => 'see_also', newvalue => $value, privs => $privs});
}
- # Since we remove also the url from the referenced bug,
- # we need to notify changes for that bug too.
- $removed_bug_url = $removed_bug_url->[0];
- if (!$skip_recursion and $removed_bug_url
- and $removed_bug_url->isa('Bugzilla::BugUrl::Bugzilla::Local')
- and $removed_bug_url->ref_bug_url)
+ # If this is a link to a local bug then save the
+ # ref bug id for sending changes email.
+ my $ref_bug = delete $field_values->{ref_bug};
+ if ( $class->isa('Bugzilla::BugUrl::Bugzilla::Local')
+ and !$skip_recursion
+ and $ref_bug->check_can_change_field('see_also', '', $self->id, \$privs))
{
- my $ref_bug
- = Bugzilla::Bug->check($removed_bug_url->ref_bug_url->bug_id);
+ $ref_bug->add_see_also($self->id, 'skip_recursion');
+ push @{$self->{_update_ref_bugs}}, $ref_bug;
+ push @{$self->{see_also_changes}}, $ref_bug->id;
+ }
+ push @{$self->{see_also}}, bless($field_values, $class);
+ }
+}
- if (Bugzilla->user->can_edit_product($ref_bug->product_id)
- and $ref_bug->check_can_change_field('see_also', $self->id, '', \$privs))
- {
- my $self_url = $removed_bug_url->local_uri($self->id);
- $ref_bug->remove_see_also($self_url, 'skip_recursion');
- push @{ $self->{_update_ref_bugs} }, $ref_bug;
- push @{ $self->{see_also_changes} }, $ref_bug->id;
- }
+sub remove_see_also {
+ my ($self, $url, $skip_recursion) = @_;
+ my $see_also = $self->see_also;
+
+ # This is needed by xt/search.t.
+ $url = $url->name if blessed($url);
+
+ my ($removed_bug_url, $new_see_also)
+ = part { lc($_->name) ne lc($url) } @$see_also;
+
+ my $privs;
+ my $can = $self->check_can_change_field('see_also', $see_also, $new_see_also,
+ \$privs);
+ if (!$can) {
+ ThrowUserError('illegal_change',
+ {field => 'see_also', oldvalue => $url, privs => $privs});
+ }
+
+ # Since we remove also the url from the referenced bug,
+ # we need to notify changes for that bug too.
+ $removed_bug_url = $removed_bug_url->[0];
+ if ( !$skip_recursion
+ and $removed_bug_url
+ and $removed_bug_url->isa('Bugzilla::BugUrl::Bugzilla::Local')
+ and $removed_bug_url->ref_bug_url)
+ {
+ my $ref_bug = Bugzilla::Bug->check($removed_bug_url->ref_bug_url->bug_id);
+
+ if (Bugzilla->user->can_edit_product($ref_bug->product_id)
+ and $ref_bug->check_can_change_field('see_also', $self->id, '', \$privs))
+ {
+ my $self_url = $removed_bug_url->local_uri($self->id);
+ $ref_bug->remove_see_also($self_url, 'skip_recursion');
+ push @{$self->{_update_ref_bugs}}, $ref_bug;
+ push @{$self->{see_also_changes}}, $ref_bug->id;
}
+ }
- $self->{see_also} = $new_see_also || [];
+ $self->{see_also} = $new_see_also || [];
}
sub add_tag {
- my ($self, $tag) = @_;
- my $dbh = Bugzilla->dbh;
- my $user = Bugzilla->user;
- $tag = $self->_check_tag_name($tag);
+ my ($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};
}
#####################################################################
@@ -3478,31 +3602,31 @@ sub tags {
# These are accessors that don't need to access the database.
# Keep them in alphabetical order.
-sub alias { return $_[0]->{alias} }
-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 alias { return $_[0]->{alias} }
+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
@@ -3521,668 +3645,691 @@ 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 him 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 him 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 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});
- return $self->{'attachments'};
+ $self->{'attachments'}
+ = Bugzilla::Attachment->get_attachments_by_bug($self, {preload => 1});
+ 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'};
- return $self->{'assigned_to_obj'}
- = new Bugzilla::User({ id => $self->{'assigned_to'}, cache => 1 });
+ my ($self) = @_;
+ return $self->{'assigned_to_obj'} if exists $self->{'assigned_to_obj'};
+ $self->{'assigned_to'} = 0 if $self->{'error'};
+ return $self->{'assigned_to_obj'}
+ = new Bugzilla::User({id => $self->{'assigned_to'}, cache => 1});
}
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
+ );
- $self->{'cc'} = undef if !scalar(@{$self->{'cc'}});
+ $self->{'cc'} = undef if !scalar(@{$self->{'cc'}});
- 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 $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 $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 $self->{classification_id} if exists $self->{classification_id};
- return 0 if $self->{error};
- ($self->{classification_id}) = Bugzilla->dbh->selectrow_array(
- 'SELECT classification_id FROM products WHERE id = ?',
- undef, $self->{product_id});
- return $self->{classification_id};
+ my ($self) = @_;
+ return $self->{classification_id} if exists $self->{classification_id};
+ return 0 if $self->{error};
+ ($self->{classification_id})
+ = Bugzilla->dbh->selectrow_array(
+ 'SELECT classification_id FROM products WHERE id = ?',
+ undef, $self->{product_id});
+ return $self->{classification_id};
}
sub classification {
- my ($self) = @_;
- return $self->{classification} if exists $self->{classification};
- return '' if $self->{error};
- ($self->{classification}) = Bugzilla->dbh->selectrow_array(
- 'SELECT name FROM classifications WHERE id = ?',
- undef, $self->classification_id);
- return $self->{classification};
+ my ($self) = @_;
+ return $self->{classification} if exists $self->{classification};
+ return '' if $self->{error};
+ ($self->{classification})
+ = Bugzilla->dbh->selectrow_array(
+ 'SELECT name FROM classifications WHERE id = ?',
+ undef, $self->classification_id);
+ return $self->{classification};
}
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,
- active_or_has_flags => $self->bug_id };
+ my $vars = {
+ target_type => 'bug',
+ product_id => $self->{product_id},
+ component_id => $self->{component_id},
+ bug_id => $self->bug_id,
+ active_or_has_flags => $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;
- return is_open_state($self->{bug_status}) ? 1 : 0;
+ my $self = shift;
+ return is_open_state($self->{bug_status}) ? 1 : 0;
}
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 has_keyword {
- my ($self, $keyword) = @_;
- $keyword = lc($keyword);
- return any { lc($_->name) eq $keyword } @{ $self->keyword_objects };
+ my ($self, $keyword) = @_;
+ $keyword = lc($keyword);
+ return any { lc($_->name) eq $keyword } @{$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;
- foreach my $comment (@{ $self->{'comments'} }) {
- $comment->{count} = $count++;
- $comment->{bug} = $self;
- weaken($comment->{bug});
- }
- # Some bugs may have no comments when upgrading old installations.
- Bugzilla::Comment->preload($self->{'comments'}) if @{ $self->{'comments'} };
- # BMO - for comment deletion support
- Bugzilla::Hook::process('bug_comments',
- { bug => $self, comments => $self->{'comments'} });
- }
- return unless defined wantarray;
-
- 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);
- }
- }
+ my ($self, $params) = @_;
+ return [] if $self->{'error'};
+ $params ||= {};
- if ($params->{after}) {
- my $from = datetime_from($params->{after});
- @comments = grep { datetime_from($_->creation_ts) > $from } @comments;
+ if (!defined $self->{'comments'}) {
+ $self->{'comments'} = Bugzilla::Comment->match({bug_id => $self->id});
+ my $count = 0;
+ foreach my $comment (@{$self->{'comments'}}) {
+ $comment->{count} = $count++;
+ $comment->{bug} = $self;
+ weaken($comment->{bug});
}
- if ($params->{to}) {
- my $to = datetime_from($params->{to});
- @comments = grep { datetime_from($_->creation_ts) <= $to } @comments;
+
+ # Some bugs may have no comments when upgrading old installations.
+ Bugzilla::Comment->preload($self->{'comments'}) if @{$self->{'comments'}};
+
+ # BMO - for comment deletion support
+ Bugzilla::Hook::process('bug_comments',
+ {bug => $self, comments => $self->{'comments'}});
+ }
+ return unless defined wantarray;
+
+ 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);
}
- return \@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 comment_count {
- my ($self) = @_;
- return $self->{comment_count} if $self->{comment_count};
- my $dbh = Bugzilla->dbh;
- return $self->{comment_count} =
- $dbh->selectrow_array('SELECT COUNT(longdescs.comment_id)
+ my ($self) = @_;
+ return $self->{comment_count} if $self->{comment_count};
+ my $dbh = Bugzilla->dbh;
+ return $self->{comment_count} = $dbh->selectrow_array(
+ 'SELECT COUNT(longdescs.comment_id)
FROM longdescs
- WHERE longdescs.bug_id = ?',
- undef, $self->id);
+ WHERE longdescs.bug_id = ?', undef, $self->id
+ );
}
# 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'};
-
- if (Bugzilla->params->{'useqacontact'} && $self->{'qa_contact'}) {
- $self->{'qa_contact_obj'}
- = new Bugzilla::User({ id => $self->{'qa_contact'}, cache => 1 });
- } else {
- # XXX - This is somewhat inconsistent with the assignee/reporter
- # methods, which will return an empty User if they get a 0.
- # However, we're keeping it this way now, for backwards-compatibility.
- $self->{'qa_contact_obj'} = undef;
- }
- return $self->{'qa_contact_obj'};
+ 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 {
+ # XXX - This is somewhat inconsistent with the assignee/reporter
+ # methods, which will return an empty User if they get a 0.
+ # However, we're keeping it this way now, for backwards-compatibility.
+ $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'};
- return $self->{'reporter'}
- = new Bugzilla::User({ id => $self->{'reporter_id'}, cache => 1 });
+ my ($self) = @_;
+ return $self->{'reporter'} if exists $self->{'reporter'};
+ $self->{'reporter_id'} = 0 if $self->{'error'};
+ return $self->{'reporter'}
+ = new Bugzilla::User({id => $self->{'reporter_id'}, cache => 1});
}
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 $self = shift;
- return [] if $self->{'error'};
- return $self->{'statuses_available'}
- if defined $self->{'statuses_available'};
+ my $self = shift;
+ return [] if $self->{'error'};
+ return $self->{'statuses_available'} if defined $self->{'statuses_available'};
- my @statuses = @{ $self->status->can_change_to };
+ my @statuses = @{$self->status->can_change_to};
- # UNCONFIRMED is only a valid status if it is enabled in this product.
- if (!$self->product_obj->allows_unconfirmed) {
- @statuses = grep { $_->name ne 'UNCONFIRMED' } @statuses;
- }
+ # UNCONFIRMED is only a valid status if it is enabled in this product.
+ if (!$self->product_obj->allows_unconfirmed) {
+ @statuses = grep { $_->name ne 'UNCONFIRMED' } @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 @available;
+ foreach my $status (@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);
- }
+ # 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);
+ }
- $self->{'statuses_available'} = \@available;
- return $self->{'statuses_available'};
+ # 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->{'statuses_available'} = \@available;
+ return $self->{'statuses_available'};
}
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'};
+ 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'});
+
+ my $rows = $sth->fetchall_arrayref();
+ foreach my $row (@$rows) {
+ my ($groupid, $name, $description, $ison, $ingroup, $isactive, $membercontrol,
+ $othercontrol)
+ = @$row;
+
+ $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'});
-
- my $rows = $sth->fetchall_arrayref();
- foreach my $row (@$rows) {
- my ($groupid, $name, $description, $ison, $ingroup, $isactive,
- $membercontrol, $othercontrol) = @$row;
-
- $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
}
- }
-
- # BMO: if required, hack in groups exposed by -visible membership
- # (eg mozilla-employee-confidential-visible), so reporters can add the
- # bug to a group on show_bug.
- # if the bug is already in the group, the user will not be able to remove
- # it unless they are a true group member.
- my $user = Bugzilla->user;
- if ($self->{'reporter_id'} == $user->id) {
- foreach my $group (@{ $user->groups }) {
- # map from -visible group to the real one
- my $group_name = $group->name;
- next unless $group_name =~ s/-visible$//;
- next if $user->in_group($group_name);
- $group = Bugzilla::Group->new({ name => $group_name, cache => 1 });
-
- # only show the group if it's visible to normal members
- my ($member_control) = $dbh->selectrow_array(
- "SELECT membercontrol
+ );
+ }
+ }
+
+ # BMO: if required, hack in groups exposed by -visible membership
+ # (eg mozilla-employee-confidential-visible), so reporters can add the
+ # bug to a group on show_bug.
+ # if the bug is already in the group, the user will not be able to remove
+ # it unless they are a true group member.
+ my $user = Bugzilla->user;
+ if ($self->{'reporter_id'} == $user->id) {
+ foreach my $group (@{$user->groups}) {
+
+ # map from -visible group to the real one
+ my $group_name = $group->name;
+ next unless $group_name =~ s/-visible$//;
+ next if $user->in_group($group_name);
+ $group = Bugzilla::Group->new({name => $group_name, cache => 1});
+
+ # only show the group if it's visible to normal members
+ my ($member_control) = $dbh->selectrow_array(
+ "SELECT membercontrol
FROM groups
LEFT JOIN group_control_map
ON group_control_map.group_id = groups.id
AND group_control_map.product_id = ?
- WHERE groups.id = ?",
- undef,
- $self->{product_id}, $group->id
- );
-
- if (
- $member_control
- && $member_control == CONTROLMAPSHOWN
- && !grep { $_->{bit} == $group->id } @groups)
- {
- push(@groups, {
- bit => $group->id,
- name => $group->name,
- ison => 0,
- ingroup => 1,
- mandatory => 0,
- description => $group->description,
- });
- }
- }
+ WHERE groups.id = ?", undef, $self->{product_id}, $group->id
+ );
+
+ if ( $member_control
+ && $member_control == CONTROLMAPSHOWN
+ && !grep { $_->{bit} == $group->id } @groups)
+ {
+ push(
+ @groups,
+ {
+ bit => $group->id,
+ name => $group->name,
+ ison => 0,
+ ingroup => 1,
+ mandatory => 0,
+ description => $group->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 $self = shift;
+ return $self->{'user'} if exists $self->{'user'};
+ return {} if $self->{'error'};
- my $user = Bugzilla->user;
+ my $user = Bugzilla->user;
- my $prod_id = $self->{'product_id'};
+ my $prod_id = $self->{'product_id'};
- my $unknown_privileges = $user->in_group('editbugs', $prod_id);
- my $canedit = $unknown_privileges
- || $user->id == $self->{'assigned_to'}
- || (Bugzilla->params->{'useqacontact'}
- && $self->{'qa_contact'}
- && $user->id == $self->{'qa_contact'});
- my $canconfirm = $unknown_privileges
- || $user->in_group('canconfirm', $prod_id);
- my $isreporter = $user->id
- && $user->id == $self->{reporter_id};
+ my $unknown_privileges = $user->in_group('editbugs', $prod_id);
+ my $canedit
+ = $unknown_privileges
+ || $user->id == $self->{'assigned_to'}
+ || (Bugzilla->params->{'useqacontact'}
+ && $self->{'qa_contact'}
+ && $user->id == $self->{'qa_contact'});
+ my $canconfirm = $unknown_privileges || $user->in_group('canconfirm', $prod_id);
+ my $isreporter = $user->id && $user->id == $self->{reporter_id};
- $self->{'user'} = {canconfirm => $canconfirm,
- canedit => $canedit,
- isreporter => $isreporter};
- return $self->{'user'};
+ $self->{'user'}
+ = {canconfirm => $canconfirm, canedit => $canedit, isreporter => $isreporter};
+ 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 $name = $self->$key;
- $choices{$key} = [grep { $_->is_active || $_->name eq $name } @{ $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 $name = $self->$key;
+ $choices{$key}
+ = [grep { $_->is_active || $_->name eq $name } @{$choices{$key}}];
+ }
+
+ $self->{'choices'} = \%choices;
+ return $self->{'choices'};
}
# Convenience Function. If you need speed, use this. If you need
@@ -4191,12 +4338,12 @@ 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) = @_;
- return undef unless Bugzilla->params->{"usebugaliases"};
- my $dbh = Bugzilla->dbh;
- trick_taint($alias);
- return $dbh->selectrow_array(
- "SELECT bug_id FROM bugs WHERE alias = ?", undef, $alias);
+ my ($alias) = @_;
+ return undef unless Bugzilla->params->{"usebugaliases"};
+ my $dbh = Bugzilla->dbh;
+ trick_taint($alias);
+ return $dbh->selectrow_array("SELECT bug_id FROM bugs WHERE alias = ?",
+ undef, $alias);
}
#####################################################################
@@ -4206,26 +4353,31 @@ 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);
- }
+ 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);
+ }
- Bugzilla::Hook::process('bug_editable_bug_fields', { fields => \@fields });
+ Bugzilla::Hook::process('bug_editable_bug_fields', {fields => \@fields});
- # Sorted because the old @::log_columns variable, which this replaces,
- # was sorted.
- return sort(@fields);
+ # Sorted because the old @::log_columns variable, which this replaces,
+ # was sorted.
+ return sort(@fields);
}
# XXX - When Bug::update() will be implemented, we should make this routine
@@ -4233,84 +4385,86 @@ 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) = @_;
- my %bug_map;
- # 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;
- }
+ my ($self, $bug_ids) = @_;
+ my %bug_map;
+
+ # 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;
}
- my $bugs = $self->new_from_list(\@missing_ids);
- foreach my $bug (@$bugs) {
- $bug_map{$bug->id} = $bug;
+ else {
+ push @missing_ids, $bug_id;
}
- return [ map { $bug_map{$_} } @$bug_ids ];
+ }
+ my $bugs = $self->new_from_list(\@missing_ids);
+ foreach my $bug (@$bugs) {
+ $bug_map{$bug->id} = $bug;
+ }
+ 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 GetBugActivity {
- my ($bug_id, $attach_id, $starttime, $include_comment_tags) = @_;
- my $dbh = Bugzilla->dbh;
-
- # Arguments passed to the SQL query.
- my @args = ($bug_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 (!Bugzilla->user->is_insider)
- {
- $suppjoins = "LEFT JOIN attachments
+ my ($bug_id, $attach_id, $starttime, $include_comment_tags) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ # Arguments passed to the SQL query.
+ my @args = ($bug_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 (!Bugzilla->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
@@ -4323,24 +4477,26 @@ sub GetBugActivity {
$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,
@@ -4352,213 +4508,227 @@ sub GetBugActivity {
$datepart
$suppwhere
";
- push @args, $bug_id;
- push @args, $starttime if defined $starttime;
+ push @args, $bug_id;
+ push @args, $starttime if defined $starttime;
+ }
+
+ $query .= "ORDER BY bug_when, comment_id";
+
+ my $list = $dbh->selectall_arrayref($query, undef, @args);
+
+ my @operations;
+ my $operation = {};
+ my $changes = [];
+ my $incomplete_data = 0;
+
+ foreach my $entry (@$list) {
+ my ($fieldname, $attachid, $when, $removed, $added, $who, $comment_id)
+ = @$entry;
+ my %change;
+ my $activity_visible = 1;
+
+ # check if the user should see this field's activity
+ if ( $fieldname eq 'remaining_time'
+ || $fieldname eq 'estimated_time'
+ || $fieldname eq 'work_time'
+ || $fieldname eq 'deadline')
+ {
+ $activity_visible = Bugzilla->user->is_timetracker;
+ }
+ elsif ($fieldname eq 'longdescs.isprivate'
+ && !Bugzilla->user->is_insider
+ && $added)
+ {
+ $activity_visible = 0;
+ }
+ else {
+ $activity_visible = 1;
}
- $query .= "ORDER BY bug_when, comment_id";
+ if ($activity_visible) {
- my $list = $dbh->selectall_arrayref($query, undef, @args);
+ # Check for the results of an old Bugzilla data corruption bug
+ if ( ($added eq '?' && $removed eq '?')
+ || ($added =~ /^\? / || $removed =~ /^\? /))
+ {
+ $incomplete_data = 1;
+ }
- my @operations;
- my $operation = {};
- my $changes = [];
- my $incomplete_data = 0;
+ # 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);
- foreach my $entry (@$list) {
- my ($fieldname, $attachid, $when, $removed, $added, $who, $comment_id) = @$entry;
- my %change;
- my $activity_visible = 1;
+ # Create new empty anonymous data structures.
+ $operation = {};
+ $changes = [];
+ }
- # check if the user should see this field's activity
- if ($fieldname eq 'remaining_time'
- || $fieldname eq 'estimated_time'
- || $fieldname eq 'work_time'
- || $fieldname eq 'deadline')
- {
- $activity_visible = Bugzilla->user->is_timetracker;
- }
- elsif ($fieldname eq 'longdescs.isprivate'
- && !Bugzilla->user->is_insider
- && $added)
- {
- $activity_visible = 0;
- }
- else {
- $activity_visible = 1;
- }
+ # If this is the same field as the previoius 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);
+ }
- 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 previoius 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);
- }
- }
+ $operation->{'who'} = $who;
+ $operation->{'when'} = $when;
+ $operation->{'fieldname'} = $change{'fieldname'} = $fieldname;
+ $operation->{'attachid'} = $change{'attachid'} = $attachid;
- if ($operation->{'who'}) {
- $operation->{'changes'} = $changes;
- push (@operations, $operation);
+ $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);
+ return (\@operations, $incomplete_data);
}
sub _join_activity_entries {
- my ($field, $current_change, $new_change) = @_;
- # We need to insert characters as these were removed by old
- # LogActivityEntry code.
-
- return $new_change if $current_change eq '';
-
- # Buglists and see_also need the comma restored
- if ($field eq 'dependson' || $field eq 'blocked' || $field eq 'see_also') {
- if (substr($new_change, 0, 1) eq ',' || substr($new_change, 0, 1) eq ' ') {
- return $current_change . $new_change;
- } else {
- return $current_change . ', ' . $new_change;
- }
- }
+ my ($field, $current_change, $new_change) = @_;
- # Assume bug_file_loc contain a single url, don't insert a delimiter
- if ($field eq 'bug_file_loc') {
- return $current_change . $new_change;
- }
+ # We need to insert characters as these were removed by old
+ # LogActivityEntry code.
+
+ return $new_change if $current_change eq '';
- # All other fields get a space unless the first character of the second
- # string is a comma or space
+ # Buglists and see_also need the comma restored
+ if ($field eq 'dependson' || $field eq 'blocked' || $field eq 'see_also') {
if (substr($new_change, 0, 1) eq ',' || substr($new_change, 0, 1) eq ' ') {
- return $current_change . $new_change;
- } else {
- return $current_change . ' ' . $new_change;
+ return $current_change . $new_change;
+ }
+ else {
+ return $current_change . ', ' . $new_change;
}
+ }
+
+ # Assume bug_file_loc contain a single url, don't insert a delimiter
+ if ($field eq 'bug_file_loc') {
+ return $current_change . $new_change;
+ }
+
+ # All other fields get a space unless the first character of the second
+ # string is a comma or space
+ if (substr($new_change, 0, 1) eq ',' || substr($new_change, 0, 1) eq ' ') {
+ return $current_change . $new_change;
+ }
+ else {
+ return $current_change . ' ' . $new_change;
+ }
}
# Update the bugs_activity table to reflect changes made in bugs.
sub LogActivityEntry {
- my ($i, $col, $removed, $added, $whoid, $timestamp, $comment_id,
- $attach_id) = @_;
- my $dbh = Bugzilla->dbh;
- # in the case of CCs, deps, and keywords, there's a possibility that someone
- # might try to add or remove a lot of them at once, which might take more
- # 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($col);
- $dbh->do(
- "INSERT INTO bugs_activity
+ my ($i, $col, $removed, $added, $whoid, $timestamp, $comment_id, $attach_id)
+ = @_;
+ my $dbh = Bugzilla->dbh;
+
+ # in the case of CCs, deps, and keywords, there's a possibility that someone
+ # might try to add or remove a lot of them at once, which might take more
+ # 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($col);
+ $dbh->do(
+ "INSERT INTO bugs_activity
(bug_id, who, bug_when, fieldid, removed, added, comment_id, attach_id)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
- undef,
- ($i, $whoid, $timestamp, $fieldid, $removestr, $addstr, $comment_id,
- $attach_id));
- }
+ undef,
+ (
+ $i, $whoid, $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 ($params, $except) = @_;
- my %field_values;
- foreach my $field (keys %$params) {
- my $field_name;
- if ($except->{$field}) {
- $field_name = $field;
- }
- else {
- $field_name = FIELD_MAP->{$field} || $field;
- }
- $field_values{$field_name} = $params->{$field};
+ my %field_values;
+ foreach my $field (keys %$params) {
+ my $field_name;
+ if ($except->{$field}) {
+ $field_name = $field;
+ }
+ else {
+ $field_name = FIELD_MAP->{$field} || $field;
}
- return \%field_values;
+ $field_values{$field_name} = $params->{$field};
+ }
+ return \%field_values;
}
# Return the groups which are no longer valid in the specified product
sub get_invalid_groups {
- my ($invocant, $params) = @_;
- my @idlist = @{ $params->{bug_ids} };
- my $product = $params->{product};
- my $gids = Bugzilla->dbh->selectcol_arrayref(
- 'SELECT bgm.group_id
+ my ($invocant, $params) = @_;
+ my @idlist = @{$params->{bug_ids}};
+ my $product = $params->{product};
+ my $gids = Bugzilla->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
@@ -4567,10 +4737,11 @@ sub get_invalid_groups {
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));
- return Bugzilla::Group->new_from_list($gids);
+ . Bugzilla->user->groups_as_string . '))
+ OR gcm.othercontrol != ?) )', undef,
+ (@idlist, $product->id, CONTROLMAPNA, CONTROLMAPNA)
+ );
+ return Bugzilla::Group->new_from_list($gids);
}
################################################################################
@@ -4590,163 +4761,186 @@ sub get_invalid_groups {
# $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;
-
- $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;
+ 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;
+
# 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 him 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.
- if ( grep($_ eq $field, TIMETRACKING_FIELDS) ) {
- 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;
+ 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 him 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.
+ if (grep($_ eq $field, TIMETRACKING_FIELDS)) {
+ if (!$user->is_timetracker) {
+ $$PrivilegesRequired = PRIVILEGES_REQUIRED_EMPOWERED;
+ return 0;
+ }
+ }
+
+ # Allow anyone with (product-specific) "editbugs" privs to change anything.
+ if ($user->in_group('editbugs', $self->{'product_id'})) {
+ return 1;
+ }
+
+ # *Only* users with (product-specific) "canconfirm" privs can confirm bugs.
+ if ($self->_changes_everconfirmed($field, $oldvalue, $newvalue)) {
+ $$PrivilegesRequired = PRIVILEGES_REQUIRED_EMPOWERED;
+ return $user->in_group('canconfirm', $self->{'product_id'});
+ }
+
+ # Make sure that a valid bug ID has been given.
+ if (!$self->{'error'}) {
+
+ # Allow the assignee to change anything else.
+ if ( $self->{'assigned_to'} == $user->id
+ || $self->{'_old_assigned_to'} && $self->{'_old_assigned_to'} == $user->id)
+ {
+ return 1;
}
- # *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 him;
+ # 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 him;
- # 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 he 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 he 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;
}
#
@@ -4755,71 +4949,75 @@ 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;
- 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("SELECT $target
+ 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;
+ 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(
+ "SELECT $target
FROM dependencies
- WHERE $me = ?", 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;
- }
- }
+ WHERE $me = ?", 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;
}
@@ -4828,60 +5026,61 @@ 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) {
- next if $field->type == FIELD_TYPE_EXTENSION;
- 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) {
+ next if $field->type == FIELD_TYPE_EXTENSION;
+ my $accessor = $class->_accessor_for($field);
+ my $name = "${class}::" . $field->name;
+ {
+ no strict 'refs';
+ next if defined *{$name};
+ *{$name} = $accessor;
}
+ }
- Bugzilla::Hook::process('bug_create_cf_accessors');
+ Bugzilla::Hook::process('bug_create_cf_accessors');
- 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;
}
sub has_attachment_with_mimetype {
- my ($self, $type) = @_;
- return any { $_->contenttype eq $type } @{ $self->attachments };
+ my ($self, $type) = @_;
+ return any { $_->contenttype eq $type } @{$self->attachments};
}
1;
diff --git a/Bugzilla/BugMail.pm b/Bugzilla/BugMail.pm
index ebfc95d51..d5c1c4c95 100644
--- a/Bugzilla/BugMail.pm
+++ b/Bugzilla/BugMail.pm
@@ -27,16 +27,17 @@ use List::MoreUtils qw(uniq firstidx);
use Sys::Hostname;
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;
}
# This is a bit of a hack, basically keeping the old system()
@@ -49,505 +50,522 @@ sub relationships {
# All the names are email addresses, not userids
# values are scalars, except for cc, which is a list
sub Send {
- my ($id, $forced, $params) = @_;
- $params ||= {};
-
- my $dbh = Bugzilla->dbh;
- my $bug = new Bugzilla::Bug($id);
-
- my $start = $bug->lastdiffed;
- my $end = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
-
- # Bugzilla::User objects of people in various roles. More than one person
- # can 'have' a role, if the person in that role has changed, or people are
- # watching.
- my @assignees = ($bug->assigned_to);
- my @qa_contacts = $bug->qa_contact || ();
-
- my @ccs = @{ $bug->cc_users };
- # Include the people passed in as being in particular roles.
- # This can include people who used to hold those roles.
- # At this point, we don't care if there are duplicates in these arrays.
- my $changer = $forced->{'changer'};
- if ($forced->{'owner'}) {
- push (@assignees, Bugzilla::User->check($forced->{'owner'}));
- }
-
- if ($forced->{'qacontact'}) {
- push (@qa_contacts, Bugzilla::User->check($forced->{'qacontact'}));
- }
-
- if ($forced->{'cc'}) {
- foreach my $cc (@{$forced->{'cc'}}) {
- push(@ccs, Bugzilla::User->check($cc));
- }
- }
- my %user_cache = map { $_->id => $_ } (@assignees, @qa_contacts, @ccs);
-
- my @diffs;
- my @referenced_bugs;
- if (!$start) {
- @diffs = _get_new_bugmail_fields($bug);
- }
-
- if ($params->{dep_only}) {
- my $fields = Bugzilla->fields({ by_name => 1 });
- push(@diffs, { field_name => 'bug_status',
- field_desc => $fields->{bug_status}->description,
- old => $params->{changes}->{bug_status}->[0],
- new => $params->{changes}->{bug_status}->[1],
- login_name => $changer->login,
- blocker => $params->{blocker} },
- { field_name => 'resolution',
- field_desc => $fields->{resolution}->description,
- old => $params->{changes}->{resolution}->[0],
- new => $params->{changes}->{resolution}->[1],
- login_name => $changer->login,
- blocker => $params->{blocker} });
- push(@referenced_bugs, $params->{blocker}->id);
+ 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));
}
- else {
- my ($diffs, $referenced) = _get_diffs($bug, $end, \%user_cache);
- push(@diffs, @$diffs);
- push(@referenced_bugs, @$referenced);
+ }
+ my %user_cache = map { $_->id => $_ } (@assignees, @qa_contacts, @ccs);
+
+ my @diffs;
+ my @referenced_bugs;
+ if (!$start) {
+ @diffs = _get_new_bugmail_fields($bug);
+ }
+
+ if ($params->{dep_only}) {
+ my $fields = Bugzilla->fields({by_name => 1});
+ push(
+ @diffs,
+ {
+ field_name => 'bug_status',
+ field_desc => $fields->{bug_status}->description,
+ old => $params->{changes}->{bug_status}->[0],
+ new => $params->{changes}->{bug_status}->[1],
+ login_name => $changer->login,
+ blocker => $params->{blocker}
+ },
+ {
+ field_name => 'resolution',
+ field_desc => $fields->{resolution}->description,
+ old => $params->{changes}->{resolution}->[0],
+ new => $params->{changes}->{resolution}->[1],
+ login_name => $changer->login,
+ blocker => $params->{blocker}
+ }
+ );
+ push(@referenced_bugs, $params->{blocker}->id);
+ }
+ else {
+ my ($diffs, $referenced) = _get_diffs($bug, $end, \%user_cache);
+ push(@diffs, @$diffs);
+ push(@referenced_bugs, @$referenced);
+ }
+
+ my $comments = $bug->comments({after => $start, to => $end});
+
+ # Skip empty comments.
+ @$comments = grep { $_->type || $_->body =~ /\S/ } @$comments;
+
+ # Add duplicate bug to referenced bug list
+ foreach my $comment (@$comments) {
+ if ($comment->type == CMT_DUPE_OF || $comment->type == CMT_HAS_DUPE) {
+ push(@referenced_bugs, $comment->extra_data);
}
+ }
- my $comments = $bug->comments({ after => $start, to => $end });
- # Skip empty comments.
- @$comments = grep { $_->type || $_->body =~ /\S/ } @$comments;
+ # Add dependencies to referenced bug list on new bugs
+ if (!$start) {
+ push @referenced_bugs, @{$bug->dependson};
+ push @referenced_bugs, @{$bug->blocked};
+ }
- # Add duplicate bug to referenced bug list
- foreach my $comment (@$comments) {
- if ($comment->type == CMT_DUPE_OF || $comment->type == CMT_HAS_DUPE) {
- push(@referenced_bugs, $comment->extra_data);
- }
- }
+ # If no changes have been made, there is no need to process further.
+ return {'sent' => []} unless scalar(@diffs) || scalar(@$comments);
- # Add dependencies to referenced bug list on new bugs
- if (!$start) {
- push @referenced_bugs, @{ $bug->dependson };
- push @referenced_bugs, @{ $bug->blocked };
- }
+ ###########################################################################
+ # Start of email filtering code
+ ###########################################################################
- # If no changes have been made, there is no need to process further.
- return {'sent' => []} unless scalar(@diffs) || scalar(@$comments);
+ # A user_id => roles hash to keep track of people.
+ my %recipients;
+ my %watching;
- ###########################################################################
- # Start of email filtering code
- ###########################################################################
+ # 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.
- # A user_id => roles hash to keep track of people.
- my %recipients;
- my %watching;
+ # CCs
+ $recipients{$_->id}->{+REL_CC} = BIT_DIRECT foreach (@ccs);
- # 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.
+ # Reporter (there's only ever one)
+ $recipients{$bug->reporter->id}->{+REL_REPORTER} = BIT_DIRECT;
- # CCs
- $recipients{$_->id}->{+REL_CC} = BIT_DIRECT foreach (@ccs);
+ # QA Contact
+ if (Bugzilla->params->{'useqacontact'}) {
+ foreach (@qa_contacts) {
- # 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 $_;
- }
+ # QA Contact can be blank; ignore it if so.
+ $recipients{$_->id}->{+REL_QA} = BIT_DIRECT if $_;
}
-
- # 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;
- }
+ }
+
+ # 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;
+ }
}
-
- # 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({ id => $user_id, cache => 1 });
+ }
+
+ # 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({id => $user_id, cache => 1});
+ }
+
+ Bugzilla::Hook::process(
+ 'bugmail_recipients',
+ {
+ bug => $bug,
+ recipients => \%recipients,
+ users => \%user_cache,
+ diffs => \@diffs
}
+ );
- Bugzilla::Hook::process('bugmail_recipients',
- { bug => $bug, recipients => \%recipients,
- users => \%user_cache, diffs => \@diffs });
-
- 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]);
- }
- }
+ if (scalar keys %recipients) {
- # Global watcher
- my @watchers = split(/\s*,\s*/ms, 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');
-
- # Remove duplicate references, and convert to bug objects
- @referenced_bugs = @{ Bugzilla::Bug->new_from_list([uniq @referenced_bugs]) };
-
- foreach my $user_id (keys %recipients) {
- my %rels_which_want;
- my $user = $user_cache{$user_id} ||= new Bugzilla::User({ id => $user_id, cache => 1 });
- # Deleted users must be excluded.
- next unless $user;
-
- # If email notifications are disabled for this account, or the bug
- # is ignored, there is no need to do additional checks.
- next if ($user->email_disabled || $user->is_bug_ignored($id));
-
- if ($user->can_see_bug($id)) {
- # Go through each role the user has and see if they want mail in
- # that role.
- foreach my $relationship (keys %{$recipients{$user_id}}) {
- if ($user->wants_bug_mail($bug,
- $relationship,
- $start ? \@diffs : [],
- $comments,
- $params->{dep_only},
- $changer))
- {
- $rels_which_want{$relationship} =
- $recipients{$user_id}->{$relationship};
- }
- }
- }
+ # Find all those user-watching anyone on the current list, who is not
+ # on it already themselves.
+ my $involved = join(",", keys %recipients);
- 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;
- }
-
- # Make sure the user isn't in the nomail list, and the dep check passed.
- # BMO: never send emails to bugs or .tld addresses. this check needs to
- # happen after the bugmail_recipients hook.
- if ($user->email_enabled && $dep_ok &&
- ($user->login !~ /\.(?:bugs|tld)$/))
- {
- # Don't show summaries for bugs the user can't access, and
- # provide a hook for extensions such as SecureMail to filter
- # this list.
- #
- # We build an array with the short_desc as a separate item to
- # allow extensions to modify the summary without touching the
- # bug object.
- my $referenced_bugs = [];
- foreach my $ref (@{ $user->visible_bugs(\@referenced_bugs) }) {
- push @$referenced_bugs, {
- bug => $ref,
- id => $ref->id,
- short_desc => $ref->short_desc,
- };
- }
- Bugzilla::Hook::process('bugmail_referenced_bugs',
- { updated_bug => $bug,
- referenced_bugs => $referenced_bugs });
-
- 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,
- referenced_bugs => $referenced_bugs,
- dep_only => $params->{dep_only}
- });
- push(@sent, $user->login) if $sent_mail;
- }
- }
- }
+ my $userwatchers = $dbh->selectall_arrayref(
+ "SELECT watcher, watched FROM watch
+ WHERE watched IN ($involved)"
+ );
- # 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;
+ # 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]);
}
-
- 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 $referenced_bugs = $params->{referenced_bugs};
- my $dep_only = $params->{dep_only};
- my $attach_id;
-
- # 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;
+ }
+
+ # Global watcher
+ my @watchers = split(/\s*,\s*/ms, 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');
+
+ # Remove duplicate references, and convert to bug objects
+ @referenced_bugs = @{Bugzilla::Bug->new_from_list([uniq @referenced_bugs])};
+
+ foreach my $user_id (keys %recipients) {
+ my %rels_which_want;
+ my $user = $user_cache{$user_id}
+ ||= new Bugzilla::User({id => $user_id, cache => 1});
+
+ # Deleted users must be excluded.
+ next unless $user;
+
+ # If email notifications are disabled for this account, or the bug
+ # is ignored, there is no need to do additional checks.
+ next if ($user->email_disabled || $user->is_bug_ignored($id));
+
+ if ($user->can_see_bug($id)) {
+
+ # Go through each role the user has and see if they want mail in
+ # that role.
+ foreach my $relationship (keys %{$recipients{$user_id}}) {
+ if ($user->wants_bug_mail(
+ $bug, $relationship, $start ? \@diffs : [],
+ $comments, $params->{dep_only}, $changer
+ ))
+ {
+ $rels_which_want{$relationship} = $recipients{$user_id}->{$relationship};
}
- push(@display_diffs, $diff) if $add_diff;
- $attach_id = $diff->{attach_id} if $diff->{attach_id};
+ }
}
- if (!$user->is_insider) {
- @send_comments = grep { !$_->is_private } @send_comments;
- }
-
- if (!scalar(@display_diffs) && !scalar(@send_comments)) {
- # Whoops, no differences!
- return 0;
+ 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;
+ }
+
+ # Make sure the user isn't in the nomail list, and the dep check passed.
+ # BMO: never send emails to bugs or .tld addresses. this check needs to
+ # happen after the bugmail_recipients hook.
+ if ($user->email_enabled && $dep_ok && ($user->login !~ /\.(?:bugs|tld)$/)) {
+
+ # Don't show summaries for bugs the user can't access, and
+ # provide a hook for extensions such as SecureMail to filter
+ # this list.
+ #
+ # We build an array with the short_desc as a separate item to
+ # allow extensions to modify the summary without touching the
+ # bug object.
+ my $referenced_bugs = [];
+ foreach my $ref (@{$user->visible_bugs(\@referenced_bugs)}) {
+ push @$referenced_bugs,
+ {bug => $ref, id => $ref->id, short_desc => $ref->short_desc,};
+ }
+ Bugzilla::Hook::process('bugmail_referenced_bugs',
+ {updated_bug => $bug, referenced_bugs => $referenced_bugs});
+
+ 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,
+ referenced_bugs => $referenced_bugs,
+ dep_only => $params->{dep_only}
+ });
+ push(@sent, $user->login) if $sent_mail;
+ }
}
+ }
- 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);
- }
+ # When sending bugmail about a blocker being reopened or resolved,
+ # we say nothing about changes in the bug being blocked, so we must
+ # not update lastdiffed in this case.
+ if (!$params->{dep_only}) {
+ $dbh->do('UPDATE bugs SET lastdiffed = ? WHERE bug_id = ?', undef, ($end, $id));
+ $bug->{lastdiffed} = $end;
+ }
- my %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 { user_id_to_login($_) } @$watchingRef;
-
- # BMO: Use field descriptions instead of field names in header
- my @changedfields = uniq map { $_->{field_desc} } @display_diffs;
- my @changedfieldnames = uniq map { $_->{field_name} } @display_diffs;
-
- # BMO: Add a field to indicate when a comment was added
- if (grep($_->type != CMT_ATTACHMENT_CREATED, @send_comments)) {
- push(@changedfields, 'Comment Created');
- push(@changedfieldnames, 'comment');
- }
+ return {'sent' => \@sent};
+}
- # Add attachments.created to changedfields if one or more
- # comments contain information about a new attachment
- if (grep($_->type == CMT_ATTACHMENT_CREATED, @send_comments)) {
- push(@changedfields, 'Attachment Created');
- push(@changedfieldnames, 'attachment.created');
+sub sendMail {
+ my $params = shift;
+
+ my $user = $params->{to};
+ my $bug = $params->{bug};
+ my @send_comments = @{$params->{comments}};
+ my $date = $params->{date};
+ my $changer = $params->{changer};
+ my $watchingRef = $params->{watchers};
+ my @diffs = @{$params->{diffs}};
+ my $relRef = $params->{rels_which_want};
+ my $referenced_bugs = $params->{referenced_bugs};
+ my $dep_only = $params->{dep_only};
+ my $attach_id;
+
+ # Only display changes the user is allowed see.
+ my @display_diffs;
+
+ foreach my $diff (@diffs) {
+ my $add_diff = 0;
+
+ if (grep { $_ eq $diff->{field_name} } TIMETRACKING_FIELDS) {
+ $add_diff = 1 if $user->is_timetracker;
}
-
- my $bugmailtype = "changed";
- $bugmailtype = "new" if !$bug->lastdiffed;
- $bugmailtype = "dep_changed" if $dep_only;
-
- my $vars = {
- date => $date,
- to_user => $user,
- bug => $bug,
- attach_id => $attach_id,
- reasons => \@reasons,
- reasons_watch => \@reasons_watch,
- reasonsheader => join(" ", @headerrel),
- reasonswatchheader => join(" ", @watchingrel),
- changer => $changer,
- diffs => \@display_diffs,
- changedfields => \@changedfields,
- changedfieldnames => \@changedfieldnames,
- new_comments => \@send_comments,
- threadingmarker => build_thread_marker($bug->id, $user->id, !$bug->lastdiffed),
- referenced_bugs => $referenced_bugs,
- bugmailtype => $bugmailtype,
- };
-
- if (Bugzilla->get_param_with_override('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;
+ $attach_id = $diff->{attach_id} if $diff->{attach_id};
+ }
+
+ 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 { user_id_to_login($_) } @$watchingRef;
+
+ # BMO: Use field descriptions instead of field names in header
+ my @changedfields = uniq map { $_->{field_desc} } @display_diffs;
+ my @changedfieldnames = uniq map { $_->{field_name} } @display_diffs;
+
+ # BMO: Add a field to indicate when a comment was added
+ if (grep($_->type != CMT_ATTACHMENT_CREATED, @send_comments)) {
+ push(@changedfields, 'Comment Created');
+ push(@changedfieldnames, 'comment');
+ }
+
+ # 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, 'Attachment Created');
+ push(@changedfieldnames, 'attachment.created');
+ }
+
+ my $bugmailtype = "changed";
+ $bugmailtype = "new" if !$bug->lastdiffed;
+ $bugmailtype = "dep_changed" if $dep_only;
+
+ my $vars = {
+ date => $date,
+ to_user => $user,
+ bug => $bug,
+ attach_id => $attach_id,
+ reasons => \@reasons,
+ reasons_watch => \@reasons_watch,
+ reasonsheader => join(" ", @headerrel),
+ reasonswatchheader => join(" ", @watchingrel),
+ changer => $changer,
+ diffs => \@display_diffs,
+ changedfields => \@changedfields,
+ changedfieldnames => \@changedfieldnames,
+ new_comments => \@send_comments,
+ threadingmarker => build_thread_marker($bug->id, $user->id, !$bug->lastdiffed),
+ referenced_bugs => $referenced_bugs,
+ bugmailtype => $bugmailtype,
+ };
+
+ if (Bugzilla->get_param_with_override('use_mailer_queue')) {
+ enqueue($vars);
+ }
+ else {
+ MessageToMTA(_generate_bugmail($vars));
+ }
+
+ return 1;
}
sub enqueue {
- my ($vars) = @_;
-
- # BMO: allow modification of the email at the time it was generated
- Bugzilla::Hook::process('bugmail_enqueue', { vars => $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});
- }
- }
- foreach my $reference (@{ $vars->{referenced_bugs} }) {
- $reference->{bug} = _flatten_object($reference->{bug});
+ my ($vars) = @_;
+
+ # BMO: allow modification of the email at the time it was generated
+ Bugzilla::Hook::process('bugmail_enqueue', {vars => $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 });
+ }
+ foreach my $reference (@{$vars->{referenced_bugs}}) {
+ $reference->{bug} = _flatten_object($reference->{bug});
+ }
+ 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);
-}
+ }
-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;
+ # generate bugmail and send
+ MessageToMTA(_generate_bugmail($vars), 1);
}
-sub _generate_bugmail {
- my ($vars) = @_;
- my $user = $vars->{to_user};
- my $template = Bugzilla->template_inner($user->setting('lang'));
- my ($msg_text, $msg_html, $msg_header);
-
- $template->process("email/bugmail-header.txt.tmpl", $vars, \$msg_header)
- || ThrowTemplateError($template->error());
-
- $template->process("email/bugmail.txt.tmpl", $vars, \$msg_text)
- || ThrowTemplateError($template->error());
-
- my @parts = (
- Email::MIME->create(
- attributes => {
- content_type => "text/plain",
- },
- body => $msg_text,
- )
- );
- if ($user->setting('email_format') eq 'html') {
- $template->process("email/bugmail.html.tmpl", $vars, \$msg_html)
- || ThrowTemplateError($template->error());
- push @parts, Email::MIME->create(
- attributes => {
- content_type => "text/html",
- },
- body => $msg_html,
- );
- }
-
- # TT trims the trailing newline, and threadingmarker may be ignored.
- my $email = new Email::MIME("$msg_header\n");
-
- # For tracking/diagnostic purposes, add our hostname
- $email->header_set('X-Generated-By' => hostname());
+sub _flatten_object {
+ my ($object) = @_;
- if (scalar(@parts) == 1) {
- $email->content_type_set($parts[0]->content_type);
- } else {
- $email->content_type_set('multipart/alternative');
- }
- $email->parts_set(\@parts);
+ # nothing to do if it's already flattened
+ return $object unless blessed($object);
- # BMO: allow modification of the email given the enqueued variables
- Bugzilla::Hook::process('bugmail_generate', { vars => $vars, email => $email });
+ # 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;
+}
- return $email;
+sub _generate_bugmail {
+ my ($vars) = @_;
+ my $user = $vars->{to_user};
+ my $template = Bugzilla->template_inner($user->setting('lang'));
+ my ($msg_text, $msg_html, $msg_header);
+
+ $template->process("email/bugmail-header.txt.tmpl", $vars, \$msg_header)
+ || ThrowTemplateError($template->error());
+
+ $template->process("email/bugmail.txt.tmpl", $vars, \$msg_text)
+ || ThrowTemplateError($template->error());
+
+ my @parts = (Email::MIME->create(
+ attributes => {content_type => "text/plain",},
+ body => $msg_text,
+ ));
+ if ($user->setting('email_format') eq 'html') {
+ $template->process("email/bugmail.html.tmpl", $vars, \$msg_html)
+ || ThrowTemplateError($template->error());
+ push @parts,
+ Email::MIME->create(
+ attributes => {content_type => "text/html",},
+ body => $msg_html,
+ );
+ }
+
+ # TT trims the trailing newline, and threadingmarker may be ignored.
+ my $email = new Email::MIME("$msg_header\n");
+
+ # For tracking/diagnostic purposes, add our hostname
+ $email->header_set('X-Generated-By' => hostname());
+
+ if (scalar(@parts) == 1) {
+ $email->content_type_set($parts[0]->content_type);
+ }
+ else {
+ $email->content_type_set('multipart/alternative');
+ }
+ $email->parts_set(\@parts);
+
+ # BMO: allow modification of the email given the enqueued variables
+ Bugzilla::Hook::process('bugmail_generate', {vars => $vars, email => $email});
+
+ return $email;
}
sub _get_diffs {
- my ($bug, $end, $user_cache) = @_;
- my $dbh = Bugzilla->dbh;
-
- my @args = ($bug->id);
- # If lastdiffed is NULL, then we don't limit the search on time.
- my $when_restriction = '';
- if ($bug->lastdiffed) {
- $when_restriction = ' AND bug_when > ? AND bug_when <= ?';
- push @args, ($bug->lastdiffed, $end);
- }
+ my ($bug, $end, $user_cache) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ my @args = ($bug->id);
- my $diffs = $dbh->selectall_arrayref(
- "SELECT fielddefs.name AS field_name,
+ # 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,
fielddefs.description AS field_desc,
bugs_activity.bug_when, bugs_activity.removed AS old,
bugs_activity.added AS new, bugs_activity.attach_id,
@@ -557,90 +575,94 @@ sub _get_diffs {
ON fielddefs.id = bugs_activity.fieldid
WHERE bugs_activity.bug_id = ?
$when_restriction
- ORDER BY bugs_activity.bug_when, fielddefs.description", {Slice=>{}}, @args);
- my $referenced_bugs = [];
-
- foreach my $diff (@$diffs) {
- $user_cache->{$diff->{who}} ||= new Bugzilla::User({ id => $diff->{who}, cache => 1 });
- $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};
- }
- elsif ($diff->{field_name} eq 'dependson' || $diff->{field_name} eq 'blocked') {
- push @$referenced_bugs, grep { /^\d+$/ } split(/[\s,]+/, $diff->{old});
- push @$referenced_bugs, grep { /^\d+$/ } split(/[\s,]+/, $diff->{new});
- }
+ ORDER BY bugs_activity.bug_when, fielddefs.description", {Slice => {}},
+ @args
+ );
+ my $referenced_bugs = [];
+
+ foreach my $diff (@$diffs) {
+ $user_cache->{$diff->{who}}
+ ||= new Bugzilla::User({id => $diff->{who}, cache => 1});
+ $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};
}
+ elsif ($diff->{field_name} eq 'dependson' || $diff->{field_name} eq 'blocked') {
+ push @$referenced_bugs, grep {/^\d+$/} split(/[\s,]+/, $diff->{old});
+ push @$referenced_bugs, grep {/^\d+$/} split(/[\s,]+/, $diff->{new});
+ }
+ }
- return ($diffs, $referenced_bugs);
+ return ($diffs, $referenced_bugs);
}
sub _get_new_bugmail_fields {
- my $bug = shift;
- my @fields = @{ Bugzilla->fields({obsolete => 0, in_new_bugmail => 1}) };
- my @diffs;
-
- # Show fields in the same order as the DEFAULT_FIELDS list, which mirrors
- # 4.0's behavour and provides sane grouping of similar fields.
- # Any additional fields are sorted by descrsiption
- my @prepend;
- foreach my $name (map { $_->{name} } Bugzilla::Field::DEFAULT_FIELDS) {
- my $idx = firstidx { $_->name eq $name } @fields;
- if ($idx != -1) {
- push(@prepend, $fields[$idx]);
- splice(@fields, $idx, 1);
- }
+ my $bug = shift;
+ my @fields = @{Bugzilla->fields({obsolete => 0, in_new_bugmail => 1})};
+ my @diffs;
+
+ # Show fields in the same order as the DEFAULT_FIELDS list, which mirrors
+ # 4.0's behavour and provides sane grouping of similar fields.
+ # Any additional fields are sorted by descrsiption
+ my @prepend;
+ foreach my $name (map { $_->{name} } Bugzilla::Field::DEFAULT_FIELDS) {
+ my $idx = firstidx { $_->name eq $name } @fields;
+ if ($idx != -1) {
+ push(@prepend, $fields[$idx]);
+ splice(@fields, $idx, 1);
}
- @fields = sort { $a->description cmp $b->description } @fields;
- @fields = (@prepend, @fields);
-
- foreach my $field (@fields) {
- my $name = $field->name;
- my $value = $bug->$name;
-
- if (ref $value eq 'ARRAY') {
- my @new_values;
- foreach my $item (@$value) {
- if (blessed($item) && $item->isa('Bugzilla::User')) {
- push(@new_values, $item->login);
- }
- else {
- push(@new_values, $item);
- }
- }
- $value = join(', ', @new_values);
- }
- elsif (blessed($value) && $value->isa('Bugzilla::User')) {
- $value = $value->login;
- }
- elsif (blessed($value) && $value->isa('Bugzilla::Object')) {
- $value = $value->name;
+ }
+ @fields = sort { $a->description cmp $b->description } @fields;
+ @fields = (@prepend, @fields);
+
+ foreach my $field (@fields) {
+ my $name = $field->name;
+ my $value = $bug->$name;
+
+ if (ref $value eq 'ARRAY') {
+ my @new_values;
+ foreach my $item (@$value) {
+ if (blessed($item) && $item->isa('Bugzilla::User')) {
+ push(@new_values, $item->login);
}
- 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;
+ else {
+ push(@new_values, $item);
}
- 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;
+ }
+ $value = join(', ', @new_values);
+ }
+ 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,
- field_desc => $field->description,
- 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;
}
- return @diffs;
+ # If there isn't anything to show, don't include this header.
+ next unless $value;
+
+ push(@diffs,
+ {field_name => $name, field_desc => $field->description, new => $value});
+ }
+
+ return @diffs;
}
1;
diff --git a/Bugzilla/BugUrl.pm b/Bugzilla/BugUrl.pm
index a824d286d..e6c68416c 100644
--- a/Bugzilla/BugUrl.pm
+++ b/Bugzilla/BugUrl.pm
@@ -27,55 +27,56 @@ 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::Chromium
- Bugzilla::BugUrl::Edge
- Bugzilla::BugUrl::Debian
- Bugzilla::BugUrl::JIRA
- Bugzilla::BugUrl::Trac
- Bugzilla::BugUrl::MantisBT
- Bugzilla::BugUrl::SourceForge
- Bugzilla::BugUrl::GitHub
- Bugzilla::BugUrl::MozSupport
- Bugzilla::BugUrl::Aha
- Bugzilla::BugUrl::WebCompat
- Bugzilla::BugUrl::ServiceNow
- Bugzilla::BugUrl::Splat
+ Bugzilla::BugUrl::Bugzilla::Local
+ Bugzilla::BugUrl::Bugzilla
+ Bugzilla::BugUrl::Launchpad
+ Bugzilla::BugUrl::Google
+ Bugzilla::BugUrl::Chromium
+ Bugzilla::BugUrl::Edge
+ Bugzilla::BugUrl::Debian
+ Bugzilla::BugUrl::JIRA
+ Bugzilla::BugUrl::Trac
+ Bugzilla::BugUrl::MantisBT
+ Bugzilla::BugUrl::SourceForge
+ Bugzilla::BugUrl::GitHub
+ Bugzilla::BugUrl::MozSupport
+ Bugzilla::BugUrl::Aha
+ Bugzilla::BugUrl::WebCompat
+ Bugzilla::BugUrl::ServiceNow
+ Bugzilla::BugUrl::Splat
);
###############################
#### Accessors ######
###############################
-sub class { return $_[0]->{class} }
+sub class { return $_[0]->{class} }
sub bug_id { return $_[0]->{bug_id} }
###############################
@@ -83,125 +84,120 @@ 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) {
- require_module($object->class);
- bless $object, $object->class;
- }
+ foreach my $object (@$objects) {
+ require_module($object->class);
+ bless $object, $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 ($class, $value) = @_;
- my $uri = URI->new($value);
- foreach my $subclass ($class->SUB_CLASSES) {
- require_module($subclass);
- return wantarray ? ($subclass, $uri) : $subclass
- if $subclass->should_handle($uri);
- }
+ my $uri = URI->new($value);
+ foreach my $subclass ($class->SUB_CLASSES) {
+ require_module($subclass);
+ return wantarray ? ($subclass, $uri) : $subclass
+ if $subclass->should_handle($uri);
+ }
- ThrowUserError('bug_url_invalid', { url => $value,
- reason => 'show_bug' });
+ ThrowUserError('bug_url_invalid', {url => $value, reason => 'show_bug'});
}
sub _check_class {
- my ($class, $subclass) = @_;
- require_module($subclass);
- return $subclass;
+ my ($class, $subclass) = @_;
+ require_module($subclass);
+ 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) {
- return $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;
}
sub _check_value {
- my ($class, $uri) = @_;
-
- my $value = $uri->as_string;
-
- if (!$value) {
- ThrowCodeError('param_required',
- { function => 'add_see_also', param => '$value' });
- }
-
- # We assume that the URL is an HTTP URL if there is no (something)://
- # in front.
- if (!$uri->scheme) {
- # This works better than setting $uri->scheme('http'), because
- # that creates URLs like "http:domain.com" and doesn't properly
- # differentiate the path from the domain.
- $uri = new URI("http://$value");
- }
- elsif ($uri->scheme ne 'http' && $uri->scheme ne 'https') {
- ThrowUserError('bug_url_invalid', { url => $value, reason => 'http' });
- }
-
- # This stops the following edge cases from being accepted:
- # * show_bug.cgi?id=1
- # * /show_bug.cgi?id=1
- # * http:///show_bug.cgi?id=1
- if (!$uri->authority or $uri->path !~ m{/}) {
- ThrowUserError('bug_url_invalid',
- { url => $value, reason => 'path_only' });
- }
-
- if (length($uri->path) > MAX_BUG_URL_LENGTH) {
- ThrowUserError('bug_url_too_long', { url => $uri->path });
- }
-
- return $uri;
+ my ($class, $uri) = @_;
+
+ my $value = $uri->as_string;
+
+ if (!$value) {
+ ThrowCodeError('param_required',
+ {function => 'add_see_also', param => '$value'});
+ }
+
+ # We assume that the URL is an HTTP URL if there is no (something)://
+ # in front.
+ if (!$uri->scheme) {
+
+ # This works better than setting $uri->scheme('http'), because
+ # that creates URLs like "http:domain.com" and doesn't properly
+ # differentiate the path from the domain.
+ $uri = new URI("http://$value");
+ }
+ elsif ($uri->scheme ne 'http' && $uri->scheme ne 'https') {
+ ThrowUserError('bug_url_invalid', {url => $value, reason => 'http'});
+ }
+
+ # This stops the following edge cases from being accepted:
+ # * show_bug.cgi?id=1
+ # * /show_bug.cgi?id=1
+ # * http:///show_bug.cgi?id=1
+ if (!$uri->authority or $uri->path !~ m{/}) {
+ ThrowUserError('bug_url_invalid', {url => $value, reason => 'path_only'});
+ }
+
+ if (length($uri->path) > MAX_BUG_URL_LENGTH) {
+ ThrowUserError('bug_url_too_long', {url => $uri->path});
+ }
+
+ return $uri;
}
1;
diff --git a/Bugzilla/BugUrl/Aha.pm b/Bugzilla/BugUrl/Aha.pm
index b467c54d8..cc733ae13 100644
--- a/Bugzilla/BugUrl/Aha.pm
+++ b/Bugzilla/BugUrl/Aha.pm
@@ -18,28 +18,28 @@ use base qw(Bugzilla::BugUrl);
###############################
sub should_handle {
- my ($class, $uri) = @_;
+ my ($class, $uri) = @_;
- return $uri =~ m!^https?://[^.]+\.aha\.io/features/(\w+-\d+)!;
+ return $uri =~ m!^https?://[^.]+\.aha\.io/features/(\w+-\d+)!;
}
sub get_feature_id {
- my ($self) = @_;
+ my ($self) = @_;
- if ($self->{value} =~ m!^https?://[^.]+\.aha\.io/features/(\w+-\d+)!) {
- return $1;
- }
+ if ($self->{value} =~ m!^https?://[^.]+\.aha\.io/features/(\w+-\d+)!) {
+ return $1;
+ }
}
sub _check_value {
- my ($class, $uri) = @_;
+ my ($class, $uri) = @_;
- $uri = $class->SUPER::_check_value($uri);
+ $uri = $class->SUPER::_check_value($uri);
- # Aha HTTP URLs redirect to HTTPS, so just use the HTTPS scheme.
- $uri->scheme('https');
+ # Aha HTTP URLs redirect to HTTPS, so just use the HTTPS scheme.
+ $uri->scheme('https');
- return $uri;
+ return $uri;
}
1;
diff --git a/Bugzilla/BugUrl/Bugzilla.pm b/Bugzilla/BugUrl/Bugzilla.pm
index 3af6d0a54..1cedb1f56 100644
--- a/Bugzilla/BugUrl/Bugzilla.pm
+++ b/Bugzilla/BugUrl/Bugzilla.pm
@@ -21,37 +21,39 @@ use Bugzilla::Util;
###############################
sub should_handle {
- my ($class, $uri) = @_;
- return ($uri->path =~ /show_bug\.cgi$/) ? 1 : 0;
+ my ($class, $uri) = @_;
+ return ($uri->path =~ /show_bug\.cgi$/) ? 1 : 0;
}
sub _check_value {
- my ($class, $uri) = @_;
-
- $uri = $class->SUPER::_check_value($uri);
-
- my $bug_id = $uri->query_param('id');
- # We don't currently allow aliases, because we can't check to see
- # if somebody's putting both an alias link and a numeric ID link.
- # When we start validating the URL by accessing the other Bugzilla,
- # we can allow aliases.
- detaint_natural($bug_id);
- if (!$bug_id) {
- my $value = $uri->as_string;
- ThrowUserError('bug_url_invalid', { url => $value, reason => 'id' });
- }
-
- # Make sure that "id" is the only query parameter.
- $uri->query("id=$bug_id");
- # And remove any # part if there is one.
- $uri->fragment(undef);
-
- return $uri;
+ my ($class, $uri) = @_;
+
+ $uri = $class->SUPER::_check_value($uri);
+
+ my $bug_id = $uri->query_param('id');
+
+ # We don't currently allow aliases, because we can't check to see
+ # if somebody's putting both an alias link and a numeric ID link.
+ # When we start validating the URL by accessing the other Bugzilla,
+ # we can allow aliases.
+ detaint_natural($bug_id);
+ if (!$bug_id) {
+ my $value = $uri->as_string;
+ ThrowUserError('bug_url_invalid', {url => $value, reason => 'id'});
+ }
+
+ # Make sure that "id" is the only query parameter.
+ $uri->query("id=$bug_id");
+
+ # And remove any # part if there is one.
+ $uri->fragment(undef);
+
+ return $uri;
}
sub target_bug_id {
- my ($self) = @_;
- return new URI($self->name)->query_param('id');
+ my ($self) = @_;
+ return new URI($self->name)->query_param('id');
}
1;
diff --git a/Bugzilla/BugUrl/Bugzilla/Local.pm b/Bugzilla/BugUrl/Bugzilla/Local.pm
index 14d03f048..73086524e 100644
--- a/Bugzilla/BugUrl/Bugzilla/Local.pm
+++ b/Bugzilla/BugUrl/Bugzilla/Local.pm
@@ -20,83 +20,80 @@ 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');
- }
-
- my $product = $ref_bug->product_obj;
- if (!Bugzilla->user->can_edit_product($product->id)) {
- ThrowUserError("product_edit_denied",
- { product => $product->name });
- }
-
- 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');
+ }
+
+ my $product = $ref_bug->product_obj;
+ if (!Bugzilla->user->can_edit_product($product->id)) {
+ ThrowUserError("product_edit_denied", {product => $product->name});
+ }
+
+ return $uri;
}
sub local_uri {
- my ($self, $bug_id) = @_;
- $bug_id ||= '';
- return Bugzilla->localconfig->{urlbase} . "show_bug.cgi?id=$bug_id";
+ my ($self, $bug_id) = @_;
+ $bug_id ||= '';
+ return Bugzilla->localconfig->{urlbase} . "show_bug.cgi?id=$bug_id";
}
1;
diff --git a/Bugzilla/BugUrl/Chromium.pm b/Bugzilla/BugUrl/Chromium.pm
index 5560df24c..2d4fcd178 100644
--- a/Bugzilla/BugUrl/Chromium.pm
+++ b/Bugzilla/BugUrl/Chromium.pm
@@ -21,29 +21,30 @@ use Bugzilla::Util;
###############################
sub should_handle {
- my ($class, $uri) = @_;
- return ($uri->authority =~ /^bugs.chromium.org$/i) ? 1 : 0;
+ my ($class, $uri) = @_;
+ return ($uri->authority =~ /^bugs.chromium.org$/i) ? 1 : 0;
}
sub _check_value {
- my ($class, $uri) = @_;
-
- $uri = $class->SUPER::_check_value($uri);
-
- my $value = $uri->as_string;
- my $project_name;
- if ($uri->path =~ m|^/p/([^/]+)/issues/detail$|) {
- $project_name = $1;
- } else {
- ThrowUserError('bug_url_invalid', { url => $value });
- }
- my $bug_id = $uri->query_param('id');
- detaint_natural($bug_id);
- if (!$bug_id) {
- ThrowUserError('bug_url_invalid', { url => $value, reason => 'id' });
- }
-
- return URI->new($value);
+ my ($class, $uri) = @_;
+
+ $uri = $class->SUPER::_check_value($uri);
+
+ my $value = $uri->as_string;
+ my $project_name;
+ if ($uri->path =~ m|^/p/([^/]+)/issues/detail$|) {
+ $project_name = $1;
+ }
+ else {
+ ThrowUserError('bug_url_invalid', {url => $value});
+ }
+ my $bug_id = $uri->query_param('id');
+ detaint_natural($bug_id);
+ if (!$bug_id) {
+ ThrowUserError('bug_url_invalid', {url => $value, reason => 'id'});
+ }
+
+ return URI->new($value);
}
1;
diff --git a/Bugzilla/BugUrl/Debian.pm b/Bugzilla/BugUrl/Debian.pm
index e018c1106..88b808382 100644
--- a/Bugzilla/BugUrl/Debian.pm
+++ b/Bugzilla/BugUrl/Debian.pm
@@ -20,33 +20,33 @@ use Bugzilla::Util;
###############################
sub should_handle {
- my ($class, $uri) = @_;
- return ($uri->authority =~ /^bugs.debian.org$/i) ? 1 : 0;
+ my ($class, $uri) = @_;
+ return ($uri->authority =~ /^bugs.debian.org$/i) ? 1 : 0;
}
sub _check_value {
- my $class = shift;
-
- my $uri = $class->SUPER::_check_value(@_);
-
- # Debian BTS URLs can look like various things:
- # http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1234
- # http://bugs.debian.org/1234
- my $bug_id;
- if ($uri->path =~ m|^/(\d+)$|) {
- $bug_id = $1;
- }
- elsif ($uri->path =~ /bugreport\.cgi$/) {
- $bug_id = $uri->query_param('bug');
- detaint_natural($bug_id);
- }
- if (!$bug_id) {
- ThrowUserError('bug_url_invalid',
- { url => $uri->path, reason => 'id' });
- }
- # This is the shortest standard URL form for Debian BTS URLs,
- # and so we reduce all URLs to this.
- return new URI("http://bugs.debian.org/" . $bug_id);
+ my $class = shift;
+
+ my $uri = $class->SUPER::_check_value(@_);
+
+ # Debian BTS URLs can look like various things:
+ # http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1234
+ # http://bugs.debian.org/1234
+ my $bug_id;
+ if ($uri->path =~ m|^/(\d+)$|) {
+ $bug_id = $1;
+ }
+ elsif ($uri->path =~ /bugreport\.cgi$/) {
+ $bug_id = $uri->query_param('bug');
+ detaint_natural($bug_id);
+ }
+ if (!$bug_id) {
+ ThrowUserError('bug_url_invalid', {url => $uri->path, reason => 'id'});
+ }
+
+ # This is the shortest standard URL form for Debian BTS URLs,
+ # and so we reduce all URLs to this.
+ return new URI("http://bugs.debian.org/" . $bug_id);
}
1;
diff --git a/Bugzilla/BugUrl/Edge.pm b/Bugzilla/BugUrl/Edge.pm
index 95d24c93a..53145dda9 100644
--- a/Bugzilla/BugUrl/Edge.pm
+++ b/Bugzilla/BugUrl/Edge.pm
@@ -26,19 +26,21 @@ use List::MoreUtils qw( any );
# https://wpdev.uservoice.com/forums/257854/suggestions/17420707
# https://wpdev.uservoice.com/forums/257854-microsoft-edge-developer/suggestions/17420707-implement-css-display-flow-root-modern-clearfi
sub should_handle {
- my ($class, $uri) = @_;
- return any { lc($uri->authority) eq $_ } qw( developer.microsoft.com wpdev.uservoice.com );
+ my ($class, $uri) = @_;
+ return any { lc($uri->authority) eq $_ }
+ qw( developer.microsoft.com wpdev.uservoice.com );
}
sub _check_value {
- my ($class, $uri) = @_;
+ my ($class, $uri) = @_;
- $uri = $class->SUPER::_check_value($uri);
+ $uri = $class->SUPER::_check_value($uri);
- return $uri if $uri->path =~ m{^/en-us/microsoft-edge/platform/issues/\d+/$};
- return $uri if $uri->path =~ m{^/forums/\d+(?:-[^/]+)?/suggestions/\d+(?:-[^/]+)?};
+ return $uri if $uri->path =~ m{^/en-us/microsoft-edge/platform/issues/\d+/$};
+ return $uri
+ if $uri->path =~ m{^/forums/\d+(?:-[^/]+)?/suggestions/\d+(?:-[^/]+)?};
- ThrowUserError('bug_url_invalid', { url => "$uri" });
+ ThrowUserError('bug_url_invalid', {url => "$uri"});
}
1;
diff --git a/Bugzilla/BugUrl/GitHub.pm b/Bugzilla/BugUrl/GitHub.pm
index 1a0219617..a81564812 100644
--- a/Bugzilla/BugUrl/GitHub.pm
+++ b/Bugzilla/BugUrl/GitHub.pm
@@ -18,25 +18,25 @@ use base qw(Bugzilla::BugUrl);
###############################
sub should_handle {
- my ($class, $uri) = @_;
-
- # GitHub issue URLs have only one form:
- # https://github.com/USER_OR_TEAM_OR_ORGANIZATION_NAME/REPOSITORY_NAME/issues/111
- # GitHub pull request URLs have only one form:
- # https://github.com/USER_OR_TEAM_OR_ORGANIZATION_NAME/REPOSITORY_NAME/pull/111
- return (lc($uri->authority) eq 'github.com'
- and $uri->path =~ m!^/[^/]+/[^/]+/(?:issues|pull)/\d+$!) ? 1 : 0;
+ my ($class, $uri) = @_;
+
+# GitHub issue URLs have only one form:
+# https://github.com/USER_OR_TEAM_OR_ORGANIZATION_NAME/REPOSITORY_NAME/issues/111
+# GitHub pull request URLs have only one form:
+# https://github.com/USER_OR_TEAM_OR_ORGANIZATION_NAME/REPOSITORY_NAME/pull/111
+ return (lc($uri->authority) eq 'github.com'
+ and $uri->path =~ m!^/[^/]+/[^/]+/(?:issues|pull)/\d+$!) ? 1 : 0;
}
sub _check_value {
- my ($class, $uri) = @_;
+ my ($class, $uri) = @_;
- $uri = $class->SUPER::_check_value($uri);
+ $uri = $class->SUPER::_check_value($uri);
- # GitHub HTTP URLs redirect to HTTPS, so just use the HTTPS scheme.
- $uri->scheme('https');
+ # GitHub HTTP URLs redirect to HTTPS, so just use the HTTPS scheme.
+ $uri->scheme('https');
- return $uri;
+ return $uri;
}
1;
diff --git a/Bugzilla/BugUrl/Google.pm b/Bugzilla/BugUrl/Google.pm
index 6c8a2f27d..d36f4eb62 100644
--- a/Bugzilla/BugUrl/Google.pm
+++ b/Bugzilla/BugUrl/Google.pm
@@ -20,35 +20,41 @@ use Bugzilla::Util;
###############################
sub should_handle {
- my ($class, $uri) = @_;
- return ($uri->authority =~ /^code.google.com$/i) ? 1 : 0;
+ my ($class, $uri) = @_;
+ return ($uri->authority =~ /^code.google.com$/i) ? 1 : 0;
}
sub _check_value {
- my ($class, $uri) = @_;
-
- $uri = $class->SUPER::_check_value($uri);
-
- my $value = $uri->as_string;
- # Google Code URLs only have one form:
- # http(s)://code.google.com/p/PROJECT_NAME/issues/detail?id=1234
- my $project_name;
- if ($uri->path =~ m|^/p/([^/]+)/issues/detail$|) {
- $project_name = $1;
- } else {
- ThrowUserError('bug_url_invalid', { url => $value });
- }
- my $bug_id = $uri->query_param('id');
- detaint_natural($bug_id);
- if (!$bug_id) {
- ThrowUserError('bug_url_invalid', { url => $value, reason => 'id' });
- }
- # While Google Code URLs can be either HTTP or HTTPS,
- # always go with the HTTP scheme, as that's the default.
- $value = "http://code.google.com/p/" . $project_name .
- "/issues/detail?id=" . $bug_id;
-
- return new URI($value);
+ my ($class, $uri) = @_;
+
+ $uri = $class->SUPER::_check_value($uri);
+
+ my $value = $uri->as_string;
+
+ # Google Code URLs only have one form:
+ # http(s)://code.google.com/p/PROJECT_NAME/issues/detail?id=1234
+ my $project_name;
+ if ($uri->path =~ m|^/p/([^/]+)/issues/detail$|) {
+ $project_name = $1;
+ }
+ else {
+ ThrowUserError('bug_url_invalid', {url => $value});
+ }
+ my $bug_id = $uri->query_param('id');
+ detaint_natural($bug_id);
+ if (!$bug_id) {
+ ThrowUserError('bug_url_invalid', {url => $value, reason => 'id'});
+ }
+
+ # While Google Code URLs can be either HTTP or HTTPS,
+ # always go with the HTTP scheme, as that's the default.
+ $value
+ = "http://code.google.com/p/"
+ . $project_name
+ . "/issues/detail?id="
+ . $bug_id;
+
+ return new URI($value);
}
1;
diff --git a/Bugzilla/BugUrl/JIRA.pm b/Bugzilla/BugUrl/JIRA.pm
index ba4b0e51b..a6cf75e93 100644
--- a/Bugzilla/BugUrl/JIRA.pm
+++ b/Bugzilla/BugUrl/JIRA.pm
@@ -20,25 +20,26 @@ use Bugzilla::Util;
###############################
sub should_handle {
- my ($class, $uri) = @_;
- return ($uri->path =~ m|/browse/[A-Z][A-Z]+-\d+$|) ? 1 : 0;
+ my ($class, $uri) = @_;
+ 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(@_);
- # 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
+ # 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
- # Make sure there are no query parameters.
- $uri->query(undef);
- # And remove any # part if there is one.
- $uri->fragment(undef);
+ # Make sure there are no query parameters.
+ $uri->query(undef);
- return $uri;
+ # And remove any # part if there is one.
+ $uri->fragment(undef);
+
+ return $uri;
}
1;
diff --git a/Bugzilla/BugUrl/Launchpad.pm b/Bugzilla/BugUrl/Launchpad.pm
index a56fed4ad..4b3bb3b0d 100644
--- a/Bugzilla/BugUrl/Launchpad.pm
+++ b/Bugzilla/BugUrl/Launchpad.pm
@@ -19,30 +19,32 @@ use Bugzilla::Error;
###############################
sub should_handle {
- my ($class, $uri) = @_;
- return ($uri->authority =~ /launchpad.net$/) ? 1 : 0;
+ my ($class, $uri) = @_;
+ return ($uri->authority =~ /launchpad.net$/) ? 1 : 0;
}
sub _check_value {
- my ($class, $uri) = @_;
-
- $uri = $class->SUPER::_check_value($uri);
-
- my $value = $uri->as_string;
- # 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"
- if ($uri->path =~ m|bugs?/(\d+)$|) {
- # This is the shortest standard URL form for Launchpad bugs,
- # and so we reduce all URLs to this.
- $value = "https://launchpad.net/bugs/$1";
- }
- else {
- ThrowUserError('bug_url_invalid', { url => $value, reason => 'id' });
- }
-
- return new URI($value);
+ my ($class, $uri) = @_;
+
+ $uri = $class->SUPER::_check_value($uri);
+
+ my $value = $uri->as_string;
+
+ # 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"
+ if ($uri->path =~ m|bugs?/(\d+)$|) {
+
+ # This is the shortest standard URL form for Launchpad bugs,
+ # and so we reduce all URLs to this.
+ $value = "https://launchpad.net/bugs/$1";
+ }
+ else {
+ ThrowUserError('bug_url_invalid', {url => $value, reason => 'id'});
+ }
+
+ return new URI($value);
}
1;
diff --git a/Bugzilla/BugUrl/MantisBT.pm b/Bugzilla/BugUrl/MantisBT.pm
index 48284c7e0..9cf49cdb8 100644
--- a/Bugzilla/BugUrl/MantisBT.pm
+++ b/Bugzilla/BugUrl/MantisBT.pm
@@ -20,22 +20,22 @@ use Bugzilla::Util;
###############################
sub should_handle {
- my ($class, $uri) = @_;
- return ($uri->path_query =~ m|view\.php\?id=\d+$|) ? 1 : 0;
+ my ($class, $uri) = @_;
+ 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(@_);
- # MantisBT URLs look like the following ('bugs' directory is optional):
- # http://www.mantisbt.org/bugs/view.php?id=1234
+ # MantisBT URLs look like the following ('bugs' directory is optional):
+ # http://www.mantisbt.org/bugs/view.php?id=1234
- # Remove any # part if there is one.
- $uri->fragment(undef);
+ # Remove any # part if there is one.
+ $uri->fragment(undef);
- return $uri;
+ return $uri;
}
1;
diff --git a/Bugzilla/BugUrl/MozSupport.pm b/Bugzilla/BugUrl/MozSupport.pm
index c2442e4df..b924f3f53 100644
--- a/Bugzilla/BugUrl/MozSupport.pm
+++ b/Bugzilla/BugUrl/MozSupport.pm
@@ -18,23 +18,23 @@ use base qw(Bugzilla::BugUrl);
###############################
sub should_handle {
- my ($class, $uri) = @_;
+ my ($class, $uri) = @_;
- # Mozilla support questions normally have the form:
- # https://support.mozilla.org/<language>/questions/<id>
- return ($uri->authority =~ /^support.mozilla.org$/i
- and $uri->path =~ m|^(/[^/]+)?/questions/\d+$|) ? 1 : 0;
+ # Mozilla support questions normally have the form:
+ # https://support.mozilla.org/<language>/questions/<id>
+ return ($uri->authority =~ /^support.mozilla.org$/i
+ and $uri->path =~ m|^(/[^/]+)?/questions/\d+$|) ? 1 : 0;
}
sub _check_value {
- my ($class, $uri) = @_;
+ my ($class, $uri) = @_;
- $uri = $class->SUPER::_check_value($uri);
+ $uri = $class->SUPER::_check_value($uri);
- # Support.mozilla.org redirects to https automatically
- $uri->scheme('https');
+ # Support.mozilla.org redirects to https automatically
+ $uri->scheme('https');
- return $uri;
+ return $uri;
}
1;
diff --git a/Bugzilla/BugUrl/ServiceNow.pm b/Bugzilla/BugUrl/ServiceNow.pm
index 8e30aa45e..d65106a90 100644
--- a/Bugzilla/BugUrl/ServiceNow.pm
+++ b/Bugzilla/BugUrl/ServiceNow.pm
@@ -14,15 +14,15 @@ use warnings;
use base qw(Bugzilla::BugUrl);
sub should_handle {
- my ($class, $uri) = @_;
- return $uri =~ m#^https?://[^.]+\.service-now\.com/nav_to\.do\?#;
+ my ($class, $uri) = @_;
+ return $uri =~ m#^https?://[^.]+\.service-now\.com/nav_to\.do\?#;
}
sub _check_value {
- my ($class, $uri) = @_;
- $uri = $class->SUPER::_check_value($uri);
- $uri->scheme('https');
- return $uri;
+ my ($class, $uri) = @_;
+ $uri = $class->SUPER::_check_value($uri);
+ $uri->scheme('https');
+ return $uri;
}
1;
diff --git a/Bugzilla/BugUrl/SourceForge.pm b/Bugzilla/BugUrl/SourceForge.pm
index 3c8dfd51d..5e106880c 100644
--- a/Bugzilla/BugUrl/SourceForge.pm
+++ b/Bugzilla/BugUrl/SourceForge.pm
@@ -20,29 +20,32 @@ use Bugzilla::Util;
###############################
sub should_handle {
- my ($class, $uri) = @_;
- return ($uri->authority =~ /^sourceforge.net$/i
- and $uri->path =~ m|/tracker/|) ? 1 : 0;
+ my ($class, $uri) = @_;
+ return ($uri->authority =~ /^sourceforge.net$/i and $uri->path =~ m|/tracker/|)
+ ? 1
+ : 0;
}
sub _check_value {
- my $class = shift;
-
- my $uri = $class->SUPER::_check_value(@_);
-
- # SourceForge tracker URLs have only one form:
- # http://sourceforge.net/tracker/?func=detail&aid=111&group_id=111&atid=111
- if ($uri->query_param('func') eq 'detail' and $uri->query_param('aid')
- and $uri->query_param('group_id') and $uri->query_param('atid'))
- {
- # Remove any # part if there is one.
- $uri->fragment(undef);
- return $uri;
- }
- else {
- my $value = $uri->as_string;
- ThrowUserError('bug_url_invalid', { url => $value });
- }
+ my $class = shift;
+
+ my $uri = $class->SUPER::_check_value(@_);
+
+ # SourceForge tracker URLs have only one form:
+ # http://sourceforge.net/tracker/?func=detail&aid=111&group_id=111&atid=111
+ if ( $uri->query_param('func') eq 'detail'
+ and $uri->query_param('aid')
+ and $uri->query_param('group_id')
+ and $uri->query_param('atid'))
+ {
+ # Remove any # part if there is one.
+ $uri->fragment(undef);
+ return $uri;
+ }
+ else {
+ my $value = $uri->as_string;
+ ThrowUserError('bug_url_invalid', {url => $value});
+ }
}
1;
diff --git a/Bugzilla/BugUrl/Splat.pm b/Bugzilla/BugUrl/Splat.pm
index 49b2b762f..da471f96d 100644
--- a/Bugzilla/BugUrl/Splat.pm
+++ b/Bugzilla/BugUrl/Splat.pm
@@ -14,15 +14,15 @@ use warnings;
use base qw(Bugzilla::BugUrl);
sub should_handle {
- my ($class, $uri) = @_;
- return $uri =~ m#^https?://hellosplat\.com/s/beanbag/tickets/\d+#;
+ my ($class, $uri) = @_;
+ return $uri =~ m#^https?://hellosplat\.com/s/beanbag/tickets/\d+#;
}
sub _check_value {
- my ($class, $uri) = @_;
- $uri = $class->SUPER::_check_value($uri);
- $uri->scheme('https'); # force https
- return $uri;
+ my ($class, $uri) = @_;
+ $uri = $class->SUPER::_check_value($uri);
+ $uri->scheme('https'); # force https
+ return $uri;
}
1;
diff --git a/Bugzilla/BugUrl/Trac.pm b/Bugzilla/BugUrl/Trac.pm
index 600b31105..61b1e99f2 100644
--- a/Bugzilla/BugUrl/Trac.pm
+++ b/Bugzilla/BugUrl/Trac.pm
@@ -20,25 +20,26 @@ use Bugzilla::Util;
###############################
sub should_handle {
- my ($class, $uri) = @_;
- return ($uri->path =~ m|/ticket/\d+$|) ? 1 : 0;
+ my ($class, $uri) = @_;
+ 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(@_);
- # Trac URLs can look like various things:
- # http://dev.mutt.org/trac/ticket/1234
- # http://trac.roundcube.net/ticket/1484130
+ # Trac URLs can look like various things:
+ # http://dev.mutt.org/trac/ticket/1234
+ # http://trac.roundcube.net/ticket/1484130
- # Make sure there are no query parameters.
- $uri->query(undef);
- # And remove any # part if there is one.
- $uri->fragment(undef);
+ # Make sure there are no query parameters.
+ $uri->query(undef);
- return $uri;
+ # And remove any # part if there is one.
+ $uri->fragment(undef);
+
+ return $uri;
}
1;
diff --git a/Bugzilla/BugUrl/WebCompat.pm b/Bugzilla/BugUrl/WebCompat.pm
index bd66dcae7..0ee8fb638 100644
--- a/Bugzilla/BugUrl/WebCompat.pm
+++ b/Bugzilla/BugUrl/WebCompat.pm
@@ -18,22 +18,22 @@ use base qw(Bugzilla::BugUrl);
###############################
sub should_handle {
- my ($class, $uri) = @_;
+ my ($class, $uri) = @_;
- # https://webcompat.com/issues/1111
- my $host = lc($uri->authority);
- return
- ($host eq 'webcompat.com' || $host eq 'www.webcompat.com')
- && $uri->path =~ m#^/issues/\d+$#;
+ # https://webcompat.com/issues/1111
+ my $host = lc($uri->authority);
+ return ($host eq 'webcompat.com' || $host eq 'www.webcompat.com')
+ && $uri->path =~ m#^/issues/\d+$#;
}
sub _check_value {
- my ($class, $uri) = @_;
- $uri = $class->SUPER::_check_value($uri);
- # force https and drop www from host
- $uri->scheme('https');
- $uri->authority('webcompat.com');
- return $uri;
+ my ($class, $uri) = @_;
+ $uri = $class->SUPER::_check_value($uri);
+
+ # force https and drop www from host
+ $uri->scheme('https');
+ $uri->authority('webcompat.com');
+ return $uri;
}
1;
diff --git a/Bugzilla/BugUserLastVisit.pm b/Bugzilla/BugUserLastVisit.pm
index f40ea17d3..d1c351959 100644
--- a/Bugzilla/BugUserLastVisit.pm
+++ b/Bugzilla/BugUserLastVisit.pm
@@ -25,25 +25,27 @@ use constant LIST_ORDER => 'id';
use constant NAME_FIELD => 'id';
# turn off auditing and exclude these objects from memcached
-use constant { AUDIT_CREATES => 0,
- AUDIT_UPDATES => 0,
- AUDIT_REMOVES => 0,
- USE_MEMCACHED => 0 };
+use constant {
+ AUDIT_CREATES => 0,
+ AUDIT_UPDATES => 0,
+ AUDIT_REMOVES => 0,
+ USE_MEMCACHED => 0
+};
#####################################################################
# Provide accessors for our columns
#####################################################################
-sub id { return $_[0]->{id} }
-sub bug_id { return $_[0]->{bug_id} }
-sub user_id { return $_[0]->{user_id} }
+sub id { return $_[0]->{id} }
+sub bug_id { return $_[0]->{bug_id} }
+sub user_id { return $_[0]->{user_id} }
sub last_visit_ts { return $_[0]->{last_visit_ts} }
sub user {
- my $self = shift;
+ my $self = shift;
- $self->{user} //= Bugzilla::User->new({id => $self->user_id, cache => 1});
- return $self->{user};
+ $self->{user} //= Bugzilla::User->new({id => $self->user_id, cache => 1});
+ return $self->{user};
}
1;
diff --git a/Bugzilla/CGI.pm b/Bugzilla/CGI.pm
index 4be384b67..f47ba734c 100644
--- a/Bugzilla/CGI.pm
+++ b/Bugzilla/CGI.pm
@@ -25,43 +25,49 @@ use File::Basename;
use URI;
BEGIN {
- if (ON_WINDOWS) {
- # Help CGI find the correct temp directory as the default list
- # isn't Windows friendly (Bug 248988)
- $ENV{'TMPDIR'} = $ENV{'TEMP'} || $ENV{'TMP'} || "$ENV{'WINDIR'}\\TEMP";
- }
- *AUTOLOAD = \&CGI::AUTOLOAD;
+ if (ON_WINDOWS) {
+
+ # Help CGI find the correct temp directory as the default list
+ # isn't Windows friendly (Bug 248988)
+ $ENV{'TMPDIR'} = $ENV{'TEMP'} || $ENV{'TMP'} || "$ENV{'WINDIR'}\\TEMP";
+ }
+ *AUTOLOAD = \&CGI::AUTOLOAD;
}
sub DEFAULT_CSP {
- my %policy = (
- default_src => [ 'self' ],
- script_src => [ 'self', 'nonce', 'unsafe-inline', 'https://www.google-analytics.com' ],
- frame_src => [ 'none', ],
- worker_src => [ 'none', ],
- img_src => [ 'self', 'blob:', 'https://secure.gravatar.com' ],
- style_src => [ 'self', 'unsafe-inline' ],
- object_src => [ 'none' ],
- connect_src => [
- 'self',
- # This is for extensions/GoogleAnalytics using beacon or XHR
- 'https://www.google-analytics.com',
- # This is from extensions/OrangeFactor/web/js/orange_factor.js
- 'https://treeherder.mozilla.org/api/failurecount/',
- ],
- form_action => [
- 'self',
- # used in template/en/default/search/search-google.html.tmpl
- 'https://www.google.com/search'
- ],
- frame_ancestors => [ 'none' ],
- report_only => 1,
- );
- if (Bugzilla->params->{github_client_id} && !Bugzilla->user->id) {
- push @{$policy{form_action}}, 'https://github.com/login/oauth/authorize', 'https://github.com/login';
- }
-
- return %policy;
+ my %policy = (
+ default_src => ['self'],
+ script_src =>
+ ['self', 'nonce', 'unsafe-inline', 'https://www.google-analytics.com'],
+ frame_src => ['none',],
+ worker_src => ['none',],
+ img_src => ['self', 'blob:', 'https://secure.gravatar.com'],
+ style_src => ['self', 'unsafe-inline'],
+ object_src => ['none'],
+ connect_src => [
+ 'self',
+
+ # This is for extensions/GoogleAnalytics using beacon or XHR
+ 'https://www.google-analytics.com',
+
+ # This is from extensions/OrangeFactor/web/js/orange_factor.js
+ 'https://treeherder.mozilla.org/api/failurecount/',
+ ],
+ form_action => [
+ 'self',
+
+ # used in template/en/default/search/search-google.html.tmpl
+ 'https://www.google.com/search'
+ ],
+ frame_ancestors => ['none'],
+ report_only => 1,
+ );
+ if (Bugzilla->params->{github_client_id} && !Bugzilla->user->id) {
+ push @{$policy{form_action}}, 'https://github.com/login/oauth/authorize',
+ 'https://github.com/login';
+ }
+
+ return %policy;
}
# Because show_bug code lives in many different .cgi files,
@@ -69,364 +75,394 @@ sub DEFAULT_CSP {
# normally the policy would just live in one .cgi file.
# Additionally, Bugzilla->localconfig->{urlbase} cannot be called at compile time, so this can't be a constant.
sub SHOW_BUG_MODAL_CSP {
- my ($bug_id) = @_;
- my %policy = (
- script_src => ['self', 'nonce', 'unsafe-inline', 'unsafe-eval', 'https://www.google-analytics.com' ],
- img_src => [ 'self', 'https://secure.gravatar.com' ],
- connect_src => [
- 'self',
- # This is for extensions/GoogleAnalytics using beacon or XHR
- 'https://www.google-analytics.com',
- # This is from extensions/OrangeFactor/web/js/orange_factor.js
- 'https://treeherder.mozilla.org/api/failurecount/',
- ],
- frame_src => [ 'self', ],
- worker_src => [ 'none', ],
- );
- if (use_attachbase() && $bug_id) {
- my $attach_base = Bugzilla->localconfig->{'attachment_base'};
- $attach_base =~ s/\%bugid\%/$bug_id/g;
- push @{ $policy{img_src} }, $attach_base;
- }
+ my ($bug_id) = @_;
+ my %policy = (
+ script_src => [
+ 'self', 'nonce',
+ 'unsafe-inline', 'unsafe-eval',
+ 'https://www.google-analytics.com'
+ ],
+ img_src => ['self', 'https://secure.gravatar.com'],
+ connect_src => [
+ 'self',
+
+ # This is for extensions/GoogleAnalytics using beacon or XHR
+ 'https://www.google-analytics.com',
+
+ # This is from extensions/OrangeFactor/web/js/orange_factor.js
+ 'https://treeherder.mozilla.org/api/failurecount/',
+ ],
+ frame_src => ['self',],
+ worker_src => ['none',],
+ );
+ if (use_attachbase() && $bug_id) {
+ my $attach_base = Bugzilla->localconfig->{'attachment_base'};
+ $attach_base =~ s/\%bugid\%/$bug_id/g;
+ push @{$policy{img_src}}, $attach_base;
+ }
- return %policy;
+ return %policy;
}
sub _init_bz_cgi_globals {
- my $invocant = shift;
- # We need to disable output buffering - see bug 179174
- $| = 1;
-
- # We don't precompile any functions here, that's done specially in
- # mod_perl code.
- $invocant->_setup_symbols(qw(:no_xhtml :oldstyle_urls :private_tempfiles
- :unique_headers));
+ my $invocant = shift;
+
+ # We need to disable output buffering - see bug 179174
+ $| = 1;
+
+ # We don't precompile any functions here, that's done specially in
+ # mod_perl code.
+ $invocant->_setup_symbols(
+ qw(:no_xhtml :oldstyle_urls :private_tempfiles
+ :unique_headers)
+ );
}
BEGIN { __PACKAGE__->_init_bz_cgi_globals() if i_am_cgi(); }
sub new {
- my ($invocant, @args) = @_;
- my $class = ref($invocant) || $invocant;
-
- # Under mod_perl, CGI's global variables get reset on each request,
- # so we need to set them up again every time.
- $class->_init_bz_cgi_globals();
-
- my $self = $class->SUPER::new(@args);
-
- # Make sure our outgoing cookie list is empty on each invocation
- $self->{Bugzilla_cookie_list} = [];
-
- # Path-Info is of no use for Bugzilla and interacts badly with IIS.
- # Moreover, it causes unexpected behaviors, such as totally breaking
- # the rendering of pages.
- my $script = basename($0);
- if (my $path = $self->path_info) {
- my @whitelist = ("rest.cgi");
- Bugzilla::Hook::process('path_info_whitelist', { whitelist => \@whitelist });
- if (!grep($_ eq $script, @whitelist)) {
- # apache collapses // to / in $ENV{PATH_INFO} but not in $self->path_info.
- # url() requires the full path in ENV in order to generate the correct url.
- $ENV{PATH_INFO} = $path;
- DEBUG("redirecting because we see PATH_INFO and don't like it");
- print $self->redirect($self->url(-path => 0, -query => 1));
- exit;
- }
+ my ($invocant, @args) = @_;
+ my $class = ref($invocant) || $invocant;
+
+ # Under mod_perl, CGI's global variables get reset on each request,
+ # so we need to set them up again every time.
+ $class->_init_bz_cgi_globals();
+
+ my $self = $class->SUPER::new(@args);
+
+ # Make sure our outgoing cookie list is empty on each invocation
+ $self->{Bugzilla_cookie_list} = [];
+
+ # Path-Info is of no use for Bugzilla and interacts badly with IIS.
+ # Moreover, it causes unexpected behaviors, such as totally breaking
+ # the rendering of pages.
+ my $script = basename($0);
+ if (my $path = $self->path_info) {
+ my @whitelist = ("rest.cgi");
+ Bugzilla::Hook::process('path_info_whitelist', {whitelist => \@whitelist});
+ if (!grep($_ eq $script, @whitelist)) {
+
+ # apache collapses // to / in $ENV{PATH_INFO} but not in $self->path_info.
+ # url() requires the full path in ENV in order to generate the correct url.
+ $ENV{PATH_INFO} = $path;
+ DEBUG("redirecting because we see PATH_INFO and don't like it");
+ print $self->redirect($self->url(-path => 0, -query => 1));
+ exit;
}
+ }
- # Send appropriate charset
- $self->charset(Bugzilla->params->{'utf8'} ? 'UTF-8' : '');
+ # Send appropriate charset
+ $self->charset(Bugzilla->params->{'utf8'} ? 'UTF-8' : '');
- # Redirect to urlbase if we are not viewing an attachment.
- if ($self->url_is_attachment_base and $script ne 'attachment.cgi') {
- DEBUG("Redirecting to urlbase because the url is in the attachment base and not attachment.cgi");
- $self->redirect_to_urlbase();
- }
+ # Redirect to urlbase if we are not viewing an attachment.
+ if ($self->url_is_attachment_base and $script ne 'attachment.cgi') {
+ DEBUG(
+ "Redirecting to urlbase because the url is in the attachment base and not attachment.cgi"
+ );
+ $self->redirect_to_urlbase();
+ }
- # Check for errors
- # All of the Bugzilla code wants to do this, so do it here instead of
- # in each script
-
- my $err = $self->cgi_error;
-
- if ($err) {
- # Note that this error block is only triggered by CGI.pm for malformed
- # multipart requests, and so should never happen unless there is a
- # browser bug.
-
- print $self->header(-status => $err);
-
- # ThrowCodeError wants to print the header, so it grabs Bugzilla->cgi
- # which creates a new Bugzilla::CGI object, which fails again, which
- # ends up here, and calls ThrowCodeError, and then recurses forever.
- # So don't use it.
- # In fact, we can't use templates at all, because we need a CGI object
- # to determine the template lang as well as the current url (from the
- # template)
- # Since this is an internal error which indicates a severe browser bug,
- # just die.
- die "CGI parsing error: $err";
- }
+ # Check for errors
+ # All of the Bugzilla code wants to do this, so do it here instead of
+ # in each script
- return $self;
+ my $err = $self->cgi_error;
+
+ if ($err) {
+
+ # Note that this error block is only triggered by CGI.pm for malformed
+ # multipart requests, and so should never happen unless there is a
+ # browser bug.
+
+ print $self->header(-status => $err);
+
+ # ThrowCodeError wants to print the header, so it grabs Bugzilla->cgi
+ # which creates a new Bugzilla::CGI object, which fails again, which
+ # ends up here, and calls ThrowCodeError, and then recurses forever.
+ # So don't use it.
+ # In fact, we can't use templates at all, because we need a CGI object
+ # to determine the template lang as well as the current url (from the
+ # template)
+ # Since this is an internal error which indicates a severe browser bug,
+ # just die.
+ die "CGI parsing error: $err";
+ }
+
+ return $self;
}
sub target_uri {
- my ($self) = @_;
-
- my $base = Bugzilla->localconfig->{urlbase};
- if (my $request_uri = $self->request_uri) {
- my $base_uri = URI->new($base);
- $base_uri->path('');
- $base_uri->query(undef);
- return $base_uri . $request_uri;
- }
- else {
- return $base . ($self->url(-relative => 1, -query => 1) || 'index.cgi');
- }
+ my ($self) = @_;
+
+ my $base = Bugzilla->localconfig->{urlbase};
+ if (my $request_uri = $self->request_uri) {
+ my $base_uri = URI->new($base);
+ $base_uri->path('');
+ $base_uri->query(undef);
+ return $base_uri . $request_uri;
+ }
+ else {
+ return $base . ($self->url(-relative => 1, -query => 1) || 'index.cgi');
+ }
}
sub content_security_policy {
- my ($self, %add_params) = @_;
- if (%add_params || !$self->{Bugzilla_csp}) {
- my %params = DEFAULT_CSP;
- delete $params{report_only} if %add_params && !$add_params{report_only};
- foreach my $key (keys %add_params) {
- if (defined $add_params{$key}) {
- $params{$key} = $add_params{$key};
- }
- else {
- delete $params{$key};
- }
- }
- $self->{Bugzilla_csp} = Bugzilla::CGI::ContentSecurityPolicy->new(%params);
+ my ($self, %add_params) = @_;
+ if (%add_params || !$self->{Bugzilla_csp}) {
+ my %params = DEFAULT_CSP;
+ delete $params{report_only} if %add_params && !$add_params{report_only};
+ foreach my $key (keys %add_params) {
+ if (defined $add_params{$key}) {
+ $params{$key} = $add_params{$key};
+ }
+ else {
+ delete $params{$key};
+ }
}
+ $self->{Bugzilla_csp} = Bugzilla::CGI::ContentSecurityPolicy->new(%params);
+ }
- return $self->{Bugzilla_csp};
+ return $self->{Bugzilla_csp};
}
sub csp_nonce {
- my ($self) = @_;
+ my ($self) = @_;
- my $csp = $self->content_security_policy;
- return $csp->has_nonce ? $csp->nonce : '';
+ my $csp = $self->content_security_policy;
+ return $csp->has_nonce ? $csp->nonce : '';
}
# We want this sorted plus the ability to exclude certain params
sub canonicalise_query {
- my ($self, @exclude) = @_;
+ my ($self, @exclude) = @_;
- # Reconstruct the URL by concatenating the sorted param=value pairs
- my @parameters;
- foreach my $key (sort($self->param())) {
- # Leave this key out if it's in the exclude list
- next if grep { $_ eq $key } @exclude;
+ # Reconstruct the URL by concatenating the sorted param=value pairs
+ my @parameters;
+ foreach my $key (sort($self->param())) {
- # Remove the Boolean Charts for standard query.cgi fields
- # They are listed in the query URL already
- next if $key =~ /^(field|type|value)(-\d+){3}$/;
+ # Leave this key out if it's in the exclude list
+ next if grep { $_ eq $key } @exclude;
- my $esc_key = url_quote($key);
+ # Remove the Boolean Charts for standard query.cgi fields
+ # They are listed in the query URL already
+ next if $key =~ /^(field|type|value)(-\d+){3}$/;
- foreach my $value ($self->param($key)) {
- # Omit params with an empty value
- if (defined($value) && $value ne '') {
- my $esc_value = url_quote($value);
+ my $esc_key = url_quote($key);
- push(@parameters, "$esc_key=$esc_value");
- }
- }
- }
+ foreach my $value ($self->param($key)) {
- return join("&", @parameters);
-}
+ # Omit params with an empty value
+ if (defined($value) && $value ne '') {
+ my $esc_value = url_quote($value);
-sub clean_search_url {
- my $self = shift;
- # Delete any empty URL parameter.
- my @cgi_params = $self->param;
-
- foreach my $param (@cgi_params) {
- if (defined $self->param($param) && $self->param($param) eq '') {
- $self->delete($param);
- $self->delete("${param}_type");
- }
-
- # Custom Search stuff is empty if it's "noop". We also keep around
- # the old Boolean Chart syntax for backwards-compatibility.
- if (($param =~ /\d-\d-\d/ || $param =~ /^[[:alpha:]]\d+$/)
- && defined $self->param($param) && $self->param($param) eq 'noop')
- {
- $self->delete($param);
- }
-
- # Any "join" for custom search that's an AND can be removed, because
- # that's the default.
- if (($param =~ /^j\d+$/ || $param eq 'j_top')
- && $self->param($param) eq 'AND')
- {
- $self->delete($param);
- }
+ push(@parameters, "$esc_key=$esc_value");
+ }
}
+ }
- # Delete leftovers from the login form
- $self->delete('Bugzilla_remember', 'GoAheadAndLogIn');
+ return join("&", @parameters);
+}
- # Delete the token if we're not performing an action which needs it
- unless ((defined $self->param('remtype')
- && ($self->param('remtype') eq 'asdefault'
- || $self->param('remtype') eq 'asnamed'))
- || (defined $self->param('remaction')
- && $self->param('remaction') eq 'forget'))
- {
- $self->delete("token");
- }
+sub clean_search_url {
+ my $self = shift;
- foreach my $num (1,2,3) {
- # If there's no value in the email field, delete the related fields.
- if (!$self->param("email$num")) {
- foreach my $field (qw(type assigned_to reporter qa_contact cc longdesc)) {
- $self->delete("email$field$num");
- }
- }
- }
+ # Delete any empty URL parameter.
+ my @cgi_params = $self->param;
- # chfieldto is set to "Now" by default in query.cgi. But if none
- # of the other chfield parameters are set, it's meaningless.
- if (!defined $self->param('chfieldfrom') && !$self->param('chfield')
- && !defined $self->param('chfieldvalue') && $self->param('chfieldto')
- && lc($self->param('chfieldto')) eq 'now')
- {
- $self->delete('chfieldto');
+ foreach my $param (@cgi_params) {
+ if (defined $self->param($param) && $self->param($param) eq '') {
+ $self->delete($param);
+ $self->delete("${param}_type");
}
- # cmdtype "doit" is the default from query.cgi, but it's only meaningful
- # if there's a remtype parameter.
- if (defined $self->param('cmdtype') && $self->param('cmdtype') eq 'doit'
- && !defined $self->param('remtype'))
+ # Custom Search stuff is empty if it's "noop". We also keep around
+ # the old Boolean Chart syntax for backwards-compatibility.
+ if ( ($param =~ /\d-\d-\d/ || $param =~ /^[[:alpha:]]\d+$/)
+ && defined $self->param($param)
+ && $self->param($param) eq 'noop')
{
- $self->delete('cmdtype');
+ $self->delete($param);
}
- # "Reuse same sort as last time" is actually the default, so we don't
- # need it in the URL.
- if ($self->param('order')
- && $self->param('order') eq 'Reuse same sort as last time')
+ # Any "join" for custom search that's an AND can be removed, because
+ # that's the default.
+ if (($param =~ /^j\d+$/ || $param eq 'j_top') && $self->param($param) eq 'AND')
{
- $self->delete('order');
+ $self->delete($param);
}
-
- # 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');
-
- # 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');
+ }
+
+ # 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');
+
+ # 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');
+ }
}
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;
- }
+ 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;
}
+ }
- return 0;
+ return 0;
}
# Overwrite to ensure nph doesn't get set, and unset HEADERS_ONCE
sub multipart_init {
- my $self = shift;
-
- # Keys are case-insensitive, map to lowercase
- my %args = @_;
- my %param;
- foreach my $key (keys %args) {
- $param{lc $key} = $args{$key};
- }
-
- # Set the MIME boundary and content-type
- my $boundary = $param{'-boundary'}
- || '------- =_' . generate_random_password(16);
- delete $param{'-boundary'};
- $self->{'separator'} = "\r\n--$boundary\r\n";
- $self->{'final_separator'} = "\r\n--$boundary--\r\n";
- $param{'-type'} = CGI::SERVER_PUSH($boundary);
-
- # Note: CGI.pm::multipart_init up to v3.04 explicitly set nph to 0
- # CGI.pm::multipart_init v3.05 explicitly sets nph to 1
- # CGI.pm's header() sets nph according to a param or $CGI::NPH, which
- # is the desired behaviour.
-
- return $self->header(
- %param,
- ) . "WARNING: YOUR BROWSER DOESN'T SUPPORT THIS SERVER-PUSH TECHNOLOGY." . $self->multipart_end;
+ my $self = shift;
+
+ # Keys are case-insensitive, map to lowercase
+ my %args = @_;
+ my %param;
+ foreach my $key (keys %args) {
+ $param{lc $key} = $args{$key};
+ }
+
+ # Set the MIME boundary and content-type
+ my $boundary
+ = $param{'-boundary'} || '------- =_' . generate_random_password(16);
+ delete $param{'-boundary'};
+ $self->{'separator'} = "\r\n--$boundary\r\n";
+ $self->{'final_separator'} = "\r\n--$boundary--\r\n";
+ $param{'-type'} = CGI::SERVER_PUSH($boundary);
+
+ # Note: CGI.pm::multipart_init up to v3.04 explicitly set nph to 0
+ # CGI.pm::multipart_init v3.05 explicitly sets nph to 1
+ # CGI.pm's header() sets nph according to a param or $CGI::NPH, which
+ # is the desired behaviour.
+
+ return
+ $self->header(%param,)
+ . "WARNING: YOUR BROWSER DOESN'T SUPPORT THIS SERVER-PUSH TECHNOLOGY."
+ . $self->multipart_end;
}
# Have to add the cookies in.
sub multipart_start {
- my $self = shift;
+ my $self = shift;
- my %args = @_;
+ my %args = @_;
- # CGI.pm::multipart_start doesn't honour its own charset information, so
- # we do it ourselves here
- if (defined $self->charset() && defined $args{-type}) {
- # Remove any existing charset specifier
- $args{-type} =~ s/;.*$//;
- # and add the specified one
- $args{-type} .= '; charset=' . $self->charset();
- }
+ # CGI.pm::multipart_start doesn't honour its own charset information, so
+ # we do it ourselves here
+ if (defined $self->charset() && defined $args{-type}) {
- my $headers = $self->SUPER::multipart_start(%args);
- # Eliminate the one extra CRLF at the end.
- $headers =~ s/$CGI::CRLF$//;
- # Add the cookies. We have to do it this way instead of
- # passing them to multpart_start, because CGI.pm's multipart_start
- # doesn't understand a '-cookie' argument pointing to an arrayref.
- foreach my $cookie (@{$self->{Bugzilla_cookie_list}}) {
- $headers .= "Set-Cookie: ${cookie}${CGI::CRLF}";
- }
- $headers .= $CGI::CRLF;
- $self->{_multipart_in_progress} = 1;
- return $headers;
+ # Remove any existing charset specifier
+ $args{-type} =~ s/;.*$//;
+
+ # and add the specified one
+ $args{-type} .= '; charset=' . $self->charset();
+ }
+
+ my $headers = $self->SUPER::multipart_start(%args);
+
+ # Eliminate the one extra CRLF at the end.
+ $headers =~ s/$CGI::CRLF$//;
+
+ # Add the cookies. We have to do it this way instead of
+ # passing them to multpart_start, because CGI.pm's multipart_start
+ # doesn't understand a '-cookie' argument pointing to an arrayref.
+ foreach my $cookie (@{$self->{Bugzilla_cookie_list}}) {
+ $headers .= "Set-Cookie: ${cookie}${CGI::CRLF}";
+ }
+ $headers .= $CGI::CRLF;
+ $self->{_multipart_in_progress} = 1;
+ return $headers;
}
sub close_standby_message {
- my ($self, $contenttype, $disp, $disp_prefix, $extension) = @_;
- $self->set_dated_content_disp($disp, $disp_prefix, $extension);
-
- if ($self->{_multipart_in_progress}) {
- print $self->multipart_end();
- print $self->multipart_start(-type => $contenttype);
- }
- else {
- 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);
+ }
+ else {
+ 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) = @_;
- state $safe_content_type_re = qr{
+ my ($self, $headers) = @_;
+ state $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
@@ -440,12 +476,13 @@ sub _prevent_unsafe_response {
# used for HTTP push responses
| multipart/x-mixed-replace)
}sx;
- state $safe_referer_re = do {
- # Note that urlbase must end with a /.
- # It almost certainly does, but let's be extra careful.
- my $urlbase = Bugzilla->localconfig->{urlbase};
- $urlbase =~ s{/$}{};
- qr{
+ state $safe_referer_re = do {
+
+ # Note that urlbase must end with a /.
+ # It almost certainly does, but let's be extra careful.
+ my $urlbase = Bugzilla->localconfig->{urlbase};
+ $urlbase =~ s{/$}{};
+ qr{
# Begins with literal urlbase
^ (*COMMIT)
\Q$urlbase\E
@@ -453,396 +490,420 @@ 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";
- }
- 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";
+ }
+ exit;
}
+ }
}
sub should_block_referrer {
- my ($self) = @_;
- return length($self->self_url) > 8000;
+ my ($self) = @_;
+ return length($self->self_url) > 8000;
}
# Override header so we can add the cookies in
sub header {
- my $self = shift;
-
- my %headers;
- my $user = Bugzilla->user;
-
- # If there's only one parameter, then it's a Content-Type.
- if (scalar(@_) == 1) {
- %headers = ('-type' => shift(@_));
- }
- else {
- %headers = @_;
+ my $self = shift;
+
+ my %headers;
+ my $user = Bugzilla->user;
+
+ # If there's only one parameter, then it's a Content-Type.
+ if (scalar(@_) == 1) {
+ %headers = ('-type' => shift(@_));
+ }
+ else {
+ %headers = @_;
+ }
+
+ $self->_prevent_unsafe_response(\%headers);
+
+ if ($self->{'_content_disp'}) {
+ $headers{'-content_disposition'} = $self->{'_content_disp'};
+ }
+
+ if (!$user->id
+ && $user->authorizer->can_login
+ && !$self->cookie('Bugzilla_login_request_cookie'))
+ {
+ my %args;
+ $args{'-secure'} = 1 if Bugzilla->params->{ssl_redirect};
+
+ $self->send_cookie(
+ -name => 'Bugzilla_login_request_cookie',
+ -value => generate_random_password(),
+ -httponly => 1,
+ %args
+ );
+ }
+
+ # We generate a cookie and store it in the request cache
+ # To initiate github login, a form POSTs to github.cgi with the
+ # github_secret as a parameter. It must match the github_secret cookie.
+ # this prevents some types of redirection attacks.
+ unless ($user->id || $self->{bz_redirecting}) {
+ $self->send_cookie(
+ -name => 'github_secret',
+ -value => Bugzilla->github_secret,
+ -httponly => 1
+ );
+ }
+
+ # Add the cookies in if we have any
+ if (scalar(@{$self->{Bugzilla_cookie_list}})) {
+ $headers{'-cookie'} = $self->{Bugzilla_cookie_list};
+ }
+
+ # Add Strict-Transport-Security (STS) header if this response
+ # is over SSL and the strict_transport_security param is turned on.
+ if ( $self->https
+ && !$self->url_is_attachment_base
+ && Bugzilla->params->{'strict_transport_security'} ne 'off')
+ {
+ my $sts_opts = 'max-age=' . MAX_STS_AGE;
+ if (Bugzilla->params->{'strict_transport_security'} eq 'include_subdomains') {
+ $sts_opts .= '; includeSubDomains';
}
+ $headers{'-strict_transport_security'} = $sts_opts;
+ }
- $self->_prevent_unsafe_response(\%headers);
+ # Add X-Frame-Options header to prevent framing and subsequent
+ # possible clickjacking problems.
+ unless ($self->url_is_attachment_base) {
+ $headers{'-x_frame_options'} = 'SAMEORIGIN';
+ }
- if ($self->{'_content_disp'}) {
- $headers{'-content_disposition'} = $self->{'_content_disp'};
- }
+ if ($self->{'_content_disp'}) {
+ $headers{'-content_disposition'} = $self->{'_content_disp'};
+ }
- if (!$user->id && $user->authorizer->can_login
- && !$self->cookie('Bugzilla_login_request_cookie'))
- {
- my %args;
- $args{'-secure'} = 1 if Bugzilla->params->{ssl_redirect};
+ # 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';
- $self->send_cookie(-name => 'Bugzilla_login_request_cookie',
- -value => generate_random_password(),
- -httponly => 1,
- %args);
- }
-
- # We generate a cookie and store it in the request cache
- # To initiate github login, a form POSTs to github.cgi with the
- # github_secret as a parameter. It must match the github_secret cookie.
- # this prevents some types of redirection attacks.
- unless ($user->id || $self->{bz_redirecting}) {
- $self->send_cookie(-name => 'github_secret',
- -value => Bugzilla->github_secret,
- -httponly => 1);
- }
- # 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;
- }
+ Bugzilla::Hook::process('cgi_headers', {cgi => $self, headers => \%headers});
+ $self->{_header_done} = 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';
+ if (Bugzilla->usage_mode == USAGE_MODE_BROWSER) {
+ if ($self->should_block_referrer) {
+ $headers{'-referrer_policy'} = 'origin';
}
-
- if ($self->{'_content_disp'}) {
- $headers{'-content_disposition'} = $self->{'_content_disp'};
+ my $csp = $self->content_security_policy;
+ if (defined $csp && !$csp->disable) {
+ $csp->add_cgi_headers(\%headers);
}
- # 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';
-
- Bugzilla::Hook::process('cgi_headers',
- { cgi => $self, headers => \%headers }
+ my @fonts = (
+ "skins/standard/fonts/FiraMono-Regular.woff2?v=3.202",
+ "skins/standard/fonts/FiraSans-Bold.woff2?v=4.203",
+ "skins/standard/fonts/FiraSans-Italic.woff2?v=4.203",
+ "skins/standard/fonts/FiraSans-Regular.woff2?v=4.203",
+ "skins/standard/fonts/FiraSans-SemiBold.woff2?v=4.203",
+ "skins/standard/fonts/MaterialIcons-Regular.woff2",
+ );
+ $headers{'-link'} = join(
+ ", ",
+ map {
+ sprintf('</static/v%s/%s>; rel="preload"; as="font"', Bugzilla->VERSION, $_)
+ } @fonts
);
- $self->{_header_done} = 1;
-
- if (Bugzilla->usage_mode == USAGE_MODE_BROWSER) {
- if ($self->should_block_referrer) {
- $headers{'-referrer_policy'} = 'origin';
- }
- my $csp = $self->content_security_policy;
- if (defined $csp && !$csp->disable) {
- $csp->add_cgi_headers(\%headers)
- }
-
- my @fonts = (
- "skins/standard/fonts/FiraMono-Regular.woff2?v=3.202",
- "skins/standard/fonts/FiraSans-Bold.woff2?v=4.203",
- "skins/standard/fonts/FiraSans-Italic.woff2?v=4.203",
- "skins/standard/fonts/FiraSans-Regular.woff2?v=4.203",
- "skins/standard/fonts/FiraSans-SemiBold.woff2?v=4.203",
- "skins/standard/fonts/MaterialIcons-Regular.woff2",
- );
- $headers{'-link'} = join(", ", map { sprintf('</static/v%s/%s>; rel="preload"; as="font"', Bugzilla->VERSION, $_) } @fonts);
- if (Bugzilla->params->{google_analytics_tracking_id}) {
- $headers{'-link'} .= ', <https://www.google-analytics.com>; rel="preconnect"; crossorigin';
- }
+ if (Bugzilla->params->{google_analytics_tracking_id}) {
+ $headers{'-link'}
+ .= ', <https://www.google-analytics.com>; rel="preconnect"; crossorigin';
+ }
+ }
+ my $headers = $self->SUPER::header(%headers) || '';
+ if ($self->server_software eq 'Bugzilla::Quantum::CGI') {
+ my $c = $Bugzilla::Quantum::CGI::C;
+ $c->res->headers->parse($headers);
+ my $status = $c->res->headers->status;
+ if ($status && $status =~ /^([0-9]+)/) {
+ $c->res->code($1);
}
- my $headers = $self->SUPER::header(%headers) || '';
- if ($self->server_software eq 'Bugzilla::Quantum::CGI') {
- my $c = $Bugzilla::Quantum::CGI::C;
- $c->res->headers->parse($headers);
- my $status = $c->res->headers->status;
- if ($status && $status =~ /^([0-9]+)/) {
- $c->res->code($1);
- }
- elsif ($c->res->headers->location) {
- $c->res->code(302);
- }
- else {
- $c->res->code(200);
- }
- return '';
+ elsif ($c->res->headers->location) {
+ $c->res->code(302);
}
else {
- LOGDIE("Bugzilla::CGI->header() should only be called from inside Bugzilla::Quantum::CGI!");
+ $c->res->code(200);
}
+ return '';
+ }
+ else {
+ LOGDIE(
+ "Bugzilla::CGI->header() should only be called from inside Bugzilla::Quantum::CGI!"
+ );
+ }
}
sub param {
- my $self = shift;
-
- # We don't let CGI.pm warn about list context, but we do it ourselves.
- local $CGI::LIST_CONTEXT_WARN = 0;
- if (0) {
- state $has_warned = {};
-
- ## no critic (Freenode::Wantarray)
- if ( wantarray && @_ ) {
- my ( $package, $filename, $line ) = caller;
- if ( $package ne 'CGI' && ! $has_warned->{"$filename:$line"}++) {
- WARN("Bugzilla::CGI::param called in list context from $package $filename:$line");
- }
- }
- ## use critic
- }
-
- # When we are just requesting the value of a parameter...
- if (scalar(@_) == 1) {
- my @result = $self->SUPER::param(@_);
-
- # Also look at the URL parameters, after we look at the POST
- # parameters. This is to allow things like login-form submissions
- # with URL parameters in the form's "target" attribute.
- if (!scalar(@result)
- && $self->request_method && $self->request_method eq 'POST')
- {
- # Some servers fail to set the QUERY_STRING parameter, which
- # causes undef issues
- $ENV{'QUERY_STRING'} = '' unless exists $ENV{'QUERY_STRING'};
- @result = $self->SUPER::url_param(@_);
- }
-
- # Fix UTF-8-ness of input parameters.
- if (Bugzilla->params->{'utf8'}) {
- @result = map { _fix_utf8($_) } @result;
- }
-
- return wantarray ? @result : $result[0];
+ my $self = shift;
+
+ # We don't let CGI.pm warn about list context, but we do it ourselves.
+ local $CGI::LIST_CONTEXT_WARN = 0;
+ if (0) {
+ state $has_warned = {};
+
+ ## no critic (Freenode::Wantarray)
+ if (wantarray && @_) {
+ my ($package, $filename, $line) = caller;
+ if ($package ne 'CGI' && !$has_warned->{"$filename:$line"}++) {
+ WARN(
+ "Bugzilla::CGI::param called in list context from $package $filename:$line");
+ }
}
- # 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')
+ ## use critic
+ }
+
+ # When we are just requesting the value of a parameter...
+ if (scalar(@_) == 1) {
+ my @result = $self->SUPER::param(@_);
+
+ # Also look at the URL parameters, after we look at the POST
+ # parameters. This is to allow things like login-form submissions
+ # with URL parameters in the form's "target" attribute.
+ if ( !scalar(@result)
+ && $self->request_method
+ && $self->request_method eq 'POST')
{
- my @post_params = $self->SUPER::param;
- my @url_params = $self->url_param;
- my %params = map { $_ => 1 } (@post_params, @url_params);
- return keys %params;
+ # Some servers fail to set the QUERY_STRING parameter, which
+ # causes undef issues
+ $ENV{'QUERY_STRING'} = '' unless exists $ENV{'QUERY_STRING'};
+ @result = $self->SUPER::url_param(@_);
}
- return $self->SUPER::param(@_);
+ # Fix UTF-8-ness of input parameters.
+ if (Bugzilla->params->{'utf8'}) {
+ @result = map { _fix_utf8($_) } @result;
+ }
+
+ return wantarray ? @result : $result[0];
+ }
+
+ # And for various other functions in CGI.pm, we need to correctly
+ # return the URL parameters in addition to the POST parameters when
+ # asked for the list of parameters.
+ elsif (!scalar(@_) && $self->request_method && $self->request_method eq 'POST')
+ {
+ my @post_params = $self->SUPER::param;
+ my @url_params = $self->url_param;
+ my %params = map { $_ => 1 } (@post_params, @url_params);
+ return keys %params;
+ }
+
+ return $self->SUPER::param(@_);
}
sub _fix_utf8 {
- my $input = shift;
- # The is_utf8 is here in case CGI gets smart about utf8 someday.
- utf8::decode($input) if defined $input && !ref $input && !utf8::is_utf8($input);
- return $input;
+ my $input = shift;
+
+ # The is_utf8 is here in case CGI gets smart about utf8 someday.
+ utf8::decode($input) if defined $input && !ref $input && !utf8::is_utf8($input);
+ return $input;
}
sub should_set {
- my ($self, $param) = @_;
- my $set = (defined $self->param($param)
- or defined $self->param("defined_$param"))
- ? 1 : 0;
- return $set;
+ my ($self, $param) = @_;
+ my $set
+ = (defined $self->param($param) or defined $self->param("defined_$param"))
+ ? 1
+ : 0;
+ return $set;
}
# The various parts of Bugzilla which create cookies don't want to have to
# pass them around to all of the callers. Instead, store them locally here,
# and then output as required from |header|.
sub send_cookie {
- my ($self, %paramhash) = @_;
+ my ($self, %paramhash) = @_;
- # Complain if -value is not given or empty (bug 268146).
- if (!exists($paramhash{'-value'}) || !$paramhash{'-value'}) {
- ThrowCodeError('cookies_need_value');
- }
+ # Complain if -value is not given or empty (bug 268146).
+ if (!exists($paramhash{'-value'}) || !$paramhash{'-value'}) {
+ ThrowCodeError('cookies_need_value');
+ }
- # Add the default path and the domain in.
- state $uri = URI->new( Bugzilla->localconfig->{urlbase} );
- $paramhash{'-path'} = $uri->path;
- # we don't set the domain.
- $paramhash{'-secure'} = 1
- if lc( $uri->scheme ) eq 'https';
+ # Add the default path and the domain in.
+ state $uri = URI->new(Bugzilla->localconfig->{urlbase});
+ $paramhash{'-path'} = $uri->path;
- $paramhash{'-samesite'} = 'Lax';
+ # we don't set the domain.
+ $paramhash{'-secure'} = 1 if lc($uri->scheme) eq 'https';
- push(@{$self->{'Bugzilla_cookie_list'}}, $self->cookie(%paramhash));
+ $paramhash{'-samesite'} = 'Lax';
+
+ 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'
+ );
}
# To avoid infinite redirection recursion, track when we're within a redirect
# request.
sub redirect {
- my $self = shift;
- $self->{bz_redirecting} = 1;
- return $self->SUPER::redirect(@_);
+ my $self = shift;
+ $self->{bz_redirecting} = 1;
+ return $self->SUPER::redirect(@_);
}
use Bugzilla::Logging;
+
# This helps implement Bugzilla::Search::Recent, and also shortens search
# URLs that get POSTed to buglist.cgi.
sub redirect_search_url {
- my $self = shift;
-
- # If there is no parameter, there is nothing to do.
- return unless $self->param;
-
- # If we're retreiving an old list, we never need to redirect or
- # do anything related to Bugzilla::Search::Recent.
- return if $self->param('regetlastlist');
-
- my $user = Bugzilla->user;
-
- if ($user->id) {
- # There are two conditions that could happen here--we could get a URL
- # with no list id, and we could get a URL with a list_id that isn't
- # ours.
- my $list_id = $self->param('list_id');
- if ($list_id) {
- # If we have a valid list_id, no need to redirect or clean.
- return if Bugzilla::Search::Recent->check_quietly(
- { id => $list_id });
- }
- }
- elsif ($self->request_method ne 'POST') {
- # Logged-out users who do a GET don't get a list_id, don't get
- # their URLs cleaned, and don't get redirected.
- return;
- }
+ my $self = shift;
- $self->clean_search_url();
-
- # Make sure we still have params still after cleaning otherwise we
- # do not want to store a list_id for an empty search.
- if ($user->id && $self->param) {
- # Insert a placeholder Bugzilla::Search::Recent, so that we know what
- # the id of the resulting search will be. This is then pulled out
- # of the Referer header when viewing show_bug.cgi to know what
- # bug list we came from.
- my $recent_search = Bugzilla::Search::Recent->create_placeholder;
- $self->param('list_id', $recent_search->id);
- }
+ # If there is no parameter, there is nothing to do.
+ return unless $self->param;
- # GET requests that lacked a list_id are always redirected. POST requests
- # are only redirected if they're under the CGI_URI_LIMIT though.
- my $self_url = $self->self_url();
- if ($self->request_method() ne 'POST' or length($self_url) < CGI_URI_LIMIT) {
- DEBUG("Redirecting search url");
- print $self->redirect(-url => $self_url);
- exit;
+ # If we're retreiving an old list, we never need to redirect or
+ # do anything related to Bugzilla::Search::Recent.
+ return if $self->param('regetlastlist');
+
+ my $user = Bugzilla->user;
+
+ if ($user->id) {
+
+ # There are two conditions that could happen here--we could get a URL
+ # with no list id, and we could get a URL with a list_id that isn't
+ # ours.
+ my $list_id = $self->param('list_id');
+ if ($list_id) {
+
+ # If we have a valid list_id, no need to redirect or clean.
+ return if Bugzilla::Search::Recent->check_quietly({id => $list_id});
}
+ }
+ elsif ($self->request_method ne 'POST') {
+
+ # Logged-out users who do a GET don't get a list_id, don't get
+ # their URLs cleaned, and don't get redirected.
+ return;
+ }
+
+ $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);
+ }
+
+ # GET requests that lacked a list_id are always redirected. POST requests
+ # are only redirected if they're under the CGI_URI_LIMIT though.
+ my $self_url = $self->self_url();
+ if ($self->request_method() ne 'POST' or length($self_url) < CGI_URI_LIMIT) {
+ DEBUG("Redirecting search url");
+ print $self->redirect(-url => $self_url);
+ exit;
+ }
}
sub redirect_to_https {
- my $self = shift;
- my $urlbase = Bugzilla->localconfig->{'urlbase'};
-
- # 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 = $urlbase . $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.
- DEBUG("Redirecting to https");
- print $self->redirect(-location => $url, -status => 301);
- exit;
+ my $self = shift;
+ my $urlbase = Bugzilla->localconfig->{'urlbase'};
+
+ # 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
+ = $urlbase . $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.
+ DEBUG("Redirecting to https");
+ print $self->redirect(-location => $url, -status => 301);
+ 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' => Bugzilla->localconfig->{urlbase} . $path);
- exit;
+ my $self = shift;
+ my $path = $self->url('-path_info' => 1, '-query' => 1, '-relative' => 1);
+ print $self->redirect('-location' => Bugzilla->localconfig->{urlbase} . $path);
+ exit;
}
sub url_is_attachment_base {
- my ($self, $id) = @_;
- return 0 if !use_attachbase() or !i_am_cgi();
- my $attach_base = Bugzilla->localconfig->{'attachment_base'};
- # If we're passed an id, we only want one specific attachment base
- # for a particular bug. If we're not passed an ID, we just want to
- # know if our current URL matches the attachment_base *pattern*.
- my $regex;
- if ($id) {
- $attach_base =~ s/\%bugid\%/$id/;
- $regex = quotemeta($attach_base);
- }
- else {
- # In this circumstance we run quotemeta first because we need to
- # insert an active regex meta-character afterward.
- $regex = quotemeta($attach_base);
- $regex =~ s/\\\%bugid\\\%/\\d+/;
- }
- $regex = "^$regex";
- return ($self->url =~ $regex) ? 1 : 0;
+ my ($self, $id) = @_;
+ return 0 if !use_attachbase() or !i_am_cgi();
+ my $attach_base = Bugzilla->localconfig->{'attachment_base'};
+
+ # If we're passed an id, we only want one specific attachment base
+ # for a particular bug. If we're not passed an ID, we just want to
+ # know if our current URL matches the attachment_base *pattern*.
+ my $regex;
+ if ($id) {
+ $attach_base =~ s/\%bugid\%/$id/;
+ $regex = quotemeta($attach_base);
+ }
+ else {
+ # In this circumstance we run quotemeta first because we need to
+ # insert an active regex meta-character afterward.
+ $regex = quotemeta($attach_base);
+ $regex =~ s/\\\%bugid\\\%/\\d+/;
+ }
+ $regex = "^$regex";
+ return ($self->url =~ $regex) ? 1 : 0;
}
sub set_dated_content_disp {
- my ($self, $type, $prefix, $ext) = @_;
+ my ($self, $type, $prefix, $ext) = @_;
- my @time = localtime(time());
- my $date = sprintf "%04d-%02d-%02d", 1900+$time[5], $time[4]+1, $time[3];
- my $filename = "$prefix-$date.$ext";
+ my @time = localtime(time());
+ my $date = sprintf "%04d-%02d-%02d", 1900 + $time[5], $time[4] + 1, $time[3];
+ my $filename = "$prefix-$date.$ext";
- $filename =~ s/\s/_/g; # Remove whitespace to avoid HTTP header tampering
- $filename =~ s/\\/_/g; # Remove backslashes as well
- $filename =~ s/"/\\"/g; # escape quotes
+ $filename =~ s/\s/_/g; # Remove whitespace to avoid HTTP header tampering
+ $filename =~ s/\\/_/g; # Remove backslashes as well
+ $filename =~ s/"/\\"/g; # escape quotes
- my $disposition = "$type; filename=\"$filename\"";
+ my $disposition = "$type; filename=\"$filename\"";
- $self->{'_content_disp'} = $disposition;
+ $self->{'_content_disp'} = $disposition;
}
##########################
@@ -852,30 +913,30 @@ sub set_dated_content_disp {
# Fix the TIEHASH interface (scalar $cgi->Vars) to return and accept
# arrayrefs.
sub STORE {
- my $self = shift;
- my ($param, $value) = @_;
- if (defined $value and ref $value eq 'ARRAY') {
- return $self->param(-name => $param, -value => $value);
- }
- return $self->SUPER::STORE(@_);
+ my $self = shift;
+ my ($param, $value) = @_;
+ if (defined $value and ref $value eq 'ARRAY') {
+ return $self->param(-name => $param, -value => $value);
+ }
+ return $self->SUPER::STORE(@_);
}
sub FETCH {
- my ($self, $param) = @_;
- return $self if $param eq 'CGI'; # CGI.pm did this, so we do too.
- my @result = $self->param($param);
- return undef if !scalar(@result);
- return $result[0] if scalar(@result) == 1;
- return \@result;
+ my ($self, $param) = @_;
+ return $self if $param eq 'CGI'; # CGI.pm did this, so we do too.
+ my @result = $self->param($param);
+ return undef if !scalar(@result);
+ return $result[0] if scalar(@result) == 1;
+ return \@result;
}
# For the Vars TIEHASH interface: the normal CGI.pm DELETE doesn't return
# the value deleted, but Perl's "delete" expects that value.
sub DELETE {
- my ($self, $param) = @_;
- my $value = $self->FETCH($param);
- $self->delete($param);
- return $value;
+ my ($self, $param) = @_;
+ my $value = $self->FETCH($param);
+ $self->delete($param);
+ return $value;
}
1;
diff --git a/Bugzilla/CGI/ContentSecurityPolicy.pm b/Bugzilla/CGI/ContentSecurityPolicy.pm
index 50a399cdc..557a896ab 100644
--- a/Bugzilla/CGI/ContentSecurityPolicy.pm
+++ b/Bugzilla/CGI/ContentSecurityPolicy.pm
@@ -17,123 +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 worker_src connect_src
- font_src img_src media_src
- object_src script_src style_src
- frame_src frame_ancestors form_action
+my @ALL_SRC = qw(
+ default_src worker_src connect_src
+ font_src img_src media_src
+ object_src script_src style_src
+ frame_src frame_ancestors form_action
);
-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');
- if ($self->report_only) {
- return map { $_ . '-Report-Only' } @names;
- }
- else {
- return @names;
- }
+ my ($self) = @_;
+ my @names = ('Content-Security-Policy');
+ 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__
diff --git a/Bugzilla/CPAN.pm b/Bugzilla/CPAN.pm
index 1b6fb93b9..96a7cee05 100644
--- a/Bugzilla/CPAN.pm
+++ b/Bugzilla/CPAN.pm
@@ -15,107 +15,110 @@ use Bugzilla::Constants qw(bz_locations);
use Bugzilla::Install::Requirements qw(check_cpan_feature);
BEGIN {
- my $json_xs_ok = eval {
- require JSON::XS;
- require JSON;
- JSON->VERSION("2.5");
- 1;
- };
- if ($json_xs_ok) {
- $ENV{PERL_JSON_BACKEND} = 'JSON::XS';
- }
+ my $json_xs_ok = eval {
+ require JSON::XS;
+ require JSON;
+ JSON->VERSION("2.5");
+ 1;
+ };
+ if ($json_xs_ok) {
+ $ENV{PERL_JSON_BACKEND} = 'JSON::XS';
+ }
}
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;
};
my (%FEATURE, %FEATURE_LOADED);
sub cpan_meta {
- my ($class) = @_;
- my $dir = bz_locations()->{libpath};
- my $file = File::Spec->catfile($dir, 'MYMETA.json');
- state $CPAN_META;
-
- return $CPAN_META if $CPAN_META;
-
- if (-f $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 = CPAN::Meta->load_json_string($str);
- }
- else {
- require Bugzilla::Error;
- Bugzilla::Error::ThrowCodeError('cpan_meta_missing');
- }
+ my ($class) = @_;
+ my $dir = bz_locations()->{libpath};
+ my $file = File::Spec->catfile($dir, 'MYMETA.json');
+ state $CPAN_META;
+
+ return $CPAN_META if $CPAN_META;
+
+ if (-f $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 = CPAN::Meta->load_json_string($str);
+ }
+ else {
+ require Bugzilla::Error;
+ Bugzilla::Error::ThrowCodeError('cpan_meta_missing');
+ }
}
sub cpan_requirements {
- my ($class, $prereqs) = @_;
- if ($prereqs->can('merged_requirements')) {
- return $prereqs->merged_requirements( [ 'configure', 'runtime' ], ['requires'] );
- }
- else {
- my $req = CPAN::Meta::Requirements->new;
- $req->add_requirements( $prereqs->requirements_for('configure', 'requires') );
- $req->add_requirements( $prereqs->requirements_for('runtime', 'requires') );
- return $req;
- }
+ my ($class, $prereqs) = @_;
+ if ($prereqs->can('merged_requirements')) {
+ return $prereqs->merged_requirements(['configure', 'runtime'], ['requires']);
+ }
+ else {
+ my $req = CPAN::Meta::Requirements->new;
+ $req->add_requirements($prereqs->requirements_for('configure', 'requires'));
+ $req->add_requirements($prereqs->requirements_for('runtime', 'requires'));
+ return $req;
+ }
}
sub has_feature {
- my ($class, $feature_name) = @_;
+ my ($class, $feature_name) = @_;
- return 0 unless _CAN_HAS_FEATURE;
- return $FEATURE{$feature_name} if exists $FEATURE{ $feature_name };
+ return 0 unless _CAN_HAS_FEATURE;
+ return $FEATURE{$feature_name} if exists $FEATURE{$feature_name};
- my $meta = $class->cpan_meta;
- my $feature = eval { $meta->feature($feature_name) };
- unless ($feature) {
- require Bugzilla::Error;
- Bugzilla::Error::ThrowCodeError('invalid_feature', { feature => $feature_name });
- }
+ my $meta = $class->cpan_meta;
+ my $feature = eval { $meta->feature($feature_name) };
+ unless ($feature) {
+ require Bugzilla::Error;
+ Bugzilla::Error::ThrowCodeError('invalid_feature', {feature => $feature_name});
+ }
- return $FEATURE{$feature_name} = check_cpan_feature($feature)->{ok};
+ return $FEATURE{$feature_name} = check_cpan_feature($feature)->{ok};
}
# 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().
sub feature {
- my ($class, $feature_name) = @_;
- return 0 unless _CAN_HAS_FEATURE;
- return 1 if $FEATURE_LOADED{$feature_name};
- return 0 unless $class->has_feature($feature_name);
-
- my $meta = $class->cpan_meta;
- my $feature = $meta->feature($feature_name);
- my @modules = $feature->prereqs->merged_requirements(['runtime'], ['requires'])->required_modules;
- Module::Runtime::require_module($_) foreach grep { !/^Test::Taint$/ } @modules;
- return $FEATURE_LOADED{$feature_name} = 1;
+ my ($class, $feature_name) = @_;
+ return 0 unless _CAN_HAS_FEATURE;
+ return 1 if $FEATURE_LOADED{$feature_name};
+ return 0 unless $class->has_feature($feature_name);
+
+ my $meta = $class->cpan_meta;
+ my $feature = $meta->feature($feature_name);
+ my @modules = $feature->prereqs->merged_requirements(['runtime'], ['requires'])
+ ->required_modules;
+ Module::Runtime::require_module($_) foreach grep { !/^Test::Taint$/ } @modules;
+ return $FEATURE_LOADED{$feature_name} = 1;
}
sub preload_features {
- my ($class) = @_;
- return 0 unless _CAN_HAS_FEATURE;
- my $meta = $class->cpan_meta;
-
- foreach my $feature ($meta->features) {
- next if $feature->identifier eq 'mod_perl';
- $class->feature($feature->identifier);
- }
+ my ($class) = @_;
+ return 0 unless _CAN_HAS_FEATURE;
+ my $meta = $class->cpan_meta;
+
+ foreach my $feature ($meta->features) {
+ next if $feature->identifier eq 'mod_perl';
+ $class->feature($feature->identifier);
+ }
}
1;
diff --git a/Bugzilla/Chart.pm b/Bugzilla/Chart.pm
index 9dce19eb9..c9aa1f2dd 100644
--- a/Bugzilla/Chart.pm
+++ b/Bugzilla/Chart.pm
@@ -26,405 +26,424 @@ use Date::Parse;
use List::Util qw(max);
sub new {
- my $invocant = shift;
- my $class = ref($invocant) || $invocant;
+ my $invocant = shift;
+ my $class = ref($invocant) || $invocant;
- # Create a ref to an empty hash and bless it
- my $self = {};
- bless($self, $class);
+ # 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 \($#_\)($_)");
- }
+ 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->param()) {
- # Store all the lines
- if ($param =~ /^line(\d+)$/) {
- foreach my $series_id ($cgi->param($param)) {
- detaint_natural($series_id)
- || ThrowCodeError("invalid_series_id");
- my $series = new Bugzilla::Series($series_id);
- push(@{$self->{'lines'}[$1]}, $series) if $series;
- }
- }
-
- # Store all the labels
- if ($param =~ /^label(\d+)$/) {
- $self->{'labels'}[$1] = $cgi->param($param);
- }
+ 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->param()) {
+
+ # Store all the lines
+ if ($param =~ /^line(\d+)$/) {
+ foreach my $series_id ($cgi->param($param)) {
+ detaint_natural($series_id) || ThrowCodeError("invalid_series_id");
+ my $series = new Bugzilla::Series($series_id);
+ push(@{$self->{'lines'}[$1]}, $series) if $series;
+ }
}
- # Store 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}});
- }
+ # Store all the labels
+ if ($param =~ /^label(\d+)$/) {
+ $self->{'labels'}[$1] = $cgi->param($param);
}
-
- # 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 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 = @_;
+ 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]);
- }
+ 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 = @_;
+ 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 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;
+ # We can't add less than two things.
+ return if scalar(@line_ids) < 2;
- my @series;
- my $label = "";
- my $biggestlength = 0;
+ 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]};
+ # 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);
- }
+ 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];
- }
+ # 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);
+ }
}
+ # We are done with the series making up this line, go to the next one
+ $line_index++;
+ }
- # Add the x-axis labels into the data structure
- my $date_progression = generateDateProgression($datefrom, $dateto);
- unshift(@data, $date_progression);
+ # calculate maximum y value
+ if ($self->{'cumulate'}) {
- if ($self->{'gt'}) {
- # Add Grand Total to label list
- push(@{$self->{'labels'}}, $self->{'labelgt'});
-
- $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;
+
+ while ($datefrom < $dateto) {
+ push(@progression, time2str("%Y-%m-%d", $datefrom));
+ $datefrom += $oneday;
+ }
- return \@progression;
+ return \@progression;
}
sub dump {
- my $self = shift;
+ my $self = shift;
- # Make sure we've read in our data
- my $data = $self->data;
+ # Make sure we've read in our data
+ my $data = $self->data;
- require Data::Dumper;
- print "<pre>Bugzilla::Chart object:\n";
- print html_quote(Data::Dumper::Dumper($self));
- print "</pre>";
+ require Data::Dumper;
+ print "<pre>Bugzilla::Chart object:\n";
+ print html_quote(Data::Dumper::Dumper($self));
+ print "</pre>";
}
1;
diff --git a/Bugzilla/Classification.pm b/Bugzilla/Classification.pm
index a931767d2..a0bcaa477 100644
--- a/Bugzilla/Classification.pm
+++ b/Bugzilla/Classification.pm
@@ -25,26 +25,26 @@ use base qw(Bugzilla::Field::ChoiceInterface Bugzilla::Object);
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,
};
###############################
@@ -52,29 +52,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();
}
@@ -83,38 +85,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;
}
#####################################
@@ -123,41 +128,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'};
}
###############################
@@ -165,7 +174,7 @@ sub products {
###############################
sub description { return $_[0]->{'description'}; }
-sub sortkey { return $_[0]->{'sortkey'}; }
+sub sortkey { return $_[0]->{'sortkey'}; }
1;
diff --git a/Bugzilla/Comment.pm b/Bugzilla/Comment.pm
index f9a6f7d3a..50bab3fec 100644
--- a/Bugzilla/Comment.pm
+++ b/Bugzilla/Comment.pm
@@ -35,47 +35,48 @@ use constant AUDIT_CREATES => 0;
use constant AUDIT_UPDATES => 0;
use constant DB_COLUMNS => qw(
- comment_id
- bug_id
- who
- bug_when
- work_time
- thetext
- isprivate
- already_wrapped
- type
- extra_data
+ comment_id
+ bug_id
+ who
+ bug_when
+ work_time
+ thetext
+ isprivate
+ already_wrapped
+ type
+ extra_data
);
use constant UPDATE_COLUMNS => qw(
- isprivate
- type
- extra_data
+ isprivate
+ type
+ extra_data
);
use constant DB_TABLE => 'longdescs';
use constant ID_FIELD => 'comment_id';
+
# In some rare cases, two comments can have identical timestamps. If
# this happens, we want to be sure that the comment added later shows up
# later in the sequence.
use constant LIST_ORDER => 'bug_when, comment_id';
use constant VALIDATORS => {
- bug_id => \&_check_bug_id,
- who => \&_check_who,
- bug_when => \&_check_bug_when,
- work_time => \&_check_work_time,
- thetext => \&_check_thetext,
- isprivate => \&_check_isprivate,
- extra_data => \&_check_extra_data,
- type => \&_check_type,
+ bug_id => \&_check_bug_id,
+ who => \&_check_who,
+ bug_when => \&_check_bug_when,
+ work_time => \&_check_work_time,
+ thetext => \&_check_thetext,
+ isprivate => \&_check_isprivate,
+ extra_data => \&_check_extra_data,
+ type => \&_check_type,
};
use constant VALIDATOR_DEPENDENCIES => {
- extra_data => ['type'],
- bug_id => ['who'],
- work_time => ['who', 'bug_id'],
- isprivate => ['who'],
+ extra_data => ['type'],
+ bug_id => ['who'],
+ work_time => ['who', 'bug_id'],
+ isprivate => ['who'],
};
with 'Bugzilla::Elastic::Role::ChildObject';
@@ -83,20 +84,20 @@ with 'Bugzilla::Elastic::Role::ChildObject';
use constant ES_TYPE => 'comment';
use constant ES_PARENT_CLASS => 'Bugzilla::Bug';
-sub ES_OBJECTS_AT_ONCE { 50 }
+sub ES_OBJECTS_AT_ONCE {50}
sub ES_PROPERTIES {
- return {
- body => { type => "string", analyzer => 'bz_text_analyzer' },
- is_private => { type => "boolean" },
- tags => { type => "string" },
- };
+ return {
+ body => {type => "string", analyzer => 'bz_text_analyzer'},
+ is_private => {type => "boolean"},
+ tags => {type => "string"},
+ };
}
sub ES_SELECT_UPDATED_SQL {
- my ($class, $mtime) = @_;
+ my ($class, $mtime) = @_;
- my $sql = q{
+ my $sql = q{
SELECT DISTINCT
comment_id
FROM
@@ -113,22 +114,19 @@ sub ES_SELECT_UPDATED_SQL {
WHERE
change_when > FROM_UNIXTIME(?)
};
- return ($sql, [$mtime, $mtime]);
+ return ($sql, [$mtime, $mtime]);
}
sub es_parent_id {
- my ($self) = @_;
+ my ($self) = @_;
- return $self->ES_PARENT_CLASS->ES_TYPE . '_' . $self->bug_id,
+ return $self->ES_PARENT_CLASS->ES_TYPE . '_' . $self->bug_id,;
}
sub es_document {
- my ($self) = @_;
+ my ($self) = @_;
- return {
- body => $self->body,
- is_private => $self->is_private,
- };
+ return {body => $self->body, is_private => $self->is_private,};
}
#########################
@@ -136,92 +134,95 @@ sub es_document {
#########################
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);
+ }
+
+ 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;
+ 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
- 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
+ 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) . "
- GROUP BY comment_id");
- foreach my $row (@$rows) {
- $comment_map{$row->[0]}->{tags} = [ split(/,/, $row->[1]) ];
- }
- foreach my $comment (@$comments) {
- $comment->{tags} //= [];
- }
+ GROUP BY comment_id"
+ );
+ foreach my $row (@$rows) {
+ $comment_map{$row->[0]}->{tags} = [split(/,/, $row->[1])];
+ }
+ foreach my $comment (@$comments) {
+ $comment->{tags} //= [];
+ }
}
###############################
@@ -229,136 +230,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 work_time {
- # Work time is returned as a string (see bug 607909)
- return 0 if $_[0]->{'work_time'} + 0 == 0;
- return $_[0]->{'work_time'};
+sub body { return $_[0]->{'thetext'}; }
+sub bug_id { return $_[0]->{'bug_id'}; }
+sub creation_ts { return $_[0]->{'bug_when'}; }
+sub is_private { return $_[0]->{'isprivate'}; }
+
+sub work_time {
+
+ # Work time is returned as a string (see bug 607909)
+ return 0 if $_[0]->{'work_time'} + 0 == 0;
+ return $_[0]->{'work_time'};
}
-sub type { return $_[0]->{'type'}; }
-sub extra_data { return $_[0]->{'extra_data'} }
+sub type { return $_[0]->{'type'}; }
+sub extra_data { return $_[0]->{'extra_data'} }
sub tags {
- my ($self) = @_;
- return [] unless Bugzilla->params->{'comment_taggers_group'};
- $self->{'tags'} ||= Bugzilla->dbh->selectcol_arrayref(
- "SELECT tag
+ my ($self) = @_;
+ return [] unless Bugzilla->params->{'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) = @_;
- return $self->{collapsed} if exists $self->{collapsed};
- return 0 unless Bugzilla->params->{'comment_taggers_group'};
- $self->{collapsed} = 0;
- Bugzilla->request_cache->{comment_tags_collapsed}
- ||= [ split(/\s*,\s*/, lc(Bugzilla->params->{'collapsed_comment_tags'})) ];
- my @collapsed_tags = @{ Bugzilla->request_cache->{comment_tags_collapsed} };
- my @reason;
- foreach my $my_tag (map { lc } @{ $self->tags }) {
- foreach my $collapsed_tag (@collapsed_tags) {
- push @reason, $my_tag if $my_tag eq $collapsed_tag;
- }
- }
- if (@reason) {
- $self->{collapsed} = 1;
- $self->{collapsed_reason} = join(', ', sort @reason);
+ my ($self) = @_;
+ return $self->{collapsed} if exists $self->{collapsed};
+ return 0 unless Bugzilla->params->{'comment_taggers_group'};
+ $self->{collapsed} = 0;
+ Bugzilla->request_cache->{comment_tags_collapsed}
+ ||= [split(/\s*,\s*/, lc(Bugzilla->params->{'collapsed_comment_tags'}))];
+ my @collapsed_tags = @{Bugzilla->request_cache->{comment_tags_collapsed}};
+ my @reason;
+ foreach my $my_tag (map {lc} @{$self->tags}) {
+
+ foreach my $collapsed_tag (@collapsed_tags) {
+ push @reason, $my_tag if $my_tag eq $collapsed_tag;
}
- return $self->{collapsed};
+ }
+ if (@reason) {
+ $self->{collapsed} = 1;
+ $self->{collapsed_reason} = join(', ', sort @reason);
+ }
+ return $self->{collapsed};
}
sub collapsed_reason {
- my ($self) = @_;
- return 0 unless $self->collapsed;
- return $self->{collapsed_reason};
+ my ($self) = @_;
+ return 0 unless $self->collapsed;
+ return $self->{collapsed_reason};
}
sub bug {
- my $self = shift;
- require Bugzilla::Bug;
- my $bug = $self->{bug} ||= new Bugzilla::Bug($self->bug_id);
- weaken($self->{bug}) unless isweak($self->{bug});
- return $bug;
+ my $self = shift;
+ require Bugzilla::Bug;
+ 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;
- return $self->{'author'}
- ||= new Bugzilla::User({ id => $self->{'who'}, cache => 1 });
+ my $self = shift;
+ return $self->{'author'}
+ ||= new Bugzilla::User({id => $self->{'who'}, cache => 1});
}
sub body_full {
- my ($self, $params) = @_;
- $params ||= {};
- my $template = Bugzilla->template_inner;
- my $body;
- if ($self->type) {
- $template->process("bug/format_comment.txt.tmpl",
- { comment => $self, %$params }, \$body)
- || ThrowTemplateError($template->error());
- $body =~ s/^X//;
- }
- else {
- $body = $self->body;
- }
- if ($params->{wrap} and !$self->already_wrapped) {
- $body = wrap_comment($body);
- }
- return $body;
+ my ($self, $params) = @_;
+ $params ||= {};
+ my $template = Bugzilla->template_inner;
+ my $body;
+ if ($self->type) {
+ $template->process("bug/format_comment.txt.tmpl", {comment => $self, %$params},
+ \$body)
+ || ThrowTemplateError($template->error());
+ $body =~ s/^X//;
+ }
+ else {
+ $body = $self->body;
+ }
+ if ($params->{wrap} and !$self->already_wrapped) {
+ $body = wrap_comment($body);
+ }
+ return $body;
}
############
# Mutators #
############
-sub set_is_private { $_[0]->set('isprivate', $_[1]); }
-sub set_type { $_[0]->set('type', $_[1]); }
-sub set_extra_data { $_[0]->set('extra_data', $_[1]); }
+sub set_is_private { $_[0]->set('isprivate', $_[1]); }
+sub set_type { $_[0]->set('type', $_[1]); }
+sub set_extra_data { $_[0]->set('extra_data', $_[1]); }
sub add_tag {
- my ($self, $tag) = @_;
- $tag = $self->_check_tag($tag);
-
- my $tags = $self->tags;
- return if grep { lc($tag) eq lc($_) } @$tags;
- push @$tags, $tag;
- $self->{'tags'} = [ sort @$tags ];
- Bugzilla::Hook::process("comment_after_add_tag",
- { comment => $self, 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];
+ Bugzilla::Hook::process("comment_after_add_tag",
+ {comment => $self, tag => $tag});
}
sub remove_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);
- Bugzilla::Hook::process("comment_after_remove_tag",
- { comment => $self, 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);
+ Bugzilla::Hook::process("comment_after_remove_tag",
+ {comment => $self, tag => $tag});
}
##############
@@ -366,168 +371,166 @@ 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) = @_;
+ my ($invocant, $thetext) = @_;
- ThrowCodeError('param_required',{function => 'Bugzilla::Comment->create',
- param => 'thetext'}) unless defined $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.
+ # 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.
- ThrowUserError('comment_too_long') if length($thetext) > MAX_COMMENT_LENGTH;
- return $thetext;
+ 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 comment_id < ?",
- undef, $self->bug_id, $self->id);
+ AND comment_id < ?", undef, $self->bug_id, $self->id
+ );
- return $self->{'count'};
+ return $self->{'count'};
}
1;
diff --git a/Bugzilla/Comment/TagWeights.pm b/Bugzilla/Comment/TagWeights.pm
index 4919244ce..e733ed8fb 100644
--- a/Bugzilla/Comment/TagWeights.pm
+++ b/Bugzilla/Comment/TagWeights.pm
@@ -21,20 +21,20 @@ use constant AUDIT_UPDATES => 0;
use constant AUDIT_REMOVES => 0;
use constant DB_COLUMNS => qw(
- id
- tag
- weight
+ id
+ tag
+ weight
);
use constant UPDATE_COLUMNS => qw(
- weight
+ weight
);
use constant DB_TABLE => 'longdescs_tags_weights';
use constant ID_FIELD => 'id';
use constant NAME_FIELD => 'tag';
use constant LIST_ORDER => 'weight DESC';
-use constant VALIDATORS => { };
+use constant VALIDATORS => {};
# There's no gain to caching these objects
use constant USE_MEMCACHED => 0;
diff --git a/Bugzilla/Component.pm b/Bugzilla/Component.pm
index 78e144a55..d117c83ad 100644
--- a/Bugzilla/Component.pm
+++ b/Bugzilla/Component.pm
@@ -28,152 +28,145 @@ 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
- triage_owner_id
+ id
+ name
+ product_id
+ initialowner
+ initialqacontact
+ description
+ isactive
+ triage_owner_id
);
use constant UPDATE_COLUMNS => qw(
- name
- initialowner
- initialqacontact
- description
- isactive
- triage_owner_id
+ name
+ initialowner
+ initialqacontact
+ description
+ isactive
+ triage_owner_id
);
-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,
- triage_owner_id => \&_check_triage_owner,
+ 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,
+ triage_owner_id => \&_check_triage_owner,
};
-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(@_);
- 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;
+ # 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;
-
- $self->_check_if_controller(); # From ChoiceInterface
-
- $dbh->bz_start_transaction();
-
- 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});
- }
+ my $self = shift;
+ my $dbh = Bugzilla->dbh;
+
+ $self->_check_if_controller(); # From ChoiceInterface
+
+ $dbh->bz_start_transaction();
+
+ 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});
}
+ }
- $dbh->do('DELETE FROM flaginclusions WHERE component_id = ?',
- undef, $self->id);
- $dbh->do('DELETE FROM flagexclusions WHERE component_id = ?',
- undef, $self->id);
- $dbh->do('DELETE FROM component_cc WHERE component_id = ?',
- undef, $self->id);
- $dbh->do('DELETE FROM components WHERE id = ?', undef, $self->id);
+ $dbh->do('DELETE FROM flaginclusions WHERE component_id = ?', undef, $self->id);
+ $dbh->do('DELETE FROM flagexclusions WHERE component_id = ?', undef, $self->id);
+ $dbh->do('DELETE FROM component_cc WHERE component_id = ?', undef, $self->id);
+ $dbh->do('DELETE FROM components WHERE id = ?', undef, $self->id);
- $dbh->bz_commit_transaction();
+ $dbh->bz_commit_transaction();
}
################################
@@ -181,76 +174,77 @@ 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];
}
sub _check_triage_owner {
- my ($invocant, $triage_owner) = @_;
- my $triage_owner_id;
- $triage_owner_id = Bugzilla::User->check($triage_owner)->id if $triage_owner;
- return $triage_owner_id;
+ my ($invocant, $triage_owner) = @_;
+ my $triage_owner_id;
+ $triage_owner_id = Bugzilla::User->check($triage_owner)->id if $triage_owner;
+ return $triage_owner_id;
}
###############################
@@ -258,183 +252,209 @@ sub _check_triage_owner {
###############################
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);
- $self->set('initialqacontact', $qa_contact);
- # Reset the default QA contact object.
- delete $self->{default_qa_contact};
+ # Reset the default QA contact object.
+ delete $self->{default_qa_contact};
}
+
sub set_cc_list {
- my ($self, $cc_list) = @_;
+ my ($self, $cc_list) = @_;
+
+ $self->{cc_ids} = $self->_check_cc_list($cc_list);
- $self->{cc_ids} = $self->_check_cc_list($cc_list);
- # Reset the list of CC user objects.
- delete $self->{initial_cc};
+ # Reset the list of CC user objects.
+ delete $self->{initial_cc};
}
+
sub set_triage_owner {
- my ($self, $triage_owner) = @_;
- $self->set('triage_owner_id', $triage_owner);
- # Reset the triage owner object
- delete $self->{triage_owner};
+ my ($self, $triage_owner) = @_;
+ $self->set('triage_owner_id', $triage_owner);
+
+ # Reset the triage owner object
+ delete $self->{triage_owner};
}
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;
- return $self->{'default_assignee'}
- ||= new Bugzilla::User({ id => $self->{'initialowner'}, cache => 1 });
+ my $self = shift;
+ return $self->{'default_assignee'}
+ ||= new Bugzilla::User({id => $self->{'initialowner'}, cache => 1});
}
sub default_qa_contact {
- my $self = shift;
-
- if (!defined $self->{'default_qa_contact'}) {
- my $params = $self->{'initialqacontact'}
- ? { id => $self->{'initialqacontact'}, cache => 1 }
- : $self->{'initialqacontact'};
- $self->{'default_qa_contact'} = new Bugzilla::User($params);
- }
- return $self->{'default_qa_contact'};
+ my $self = shift;
+
+ if (!defined $self->{'default_qa_contact'}) {
+ my $params
+ = $self->{'initialqacontact'}
+ ? {id => $self->{'initialqacontact'}, cache => 1}
+ : $self->{'initialqacontact'};
+ $self->{'default_qa_contact'} = new Bugzilla::User($params);
+ }
+ return $self->{'default_qa_contact'};
}
sub triage_owner {
- my $self = shift;
- if (!defined $self->{'triage_owner'}) {
- my $params = $self->{'triage_owner_id'}
- ? { id => $self->{'triage_owner_id'}, cache => 1 }
- : $self->{'triage_owner_id'};
- $self->{'triage_owner'} = Bugzilla::User->new($params);
- }
- return $self->{'triage_owner'};
+ my $self = shift;
+ if (!defined $self->{'triage_owner'}) {
+ my $params
+ = $self->{'triage_owner_id'}
+ ? {id => $self->{'triage_owner_id'}, cache => 1}
+ : $self->{'triage_owner_id'};
+ $self->{'triage_owner'} = Bugzilla::User->new($params);
+ }
+ return $self->{'triage_owner'};
}
sub flag_types {
- my ($self, $params) = @_;
- $params ||= {};
-
- if (!defined $self->{'flag_types'}) {
- my $flagtypes = Bugzilla::FlagType::match({ product_id => $self->product_id,
- component_id => $self->id,
- %$params });
-
- $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, $params) = @_;
+ $params ||= {};
+
+ if (!defined $self->{'flag_types'}) {
+ my $flagtypes
+ = Bugzilla::FlagType::match({
+ product_id => $self->product_id, component_id => $self->id, %$params
+ });
+
+ $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 find_first_flag_type {
- my ($self, $target_type, $name) = @_;
+ my ($self, $target_type, $name) = @_;
- return first { $_->name eq $name } @{ $self->flag_types->{$target_type} };
+ return first { $_->name eq $name } @{$self->flag_types->{$target_type}};
}
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'};
}
###############################
@@ -442,8 +462,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'}; }
sub triage_owner_id { return $_[0]->{'triage_owner_id'} }
@@ -455,11 +475,12 @@ use constant FIELD_NAME => 'component';
use constant is_default => 0;
sub is_set_on_bug {
- my ($self, $bug) = @_;
- # We treat it like a hash always, so that we don't have to check if it's
- # a hash or an object.
- return 0 if !defined $bug->{component_id};
- $bug->{component_id} == $self->id ? 1 : 0;
+ my ($self, $bug) = @_;
+
+ # We treat it like a hash always, so that we don't have to check if it's
+ # a hash or an object.
+ return 0 if !defined $bug->{component_id};
+ $bug->{component_id} == $self->id ? 1 : 0;
}
###############################
diff --git a/Bugzilla/Config.pm b/Bugzilla/Config.pm
index 1016d51e4..bc907424e 100644
--- a/Bugzilla/Config.pm
+++ b/Bugzilla/Config.pm
@@ -21,296 +21,296 @@ use Module::Runtime qw(require_module);
# Don't export localvars by default - people should have to explicitly
# ask for it, as a (probably futile) attempt to stop code using it
# when it shouldn't
-%Bugzilla::Config::EXPORT_TAGS =
- (
- admin => [qw(update_params SetParam write_params)],
- );
+%Bugzilla::Config::EXPORT_TAGS
+ = (admin => [qw(update_params SetParam write_params)],);
Exporter::export_ok_tags('admin');
# INITIALISATION CODE
# Perl throws a warning if we use bz_locations() directly after do.
our %params;
+
# Load in the param definitions
sub _load_params {
- my $panels = param_panels();
- my %hook_panels;
- foreach my $panel (keys %$panels) {
- my $module = $panels->{$panel};
- require_module($module);
- 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};
+ require_module($module);
+ my @new_param_list = $module->get_param_list();
+ $hook_panels{lc($panel)} = {params => \@new_param_list};
+ }
+
+ # This hook is also called in editparams.cgi. This call here is required
+ # to make SetParam work.
+ Bugzilla::Hook::process('config_modify_panels', {panels => \%hook_panels});
+
+ foreach my $panel (keys %hook_panels) {
+ foreach my $item (@{$hook_panels{$panel}->{params}}) {
+ $params{$item->{'name'}} = $item;
}
+ }
}
+
# END INIT CODE
# Subroutines go here
sub param_panels {
- my $param_panels = {};
- my $libpath = bz_locations()->{'libpath'};
- foreach my $item ((glob "$libpath/Bugzilla/Config/*.pm")) {
- $item =~ m#/([^/]+)\.pm$#;
- my $module = $1;
- $param_panels->{$module} = "Bugzilla::Config::$module" unless $module eq 'Common';
- }
- # Now check for any hooked params
- Bugzilla::Hook::process('config_add_panels',
- { panel_modules => $param_panels });
- return $param_panels;
+ my $param_panels = {};
+ my $libpath = bz_locations()->{'libpath'};
+ foreach my $item ((glob "$libpath/Bugzilla/Config/*.pm")) {
+ $item =~ m#/([^/]+)\.pm$#;
+ my $module = $1;
+ $param_panels->{$module} = "Bugzilla::Config::$module"
+ unless $module eq 'Common';
+ }
+
+ # Now check for any hooked params
+ Bugzilla::Hook::process('config_add_panels', {panel_modules => $param_panels});
+ return $param_panels;
}
sub SetParam {
- my ($name, $value) = @_;
+ my ($name, $value) = @_;
- _load_params unless %params;
- die "Unknown param $name" unless (exists $params{$name});
+ _load_params unless %params;
+ die "Unknown param $name" unless (exists $params{$name});
- my $entry = $params{$name};
+ my $entry = $params{$name};
- # sanity check the value
+ # sanity check the value
- # XXX - This runs the checks. Which would be good, except that
- # check_shadowdb creates the database as a side effect, and so the
- # checker fails the second time around...
- if ($name ne 'shadowdb' && exists $entry->{'checker'}) {
- my $err = $entry->{'checker'}->($value, $entry);
- die "Param $name is not valid: $err" unless $err eq '';
- }
+ # XXX - This runs the checks. Which would be good, except that
+ # check_shadowdb creates the database as a side effect, and so the
+ # checker fails the second time around...
+ if ($name ne 'shadowdb' && exists $entry->{'checker'}) {
+ my $err = $entry->{'checker'}->($value, $entry);
+ die "Param $name is not valid: $err" unless $err eq '';
+ }
- Bugzilla->params->{$name} = $value;
+ Bugzilla->params->{$name} = $value;
}
sub update_params {
- my ($params) = @_;
- my $answer = Bugzilla->installation_answers;
-
- my $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';
- }
- }
+ my ($params) = @_;
+ my $answer = Bugzilla->installation_answers;
+
+ my $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';
+ }
}
-
- # Change from a boolean for quips to multi-state
- if (exists $param->{'usequip'} && !exists $param->{'enablequips'}) {
- $new_params{'enablequips'} = $param->{'usequip'} ? 'on' : 'off';
+ }
+
+ # Change from a boolean for quips to multi-state
+ if (exists $param->{'usequip'} && !exists $param->{'enablequips'}) {
+ $new_params{'enablequips'} = $param->{'usequip'} ? 'on' : 'off';
+ }
+
+ # Change from old product groups to controls for group_control_map
+ # 2002-10-14 bug 147275 bugreport@peshkin.net
+ if (exists $param->{'usebuggroups'} && !exists $param->{'makeproductgroups'}) {
+ $new_params{'makeproductgroups'} = $param->{'usebuggroups'};
+ }
+
+ # Modularise auth code
+ if (exists $param->{'useLDAP'} && !exists $param->{'loginmethod'}) {
+ $new_params{'loginmethod'} = $param->{'useLDAP'} ? "LDAP" : "DB";
+ }
+
+ # set verify method to whatever loginmethod was
+ if (exists $param->{'loginmethod'} && !exists $param->{'user_verify_class'}) {
+ $new_params{'user_verify_class'} = $param->{'loginmethod'};
+ }
+
+ # Remove quip-display control from parameters
+ # and give it to users via User Settings (Bug 41972)
+ if (exists $param->{'enablequips'}
+ && !exists $param->{'quip_list_entry_control'})
+ {
+ my $new_value;
+ ($param->{'enablequips'} eq 'on') && do { $new_value = 'open'; };
+ ($param->{'enablequips'} eq 'approved') && do { $new_value = 'moderated'; };
+ ($param->{'enablequips'} eq 'frozen') && do { $new_value = 'closed'; };
+ ($param->{'enablequips'} eq 'off') && do { $new_value = 'closed'; };
+ $new_params{'quip_list_entry_control'} = $new_value;
+ }
+
+ # Old mail_delivery_method choices contained no uppercase characters
+ if (exists $param->{'mail_delivery_method'}
+ && $param->{'mail_delivery_method'} !~ /[A-Z]/)
+ {
+ my $method = $param->{'mail_delivery_method'};
+ my %translation = (
+ 'sendmail' => 'Sendmail',
+ 'smtp' => 'SMTP',
+ 'qmail' => 'Qmail',
+ 'testfile' => 'Test',
+ 'none' => 'None'
+ );
+ $param->{'mail_delivery_method'} = $translation{$method};
+ }
+
+ # Convert the old "ssl" parameter to the new "ssl_redirect" parameter.
+ # Both "authenticated sessions" and "always" turn on "ssl_redirect"
+ # when upgrading.
+ if (exists $param->{'ssl'} and $param->{'ssl'} ne 'never') {
+ $new_params{'ssl_redirect'} = 1;
+ }
+
+# "specific_search_allow_empty_words" has been renamed to "search_allow_no_criteria".
+ if (exists $param->{'specific_search_allow_empty_words'}) {
+ $new_params{'search_allow_no_criteria'}
+ = $param->{'specific_search_allow_empty_words'};
+ }
+
+ # --- DEFAULTS FOR NEW PARAMS ---
+
+ _load_params unless %params;
+ foreach my $name (keys %params) {
+ my $item = $params{$name};
+ unless (exists $param->{$name}) {
+ print "New parameter: $name\n" unless $new_install;
+ if (exists $new_params{$name}) {
+ $param->{$name} = $new_params{$name};
+ }
+ elsif (exists $answer->{$name}) {
+ $param->{$name} = $answer->{$name};
+ }
+ else {
+ $param->{$name} = $item->{'default'};
+ }
}
-
- # Change from old product groups to controls for group_control_map
- # 2002-10-14 bug 147275 bugreport@peshkin.net
- if (exists $param->{'usebuggroups'} &&
- !exists $param->{'makeproductgroups'})
- {
- $new_params{'makeproductgroups'} = $param->{'usebuggroups'};
- }
-
- # Modularise auth code
- if (exists $param->{'useLDAP'} && !exists $param->{'loginmethod'}) {
- $new_params{'loginmethod'} = $param->{'useLDAP'} ? "LDAP" : "DB";
- }
-
- # set verify method to whatever loginmethod was
- if (exists $param->{'loginmethod'}
- && !exists $param->{'user_verify_class'})
- {
- $new_params{'user_verify_class'} = $param->{'loginmethod'};
- }
-
- # Remove quip-display control from parameters
- # and give it to users via User Settings (Bug 41972)
- if ( exists $param->{'enablequips'}
- && !exists $param->{'quip_list_entry_control'})
- {
- my $new_value;
- ($param->{'enablequips'} eq 'on') && do {$new_value = 'open';};
- ($param->{'enablequips'} eq 'approved') && do {$new_value = 'moderated';};
- ($param->{'enablequips'} eq 'frozen') && do {$new_value = 'closed';};
- ($param->{'enablequips'} eq 'off') && do {$new_value = 'closed';};
- $new_params{'quip_list_entry_control'} = $new_value;
- }
-
- # Old mail_delivery_method choices contained no uppercase characters
- if (exists $param->{'mail_delivery_method'}
- && $param->{'mail_delivery_method'} !~ /[A-Z]/) {
- my $method = $param->{'mail_delivery_method'};
- my %translation = (
- 'sendmail' => 'Sendmail',
- 'smtp' => 'SMTP',
- 'qmail' => 'Qmail',
- 'testfile' => 'Test',
- 'none' => 'None');
- $param->{'mail_delivery_method'} = $translation{$method};
- }
-
- # Convert the old "ssl" parameter to the new "ssl_redirect" parameter.
- # Both "authenticated sessions" and "always" turn on "ssl_redirect"
- # when upgrading.
- if (exists $param->{'ssl'} and $param->{'ssl'} ne 'never') {
- $new_params{'ssl_redirect'} = 1;
- }
-
- # "specific_search_allow_empty_words" has been renamed to "search_allow_no_criteria".
- if (exists $param->{'specific_search_allow_empty_words'}) {
- $new_params{'search_allow_no_criteria'} = $param->{'specific_search_allow_empty_words'};
- }
-
- # --- DEFAULTS FOR NEW PARAMS ---
-
- _load_params unless %params;
- foreach my $name (keys %params) {
- my $item = $params{$name};
- unless (exists $param->{$name}) {
- print "New parameter: $name\n" unless $new_install;
- if (exists $new_params{$name}) {
- $param->{$name} = $new_params{$name};
- }
- elsif (exists $answer->{$name}) {
- $param->{$name} = $answer->{$name};
- }
- else {
- $param->{$name} = $item->{'default'};
- }
+ else {
+ my $checker = $item->{'checker'};
+ my $updater = $item->{'updater'};
+ if ($checker) {
+ my $error = $checker->($param->{$name}, $item);
+ if ($error && $updater) {
+ my $new_val = $updater->($param->{$name});
+ $param->{$name} = $new_val unless $checker->($new_val, $item);
}
- else {
- my $checker = $item->{'checker'};
- my $updater = $item->{'updater'};
- if ($checker) {
- my $error = $checker->($param->{$name}, $item);
- if ($error && $updater) {
- my $new_val = $updater->( $param->{$name} );
- $param->{$name} = $new_val unless $checker->($new_val, $item);
- }
- elsif ($error) {
- warn "Invalid parameter: $name\n";
- }
- }
+ elsif ($error) {
+ warn "Invalid parameter: $name\n";
}
+ }
}
-
- # Generate unique Duo integration secret key
- if ($param->{duo_akey} eq '') {
- require Bugzilla::Util;
- $param->{duo_akey} = Bugzilla::Util::generate_random_password(40);
+ }
+
+ # Generate unique Duo integration secret key
+ if ($param->{duo_akey} eq '') {
+ require Bugzilla::Util;
+ $param->{duo_akey} = Bugzilla::Util::generate_random_password(40);
+ }
+
+ $param->{'utf8'} = 1 if $new_install;
+
+ my %oldparams;
+
+ if ( ON_WINDOWS
+ && !-e SENDMAIL_EXE
+ && $param->{'mail_delivery_method'} eq 'Sendmail')
+ {
+ my $smtp = $answer->{'SMTP_SERVER'};
+ if (!$smtp) {
+ print "\nBugzilla requires an SMTP server to function on",
+ " Windows.\nPlease enter your SMTP server's hostname: ";
+ $smtp = <STDIN>;
+ chomp $smtp;
+ if ($smtp) {
+ $param->{'smtpserver'} = $smtp;
+ }
+ else {
+ print "\nWarning: No SMTP Server provided, defaulting to", " localhost\n";
+ }
}
- $param->{'utf8'} = 1 if $new_install;
-
- my %oldparams;
-
- if (ON_WINDOWS && !-e SENDMAIL_EXE
- && $param->{'mail_delivery_method'} eq 'Sendmail')
- {
- my $smtp = $answer->{'SMTP_SERVER'};
- if (!$smtp) {
- print "\nBugzilla requires an SMTP server to function on",
- " Windows.\nPlease enter your SMTP server's hostname: ";
- $smtp = <STDIN>;
- chomp $smtp;
- if ($smtp) {
- $param->{'smtpserver'} = $smtp;
- }
- else {
- print "\nWarning: No SMTP Server provided, defaulting to",
- " localhost\n";
- }
- }
+ $param->{'mail_delivery_method'} = 'SMTP';
+ }
- $param->{'mail_delivery_method'} = 'SMTP';
- }
+ write_params($param);
- write_params($param);
-
- # 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_data) = @_;
+ $param_data ||= Bugzilla->params;
- local $Data::Dumper::Sortkeys = 1;
+ local $Data::Dumper::Sortkeys = 1;
- my %params = %$param_data;
- $params{urlbase} = Bugzilla->localconfig->{urlbase};
- __PACKAGE__->_write_file( Data::Dumper->Dump([\%params], ['*param']) );
+ my %params = %$param_data;
+ $params{urlbase} = Bugzilla->localconfig->{urlbase};
+ __PACKAGE__->_write_file(Data::Dumper->Dump([\%params], ['*param']));
- # 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 $datadir = bz_locations()->{'datadir'};
- if (-e "$datadir/params") {
- # Note that checksetup.pl sets file permissions on '$datadir/params'
-
- # Using Safe mode is _not_ a guarantee of safety if someone does
- # manage to write to the file. However, it won't hurt...
- # See bug 165144 for not needing to eval this at all
- my $s = new Safe;
-
- $s->rdo("$datadir/params");
- die "Error reading $datadir/params: $!" if $!;
- die "Error evaluating $datadir/params: $@" if $@;
-
- # Now read the param back out from the sandbox
- %params = %{$s->varglob('param')};
- }
- elsif ($ENV{'SERVER_SOFTWARE'}) {
- # We're in a CGI, but the params file doesn't exist. We can't
- # Template Toolkit, or even install_string, since checksetup
- # might not have thrown an error. Bugzilla::CGI->new
- # hasn't even been called yet, so we manually use CGI::Carp here
- # so that the user sees the error.
- require CGI::Carp;
- CGI::Carp->import('fatalsToBrowser');
- die "The $datadir/params file does not exist."
- . ' You probably need to run checksetup.pl.',
- }
- return \%params;
+ my %params;
+ my $datadir = bz_locations()->{'datadir'};
+ if (-e "$datadir/params") {
+
+ # Note that checksetup.pl sets file permissions on '$datadir/params'
+
+ # Using Safe mode is _not_ a guarantee of safety if someone does
+ # manage to write to the file. However, it won't hurt...
+ # See bug 165144 for not needing to eval this at all
+ my $s = new Safe;
+
+ $s->rdo("$datadir/params");
+ die "Error reading $datadir/params: $!" if $!;
+ die "Error evaluating $datadir/params: $@" if $@;
+
+ # Now read the param back out from the sandbox
+ %params = %{$s->varglob('param')};
+ }
+ elsif ($ENV{'SERVER_SOFTWARE'}) {
+
+ # We're in a CGI, but the params file doesn't exist. We can't
+ # Template Toolkit, or even install_string, since checksetup
+ # might not have thrown an error. Bugzilla::CGI->new
+ # hasn't even been called yet, so we manually use CGI::Carp here
+ # so that the user sees the error.
+ require CGI::Carp;
+ CGI::Carp->import('fatalsToBrowser');
+ die "The $datadir/params file does not exist."
+ . ' You probably need to run checksetup.pl.',;
+ }
+ return \%params;
}
sub _write_file {
- my ($class, $str) = @_;
- my $datadir = bz_locations()->{'datadir'};
- my $param_file = "$datadir/params";
- my ($fh, $tmpname) = File::Temp::tempfile('params.XXXXX',
- DIR => $datadir );
- print $fh $str || die "Can't write param file: $!";
- close $fh || die "Can't close param file: $!";
-
- rename $tmpname, $param_file
- or die "Can't rename $tmpname to $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);
+ my ($class, $str) = @_;
+ my $datadir = bz_locations()->{'datadir'};
+ my $param_file = "$datadir/params";
+ my ($fh, $tmpname) = File::Temp::tempfile('params.XXXXX', DIR => $datadir);
+ print $fh $str || die "Can't write param file: $!";
+ close $fh || die "Can't close param file: $!";
+
+ rename $tmpname, $param_file or die "Can't rename $tmpname to $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);
}
1;
diff --git a/Bugzilla/Config/Admin.pm b/Bugzilla/Config/Admin.pm
index ac1c4ca0e..cae4cb25d 100644
--- a/Bugzilla/Config/Admin.pm
+++ b/Bugzilla/Config/Admin.pm
@@ -19,78 +19,59 @@ use Scalar::Util qw(looks_like_number);
our $sortkey = 200;
sub get_param_list {
- 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 => 'rate_limit_active',
- type => 'b',
- default => 1,
- },
-
- {
- name => 'rate_limit_rules',
- type => 'l',
- default => '{"get_bug": [75, 60], "show_bug": [75, 60], "github": [10, 60]}',
- checker => \&check_rate_limit_rules,
- updater => \&update_rate_limit_rules,
- },
-
- {
- name => 'log_user_requests',
- type => 'b',
- default => 0,
- }
- );
- return @param_list;
+ 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 => 'rate_limit_active', type => 'b', default => 1,},
+
+ {
+ name => 'rate_limit_rules',
+ type => 'l',
+ default => '{"get_bug": [75, 60], "show_bug": [75, 60], "github": [10, 60]}',
+ checker => \&check_rate_limit_rules,
+ updater => \&update_rate_limit_rules,
+ },
+
+ {name => 'log_user_requests', type => 'b', default => 0,}
+ );
+ return @param_list;
}
sub check_rate_limit_rules {
- my $rules = shift;
+ my $rules = shift;
- my $val = eval { decode_json($rules) };
- return "failed to parse json" unless defined $val;
- return "value is not HASH" unless ref $val && ref($val) eq 'HASH';
- return "rules are invalid" unless all {
- ref($_) eq 'ARRAY' && looks_like_number( $_->[0] ) && looks_like_number( $_->[1] )
- } values %$val;
+ my $val = eval { decode_json($rules) };
+ return "failed to parse json" unless defined $val;
+ return "value is not HASH" unless ref $val && ref($val) eq 'HASH';
+ return "rules are invalid" unless all {
+ ref($_) eq 'ARRAY' && looks_like_number($_->[0]) && looks_like_number($_->[1])
+ }
+ values %$val;
- foreach my $required (qw( show_bug get_bug github )) {
- return "missing $required" unless exists $val->{$required};
- }
+ foreach my $required (qw( show_bug get_bug github )) {
+ return "missing $required" unless exists $val->{$required};
+ }
- return "";
+ return "";
}
sub update_rate_limit_rules {
- my ($rules) = @_;
- my $val = decode_json($rules);
- $val->{github} = [10, 60];
- return encode_json($val);
+ my ($rules) = @_;
+ my $val = decode_json($rules);
+ $val->{github} = [10, 60];
+ return encode_json($val);
}
1;
diff --git a/Bugzilla/Config/Advanced.pm b/Bugzilla/Config/Advanced.pm
index 398f02701..7b76944e1 100644
--- a/Bugzilla/Config/Advanced.pm
+++ b/Bugzilla/Config/Advanced.pm
@@ -17,25 +17,17 @@ use Bugzilla::Util qw(validate_ip);
our $sortkey = 1700;
use constant get_param_list => (
- {
- 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 => 'disable_bug_updates',
- type => 'b',
- default => 0
- },
+ {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 => 'disable_bug_updates', type => 'b', default => 0},
);
1;
diff --git a/Bugzilla/Config/Attachment.pm b/Bugzilla/Config/Attachment.pm
index c3dbd03ed..821996eba 100644
--- a/Bugzilla/Config/Attachment.pm
+++ b/Bugzilla/Config/Attachment.pm
@@ -16,77 +16,57 @@ use Bugzilla::Config::Common;
our $sortkey = 400;
sub get_param_list {
- my $class = shift;
- my @param_list = (
- {
- name => 'allow_attachment_display',
- type => 'b',
- default => 0
- },
- {
- name => 'allow_attachment_deletion',
- type => 'b',
- default => 0
- },
- {
- name => 'maxattachmentsize',
- type => 't',
- default => '1000',
- checker => \&check_maxattachmentsize
- },
- {
- name => 'attachment_storage',
- type => 's',
- choices => [ 'database', 'filesystem', 's3' ],
- default => 'database',
- checker => \&check_storage
- },
- {
- name => 's3_bucket',
- type => 't',
- default => '',
- },
- {
- name => 'aws_access_key_id',
- type => 't',
- default => '',
- },
- {
- name => 'aws_secret_access_key',
- type => 't',
- default => '',
- },
- );
- return @param_list;
+ my $class = shift;
+ my @param_list = (
+ {name => 'allow_attachment_display', type => 'b', default => 0},
+ {name => 'allow_attachment_deletion', type => 'b', default => 0},
+ {
+ name => 'maxattachmentsize',
+ type => 't',
+ default => '1000',
+ checker => \&check_maxattachmentsize
+ },
+ {
+ name => 'attachment_storage',
+ type => 's',
+ choices => ['database', 'filesystem', 's3'],
+ default => 'database',
+ checker => \&check_storage
+ },
+ {name => 's3_bucket', type => 't', default => '',},
+ {name => 'aws_access_key_id', type => 't', default => '',},
+ {name => 'aws_secret_access_key', type => 't', default => '',},
+ );
+ return @param_list;
}
sub check_params {
- my ( $class, $params ) = @_;
- return '' unless $params->{attachment_storage} eq 's3';
+ my ($class, $params) = @_;
+ return '' unless $params->{attachment_storage} eq 's3';
- if ( $params->{s3_bucket} eq ''
- || $params->{aws_access_key_id} eq ''
- || $params->{aws_secret_access_key} eq '' )
- {
- return
- "You must set s3_bucket, aws_access_key_id, and aws_secret_access_key when attachment_storage is set to S3";
- }
- return '';
+ if ( $params->{s3_bucket} eq ''
+ || $params->{aws_access_key_id} eq ''
+ || $params->{aws_secret_access_key} eq '')
+ {
+ return
+ "You must set s3_bucket, aws_access_key_id, and aws_secret_access_key when attachment_storage is set to S3";
+ }
+ return '';
}
sub check_storage {
- my ( $value, $param ) = (@_);
- my $check_multi = check_multi( $value, $param );
- return $check_multi if $check_multi;
+ my ($value, $param) = (@_);
+ my $check_multi = check_multi($value, $param);
+ return $check_multi if $check_multi;
- if ( $value eq 's3' ) {
- return Bugzilla->feature('s3')
- ? ''
- : 'The perl modules required for S3 support are not installed';
- }
- else {
- return '';
- }
+ if ($value eq 's3') {
+ return Bugzilla->feature('s3')
+ ? ''
+ : 'The perl modules required for S3 support are not installed';
+ }
+ else {
+ return '';
+ }
}
1;
diff --git a/Bugzilla/Config/Auth.pm b/Bugzilla/Config/Auth.pm
index 965d922d7..664c1b263 100644
--- a/Bugzilla/Config/Auth.pm
+++ b/Bugzilla/Config/Auth.pm
@@ -18,241 +18,195 @@ use Types::Common::Numeric qw(PositiveInt);
our $sortkey = 300;
sub get_param_list {
- my $class = shift;
- my @param_list = (
- {
- name => 'auth_env_id',
- type => 't',
- default => '',
- },
-
- {
- name => 'auth_env_email',
- type => 't',
- default => '',
- },
-
- {
- name => 'auth_env_realname',
- type => 't',
- default => '',
- },
-
- # XXX in the future:
- #
- # user_verify_class and user_info_class should have choices gathered from
- # whatever sits in their respective directories
- #
- # rather than comma-separated lists, these two should eventually become
- # arrays, but that requires alterations to editparams first
-
- {
- name => 'user_info_class',
- type => 's',
- choices => [ 'CGI', 'Env', 'Env,CGI' ],
- default => 'CGI',
- checker => \&check_multi
- },
-
- {
- name => 'user_verify_class',
- type => 'o',
- choices => [ 'DB', 'RADIUS', 'LDAP' ],
- default => 'DB',
- checker => \&check_user_verify_class
- },
-
- {
- name => 'rememberlogin',
- type => 's',
- choices => [ 'on', 'defaulton', 'defaultoff', 'off' ],
- default => 'on',
- checker => \&check_multi
- },
-
- {
- name => 'requirelogin',
- type => 'b',
- default => '0'
- },
-
- {
- name => 'webservice_email_filter',
- type => 'b',
- default => 0
- },
-
- {
- name => 'emailregexp',
- type => 't',
- default => q:^[\\w\\.\\+\\-=]+@[\\w\\.\\-]+\\.[\\w\\-]+$:,
- checker => \&check_regexp
- },
-
- {
- name => 'emailregexpdesc',
- type => 'l',
- default => 'A legal address must contain exactly one \'@\', and at least ' . 'one \'.\' after the @.'
- },
-
- {
- name => 'emailsuffix',
- type => 't',
- default => ''
- },
-
- {
- name => 'createemailregexp',
- type => 't',
- default => q:.*:,
- checker => \&check_regexp
- },
-
- {
- name => 'password_complexity',
- type => 's',
- choices => [ 'no_constraints', 'bmo' ],
- default => 'no_constraints',
- checker => \&check_multi
- },
-
- {
- name => 'password_check_on_login',
- type => 'b',
- default => '1'
- },
-
- {
- name => 'passwdqc_min',
- type => 't',
- default => 'undef, 24, 11, 8, 7',
- checker => \&_check_passwdqc_min,
- },
-
- {
- name => 'passwdqc_max',
- type => 't',
- default => '40',
- checker => \&_check_passwdqc_max,
- },
-
- {
- name => 'passwdqc_passphrase_words',
- type => 't',
- default => '3',
- checker => \&check_numeric,
- },
-
- {
- name => 'passwdqc_match_length',
- type => 't',
- default => '4',
- checker => \&check_numeric,
- },
-
- {
- name => 'passwdqc_random_bits',
- type => 't',
- default => '47',
- checker => \&_check_passwdqc_random_bits,
- },
-
- {
- name => 'passwdqc_desc',
- type => 'l',
- default => 'The password must be complex.',
- },
-
- {
- name => 'auth_delegation',
- type => 'b',
- default => 0,
- },
-
- {
- name => 'duo_host',
- type => 't',
- default => '',
- },
- {
- name => 'duo_akey',
- type => 't',
- default => '',
- },
- {
- name => 'duo_ikey',
- type => 't',
- default => '',
- },
- {
- name => 'duo_skey',
- type => 't',
- default => '',
- },
-
- {
- name => 'mfa_group',
- type => 's',
- choices => \&get_all_group_names,
- default => '',
- checker => \&check_group,
- },
-
- {
- name => 'mfa_group_grace_period',
- type => 't',
- default => '7',
- checker => \&check_numeric,
- }
- );
- return @param_list;
+ my $class = shift;
+ my @param_list = (
+ {name => 'auth_env_id', type => 't', default => '',},
+
+ {name => 'auth_env_email', type => 't', default => '',},
+
+ {name => 'auth_env_realname', type => 't', default => '',},
+
+ # XXX in the future:
+ #
+ # user_verify_class and user_info_class should have choices gathered from
+ # whatever sits in their respective directories
+ #
+ # rather than comma-separated lists, these two should eventually become
+ # arrays, but that requires alterations to editparams first
+
+ {
+ name => 'user_info_class',
+ type => 's',
+ choices => ['CGI', 'Env', 'Env,CGI'],
+ default => 'CGI',
+ checker => \&check_multi
+ },
+
+ {
+ name => 'user_verify_class',
+ type => 'o',
+ choices => ['DB', 'RADIUS', 'LDAP'],
+ default => 'DB',
+ checker => \&check_user_verify_class
+ },
+
+ {
+ name => 'rememberlogin',
+ type => 's',
+ choices => ['on', 'defaulton', 'defaultoff', 'off'],
+ default => 'on',
+ checker => \&check_multi
+ },
+
+ {name => 'requirelogin', type => 'b', default => '0'},
+
+ {name => 'webservice_email_filter', type => 'b', default => 0},
+
+ {
+ name => 'emailregexp',
+ type => 't',
+ default => q:^[\\w\\.\\+\\-=]+@[\\w\\.\\-]+\\.[\\w\\-]+$:,
+ checker => \&check_regexp
+ },
+
+ {
+ name => 'emailregexpdesc',
+ type => 'l',
+ default => 'A legal address must contain exactly one \'@\', and at least '
+ . 'one \'.\' after the @.'
+ },
+
+ {name => 'emailsuffix', type => 't', default => ''},
+
+ {
+ name => 'createemailregexp',
+ type => 't',
+ default => q:.*:,
+ checker => \&check_regexp
+ },
+
+ {
+ name => 'password_complexity',
+ type => 's',
+ choices => ['no_constraints', 'bmo'],
+ default => 'no_constraints',
+ checker => \&check_multi
+ },
+
+ {name => 'password_check_on_login', type => 'b', default => '1'},
+
+ {
+ name => 'passwdqc_min',
+ type => 't',
+ default => 'undef, 24, 11, 8, 7',
+ checker => \&_check_passwdqc_min,
+ },
+
+ {
+ name => 'passwdqc_max',
+ type => 't',
+ default => '40',
+ checker => \&_check_passwdqc_max,
+ },
+
+ {
+ name => 'passwdqc_passphrase_words',
+ type => 't',
+ default => '3',
+ checker => \&check_numeric,
+ },
+
+ {
+ name => 'passwdqc_match_length',
+ type => 't',
+ default => '4',
+ checker => \&check_numeric,
+ },
+
+ {
+ name => 'passwdqc_random_bits',
+ type => 't',
+ default => '47',
+ checker => \&_check_passwdqc_random_bits,
+ },
+
+ {
+ name => 'passwdqc_desc',
+ type => 'l',
+ default => 'The password must be complex.',
+ },
+
+ {name => 'auth_delegation', type => 'b', default => 0,},
+
+ {name => 'duo_host', type => 't', default => '',},
+ {name => 'duo_akey', type => 't', default => '',},
+ {name => 'duo_ikey', type => 't', default => '',},
+ {name => 'duo_skey', type => 't', default => '',},
+
+ {
+ name => 'mfa_group',
+ type => 's',
+ choices => \&get_all_group_names,
+ default => '',
+ checker => \&check_group,
+ },
+
+ {
+ name => 'mfa_group_grace_period',
+ type => 't',
+ default => '7',
+ checker => \&check_numeric,
+ }
+ );
+ return @param_list;
}
-my $passwdqc_min = Tuple[
- Maybe[PositiveInt],
- Maybe[PositiveInt],
- Maybe[PositiveInt],
- Maybe[PositiveInt],
- Maybe[PositiveInt],
+my $passwdqc_min = Tuple [
+ Maybe [PositiveInt],
+ Maybe [PositiveInt],
+ Maybe [PositiveInt],
+ Maybe [PositiveInt],
+ Maybe [PositiveInt],
];
sub _check_passwdqc_min {
- my ($value) = @_;
- my @values = map { $_ eq 'undef' ? undef : $_ } split( /\s*,\s*/, $value );
-
- unless ( $passwdqc_min->check( \@values ) ) {
- return "must be list of five values, that are either integers > 0 or undef";
+ my ($value) = @_;
+ my @values = map { $_ eq 'undef' ? undef : $_ } split(/\s*,\s*/, $value);
+
+ unless ($passwdqc_min->check(\@values)) {
+ return "must be list of five values, that are either integers > 0 or undef";
+ }
+
+ my ($max, $max_pos);
+ my $pos = 0;
+ foreach my $value (@values) {
+ if (defined $max && defined $value) {
+ if ($value > $max) {
+ return "Int$pos is larger than Int$max_pos ($max)";
+ }
}
-
- my ( $max, $max_pos );
- my $pos = 0;
- foreach my $value (@values) {
- if ( defined $max && defined $value ) {
- if ( $value > $max ) {
- return "Int$pos is larger than Int$max_pos ($max)";
- }
- }
- elsif ( defined $value ) {
- $max = $value;
- $max_pos = $pos;
- }
- $pos++;
+ elsif (defined $value) {
+ $max = $value;
+ $max_pos = $pos;
}
- return "";
+ $pos++;
+ }
+ return "";
}
sub _check_passwdqc_max {
- my ($value) = @_;
- return "must be a positive integer" unless PositiveInt->check($value);
- return "must be greater than 8" unless $value > 8;
- return "";
+ my ($value) = @_;
+ return "must be a positive integer" unless PositiveInt->check($value);
+ return "must be greater than 8" unless $value > 8;
+ return "";
}
sub _check_passwdqc_random_bits {
- my ($value) = @_;
- return "must be a positive integer" unless PositiveInt->check($value);
- return "must be between 24 and 85 inclusive" unless $value >= 24 && $value <= 85;
- return "";
+ my ($value) = @_;
+ return "must be a positive integer" unless PositiveInt->check($value);
+ return "must be between 24 and 85 inclusive"
+ unless $value >= 24 && $value <= 85;
+ return "";
}
-1; \ No newline at end of file
+1;
diff --git a/Bugzilla/Config/BugChange.pm b/Bugzilla/Config/BugChange.pm
index f9c33512a..ad1cafefc 100644
--- a/Bugzilla/Config/BugChange.pm
+++ b/Bugzilla/Config/BugChange.pm
@@ -17,67 +17,43 @@ use Bugzilla::Status;
our $sortkey = 500;
sub get_param_list {
- my $class = shift;
-
- # Hardcoded bug statuses which existed before Bugzilla 3.1.
- my @closed_bug_statuses = ( 'RESOLVED', 'VERIFIED', 'CLOSED' );
-
- # If we are upgrading from 3.0 or older, bug statuses are not customisable
- # 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 @param_list = (
- {
- name => 'duplicate_or_move_bug_status',
- type => 's',
- choices => \@closed_bug_statuses,
- default => $closed_bug_statuses[0],
- checker => \&check_bug_status
- },
-
- {
- name => 'letsubmitterchoosepriority',
- type => 'b',
- default => 1
- },
-
- {
- name => 'letsubmitterchoosemilestone',
- type => 'b',
- default => 1
- },
-
- {
- name => 'musthavemilestoneonaccept',
- type => 'b',
- default => 0
- },
-
- {
- name => 'commentonchange_resolution',
- type => 'b',
- default => 0
- },
-
- {
- name => 'commentonduplicate',
- type => 'b',
- default => 0
- },
-
- {
- name => 'noresolveonopenblockers',
- type => 'b',
- default => 0,
- }
- );
- return @param_list;
+ my $class = shift;
+
+ # Hardcoded bug statuses which existed before Bugzilla 3.1.
+ my @closed_bug_statuses = ('RESOLVED', 'VERIFIED', 'CLOSED');
+
+ # If we are upgrading from 3.0 or older, bug statuses are not customisable
+ # 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 @param_list = (
+ {
+ name => 'duplicate_or_move_bug_status',
+ type => 's',
+ choices => \@closed_bug_statuses,
+ default => $closed_bug_statuses[0],
+ checker => \&check_bug_status
+ },
+
+ {name => 'letsubmitterchoosepriority', type => 'b', default => 1},
+
+ {name => 'letsubmitterchoosemilestone', type => 'b', default => 1},
+
+ {name => 'musthavemilestoneonaccept', type => 'b', default => 0},
+
+ {name => 'commentonchange_resolution', type => 'b', default => 0},
+
+ {name => 'commentonduplicate', type => 'b', default => 0},
+
+ {name => 'noresolveonopenblockers', type => 'b', default => 0,}
+ );
+ return @param_list;
}
1;
diff --git a/Bugzilla/Config/BugFields.pm b/Bugzilla/Config/BugFields.pm
index 94a16b7c2..87ecc6122 100644
--- a/Bugzilla/Config/BugFields.pm
+++ b/Bugzilla/Config/BugFields.pm
@@ -17,89 +17,61 @@ use Bugzilla::Field;
our $sortkey = 600;
sub get_param_list {
- my $class = shift;
-
- my @legal_priorities = @{ get_legal_field_values('priority') };
- my @legal_severities = @{ get_legal_field_values('bug_severity') };
- my @legal_platforms = @{ get_legal_field_values('rep_platform') };
- 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 => 'usebugaliases',
- 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;
+ my $class = shift;
+
+ my @legal_priorities = @{get_legal_field_values('priority')};
+ my @legal_severities = @{get_legal_field_values('bug_severity')};
+ my @legal_platforms = @{get_legal_field_values('rep_platform')};
+ 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 => 'usebugaliases', 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;
}
1;
diff --git a/Bugzilla/Config/Common.pm b/Bugzilla/Config/Common.pm
index 24b636099..b3d0cb5f8 100644
--- a/Bugzilla/Config/Common.pm
+++ b/Bugzilla/Config/Common.pm
@@ -22,311 +22,318 @@ use Bugzilla::Status;
use base qw(Exporter);
@Bugzilla::Config::Common::EXPORT = qw(
- check_multi check_numeric check_regexp check_url check_group
- check_priority check_severity check_platform
- check_opsys check_shadowdb check_urlbase check_webdotbase
- check_user_verify_class
- check_mail_delivery_method check_notification check_utf8
- check_bug_status check_smtp_auth check_theschwartz_available
- check_maxattachmentsize check_email
- check_comment_taggers_group
- get_all_group_names
+ check_multi check_numeric check_regexp check_url check_group
+ check_priority check_severity check_platform
+ check_opsys check_shadowdb check_urlbase check_webdotbase
+ check_user_verify_class
+ check_mail_delivery_method check_notification check_utf8
+ check_bug_status check_smtp_auth check_theschwartz_available
+ check_maxattachmentsize check_email
+ check_comment_taggers_group
+ get_all_group_names
);
# 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 "";
- }
- elsif ( $param->{'type'} eq 'm' || $param->{'type'} eq 'o' ) {
- foreach my $chkParam ( split( ',', $value ) ) {
- unless ( scalar( grep { $_ eq $chkParam } ( @{ $param->{'choices'} } ) ) ) {
- return "Invalid choice '$chkParam' for multi-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'}'";
}
- else {
- return "Invalid param type '$param->{'type'}' for check_multi(); " . "contact your Bugzilla administrator";
+
+ return "";
+ }
+ elsif ($param->{'type'} eq 'm' || $param->{'type'} eq 'o') {
+ foreach my $chkParam (split(',', $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_utf8 {
- my ($utf8, $entry) = @_;
+ my ($utf8, $entry) = @_;
- # You cannot turn off the UTF-8 parameter.
- if ( !$utf8 ) {
- return "You cannot disable UTF-8 support.";
- }
- elsif ($entry eq 'utf8mb4' && $utf8 ne 'utf8mb4') {
- return "You cannot disable UTF8-MB4 support.";
- }
+ # You cannot turn off the UTF-8 parameter.
+ if (!$utf8) {
+ return "You cannot disable UTF-8 support.";
+ }
+ elsif ($entry eq 'utf8mb4' && $utf8 ne 'utf8mb4') {
+ return "You cannot disable UTF8-MB4 support.";
+ }
- return "";
+ return "";
}
sub check_priority {
- my ($value) = (@_);
- my $legal_priorities = get_legal_field_values('priority');
- if ( !grep( $_ eq $value, @$legal_priorities ) ) {
- return "Must be a legal priority value: one of " . join( ", ", @$legal_priorities );
- }
- return "";
+ my ($value) = (@_);
+ my $legal_priorities = get_legal_field_values('priority');
+ if (!grep($_ eq $value, @$legal_priorities)) {
+ return "Must be a legal priority value: one of "
+ . join(", ", @$legal_priorities);
+ }
+ return "";
}
sub check_severity {
- my ($value) = (@_);
- my $legal_severities = get_legal_field_values('bug_severity');
- if ( !grep( $_ eq $value, @$legal_severities ) ) {
- return "Must be a legal severity value: one of " . join( ", ", @$legal_severities );
- }
- return "";
+ my ($value) = (@_);
+ my $legal_severities = get_legal_field_values('bug_severity');
+ if (!grep($_ eq $value, @$legal_severities)) {
+ return "Must be a legal severity value: one of "
+ . join(", ", @$legal_severities);
+ }
+ return "";
}
sub check_platform {
- my ($value) = (@_);
- my $legal_platforms = get_legal_field_values('rep_platform');
- if ( !grep( $_ eq $value, '', @$legal_platforms ) ) {
- return "Must be empty or a legal platform value: one of " . join( ", ", @$legal_platforms );
- }
- return "";
+ my ($value) = (@_);
+ my $legal_platforms = get_legal_field_values('rep_platform');
+ if (!grep($_ eq $value, '', @$legal_platforms)) {
+ return "Must be empty or a legal platform value: one of "
+ . join(", ", @$legal_platforms);
+ }
+ return "";
}
sub check_opsys {
- my ($value) = (@_);
- my $legal_OS = get_legal_field_values('op_sys');
- if ( !grep( $_ eq $value, '', @$legal_OS ) ) {
- return "Must be empty or a legal operating system value: one of " . join( ", ", @$legal_OS );
- }
- return "";
+ my ($value) = (@_);
+ my $legal_OS = get_legal_field_values('op_sys');
+ if (!grep($_ eq $value, '', @$legal_OS)) {
+ return "Must be empty or a legal operating system value: one of "
+ . join(", ", @$legal_OS);
+ }
+ return "";
}
sub check_bug_status {
- my $bug_status = shift;
- my @closed_bug_statuses = map { $_->name } closed_bug_statuses();
- if ( !grep( $_ eq $bug_status, @closed_bug_statuses ) ) {
- return "Must be a valid closed status: one of " . join( ', ', @closed_bug_statuses );
- }
- return "";
+ my $bug_status = shift;
+ my @closed_bug_statuses = map { $_->name } closed_bug_statuses();
+ if (!grep($_ eq $bug_status, @closed_bug_statuses)) {
+ return "Must be a valid closed status: one of "
+ . join(', ', @closed_bug_statuses);
+ }
+ return "";
}
sub check_group {
- my $group_name = shift;
- return "" unless $group_name;
- my $group = new Bugzilla::Group( { 'name' => $group_name } );
- unless ( defined $group ) {
- return "Must be an existing group name";
- }
- return "";
+ my $group_name = shift;
+ return "" unless $group_name;
+ my $group = new Bugzilla::Group({'name' => $group_name});
+ unless (defined $group) {
+ return "Must be an existing group name";
+ }
+ return "";
}
sub check_shadowdb {
- my ($value) = (@_);
- $value = trim($value);
- if ( $value eq "" ) {
- return "";
- }
+ my ($value) = (@_);
+ $value = trim($value);
+ if ($value eq "") {
+ return "";
+ }
- if ( !Bugzilla->params->{'shadowdbhost'} ) {
- return "You need to specify a host when using a shadow database";
- }
+ if (!Bugzilla->params->{'shadowdbhost'}) {
+ return "You need to specify a host when using a shadow database";
+ }
- # Can't test existence of this because ConnectToDatabase uses the param,
- # but we can't set this before testing....
- # This can really only be fixed after we can use the DBI more openly
- return "";
+ # Can't test existence of this because ConnectToDatabase uses the param,
+ # but we can't set this before testing....
+ # This can really only be fixed after we can use the DBI more openly
+ return "";
}
sub check_urlbase {
- my ($url) = (@_);
- if ( $url && $url !~ m:^http.*/$: ) {
- return "must be a legal URL, that starts with http and ends with a slash.";
- }
- return "";
+ my ($url) = (@_);
+ if ($url && $url !~ m:^http.*/$:) {
+ return "must be a legal URL, that starts with http and ends with a slash.";
+ }
+ return "";
}
sub check_url {
- my ($url) = (@_);
- return '' if $url eq ''; # Allow empty URLs
- if ( $url !~ m:/$: ) {
- return 'must be a legal URL, absolute or relative, ending with a slash.';
- }
- return '';
+ my ($url) = (@_);
+ return '' if $url eq ''; # Allow empty URLs
+ if ($url !~ m:/$:) {
+ return 'must be a legal URL, absolute or relative, ending with a slash.';
+ }
+ return '';
}
sub check_webdotbase {
- my ($value) = (@_);
- $value = trim($value);
- if ( $value eq "" ) {
- return "";
+ my ($value) = (@_);
+ $value = trim($value);
+ if ($value eq "") {
+ return "";
+ }
+ if ($value !~ /^https?:/) {
+ if (!-x $value) {
+ return
+ "The file path \"$value\" is not a valid executable. Please specify the complete file path to 'dot' if you intend to generate graphs locally.";
}
- if ( $value !~ /^https?:/ ) {
- if ( !-x $value ) {
- return
- "The file path \"$value\" is not a valid executable. Please specify the complete file path to 'dot' if you intend to generate graphs locally.";
- }
-
- # Check .htaccess allows access to generated images
- my $webdotdir = bz_locations()->{'webdotdir'};
- if ( -e "$webdotdir/.htaccess" ) {
- open HTACCESS, "<", "$webdotdir/.htaccess";
- if ( !grep( / \\\.png\$/, <HTACCESS> ) ) {
- return
- "Dependency graph images are not accessible.\nAssuming that you have not modified the file, delete $webdotdir/.htaccess and re-run checksetup.pl to rectify.\n";
- }
- close HTACCESS;
- }
+
+ # Check .htaccess allows access to generated images
+ my $webdotdir = bz_locations()->{'webdotdir'};
+ if (-e "$webdotdir/.htaccess") {
+ open HTACCESS, "<", "$webdotdir/.htaccess";
+ if (!grep(/ \\\.png\$/, <HTACCESS>)) {
+ return
+ "Dependency graph images are not accessible.\nAssuming that you have not modified the file, delete $webdotdir/.htaccess and re-run checksetup.pl to rectify.\n";
+ }
+ close HTACCESS;
}
- return "";
+ }
+ 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_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_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);
}
sub get_all_group_names {
- return [
- '',
- map { $_->name } Bugzilla::Group->get_all,
- ];
+ return ['', map { $_->name } Bugzilla::Group->get_all,];
}
# OK, here are the parameter definitions themselves.
@@ -393,7 +400,7 @@ sub get_all_group_names {
# for list (single and multiple) parameter types.
sub get_param_list {
- return;
+ return;
}
1;
diff --git a/Bugzilla/Config/DependencyGraph.pm b/Bugzilla/Config/DependencyGraph.pm
index 965be803a..2b48d634c 100644
--- a/Bugzilla/Config/DependencyGraph.pm
+++ b/Bugzilla/Config/DependencyGraph.pm
@@ -40,16 +40,14 @@ use Bugzilla::Config::Common;
our $sortkey = 800;
sub get_param_list {
- my $class = shift;
- my @param_list = (
- {
- name => 'webdotbase',
- type => 't',
- default => 'http://www.research.att.com/~north/cgi-bin/webdot.cgi/%urlbase%',
- checker => \&check_webdotbase
- }
- );
- return @param_list;
+ my $class = shift;
+ my @param_list = ({
+ name => 'webdotbase',
+ type => 't',
+ default => 'http://www.research.att.com/~north/cgi-bin/webdot.cgi/%urlbase%',
+ checker => \&check_webdotbase
+ });
+ return @param_list;
}
1;
diff --git a/Bugzilla/Config/Elastic.pm b/Bugzilla/Config/Elastic.pm
index 690f5fac5..d0ca7a02a 100644
--- a/Bugzilla/Config/Elastic.pm
+++ b/Bugzilla/Config/Elastic.pm
@@ -16,23 +16,11 @@ use Bugzilla::Config::Common;
our $sortkey = 1550;
sub get_param_list {
- return (
- {
- name => 'elasticsearch',
- type => 'b',
- default => 0,
- },
- {
- name => 'elasticsearch_nodes',
- type => 't',
- default => 'localhost:9200',
- },
- {
- name => 'elasticsearch_index',
- type => 't',
- default => 'bugzilla',
- },
- );
+ return (
+ {name => 'elasticsearch', type => 'b', default => 0,},
+ {name => 'elasticsearch_nodes', type => 't', default => 'localhost:9200',},
+ {name => 'elasticsearch_index', type => 't', default => 'bugzilla',},
+ );
}
1;
diff --git a/Bugzilla/Config/General.pm b/Bugzilla/Config/General.pm
index fa7cf2d08..bf4088248 100644
--- a/Bugzilla/Config/General.pm
+++ b/Bugzilla/Config/General.pm
@@ -16,50 +16,49 @@ use Bugzilla::Config::Common;
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 => 'nobody_user',
- type => 't',
- no_reset => '1',
- default => 'nobody@mozilla.org',
- checker => \&check_email
- },
+ {
+ name => 'nobody_user',
+ type => 't',
+ no_reset => '1',
+ default => 'nobody@mozilla.org',
+ checker => \&check_email
+ },
- {
- name => 'docs_urlbase',
- type => 't',
- default => 'docs/%lang%/html/',
- checker => \&check_url
- },
+ {
+ name => 'docs_urlbase',
+ type => 't',
+ default => 'docs/%lang%/html/',
+ checker => \&check_url
+ },
- {
- name => 'utf8',
- type => 's',
- choices => [ '1', 'utf8', 'utf8mb4' ],
- default => 'utf8',
- checker => \&check_utf8
- },
+ {
+ name => 'utf8',
+ type => 's',
+ choices => ['1', 'utf8', 'utf8mb4'],
+ default => 'utf8',
+ checker => \&check_utf8
+ },
- {
- 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
+ },
);
1;
diff --git a/Bugzilla/Config/GroupSecurity.pm b/Bugzilla/Config/GroupSecurity.pm
index 68c852fe6..f6f824098 100644
--- a/Bugzilla/Config/GroupSecurity.pm
+++ b/Bugzilla/Config/GroupSecurity.pm
@@ -17,78 +17,65 @@ use Bugzilla::Group;
our $sortkey = 900;
sub get_param_list {
- my $class = shift;
-
- my @param_list = (
- {
- name => 'makeproductgroups',
- type => 'b',
- default => 0
- },
-
- {
- name => 'chartgroup',
- type => 's',
- choices => \&get_all_group_names,
- default => 'editbugs',
- checker => \&check_group
- },
-
- {
- name => 'insidergroup',
- type => 's',
- choices => \&get_all_group_names,
- default => '',
- checker => \&check_group
- },
-
- {
- name => 'timetrackinggroup',
- type => 's',
- choices => \&get_all_group_names,
- default => 'editbugs',
- checker => \&check_group
- },
-
- {
- name => 'querysharegroup',
- type => 's',
- choices => \&get_all_group_names,
- default => 'editbugs',
- checker => \&check_group
- },
-
- {
- name => 'comment_taggers_group',
- type => 's',
- choices => \&get_all_group_names,
- default => 'editbugs',
- checker => \&check_comment_taggers_group
- },
-
- {
- name => 'debug_group',
- type => 's',
- choices => \&get_all_group_names,
- default => 'admin',
- checker => \&check_group
- },
-
- {
- name => 'usevisibilitygroups',
- type => 'b',
- default => 0
- },
-
- {
- name => 'strict_isolation',
- type => 'b',
- default => 0
- }
- );
- return @param_list;
+ my $class = shift;
+
+ my @param_list = (
+ {name => 'makeproductgroups', type => 'b', default => 0},
+
+ {
+ name => 'chartgroup',
+ type => 's',
+ choices => \&get_all_group_names,
+ default => 'editbugs',
+ checker => \&check_group
+ },
+
+ {
+ name => 'insidergroup',
+ type => 's',
+ choices => \&get_all_group_names,
+ default => '',
+ checker => \&check_group
+ },
+
+ {
+ name => 'timetrackinggroup',
+ type => 's',
+ choices => \&get_all_group_names,
+ default => 'editbugs',
+ checker => \&check_group
+ },
+
+ {
+ name => 'querysharegroup',
+ type => 's',
+ choices => \&get_all_group_names,
+ default => 'editbugs',
+ checker => \&check_group
+ },
+
+ {
+ name => 'comment_taggers_group',
+ type => 's',
+ choices => \&get_all_group_names,
+ default => 'editbugs',
+ checker => \&check_comment_taggers_group
+ },
+
+ {
+ name => 'debug_group',
+ type => 's',
+ choices => \&get_all_group_names,
+ default => 'admin',
+ checker => \&check_group
+ },
+
+ {name => 'usevisibilitygroups', type => 'b', default => 0},
+
+ {name => 'strict_isolation', type => 'b', default => 0}
+ );
+ return @param_list;
}
-
1;
diff --git a/Bugzilla/Config/LDAP.pm b/Bugzilla/Config/LDAP.pm
index a3ce04fbe..75f58e141 100644
--- a/Bugzilla/Config/LDAP.pm
+++ b/Bugzilla/Config/LDAP.pm
@@ -16,51 +16,23 @@ use Bugzilla::Config::Common;
our $sortkey = 1000;
sub get_param_list {
- my $class = shift;
- my @param_list = (
- {
- name => 'LDAPserver',
- type => 't',
- default => ''
- },
+ my $class = shift;
+ my @param_list = (
+ {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 => '',
- }
- );
- return @param_list;
+ {name => 'LDAPfilter', type => 't', default => '',}
+ );
+ return @param_list;
}
1;
diff --git a/Bugzilla/Config/MTA.pm b/Bugzilla/Config/MTA.pm
index c23c324d9..c57989ee4 100644
--- a/Bugzilla/Config/MTA.pm
+++ b/Bugzilla/Config/MTA.pm
@@ -41,78 +41,51 @@ use Bugzilla::Config::Common;
# deprecated module. We have to set NO_CLUCK = 1 before loading Email::Send
# to disable these warnings.
BEGIN {
- $Return::Value::NO_CLUCK = 1;
+ $Return::Value::NO_CLUCK = 1;
}
use Email::Send;
our $sortkey = 1200;
sub get_param_list {
- my $class = shift;
- my @param_list = (
- {
- name => 'mail_delivery_method',
- type => 's',
+ my $class = shift;
+ my @param_list = (
+ {
+ name => 'mail_delivery_method',
+ type => 's',
- # Bugzilla is not ready yet to send mails to newsgroups, and 'IO'
- # is of no use for now as we already have our own 'Test' mode.
- choices => [ grep { $_ ne 'NNTP' && $_ ne 'IO' } Email::Send->new()->all_mailers(), 'None' ],
- default => 'Sendmail',
- checker => \&check_mail_delivery_method
- },
+ # Bugzilla is not ready yet to send mails to newsgroups, and 'IO'
+ # is of no use for now as we already have our own 'Test' mode.
+ choices => [
+ grep { $_ ne 'NNTP' && $_ ne 'IO' } Email::Send->new()->all_mailers(), 'None'
+ ],
+ default => 'Sendmail',
+ checker => \&check_mail_delivery_method
+ },
- {
- name => 'mailfrom',
- type => 't',
- default => 'bugzilla-daemon'
- },
+ {name => 'mailfrom', type => 't', default => 'bugzilla-daemon'},
- {
- name => 'use_mailer_queue',
- type => 'b',
- default => 0,
- checker => \&check_theschwartz_available,
- },
+ {
+ name => 'use_mailer_queue',
+ type => 'b',
+ default => 0,
+ checker => \&check_theschwartz_available,
+ },
- {
- name => 'smtpserver',
- type => 't',
- default => 'localhost'
- },
- {
- name => 'smtp_username',
- type => 't',
- default => '',
- checker => \&check_smtp_auth
- },
- {
- name => 'smtp_password',
- type => 'p',
- default => ''
- },
- {
- name => 'smtp_debug',
- type => 'b',
- default => 0
- },
- {
- name => 'whinedays',
- type => 't',
- default => 7,
- checker => \&check_numeric
- },
- {
- name => 'globalwatchers',
- type => 't',
- default => '',
- },
- {
- name => 'silent_users',
- type => 't',
- default => '',
- },
- );
- return @param_list;
+ {name => 'smtpserver', type => 't', default => 'localhost'},
+ {
+ name => 'smtp_username',
+ type => 't',
+ default => '',
+ checker => \&check_smtp_auth
+ },
+ {name => 'smtp_password', type => 'p', default => ''},
+ {name => 'smtp_debug', type => 'b', default => 0},
+ {name => 'whinedays', type => 't', default => 7, checker => \&check_numeric},
+ {name => 'globalwatchers', type => 't', default => '',},
+ {name => 'silent_users', type => 't', default => '',},
+ );
+ return @param_list;
}
1;
diff --git a/Bugzilla/Config/PatchViewer.pm b/Bugzilla/Config/PatchViewer.pm
index 98c8c675f..e735aac63 100644
--- a/Bugzilla/Config/PatchViewer.pm
+++ b/Bugzilla/Config/PatchViewer.pm
@@ -40,39 +40,19 @@ use Bugzilla::Config::Common;
our $sortkey = 1300;
sub get_param_list {
- my $class = shift;
- my @param_list = (
- {
- name => 'cvsroot',
- type => 't',
- default => '',
- },
+ my $class = shift;
+ my @param_list = (
+ {name => 'cvsroot', type => 't', default => '',},
- {
- name => 'cvsroot_get',
- type => 't',
- default => '',
- },
+ {name => 'cvsroot_get', type => 't', default => '',},
- {
- name => 'bonsai_url',
- type => 't',
- default => ''
- },
+ {name => 'bonsai_url', type => 't', default => ''},
- {
- name => 'lxr_url',
- type => 't',
- default => ''
- },
+ {name => 'lxr_url', type => 't', default => ''},
- {
- name => 'lxr_root',
- type => 't',
- default => '',
- }
- );
- return @param_list;
+ {name => 'lxr_root', type => 't', default => '',}
+ );
+ return @param_list;
}
1;
diff --git a/Bugzilla/Config/Query.pm b/Bugzilla/Config/Query.pm
index b6397a835..83115717a 100644
--- a/Bugzilla/Config/Query.pm
+++ b/Bugzilla/Config/Query.pm
@@ -16,58 +16,54 @@ use Bugzilla::Config::Common;
our $sortkey = 1400;
sub get_param_list {
- my $class = shift;
- my @param_list = (
- {
- name => 'quip_list_entry_control',
- type => 's',
- choices => [ 'open', 'moderated', 'closed' ],
- default => 'open',
- checker => \&check_multi
- },
+ my $class = shift;
+ my @param_list = (
+ {
+ name => 'quip_list_entry_control',
+ type => 's',
+ choices => ['open', 'moderated', 'closed'],
+ default => 'open',
+ checker => \&check_multi
+ },
- {
- name => 'mostfreqthreshold',
- type => 't',
- default => '2',
- checker => \&check_numeric
- },
+ {
+ name => 'mostfreqthreshold',
+ type => 't',
+ default => '2',
+ checker => \&check_numeric
+ },
- {
- name => 'mybugstemplate',
- type => 't',
- default =>
- 'buglist.cgi?resolution=---&amp;emailassigned_to1=1&amp;emailreporter1=1&amp;emailtype1=exact&amp;email1=%userid%'
- },
+ {
+ 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 => '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 => 'search_allow_no_criteria', type => 'b', default => 1},
- {
- name => 'default_search_limit',
- type => 't',
- default => '500',
- checker => \&check_numeric
- },
+ {
+ 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;
+ {
+ name => 'max_search_results',
+ type => 't',
+ default => '10000',
+ checker => \&check_numeric
+ },
+ );
+ return @param_list;
}
1;
diff --git a/Bugzilla/Config/RADIUS.pm b/Bugzilla/Config/RADIUS.pm
index bc980a1ec..b0a5ddbf5 100644
--- a/Bugzilla/Config/RADIUS.pm
+++ b/Bugzilla/Config/RADIUS.pm
@@ -16,33 +16,17 @@ use Bugzilla::Config::Common;
our $sortkey = 1100;
sub get_param_list {
- 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 => ''
- },
- );
- return @param_list;
+ 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 => ''},
+ );
+ return @param_list;
}
1;
diff --git a/Bugzilla/Config/Reports.pm b/Bugzilla/Config/Reports.pm
index 26c5aad57..dfb6db7a3 100644
--- a/Bugzilla/Config/Reports.pm
+++ b/Bugzilla/Config/Reports.pm
@@ -16,22 +16,14 @@ use Bugzilla::Config::Common;
our $sortkey = 1100;
sub get_param_list {
- my $class = shift;
- my @param_list = (
- {
- name => 'report_secbugs_active',
- type => 'b',
- default => 1,
- },
- {
- name => 'report_secbugs_emails',
- type => 't',
- default => 'bugzilla-admin@mozilla.org'
- },
- {
- name => 'report_secbugs_products',
- type => 'l',
- default => '[]'
- },
- );
+ my $class = shift;
+ my @param_list = (
+ {name => 'report_secbugs_active', type => 'b', default => 1,},
+ {
+ name => 'report_secbugs_emails',
+ type => 't',
+ default => 'bugzilla-admin@mozilla.org'
+ },
+ {name => 'report_secbugs_products', type => 'l', default => '[]'},
+ );
}
diff --git a/Bugzilla/Config/ShadowDB.pm b/Bugzilla/Config/ShadowDB.pm
index 772df5675..101e4678f 100644
--- a/Bugzilla/Config/ShadowDB.pm
+++ b/Bugzilla/Config/ShadowDB.pm
@@ -16,37 +16,24 @@ use Bugzilla::Config::Common;
our $sortkey = 1500;
sub get_param_list {
- 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
- }
- );
- return @param_list;
+ 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}
+ );
+ return @param_list;
}
1;
diff --git a/Bugzilla/Config/UserMatch.pm b/Bugzilla/Config/UserMatch.pm
index ddb850f3b..a1f8a3eb2 100644
--- a/Bugzilla/Config/UserMatch.pm
+++ b/Bugzilla/Config/UserMatch.pm
@@ -16,34 +16,22 @@ use Bugzilla::Config::Common;
our $sortkey = 1600;
sub get_param_list {
- 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,
- }
- );
- return @param_list;
+ 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,}
+ );
+ return @param_list;
}
1;
diff --git a/Bugzilla/Constants.pm b/Bugzilla/Constants.pm
index cd478c33e..26341967d 100644
--- a/Bugzilla/Constants.pm
+++ b/Bugzilla/Constants.pm
@@ -19,185 +19,185 @@ use Cwd qw(realpath);
use Memoize;
@Bugzilla::Constants::EXPORT = qw(
- BUGZILLA_VERSION
+ BUGZILLA_VERSION
- REMOTE_FILE
- LOCAL_FILE
+ REMOTE_FILE
+ LOCAL_FILE
- bz_locations
+ bz_locations
- IS_NULL
- NOT_NULL
+ IS_NULL
+ NOT_NULL
- CONTROLMAPNA
- CONTROLMAPSHOWN
- CONTROLMAPDEFAULT
- CONTROLMAPMANDATORY
+ CONTROLMAPNA
+ CONTROLMAPSHOWN
+ CONTROLMAPDEFAULT
+ CONTROLMAPMANDATORY
- AUTH_OK
- AUTH_NODATA
- AUTH_ERROR
- AUTH_LOGINFAILED
- AUTH_DISABLED
- AUTH_NO_SUCH_USER
- AUTH_LOCKOUT
+ AUTH_OK
+ AUTH_NODATA
+ AUTH_ERROR
+ AUTH_LOGINFAILED
+ AUTH_DISABLED
+ AUTH_NO_SUCH_USER
+ AUTH_LOCKOUT
- USER_PASSWORD_MIN_LENGTH
+ USER_PASSWORD_MIN_LENGTH
- LOGIN_OPTIONAL
- LOGIN_NORMAL
- LOGIN_REQUIRED
+ LOGIN_OPTIONAL
+ LOGIN_NORMAL
+ LOGIN_REQUIRED
- LOGOUT_ALL
- LOGOUT_CURRENT
- LOGOUT_KEEP_CURRENT
+ LOGOUT_ALL
+ LOGOUT_CURRENT
+ LOGOUT_KEEP_CURRENT
- GRANT_DIRECT
- GRANT_REGEXP
+ GRANT_DIRECT
+ GRANT_REGEXP
- GROUP_MEMBERSHIP
- GROUP_BLESS
- GROUP_VISIBLE
-
- MAILTO_USER
- MAILTO_GROUP
+ GROUP_MEMBERSHIP
+ GROUP_BLESS
+ GROUP_VISIBLE
+
+ MAILTO_USER
+ MAILTO_GROUP
- DEFAULT_COLUMN_LIST
- DEFAULT_QUERY_NAME
- DEFAULT_MILESTONE
+ DEFAULT_COLUMN_LIST
+ DEFAULT_QUERY_NAME
+ DEFAULT_MILESTONE
- SAVE_NUM_SEARCHES
+ SAVE_NUM_SEARCHES
- COMMENT_COLS
- MAX_COMMENT_LENGTH
-
- MIN_COMMENT_TAG_LENGTH
- MAX_COMMENT_TAG_LENGTH
-
- CMT_NORMAL
- CMT_DUPE_OF
- CMT_HAS_DUPE
- CMT_ATTACHMENT_CREATED
- CMT_ATTACHMENT_UPDATED
-
- THROW_ERROR
-
- RELATIONSHIPS
- REL_ASSIGNEE REL_QA REL_REPORTER REL_CC REL_GLOBAL_WATCHER
- REL_ANY
-
- POS_EVENTS
- EVT_OTHER EVT_ADDED_REMOVED EVT_COMMENT EVT_ATTACHMENT EVT_ATTACHMENT_DATA
- EVT_PROJ_MANAGEMENT EVT_OPENED_CLOSED EVT_KEYWORD EVT_CC EVT_DEPEND_BLOCK
- EVT_BUG_CREATED EVT_COMPONENT
-
- NEG_EVENTS
- EVT_UNCONFIRMED EVT_CHANGED_BY_ME
-
- GLOBAL_EVENTS
- EVT_FLAG_REQUESTED EVT_REQUESTED_FLAG
+ COMMENT_COLS
+ MAX_COMMENT_LENGTH
+
+ MIN_COMMENT_TAG_LENGTH
+ MAX_COMMENT_TAG_LENGTH
+
+ CMT_NORMAL
+ CMT_DUPE_OF
+ CMT_HAS_DUPE
+ CMT_ATTACHMENT_CREATED
+ CMT_ATTACHMENT_UPDATED
+
+ THROW_ERROR
+
+ RELATIONSHIPS
+ REL_ASSIGNEE REL_QA REL_REPORTER REL_CC REL_GLOBAL_WATCHER
+ REL_ANY
+
+ POS_EVENTS
+ EVT_OTHER EVT_ADDED_REMOVED EVT_COMMENT EVT_ATTACHMENT EVT_ATTACHMENT_DATA
+ EVT_PROJ_MANAGEMENT EVT_OPENED_CLOSED EVT_KEYWORD EVT_CC EVT_DEPEND_BLOCK
+ EVT_BUG_CREATED EVT_COMPONENT
+
+ NEG_EVENTS
+ EVT_UNCONFIRMED EVT_CHANGED_BY_ME
+
+ GLOBAL_EVENTS
+ EVT_FLAG_REQUESTED EVT_REQUESTED_FLAG
- ADMIN_GROUP_NAME
- PER_PRODUCT_PRIVILEGES
+ ADMIN_GROUP_NAME
+ PER_PRODUCT_PRIVILEGES
- SENDMAIL_EXE
- SENDMAIL_PATH
+ 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_EXTENSION
+ 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_EXTENSION
- FIELD_TYPE_HIGHEST_PLUS_ONE
+ FIELD_TYPE_HIGHEST_PLUS_ONE
- EMPTY_DATETIME_REGEX
+ EMPTY_DATETIME_REGEX
- ABNORMAL_SELECTS
-
- TIMETRACKING_FIELDS
+ 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
- USAGE_MODE_MOJO
-
- ERROR_MODE_WEBPAGE
- ERROR_MODE_DIE
- ERROR_MODE_DIE_SOAP_FAULT
- ERROR_MODE_JSON_RPC
- ERROR_MODE_TEST
- ERROR_MODE_REST
- ERROR_MODE_MOJO
+ USAGE_MODE_BROWSER
+ USAGE_MODE_CMDLINE
+ USAGE_MODE_XMLRPC
+ USAGE_MODE_EMAIL
+ USAGE_MODE_JSON
+ USAGE_MODE_TEST
+ USAGE_MODE_REST
+ USAGE_MODE_MOJO
+
+ ERROR_MODE_WEBPAGE
+ ERROR_MODE_DIE
+ ERROR_MODE_DIE_SOAP_FAULT
+ ERROR_MODE_JSON_RPC
+ ERROR_MODE_TEST
+ ERROR_MODE_REST
+ ERROR_MODE_MOJO
- COLOR_ERROR
- COLOR_SUCCESS
+ COLOR_ERROR
+ COLOR_SUCCESS
- INSTALLATION_MODE_INTERACTIVE
- INSTALLATION_MODE_NON_INTERACTIVE
-
- DB_MODULE
- ROOT_USER
- ON_WINDOWS
- ON_ACTIVESTATE
+ INSTALLATION_MODE_INTERACTIVE
+ INSTALLATION_MODE_NON_INTERACTIVE
+
+ DB_MODULE
+ ROOT_USER
+ ON_WINDOWS
+ ON_ACTIVESTATE
- MAX_TOKEN_AGE
- MAX_SHORT_TOKEN_HOURS
- MAX_LOGINCOOKIE_AGE
- MAX_SUDO_TOKEN_AGE
- MAX_LOGIN_ATTEMPTS
- LOGIN_LOCKOUT_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_FREETEXT_LENGTH
- MAX_BUG_URL_LENGTH
- MAX_POSSIBLE_DUPLICATES
- 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
-
- EMAIL_LIMIT_PER_MINUTE
- EMAIL_LIMIT_PER_HOUR
- EMAIL_LIMIT_EXCEPTION
-
- JOB_QUEUE_VIEW_MAX_JOBS
-
- BZ_PERSISTENT
+ MAX_TOKEN_AGE
+ MAX_SHORT_TOKEN_HOURS
+ MAX_LOGINCOOKIE_AGE
+ MAX_SUDO_TOKEN_AGE
+ MAX_LOGIN_ATTEMPTS
+ LOGIN_LOCKOUT_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_FREETEXT_LENGTH
+ MAX_BUG_URL_LENGTH
+ MAX_POSSIBLE_DUPLICATES
+ 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
+
+ EMAIL_LIMIT_PER_MINUTE
+ EMAIL_LIMIT_PER_HOUR
+ EMAIL_LIMIT_EXCEPTION
+
+ JOB_QUEUE_VIEW_MAX_JOBS
+
+ BZ_PERSISTENT
);
@Bugzilla::Constants::EXPORT_OK = qw(contenttypes);
@@ -208,14 +208,14 @@ use Memoize;
# BMO: we don't map exactly to a specific bugzilla version, so override our
# reported version with a parameter.
sub BUGZILLA_VERSION {
- my $bugzilla_version = '4.2';
- eval { require Bugzilla } || return $bugzilla_version;
- eval { Bugzilla->VERSION } || $bugzilla_version;
+ my $bugzilla_version = '4.2';
+ eval { require Bugzilla } || return $bugzilla_version;
+ eval { Bugzilla->VERSION } || $bugzilla_version;
}
# Location of the remote and local XML files to track new releases.
use constant REMOTE_FILE => 'https://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.
# 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
@@ -255,47 +255,47 @@ 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.
# BMO uses 8 characters.
use constant USER_PASSWORD_MIN_LENGTH => 8;
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
@@ -310,6 +310,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;
@@ -318,9 +319,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;
@@ -330,27 +332,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
@@ -358,34 +359,34 @@ use constant REL_ANY => 100;
#
# Exactly when each event fires is defined in wants_bug_mail() in User.pm; I'm
# not commenting them here in case the comments and the code get out of sync.
-use constant EVT_OTHER => 0;
-use constant EVT_ADDED_REMOVED => 1;
-use constant EVT_COMMENT => 2;
-use constant EVT_ATTACHMENT => 3;
-use constant EVT_ATTACHMENT_DATA => 4;
-use constant EVT_PROJ_MANAGEMENT => 5;
-use constant EVT_OPENED_CLOSED => 6;
-use constant EVT_KEYWORD => 7;
-use constant EVT_CC => 8;
-use constant EVT_DEPEND_BLOCK => 9;
-use constant EVT_BUG_CREATED => 10;
-use constant EVT_COMPONENT => 11;
-
-use constant POS_EVENTS => EVT_OTHER, EVT_ADDED_REMOVED, EVT_COMMENT,
- EVT_ATTACHMENT, EVT_ATTACHMENT_DATA,
- EVT_PROJ_MANAGEMENT, EVT_OPENED_CLOSED, EVT_KEYWORD,
- EVT_CC, EVT_DEPEND_BLOCK, EVT_BUG_CREATED,
- EVT_COMPONENT;
-
-use constant EVT_UNCONFIRMED => 50;
-use constant EVT_CHANGED_BY_ME => 51;
+use constant EVT_OTHER => 0;
+use constant EVT_ADDED_REMOVED => 1;
+use constant EVT_COMMENT => 2;
+use constant EVT_ATTACHMENT => 3;
+use constant EVT_ATTACHMENT_DATA => 4;
+use constant EVT_PROJ_MANAGEMENT => 5;
+use constant EVT_OPENED_CLOSED => 6;
+use constant EVT_KEYWORD => 7;
+use constant EVT_CC => 8;
+use constant EVT_DEPEND_BLOCK => 9;
+use constant EVT_BUG_CREATED => 10;
+use constant EVT_COMPONENT => 11;
+
+use constant
+ POS_EVENTS => EVT_OTHER,
+ EVT_ADDED_REMOVED, EVT_COMMENT, EVT_ATTACHMENT, EVT_ATTACHMENT_DATA,
+ EVT_PROJ_MANAGEMENT, EVT_OPENED_CLOSED, EVT_KEYWORD, EVT_CC,
+ EVT_DEPEND_BLOCK, EVT_BUG_CREATED, EVT_COMPONENT;
+
+use constant EVT_UNCONFIRMED => 50;
+use constant EVT_CHANGED_BY_ME => 51;
use constant NEG_EVENTS => EVT_UNCONFIRMED, EVT_CHANGED_BY_ME;
# These are the "global" flags, which aren't tied to a particular relationship.
# and so use REL_ANY.
-use constant EVT_FLAG_REQUESTED => 100; # Flag has been requested of me
-use constant EVT_REQUESTED_FLAG => 101; # I have requested a flag
+use constant EVT_FLAG_REQUESTED => 100; # Flag has been requested of me
+use constant EVT_REQUESTED_FLAG => 101; # I have requested a flag
use constant GLOBAL_EVENTS => EVT_FLAG_REQUESTED, EVT_REQUESTED_FLAG;
@@ -393,10 +394,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';
@@ -408,18 +411,18 @@ use constant SENDMAIL_PATH => '/usr/lib:/usr/sbin:/usr/ucblib';
# display a user picker). Fields of type FIELD_TYPE_EXTENSION should generally
# be ignored by the core code and is used primary by extensions.
-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_EXTENSION => 99;
+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_EXTENSION => 99;
# Add new field types above this line, and change the below value in the
# obvious fashion
@@ -429,29 +432,30 @@ 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 deadline);
+ qw(estimated_time remaining_time work_time actual_time
+ percentage_complete deadline);
# The maximum number of days a token will remain valid.
use constant MAX_TOKEN_AGE => 3;
+
# The maximum number of hours a short-lived token will remain valid.
use constant MAX_SHORT_TOKEN_HOURS => 1;
+
# How many days a logincookie will remain valid if not used.
use constant MAX_LOGINCOOKIE_AGE => 7;
+
# 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;
@@ -461,38 +465,41 @@ use constant LOGIN_LOCKOUT_INTERVAL => 30;
use constant MAX_STS_AGE => 31536000;
# 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" ,
- "txt" => "text/plain",
- };
+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",
+ "txt" => "text/plain",
+};
# 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_MOJO => 7;
+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_MOJO => 7;
# Error modes. Default set by Bugzilla->usage_mode (so ERROR_MODE_WEBPAGE
# usually). Use with Bugzilla->error_mode.
@@ -505,58 +512,69 @@ use constant ERROR_MODE_REST => 5;
use constant ERROR_MODE_MOJO => 6;
# 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 => {
- # Require MySQL 5.6.x for innodb's fulltext support
- 'mysql' => {db => 'Bugzilla::DB::Mysql', db_version => '5.6.12',
- dbd => {
- package => 'DBD-mysql',
- module => 'DBD::mysql',
- # Disallow development versions
- blacklist => ['_'],
- # For UTF-8 support. 4.001 makes sure that blobs aren't
- # marked as UTF-8.
- version => '4.001',
- },
- name => 'MySQL'},
- # Also see Bugzilla::DB::Pg::bz_check_server_version, which has special
- # code to require DBD::Pg 2.17.2 for PostgreSQL 9 and above.
- 'pg' => {db => 'Bugzilla::DB::Pg', db_version => '8.03.0000',
- dbd => {
- package => 'DBD-Pg',
- module => 'DBD::Pg',
- version => '1.45',
- },
- name => 'PostgreSQL'},
- 'oracle'=> {db => 'Bugzilla::DB::Oracle', db_version => '10.02.0',
- dbd => {
- package => 'DBD-Oracle',
- module => 'DBD::Oracle',
- version => '1.19',
- },
- name => 'Oracle'},
- # SQLite 3.6.22 fixes a WHERE clause problem that may affect us.
- sqlite => {db => 'Bugzilla::DB::Sqlite', db_version => '3.6.22',
- dbd => {
- package => 'DBD-SQLite',
- module => 'DBD::SQLite',
- # 1.29 is the version that contains 3.6.22.
- version => '1.29',
- },
- name => 'SQLite'},
+
+ # Require MySQL 5.6.x for innodb's fulltext support
+ 'mysql' => {
+ db => 'Bugzilla::DB::Mysql',
+ db_version => '5.6.12',
+ dbd => {
+ package => 'DBD-mysql',
+ module => 'DBD::mysql',
+
+ # Disallow development versions
+ blacklist => ['_'],
+
+ # For UTF-8 support. 4.001 makes sure that blobs aren't
+ # marked as UTF-8.
+ version => '4.001',
+ },
+ name => 'MySQL'
+ },
+
+ # Also see Bugzilla::DB::Pg::bz_check_server_version, which has special
+ # code to require DBD::Pg 2.17.2 for PostgreSQL 9 and above.
+ 'pg' => {
+ db => 'Bugzilla::DB::Pg',
+ db_version => '8.03.0000',
+ dbd => {package => 'DBD-Pg', module => 'DBD::Pg', version => '1.45',},
+ name => 'PostgreSQL'
+ },
+ 'oracle' => {
+ db => 'Bugzilla::DB::Oracle',
+ db_version => '10.02.0',
+ dbd => {package => 'DBD-Oracle', module => 'DBD::Oracle', version => '1.19',},
+ name => 'Oracle'
+ },
+
+ # SQLite 3.6.22 fixes a WHERE clause problem that may affect us.
+ sqlite => {
+ db => 'Bugzilla::DB::Sqlite',
+ db_version => '3.6.22',
+ dbd => {
+ package => 'DBD-SQLite',
+ module => 'DBD::SQLite',
+
+ # 1.29 is the version that contains 3.6.22.
+ version => '1.29',
+ },
+ name => 'SQLite'
+ },
};
# True if we're on Win32.
use constant ON_WINDOWS => ($^O =~ /MSWin32/i) ? 1 : 0;
+
# True if we're using ActiveState Perl (as opposed to Strawberry) on Windows.
-use constant ON_ACTIVESTATE => eval { &Win32::BuildNumber };
+use constant ON_ACTIVESTATE => eval {&Win32::BuildNumber};
# The user who should be considered "root" when we're giving
# instructions to Bugzilla administrators.
@@ -564,7 +582,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;
@@ -602,6 +620,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 changes his password.
use constant PASSWORD_DIGEST_ALGORITHM => 'SHA-256';
+
# How long of a salt should we use? Note that if you change this, none
# of your users will be able to log in until they reset their passwords.
use constant PASSWORD_SALT_LENGTH => 8;
@@ -630,82 +649,90 @@ use constant AUDIT_REMOVE => '__remove__';
# 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 => 2500;
sub bz_locations {
- # Force memoize() to re-compute data per project, to avoid
- # sharing the same data across different installations.
- return _bz_locations($ENV{'PROJECT'});
+
+ # Force memoize() to re-compute data per project, to avoid
+ # sharing the same data across different installations.
+ return _bz_locations($ENV{'PROJECT'});
}
sub _bz_locations {
- my $project = shift;
- # We know that Bugzilla/Constants.pm must be in %INC at this point.
- # So the only question is, what's the name of the directory
- # above it? This is the most reliable way to get our current working
- # directory under both mod_cgi and mod_perl. We call dirname twice
- # to get the name of the directory above the "Bugzilla/" directory.
- #
- # Always use an absolute path, based on the location of this file.
- my $libpath = realpath(dirname(dirname(__FILE__)));
- # We have to detaint $libpath, but we can't use Bugzilla::Util here.
- $libpath =~ /(.*)/;
- $libpath = $1;
-
- my ($localconfig, $datadir, $confdir);
- if ($project && $project =~ /^(\w+)$/) {
- $project = $1;
- $localconfig = "localconfig.$project";
- $datadir = "data/$project";
- $confdir = "conf/$project";
- } else {
- $project = undef;
- $localconfig = "localconfig";
- $datadir = "data";
- $confdir = "conf";
- }
-
- $datadir = "$libpath/$datadir";
- $confdir = "$libpath/$confdir";
- # 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' => "$libpath/template_cache",
- '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",
- 'logsdir' => "$libpath/logs",
- 'assetsdir' => "$datadir/assets",
- 'confdir' => $confdir,
- };
+ 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.
+ #
+ # Always use an absolute path, based on the location of this file.
+ my $libpath = realpath(dirname(dirname(__FILE__)));
+
+ # We have to detaint $libpath, but we can't use Bugzilla::Util here.
+ $libpath =~ /(.*)/;
+ $libpath = $1;
+
+ my ($localconfig, $datadir, $confdir);
+ if ($project && $project =~ /^(\w+)$/) {
+ $project = $1;
+ $localconfig = "localconfig.$project";
+ $datadir = "data/$project";
+ $confdir = "conf/$project";
+ }
+ else {
+ $project = undef;
+ $localconfig = "localconfig";
+ $datadir = "data";
+ $confdir = "conf";
+ }
+
+ $datadir = "$libpath/$datadir";
+ $confdir = "$libpath/$confdir";
+
+ # 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' => "$libpath/template_cache",
+ '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",
+ 'logsdir' => "$libpath/logs",
+ 'assetsdir' => "$datadir/assets",
+ 'confdir' => $confdir,
+ };
}
use constant BZ_PERSISTENT => $main::BUGZILLA_PERSISTENT;
# This makes us not re-compute all the bz_locations data every time it's
# called.
-BEGIN { memoize('_bz_locations') };
+BEGIN { memoize('_bz_locations') }
1;
diff --git a/Bugzilla/DB.pm b/Bugzilla/DB.pm
index 87110aaaa..1003c4673 100644
--- a/Bugzilla/DB.pm
+++ b/Bugzilla/DB.pm
@@ -13,10 +13,7 @@ use Moo;
use DBI;
use DBIx::Connector;
-has 'connector' => (
- is => 'lazy',
- handles => [ qw( dbh ) ],
-);
+has 'connector' => (is => 'lazy', handles => [qw( dbh )],);
use Bugzilla::Logging;
use Bugzilla::Constants;
@@ -36,39 +33,36 @@ use Storable qw(dclone);
use English qw(-no_match_vars);
use Module::Runtime qw(require_module);
-has [qw(dsn user pass attrs)] => (
- is => 'ro',
- required => 1,
-);
+has [qw(dsn user pass attrs)] => (is => 'ro', required => 1,);
# Install proxy methods to the DBI object.
# We can't use handles() as DBIx::Connector->dbh has to be called each
# time we need a DBI handle to ensure the connection is alive.
{
- my @DBI_METHODS = qw(
- begin_work column_info commit do errstr get_info last_insert_id ping prepare
- primary_key quote_identifier rollback selectall_arrayref selectall_hashref
- selectcol_arrayref selectrow_array selectrow_arrayref selectrow_hashref table_info
+ my @DBI_METHODS = qw(
+ begin_work column_info commit do errstr get_info last_insert_id ping prepare
+ primary_key quote_identifier rollback selectall_arrayref selectall_hashref
+ selectcol_arrayref selectrow_array selectrow_arrayref selectrow_hashref table_info
+ );
+ my $stash = Package::Stash->new(__PACKAGE__);
+
+ foreach my $method (@DBI_METHODS) {
+ my $symbol = '&' . $method;
+ $stash->add_symbol(
+ $symbol => sub {
+ my $self = shift;
+ return $self->dbh->$method(@_);
+ }
);
- my $stash = Package::Stash->new(__PACKAGE__);
-
- foreach my $method (@DBI_METHODS) {
- my $symbol = '&' . $method;
- $stash->add_symbol(
- $symbol => sub {
- my $self = shift;
- return $self->dbh->$method(@_);
- }
- );
- }
+ }
}
#####################################################################
# 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
@@ -81,14 +75,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,
@@ -122,10 +116,10 @@ use constant INDEX_DROPS_REQUIRE_FK_DROPS => 1;
#####################################################################
sub quote {
- my $self = shift;
- my $retval = $self->dbh->quote(@_);
- trick_taint($retval) if defined $retval;
- return $retval;
+ my $self = shift;
+ my $retval = $self->dbh->quote(@_);
+ trick_taint($retval) if defined $retval;
+ return $retval;
}
#####################################################################
@@ -133,203 +127,210 @@ sub quote {
#####################################################################
sub connect_shadow {
- state $shadow_dbh;
- if ($shadow_dbh && $shadow_dbh->bz_in_transaction) {
- FATAL("Somehow in a transaction at connection time");
- $shadow_dbh->bz_rollback_transaction();
- }
- return $shadow_dbh if $shadow_dbh;
- my $params = Bugzilla->params;
- die "Tried to connect to non-existent shadowdb"
- unless Bugzilla->get_param_with_override('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 = dclone(Bugzilla->localconfig);
- $connect_params->{db_host} = Bugzilla->get_param_with_override('shadowdbhost');
- $connect_params->{db_name} = Bugzilla->get_param_with_override('shadowdb');
- $connect_params->{db_port} = Bugzilla->get_param_with_override('shadowdbport');
- $connect_params->{db_sock} = Bugzilla->get_param_with_override('shadowdbsock');
-
- if ( Bugzilla->localconfig->{'shadowdb_user'} && Bugzilla->localconfig->{'shadowdb_pass'} ) {
- $connect_params->{db_user} = Bugzilla->localconfig->{'shadowdb_user'};
- $connect_params->{db_pass} = Bugzilla->localconfig->{'shadowdb_pass'};
- }
- return $shadow_dbh = _connect($connect_params);
+ state $shadow_dbh;
+ if ($shadow_dbh && $shadow_dbh->bz_in_transaction) {
+ FATAL("Somehow in a transaction at connection time");
+ $shadow_dbh->bz_rollback_transaction();
+ }
+ return $shadow_dbh if $shadow_dbh;
+ my $params = Bugzilla->params;
+ die "Tried to connect to non-existent shadowdb"
+ unless Bugzilla->get_param_with_override('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 = dclone(Bugzilla->localconfig);
+ $connect_params->{db_host} = Bugzilla->get_param_with_override('shadowdbhost');
+ $connect_params->{db_name} = Bugzilla->get_param_with_override('shadowdb');
+ $connect_params->{db_port} = Bugzilla->get_param_with_override('shadowdbport');
+ $connect_params->{db_sock} = Bugzilla->get_param_with_override('shadowdbsock');
+
+ if ( Bugzilla->localconfig->{'shadowdb_user'}
+ && Bugzilla->localconfig->{'shadowdb_pass'})
+ {
+ $connect_params->{db_user} = Bugzilla->localconfig->{'shadowdb_user'};
+ $connect_params->{db_pass} = Bugzilla->localconfig->{'shadowdb_pass'};
+ }
+ return $shadow_dbh = _connect($connect_params);
}
sub connect_main {
- state $main_dbh = _connect(Bugzilla->localconfig);
- if ($main_dbh->bz_in_transaction) {
- FATAL("Somehow in a transaction at connection time");
- $main_dbh->bz_rollback_transaction();
- }
- return $main_dbh;
+ state $main_dbh = _connect(Bugzilla->localconfig);
+ if ($main_dbh->bz_in_transaction) {
+ FATAL("Somehow in a transaction at connection time");
+ $main_dbh->bz_rollback_transaction();
+ }
+ return $main_dbh;
}
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_module($pkg_module) }
- || die ("'$driver' is not a valid choice for \$db_driver in "
- . " localconfig: " . $@);
+ # do the actual import
+ eval { require_module($pkg_module) }
+ || die(
+ "'$driver' is not a valid choice for \$db_driver in " . " localconfig: " . $@);
- # instantiate the correct DB specific module
+ # instantiate the correct DB specific module
- return $pkg_module->new($params);
+ return $pkg_module->new($params);
}
sub _handle_error {
- require Carp;
-
- # Cut down the error string to a reasonable size
- $_[0] = substr($_[0], 0, 2000) . ' ... ' . substr($_[0], -2000)
- if length($_[0]) > 4000;
- # BMO: stracktrace disabled:
- # $_[0] = Carp::longmess($_[0]);
-
- # BMO: catch long running query timeouts and translate into a sane message
- #if ($_[0] =~ /Lost connection to MySQL server during query/) {
- # warn(Carp::longmess($_[0]));
- # $_[0] = "The database query took too long to complete and has been canceled.\n"
- # . "(Lost connection to MySQL server during query)";
- #}
-
- #if (Bugzilla->usage_mode == USAGE_MODE_BROWSER) {
- # ThrowCodeError("db_error", { err_message => $_[0] });
- #}
-
- # keep tests happy
- if (0) {
- ThrowCodeError("db_error", { err_message => $_[0] });
- }
+ require Carp;
+
+ # Cut down the error string to a reasonable size
+ $_[0] = substr($_[0], 0, 2000) . ' ... ' . substr($_[0], -2000)
+ if length($_[0]) > 4000;
+
+ # BMO: stracktrace disabled:
+ # $_[0] = Carp::longmess($_[0]);
+
+# BMO: catch long running query timeouts and translate into a sane message
+#if ($_[0] =~ /Lost connection to MySQL server during query/) {
+# warn(Carp::longmess($_[0]));
+# $_[0] = "The database query took too long to complete and has been canceled.\n"
+# . "(Lost connection to MySQL server during query)";
+#}
+
+ #if (Bugzilla->usage_mode == USAGE_MODE_BROWSER) {
+ # ThrowCodeError("db_error", { err_message => $_[0] });
+ #}
+
+ # keep tests happy
+ if (0) {
+ ThrowCodeError("db_error", {err_message => $_[0]});
+ }
- 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;
+ my $sql_vers = $self->bz_server_version;
- 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();
- print "Creating database $db_name...\n";
-
- # 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();
+ print "Creating database $db_name...\n";
+
+ # 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();
}
+ }
}
# A helper for bz_create_database and bz_check_requirements.
sub _get_no_db_connection {
- my ($sql_server) = @_;
- my $dbh;
- my %connect_params = %{ Bugzilla->localconfig };
- $connect_params{db_name} = '';
- my $conn_success = eval {
- $dbh = _connect(\%connect_params);
- };
- if (!$conn_success) {
- my $driver = $connect_params{db_driver};
- my $sql_server = DB_MODULE->{lc($driver)}->{name};
- # Can't use $dbh->errstr because $dbh is undef.
- my $error = $DBI::errstr || $@;
- chomp($error);
- die "There was an error connecting to $sql_server:\n\n",
- " $error\n\n", _bz_connect_error_reasons(), "\n";
- }
- return $dbh;
+ my ($sql_server) = @_;
+ my $dbh;
+ my %connect_params = %{Bugzilla->localconfig};
+ $connect_params{db_name} = '';
+ my $conn_success = eval { $dbh = _connect(\%connect_params); };
+ if (!$conn_success) {
+ my $driver = $connect_params{db_driver};
+ my $sql_server = DB_MODULE->{lc($driver)}->{name};
+
+ # Can't use $dbh->errstr because $dbh is undef.
+ my $error = $DBI::errstr || $@;
+ chomp($error);
+ die "There was an error connecting to $sql_server:\n\n", " $error\n\n",
+ _bz_connect_error_reasons(), "\n";
+ }
+ return $dbh;
}
# Just a helper because we have to re-use this text.
# We don't use this in db_new because it gives away the database
# username, and db_new errors can show up on CGIs.
sub _bz_connect_error_reasons {
- my $lc_file = bz_locations()->{'localconfig'};
- my $lc = Bugzilla->localconfig;
- my $db = DB_MODULE->{lc($lc->{db_driver})};
- my $server = $db->{name};
+ my $lc_file = bz_locations()->{'localconfig'};
+ my $lc = Bugzilla->localconfig;
+ my $db = DB_MODULE->{lc($lc->{db_driver})};
+ my $server = $db->{name};
-return <<EOT;
+ return <<EOT;
This might have several reasons:
* $server is not running.
@@ -348,137 +349,140 @@ 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_prefix_match {
- my ($self, $column, $str) = @_;
- my $must_escape = $str =~ s/([_%!])/!$1/g;
- my $escape = $must_escape ? q/ESCAPE '!'/ : '';
- my $quoted_str = $self->quote("$str%");
- return "$column LIKE $quoted_str $escape";
+ my ($self, $column, $str) = @_;
+ my $must_escape = $str =~ s/([_%!])/!$1/g;
+ my $escape = $must_escape ? q/ESCAPE '!'/ : '';
+ my $quoted_str = $self->quote("$str%");
+ return "$column LIKE $quoted_str $escape";
}
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_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) = @_;
+ my ($self, @params) = @_;
- return '(' . join(' || ', @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";
}
#####################################################################
@@ -487,24 +491,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});
}
#####################################################################
@@ -512,99 +519,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) {
- print install_string('db_table_setup'), "\n";
- }
+ 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) {
+ print install_string('db_table_setup'), "\n";
+ }
+
+ 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) {
- print get_text('install_fk_setup'), "\n";
- }
+ 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) {
+ print get_text('install_fk_setup'), "\n";
+ }
+
+ 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);
}
+ }
}
#####################################################################
@@ -612,119 +620,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;
+ }
}
@@ -750,39 +760,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);
- print "Updating column $name in table $table ...\n";
- if (defined $current_def) {
- my $old_ddl = $self->_bz_schema->get_type_ddl($current_def);
- print "Old: $old_ddl\n";
- }
- print "New: $new_ddl\n";
- $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);
+ print "Updating column $name in table $table ...\n";
+ if (defined $current_def) {
+ my $old_ddl = $self->_bz_schema->get_type_ddl($current_def);
+ print "Old: $old_ddl\n";
+ }
+ print "New: $new_ddl\n";
+ $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)
@@ -802,36 +813,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;
- }
+ 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));
- $self->_bz_real_schema->add_table($name, $table_def);
- $self->_bz_store_real_schema;
+ 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
@@ -849,158 +860,168 @@ sub bz_add_table {
# 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})
- {
- print install_string('db_table_new', { table => $name }), "\n";
- }
- $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}) {
+ print install_string('db_table_new', {table => $name}), "\n";
+ }
+ $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) = @_;
-sub bz_add_field_tables {
- my ($self, $field) = @_;
+ # We do nothing if the table already exists.
+ return if $self->bz_table_info($name);
- $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);
+ # 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};
+ }
- $self->bz_add_fks($ms_table,
- { bug_id => {TABLE => 'bugs', COLUMN => 'bug_id',
- DELETE => 'CASCADE'},
+ # INDEXES is supposed to be an arrayref, so we have to convert back.
+ my @indexes_array = %fixed_indexes;
+ $table_schema{INDEXES} = \@indexes_array;
- value => {TABLE => $field->name, COLUMN => 'value'} });
- }
+ # 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'}
+ }
+ );
+ }
}
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)
@@ -1020,108 +1041,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;
}
#####################################################################
@@ -1129,12 +1153,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()
@@ -1148,53 +1172,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();
}
#####################################################################
@@ -1213,9 +1238,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()
@@ -1225,9 +1250,9 @@ sub bz_table_columns_real {
# Params: none
# Returns: An array containing table names.
sub bz_table_list_real {
- my ($self) = @_;
- my $table_sth = $self->table_info(undef, undef, undef, "TABLE");
- return @{$self->selectcol_arrayref($table_sth, { Columns => [3] })};
+ my ($self) = @_;
+ my $table_sth = $self->table_info(undef, undef, undef, "TABLE");
+ return @{$self->selectcol_arrayref($table_sth, {Columns => [3]})};
}
#####################################################################
@@ -1235,54 +1260,58 @@ sub bz_table_list_real {
#####################################################################
sub bz_in_transaction {
- return $_[0]->{private_bz_transaction_count} ? 1 : 0;
+ return $_[0]->{private_bz_transaction_count} ? 1 : 0;
}
sub bz_start_transaction {
- my ($self) = @_;
-
- if ($self->bz_in_transaction) {
- $self->{private_bz_transaction_count}++;
- } else {
- # Turn AutoCommit off and start a new transaction
- $self->begin_work();
- # REPEATABLE READ means "We work on a snapshot of the DB that
- # is created when we execute our first SQL statement." It's
- # what we need in Bugzilla to be safe, for what we do.
- # Different DBs have different defaults for their isolation
- # level, so we just set it here manually.
- if ($self->ISOLATION_LEVEL) {
- $self->do('SET TRANSACTION ISOLATION LEVEL '
- . $self->ISOLATION_LEVEL);
- }
- $self->{private_bz_transaction_count} = 1;
+ my ($self) = @_;
+
+ if ($self->bz_in_transaction) {
+ $self->{private_bz_transaction_count}++;
+ }
+ else {
+ # Turn AutoCommit off and start a new transaction
+ $self->begin_work();
+
+ # REPEATABLE READ means "We work on a snapshot of the DB that
+ # is created when we execute our first SQL statement." It's
+ # what we need in Bugzilla to be safe, for what we do.
+ # Different DBs have different defaults for their isolation
+ # level, so we just set it here manually.
+ if ($self->ISOLATION_LEVEL) {
+ $self->do('SET TRANSACTION ISOLATION LEVEL ' . $self->ISOLATION_LEVEL);
}
+ $self->{private_bz_transaction_count} = 1;
+ }
}
sub bz_commit_transaction {
- my ($self) = @_;
-
- if ($self->{private_bz_transaction_count} > 1) {
- $self->{private_bz_transaction_count}--;
- } elsif ($self->bz_in_transaction) {
- $self->commit();
- $self->{private_bz_transaction_count} = 0;
- } 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;
+ }
+ 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;
+ }
}
#####################################################################
@@ -1290,43 +1319,45 @@ sub bz_rollback_transaction {
#####################################################################
sub _build_connector {
- my ($self) = @_;
- my ($dsn, $user, $pass, $override_attrs) =
- map { $self->$_ } 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 => 1,
- AutoCommit => 1,
- PrintError => 0,
- ShowErrorStatement => 1,
- HandleError => \&_handle_error,
- TaintIn => 1,
- FetchHashKeyName => 'NAME',
- # Note: NAME_lc causes crash on ActiveState Perl
- # 5.8.4 (see Bug 253696)
- # XXX - This will likely cause problems in DB
- # back ends that twiddle column case (Oracle?)
- };
-
- if ($override_attrs) {
- foreach my $key (keys %$override_attrs) {
- $attributes->{$key} = $override_attrs->{$key};
- }
+ my ($self) = @_;
+ my ($dsn, $user, $pass, $override_attrs)
+ = map { $self->$_ } 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 => 1,
+ AutoCommit => 1,
+ PrintError => 0,
+ ShowErrorStatement => 1,
+ HandleError => \&_handle_error,
+ TaintIn => 1,
+ FetchHashKeyName => 'NAME',
+
+ # Note: NAME_lc causes crash on ActiveState Perl
+ # 5.8.4 (see Bug 253696)
+ # XXX - This will likely cause problems in DB
+ # back ends that twiddle column case (Oracle?)
+ };
+
+ if ($override_attrs) {
+ foreach my $key (keys %$override_attrs) {
+ $attributes->{$key} = $override_attrs->{$key};
}
- my $class = ref $self;
- weaken($self);
- $attributes->{Callbacks} = {
- connected => sub {
- my ($dbh, $dsn) = @_;
- INFO("$PROGRAM_NAME connected mysql $dsn");
- ThrowCodeError('not_in_transaction') if $self && $self->bz_in_transaction;
- $class->on_dbi_connected(@_) if $class->can('on_dbi_connected');
- return
- },
- };
-
- return DBIx::Connector->new($dsn, $user, $pass, $attributes);
+ }
+ my $class = ref $self;
+ weaken($self);
+ $attributes->{Callbacks} = {
+ connected => sub {
+ my ($dbh, $dsn) = @_;
+ INFO("$PROGRAM_NAME connected mysql $dsn");
+ ThrowCodeError('not_in_transaction') if $self && $self->bz_in_transaction;
+ $class->on_dbi_connected(@_) if $class->can('on_dbi_connected');
+ return;
+ },
+ };
+
+ return DBIx::Connector->new($dsn, $user, $pass, $attributes);
}
#####################################################################
@@ -1350,55 +1381,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');
- }
+ print install_string('db_schema_init'), "\n";
+ 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();
- print install_string('db_schema_init'), "\n";
- 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()>
@@ -1412,24 +1442,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()>
@@ -1449,106 +1478,135 @@ sub _bz_real_schema {
=cut
sub _bz_store_real_schema {
- my ($self) = @_;
-
- # Make sure that there's a schema to update
- my $table_size = $self->selectrow_array("SELECT COUNT(*) FROM bz_schema");
-
- die "Attempted to update the bz_schema table but there's nothing "
- . "there to update. Run checksetup." unless $table_size;
-
- # We want to store the current object, not one
- # that we read from the database. So we use the actual hash
- # member instead of the subroutine call. If the hash
- # member is not defined, we will (and should) fail.
- my $update_schema = $self->{private_real_schema};
- my $store_me = $update_schema->serialize_abstract();
- my $schema_version = $update_schema->SCHEMA_VERSION;
- my $sth = $self->prepare("UPDATE bz_schema
- SET schema_data = ?, version = ?");
- $sth->bind_param(1, $store_me, $self->BLOB_TYPE);
- $sth->bind_param(2, $schema_version);
- $sth->execute();
+ my ($self) = @_;
+
+ # Make sure that there's a schema to update
+ my $table_size = $self->selectrow_array("SELECT COUNT(*) FROM bz_schema");
- Bugzilla->memcached->clear({ key => 'bz_schema' });
+ die "Attempted to update the bz_schema table but there's nothing "
+ . "there to update. Run checksetup."
+ unless $table_size;
+
+ # We want to store the current object, not one
+ # that we read from the database. So we use the actual hash
+ # member instead of the subroutine call. If the hash
+ # member is not defined, we will (and should) fail.
+ my $update_schema = $self->{private_real_schema};
+ my $store_me = $update_schema->serialize_abstract();
+ my $schema_version = $update_schema->SCHEMA_VERSION;
+ my $sth = $self->prepare(
+ "UPDATE bz_schema
+ SET schema_data = ?, version = ?"
+ );
+ $sth->bind_param(1, $store_me, $self->BLOB_TYPE);
+ $sth->bind_param(2, $schema_version);
+ $sth->execute();
+
+ Bugzilla->memcached->clear({key => 'bz_schema'});
}
# For bz_populate_enum_tables
sub _bz_populate_enum_table {
- my ($self, $table, $valuelist) = @_;
-
- my $sql_table = $self->quote_identifier($table);
-
- # Check if there are any table entries
- my $table_size = $self->selectrow_array("SELECT COUNT(*) FROM $sql_table");
-
- # If the table is empty...
- if (!$table_size) {
- print " $table";
- my $insert = $self->prepare(
- "INSERT INTO $sql_table (value,sortkey) VALUES (?,?)");
- my $sortorder = 0;
- my $maxlen = max(map(length($_), @$valuelist)) + 2;
- foreach my $value (@$valuelist) {
- $sortorder += 100;
- $insert->execute($value, $sortorder);
- }
+ my ($self, $table, $valuelist) = @_;
+
+ my $sql_table = $self->quote_identifier($table);
+
+ # Check if there are any table entries
+ my $table_size = $self->selectrow_array("SELECT COUNT(*) FROM $sql_table");
+
+ # If the table is empty...
+ if (!$table_size) {
+ print " $table";
+ my $insert
+ = $self->prepare("INSERT INTO $sql_table (value,sortkey) VALUES (?,?)");
+ my $sortorder = 0;
+ my $maxlen = max(map(length($_), @$valuelist)) + 2;
+ foreach my $value (@$valuelist) {
+ $sortorder += 100;
+ $insert->execute($value, $sortorder);
}
+ }
}
# This is used before adding a foreign key to a column, to make sure
# that the database won't fail adding the key.
sub _check_references {
- my ($self, $table, $column, $fk) = @_;
- my $foreign_table = $fk->{TABLE};
- my $foreign_column = $fk->{COLUMN};
-
- # We use table aliases because sometimes we join a table to itself,
- # and we can't use the same table name on both sides of the join.
- # We also can't use the words "table" or "foreign" because those are
- # reserved words.
- my $bad_values = $self->selectcol_arrayref(
- "SELECT DISTINCT tabl.$column
+ my ($self, $table, $column, $fk) = @_;
+ my $foreign_table = $fk->{TABLE};
+ my $foreign_column = $fk->{COLUMN};
+
+ # We use table aliases because sometimes we join a table to itself,
+ # and we can't use the same table name on both sides of the join.
+ # We also can't use the words "table" or "foreign" because those are
+ # reserved words.
+ my $bad_values = $self->selectcol_arrayref(
+ "SELECT DISTINCT tabl.$column
FROM $table AS tabl LEFT JOIN $foreign_table AS forn
ON tabl.$column = forn.$foreign_column
WHERE forn.$foreign_column IS NULL
- AND tabl.$column IS NOT NULL");
-
- if (@$bad_values) {
- my $delete_action = $fk->{DELETE} || '';
- if ($delete_action eq 'CASCADE') {
- $self->do("DELETE FROM $table WHERE $column IN ("
- . join(',', ('?') x @$bad_values) . ")",
- undef, @$bad_values);
- if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) {
- print "\n", get_text('install_fk_invalid_fixed',
- { table => $table, column => $column,
- foreign_table => $foreign_table,
- foreign_column => $foreign_column,
- 'values' => $bad_values, action => 'delete' }), "\n";
- }
- }
- elsif ($delete_action eq 'SET NULL') {
- $self->do("UPDATE $table SET $column = NULL
+ AND tabl.$column IS NOT NULL"
+ );
+
+ if (@$bad_values) {
+ my $delete_action = $fk->{DELETE} || '';
+ if ($delete_action eq 'CASCADE') {
+ $self->do(
+ "DELETE FROM $table WHERE $column IN (" . join(',', ('?') x @$bad_values) . ")",
+ undef, @$bad_values
+ );
+ if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) {
+ print "\n",
+ get_text(
+ 'install_fk_invalid_fixed',
+ {
+ table => $table,
+ column => $column,
+ foreign_table => $foreign_table,
+ foreign_column => $foreign_column,
+ 'values' => $bad_values,
+ action => 'delete'
+ }
+ ),
+ "\n";
+ }
+ }
+ elsif ($delete_action eq 'SET NULL') {
+ $self->do(
+ "UPDATE $table SET $column = NULL
WHERE $column IN ("
- . join(',', ('?') x @$bad_values) . ")",
- undef, @$bad_values);
- if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) {
- print "\n", get_text('install_fk_invalid_fixed',
- { table => $table, column => $column,
- foreign_table => $foreign_table,
- foreign_column => $foreign_column,
- 'values' => $bad_values, action => 'null' }), "\n";
- }
- }
- else {
- die "\n", get_text('install_fk_invalid',
- { table => $table, column => $column,
- foreign_table => $foreign_table,
- foreign_column => $foreign_column,
- 'values' => $bad_values }), "\n";
+ . join(',', ('?') x @$bad_values) . ")", undef, @$bad_values
+ );
+ if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) {
+ print "\n",
+ get_text(
+ 'install_fk_invalid_fixed',
+ {
+ table => $table,
+ column => $column,
+ foreign_table => $foreign_table,
+ foreign_column => $foreign_column,
+ 'values' => $bad_values,
+ action => 'null'
+ }
+ ),
+ "\n";
+ }
+ }
+ else {
+ die "\n",
+ get_text(
+ 'install_fk_invalid',
+ {
+ table => $table,
+ column => $column,
+ foreign_table => $foreign_table,
+ foreign_column => $foreign_column,
+ 'values' => $bad_values
}
+ ),
+ "\n";
}
+ }
}
1;
diff --git a/Bugzilla/DB/Mysql.pm b/Bugzilla/DB/Mysql.pm
index 4dd2620d3..640cf89ec 100644
--- a/Bugzilla/DB/Mysql.pm
+++ b/Bugzilla/DB/Mysql.pm
@@ -44,224 +44,231 @@ use constant MAX_COMMENTS => 50;
use constant FULLTEXT_OR => '|';
sub BUILDARGS {
- my ($class, $params) = @_;
- my ($user, $pass, $host, $dbname, $port, $sock) =
- @$params{qw(db_user db_pass db_host db_name db_port db_sock)};
+ 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;
+ # 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 );
+ my %attrs = (mysql_enable_utf8 => 1);
- return { dsn => $dsn, user => $user, pass => $pass, attrs => \%attrs };
+ return {dsn => $dsn, user => $user, pass => $pass, attrs => \%attrs};
}
sub on_dbi_connected {
- my ($class, $dbh) = @_;
-
- # This makes sure that if the tables are encoded as UTF-8, we
- # return their data correctly.
- my $charset = $class->utf8_charset;
- my $collate = $class->utf8_collate;
- $dbh->do("SET NAMES $charset COLLATE $collate");
-
- # Bug 321645 - disable MySQL strict mode, if set
- my ($var, $sql_mode) = $dbh->selectrow_array(
- "SHOW VARIABLES LIKE 'sql\\_mode'");
-
- if ($sql_mode) {
- # STRICT_TRANS_TABLE or STRICT_ALL_TABLES enable MySQL strict mode,
- # causing bug 321645. TRADITIONAL sets these modes (among others) as
- # well, so it has to be stipped as well
- my $new_sql_mode =
- join(",", grep {$_ !~ /^STRICT_(?:TRANS|ALL)_TABLES|TRADITIONAL$/}
- split(/,/, $sql_mode));
-
- if ($sql_mode ne $new_sql_mode) {
- $dbh->do("SET SESSION sql_mode = ?", undef, $new_sql_mode);
- }
+ my ($class, $dbh) = @_;
+
+ # This makes sure that if the tables are encoded as UTF-8, we
+ # return their data correctly.
+ my $charset = $class->utf8_charset;
+ my $collate = $class->utf8_collate;
+ $dbh->do("SET NAMES $charset COLLATE $collate");
+
+ # Bug 321645 - disable MySQL strict mode, if set
+ my ($var, $sql_mode)
+ = $dbh->selectrow_array("SHOW VARIABLES LIKE 'sql\\_mode'");
+
+ if ($sql_mode) {
+
+ # STRICT_TRANS_TABLE or STRICT_ALL_TABLES enable MySQL strict mode,
+ # causing bug 321645. TRADITIONAL sets these modes (among others) as
+ # well, so it has to be stipped as well
+ my $new_sql_mode = join(",",
+ grep { $_ !~ /^STRICT_(?:TRANS|ALL)_TABLES|TRADITIONAL$/ }
+ split(/,/, $sql_mode));
+
+ if ($sql_mode ne $new_sql_mode) {
+ $dbh->do("SET SESSION sql_mode = ?", undef, $new_sql_mode);
}
+ }
- # Allow large GROUP_CONCATs (largely for inserting comments
- # into bugs_fulltext).
- $dbh->do('SET SESSION group_concat_max_len = 128000000');
+ # Allow large GROUP_CONCATs (largely for inserting comments
+ # into bugs_fulltext).
+ $dbh->do('SET SESSION group_concat_max_len = 128000000');
}
# 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) = @_;
- $separator = $self->quote(', ') if !defined $separator;
- $sort = 1 if !defined $sort;
- if ($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) = @_;
+ $separator = $self->quote(', ') if !defined $separator;
+ $sort = 1 if !defined $sort;
+ if ($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) = @_;
+ my ($self, @params) = @_;
- return 'CONCAT(' . join(', ', @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 = grep { defined } 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 . '"';
- # match words that contain only boolean operators
- } elsif ($word =~ /^[\+\-\<\>\~\*]+$/) {
- $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 = grep {defined} 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 . '"';
+
+ # match words that contain only boolean operators
}
- $text = join(FULLTEXT_OR, @terms);
+ elsif ($word =~ /^[\+\-\<\>\~\*]+$/) {
+ $word = '"' . $word . '"';
+ }
+ }
+ $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) = @_;
+ my ($self, $string) = @_;
- return $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) = @_;
+ my ($self, $date, $operator, $interval, $units) = @_;
- return "$date $operator INTERVAL $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();
}
#####################################################################
@@ -269,435 +276,451 @@ sub _bz_get_initial_schema {
#####################################################################
sub bz_check_server_version {
- my $self = shift;
+ my $self = shift;
- my $lc = Bugzilla->localconfig;
- if (lc(Bugzilla->localconfig->{db_name}) eq 'mysql') {
- die "It is not safe to run Bugzilla inside a database named 'mysql'.\n"
- . " Please pick a different value for \$db_name in localconfig.\n";
- }
+ my $lc = Bugzilla->localconfig;
+ if (lc(Bugzilla->localconfig->{db_name}) eq 'mysql') {
+ die "It is not safe to run Bugzilla inside a database named 'mysql'.\n"
+ . " Please pick a different value for \$db_name in localconfig.\n";
+ }
- $self->SUPER::bz_check_server_version(@_);
+ $self->SUPER::bz_check_server_version(@_);
}
sub bz_setup_database {
- my ($self) = @_;
-
- # The "comments" field of the bugs_fulltext table could easily exceed
- # MySQL's default max_allowed_packet. Also, MySQL should never have
- # a max_allowed_packet smaller than our max_attachment_size. So, we
- # warn the user here if max_allowed_packet is too small.
- my $min_max_allowed = MAX_COMMENTS * MAX_COMMENT_LENGTH;
- my (undef, $current_max_allowed) = $self->selectrow_array(
- q{SHOW VARIABLES LIKE 'max\_allowed\_packet'});
- # This parameter is not yet defined when the DB is being built for
- # the very first time. The code below still works properly, however,
- # because the default maxattachmentsize is smaller than $min_max_allowed.
- my $max_attachment = (Bugzilla->params->{'maxattachmentsize'} || 0) * 1024;
- my $needed_max_allowed = max($min_max_allowed, $max_attachment);
- if ($current_max_allowed < $needed_max_allowed) {
- warn install_string('max_allowed_packet',
- { current => $current_max_allowed,
- needed => $needed_max_allowed }) . "\n";
+ my ($self) = @_;
+
+ # The "comments" field of the bugs_fulltext table could easily exceed
+ # MySQL's default max_allowed_packet. Also, MySQL should never have
+ # a max_allowed_packet smaller than our max_attachment_size. So, we
+ # warn the user here if max_allowed_packet is too small.
+ my $min_max_allowed = MAX_COMMENTS * MAX_COMMENT_LENGTH;
+ my (undef, $current_max_allowed)
+ = $self->selectrow_array(q{SHOW VARIABLES LIKE 'max\_allowed\_packet'});
+
+ # This parameter is not yet defined when the DB is being built for
+ # the very first time. The code below still works properly, however,
+ # because the default maxattachmentsize is smaller than $min_max_allowed.
+ my $max_attachment = (Bugzilla->params->{'maxattachmentsize'} || 0) * 1024;
+ my $needed_max_allowed = max($min_max_allowed, $max_attachment);
+ if ($current_max_allowed < $needed_max_allowed) {
+ warn install_string('max_allowed_packet',
+ {current => $current_max_allowed, needed => $needed_max_allowed})
+ . "\n";
+ }
+
+ # Make sure the installation has InnoDB turned on, or we're going to be
+ # doing silly things like making foreign keys on MyISAM tables, which is
+ # hard to fix later. We do this up here because none of the code below
+ # works if InnoDB is off. (Particularly if we've already converted the
+ # tables to InnoDB.)
+ my %engines = @{$self->selectcol_arrayref('SHOW ENGINES', {Columns => [1, 2]})};
+ if (!$engines{InnoDB} || $engines{InnoDB} !~ /^(YES|DEFAULT)$/) {
+ die install_string('mysql_innodb_disabled');
+ }
+
+ if ($self->utf8_charset eq 'utf8mb4') {
+ my %global = map {@$_}
+ @{$self->selectall_arrayref(q(SHOW GLOBAL VARIABLES LIKE 'innodb_%'))};
+ my $utf8mb4_supported
+ = $global{innodb_file_format} eq 'Barracuda'
+ && $global{innodb_file_per_table} eq 'ON'
+ && $global{innodb_large_prefix} eq 'ON';
+
+ die install_string('mysql_innodb_settings') unless $utf8mb4_supported;
+
+ my $tables = $self->selectall_arrayref('SHOW TABLE STATUS');
+ foreach my $table (@$tables) {
+ my ($table, undef, undef, $row_format) = @$table;
+ my $new_row_format = $self->default_row_format($table);
+ next if $new_row_format =~ /compact/i;
+ if (lc($new_row_format) ne lc($row_format)) {
+ print install_string(
+ 'mysql_row_format_conversion', {table => $table, format => $new_row_format}
+ ),
+ "\n";
+ $self->do(sprintf 'ALTER TABLE %s ROW_FORMAT=%s', $table, $new_row_format);
+ }
}
+ }
- # 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();
- if ($self->utf8_charset eq 'utf8mb4') {
- my %global = map { @$_ } @{ $self->selectall_arrayref(q(SHOW GLOBAL VARIABLES LIKE 'innodb_%')) };
- my $utf8mb4_supported
- = $global{innodb_file_format} eq 'Barracuda'
- && $global{innodb_file_per_table} eq 'ON'
- && $global{innodb_large_prefix} eq 'ON';
-
- die install_string('mysql_innodb_settings') unless $utf8mb4_supported;
-
- my $tables = $self->selectall_arrayref('SHOW TABLE STATUS');
- foreach my $table (@$tables) {
- my ($table, undef, undef, $row_format) = @$table;
- my $new_row_format = $self->default_row_format($table);
- next if $new_row_format =~ /compact/i;
- if (lc($new_row_format) ne lc($row_format)) {
- print install_string('mysql_row_format_conversion', { table => $table, format => $new_row_format }), "\n";
- $self->do(sprintf 'ALTER TABLE %s ROW_FORMAT=%s', $table, $new_row_format);
- }
- }
+ # 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');
}
-
- 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 ($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 ($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 (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');
}
-
- # 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');
-
- 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 ($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'
+ );
+
+ 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 the utf8 parameter is true or there are no params at all.
- # This kind of situation happens when people create the database
- # themselves, and if we don't do this they will get the big
- # scary WARNING statement about conversion to UTF8.
- unless ( $self->bz_db_is_utf8 ) {
- $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;
}
- # 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");
+ # 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');
}
- # Convert the database to UTF-8 if the utf8 parameter is on.
- # We check if any table isn't utf8, because lots of crazy
- # partial-conversion situations can happen, and this handles anything
- # that could come up (including having the DB charset be utf8 but not
- # the table charsets.
- #
- # TABLE_COLLATION IS NOT NULL prevents us from trying to convert views.
- my $charset = $self->utf8_charset;
- my $collate = $self->utf8_collate;
- my $non_utf8_tables = $self->selectrow_array(
- "SELECT 1 FROM information_schema.TABLES
+ # Go through all the tables.
+ foreach my $table (@tables) {
+
+ # Will contain the names of old indexes as keys, and the
+ # definition of the new indexes as a value. The values
+ # include an extra hash key, NAME, with the new name of
+ # the index.
+ my %rename_indexes;
+
+ # And go through all the columns on each table.
+ my @columns = $self->bz_table_columns_real($table);
+
+ # We also want to fix the silly naming of unique indexes
+ # that happened when we first checked-in Bugzilla::DB::Schema.
+ if ($table eq 'series_categories') {
+
+ # The series_categories index had a nonstandard name.
+ push(@columns, 'series_cats_unique_idx');
+ }
+ elsif ($table eq 'email_setting') {
+
+ # The email_setting table had a similar problem.
+ push(@columns, 'email_settings_unique_idx');
+ }
+ else {
+ push(@columns, "${table}_unique_idx");
+ }
+
+ # And this is how we fix the other inconsistent Schema naming.
+ push(@columns, @{$bad_names->{$table}}) if (exists $bad_names->{$table});
+ foreach my $column (@columns) {
+
+ # If we have an index named after this column, it's an
+ # old-style-name index.
+ if (my $index = $self->bz_index_info_real($table, $column)) {
+
+ # Fix the name to fit in with the new naming scheme.
+ $index->{NAME} = $table . "_" . $index->{FIELDS}->[0] . "_idx";
+ print "Renaming index $column to " . $index->{NAME} . "...\n";
+ $rename_indexes{$column} = $index;
+ } # if
+ } # foreach column
+
+ my @rename_sql
+ = $self->_bz_schema->get_rename_indexes_ddl($table, %rename_indexes);
+ $self->do($_) foreach (@rename_sql);
+
+ } # foreach table
+ } # if old-name indexes
+
+ # If there are no tables, but the DB isn't utf8 and it should be,
+ # then we should alter the database to be utf8. We know it should be
+ # if the utf8 parameter is true or there are no params at all.
+ # This kind of situation happens when people create the database
+ # themselves, and if we don't do this they will get the big
+ # scary WARNING statement about conversion to UTF8.
+ unless ($self->bz_db_is_utf8) {
+ $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;
+ }
+
+ # 2005-09-24 - bugreport@peshkin.net, bug 307602
+ # Make sure that default 4G table limit is overridden
+ my $attach_data_create = $self->selectrow_array(
+ 'SELECT CREATE_OPTIONS FROM information_schema.TABLES
+ WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?', undef, $db_name, 'attach_data'
+ );
+ if ($attach_data_create !~ /MAX_ROWS/i) {
+ print "Converting attach_data maximum size to 100G...\n";
+ $self->do(
+ "ALTER TABLE attach_data
+ AVG_ROW_LENGTH=1000000,
+ MAX_ROWS=100000"
+ );
+ }
+
+ # Convert the database to UTF-8 if the utf8 parameter is on.
+ # We check if any table isn't utf8, because lots of crazy
+ # partial-conversion situations can happen, and this handles anything
+ # that could come up (including having the DB charset be utf8 but not
+ # the table charsets.
+ #
+ # TABLE_COLLATION IS NOT NULL prevents us from trying to convert views.
+ my $charset = $self->utf8_charset;
+ my $collate = $self->utf8_collate;
+ 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 != ?
- LIMIT 1", undef, $db_name, $collate);
-
- if (Bugzilla->params->{'utf8'} && $non_utf8_tables) {
- print "\n", install_string('mysql_utf8_conversion');
-
- if (!Bugzilla->installation_answers->{NO_PAUSE}) {
- if (Bugzilla->installation_mode ==
- INSTALLATION_MODE_NON_INTERACTIVE)
- {
- die install_string('continue_without_answers'), "\n";
- }
- else {
- print "\n " . install_string('enter_or_ctrl_c');
- getc;
- }
- }
-
- print "Converting table storage format to $charset (collate $collate). This may take a while.\n";
- foreach my $table ($self->bz_table_list_real) {
- my $info_sth = $self->prepare("SHOW FULL COLUMNS FROM $table");
- $info_sth->execute();
- my (@binary_sql, @utf8_sql);
- while (my $column = $info_sth->fetchrow_hashref) {
- # Our conversion code doesn't work on enum fields, but they
- # all go away later in checksetup anyway.
- next if $column->{Type} =~ /enum/i;
-
- # If this particular column isn't stored in utf-8
- if ($column->{Collation}
- && $column->{Collation} ne 'NULL'
- && $column->{Collation} ne $collate)
- {
- my $name = $column->{Field};
-
- print "$table.$name needs to be converted to $charset (collate $collate)...\n";
-
- # These will be automatically re-created at the end
- # of checksetup.
- $self->bz_drop_related_fks($table, $name);
-
- my $col_info =
- $self->bz_column_info_real($table, $name);
- # CHANGE COLUMN doesn't take PRIMARY KEY
- delete $col_info->{PRIMARYKEY};
- my $sql_def = $self->_bz_schema->get_type_ddl($col_info);
- # We don't want MySQL to actually try to *convert*
- # from our current charset to UTF-8, we just want to
- # transfer the bytes directly. This is how we do that.
-
- # The CHARACTER SET part of the definition has to come
- # right after the type, which will always come first.
- my ($binary, $utf8) = ($sql_def, $sql_def);
- my $type = $self->_bz_schema->convert_type($col_info->{TYPE});
- $binary =~ s/(\Q$type\E)/$1 CHARACTER SET binary/;
- $utf8 =~ s/(\Q$type\E)/$1 CHARACTER SET $charset COLLATE $collate/;
- push(@binary_sql, "MODIFY COLUMN $name $binary");
- push(@utf8_sql, "MODIFY COLUMN $name $utf8");
- }
- } # foreach column
-
- if (@binary_sql) {
- my %indexes = %{ $self->bz_table_indexes($table) };
- foreach my $index_name (keys %indexes) {
- my $index = $indexes{$index_name};
- if ($index->{TYPE} and $index->{TYPE} eq 'FULLTEXT') {
- $self->bz_drop_index($table, $index_name);
- }
- else {
- delete $indexes{$index_name};
- }
- }
-
- print "Converting the $table table to UTF-8...\n";
- my $bin = "ALTER TABLE $table " . join(', ', @binary_sql);
- my $utf = "ALTER TABLE $table " . join(', ', @utf8_sql,
- "DEFAULT CHARACTER SET $charset COLLATE $collate");
- $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 $charset COLLATE $collate");
- }
-
- } # foreach my $table (@tables)
+ LIMIT 1", undef, $db_name, $collate
+ );
+
+ if (Bugzilla->params->{'utf8'} && $non_utf8_tables) {
+ print "\n", install_string('mysql_utf8_conversion');
+
+ if (!Bugzilla->installation_answers->{NO_PAUSE}) {
+ if (Bugzilla->installation_mode == INSTALLATION_MODE_NON_INTERACTIVE) {
+ die install_string('continue_without_answers'), "\n";
+ }
+ else {
+ print "\n " . install_string('enter_or_ctrl_c');
+ getc;
+ }
}
- # Sometimes you can have a situation where all the tables are utf8,
- # but the database isn't. (This tends to happen when you've done
- # a mysqldump.) So we have this change outside of the above block,
- # so that it just happens silently if no actual *table* conversion
- # needs to happen.
- unless ($self->bz_db_is_utf8) {
- $self->_alter_db_charset_to_utf8();
- }
+ print
+ "Converting table storage format to $charset (collate $collate). This may take a while.\n";
+ foreach my $table ($self->bz_table_list_real) {
+ my $info_sth = $self->prepare("SHOW FULL COLUMNS FROM $table");
+ $info_sth->execute();
+ my (@binary_sql, @utf8_sql);
+ while (my $column = $info_sth->fetchrow_hashref) {
+
+ # Our conversion code doesn't work on enum fields, but they
+ # all go away later in checksetup anyway.
+ next if $column->{Type} =~ /enum/i;
+
+ # If this particular column isn't stored in utf-8
+ if ( $column->{Collation}
+ && $column->{Collation} ne 'NULL'
+ && $column->{Collation} ne $collate)
+ {
+ my $name = $column->{Field};
- $self->_fix_defaults();
+ print "$table.$name needs to be converted to $charset (collate $collate)...\n";
- # Bug 451735 highlighted a bug in bz_drop_index() which didn't
- # check for FKs before trying to delete an index. Consequently,
- # the series_creator_idx index was considered to be deleted
- # despite it was still present in the DB. That's why we have to
- # force the deletion, bypassing the DB schema.
- if (!$self->bz_index_info('series', 'series_category_idx')) {
- if (!$self->bz_index_info('series', 'series_creator_idx')
- && $self->bz_index_info_real('series', 'series_creator_idx'))
- {
- foreach my $column (qw(creator category subcategory name)) {
- $self->bz_drop_related_fks('series', $column);
- }
- $self->bz_drop_index_raw('series', 'series_creator_idx');
+ # These will be automatically re-created at the end
+ # of checksetup.
+ $self->bz_drop_related_fks($table, $name);
+
+ my $col_info = $self->bz_column_info_real($table, $name);
+
+ # CHANGE COLUMN doesn't take PRIMARY KEY
+ delete $col_info->{PRIMARYKEY};
+ my $sql_def = $self->_bz_schema->get_type_ddl($col_info);
+
+ # We don't want MySQL to actually try to *convert*
+ # from our current charset to UTF-8, we just want to
+ # transfer the bytes directly. This is how we do that.
+
+ # The CHARACTER SET part of the definition has to come
+ # right after the type, which will always come first.
+ my ($binary, $utf8) = ($sql_def, $sql_def);
+ my $type = $self->_bz_schema->convert_type($col_info->{TYPE});
+ $binary =~ s/(\Q$type\E)/$1 CHARACTER SET binary/;
+ $utf8 =~ s/(\Q$type\E)/$1 CHARACTER SET $charset COLLATE $collate/;
+ push(@binary_sql, "MODIFY COLUMN $name $binary");
+ push(@utf8_sql, "MODIFY COLUMN $name $utf8");
+ }
+ } # foreach column
+
+ if (@binary_sql) {
+ my %indexes = %{$self->bz_table_indexes($table)};
+ foreach my $index_name (keys %indexes) {
+ my $index = $indexes{$index_name};
+ if ($index->{TYPE} and $index->{TYPE} eq 'FULLTEXT') {
+ $self->bz_drop_index($table, $index_name);
+ }
+ else {
+ delete $indexes{$index_name};
+ }
+ }
+
+ print "Converting the $table table to UTF-8...\n";
+ my $bin = "ALTER TABLE $table " . join(', ', @binary_sql);
+ my $utf = "ALTER TABLE $table "
+ . join(', ', @utf8_sql, "DEFAULT CHARACTER SET $charset COLLATE $collate");
+ $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 $charset COLLATE $collate");
+ }
+
+ } # 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.
+ unless ($self->bz_db_is_utf8) {
+ $self->_alter_db_charset_to_utf8();
+ }
+
+ $self->_fix_defaults();
+
+ # Bug 451735 highlighted a bug in bz_drop_index() which didn't
+ # check for FKs before trying to delete an index. Consequently,
+ # the series_creator_idx index was considered to be deleted
+ # despite it was still present in the DB. That's why we have to
+ # force the deletion, bypassing the DB schema.
+ if (!$self->bz_index_info('series', 'series_category_idx')) {
+ if (!$self->bz_index_info('series', 'series_creator_idx')
+ && $self->bz_index_info_real('series', 'series_creator_idx'))
+ {
+ foreach my $column (qw(creator category subcategory name)) {
+ $self->bz_drop_related_fks('series', $column);
+ }
+ $self->bz_drop_index_raw('series', 'series_creator_idx');
}
+ }
}
# When you import a MySQL 3/4 mysqldump into MySQL 5, columns that
@@ -707,151 +730,160 @@ 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 utf8_charset {
- return 'utf8' unless Bugzilla->params->{'utf8'};
- return Bugzilla->params->{'utf8'} eq 'utf8mb4' ? 'utf8mb4' : 'utf8';
+ return 'utf8' unless Bugzilla->params->{'utf8'};
+ return Bugzilla->params->{'utf8'} eq 'utf8mb4' ? 'utf8mb4' : 'utf8';
}
sub utf8_collate {
- my $charset = utf8_charset();
- if ($charset eq 'utf8') {
- return 'utf8_general_ci';
- }
- elsif ($charset eq 'utf8mb4') {
- return 'utf8mb4_unicode_520_ci';
- }
- else {
- croak "invalid charset: $charset";
- }
+ my $charset = utf8_charset();
+ if ($charset eq 'utf8') {
+ return 'utf8_general_ci';
+ }
+ elsif ($charset eq 'utf8mb4') {
+ return 'utf8mb4_unicode_520_ci';
+ }
+ else {
+ croak "invalid charset: $charset";
+ }
}
sub default_row_format {
- my ($class, $table) = @_;
- my $charset = utf8_charset();
- if ($charset eq 'utf8') {
- return 'Compact';
- }
- elsif ($charset eq 'utf8mb4') {
- my @no_compress = qw(
- bug_user_last_visit
- cc
- email_rates
- logincookies
- token_data
- tokens
- ts_error
- ts_exitstatus
- ts_funcmap
- ts_job
- ts_note
- user_request_log
- votes
- );
- return 'Dynamic' if any { $table eq $_ } @no_compress;
- return 'Compressed';
- }
- else {
- croak "invalid charset: $charset";
- }
+ my ($class, $table) = @_;
+ my $charset = utf8_charset();
+ if ($charset eq 'utf8') {
+ return 'Compact';
+ }
+ elsif ($charset eq 'utf8mb4') {
+ my @no_compress = qw(
+ bug_user_last_visit
+ cc
+ email_rates
+ logincookies
+ token_data
+ tokens
+ ts_error
+ ts_exitstatus
+ ts_funcmap
+ ts_job
+ ts_note
+ user_request_log
+ votes
+ );
+ return 'Dynamic' if any { $table eq $_ } @no_compress;
+ return 'Compressed';
+ }
+ else {
+ croak "invalid charset: $charset";
+ }
}
sub _alter_db_charset_to_utf8 {
- my $self = shift;
- my $db_name = Bugzilla->localconfig->{db_name};
- my $charset = $self->utf8_charset;
- my $collate = $self->utf8_collate;
- $self->do("ALTER DATABASE $db_name CHARACTER SET $charset COLLATE $collate");
+ my $self = shift;
+ my $db_name = Bugzilla->localconfig->{db_name};
+ my $charset = $self->utf8_charset;
+ my $collate = $self->utf8_collate;
+ $self->do("ALTER DATABASE $db_name CHARACTER SET $charset COLLATE $collate");
}
sub bz_db_is_utf8 {
- my $self = shift;
- my $db_charset = $self->selectrow_arrayref(
- "SHOW VARIABLES LIKE 'character_set_database'");
- # First column holds the variable name, second column holds the value.
- my $charset = $self->utf8_charset;
- return $db_charset->[1] eq $charset ? 1 : 0;
+ my $self = shift;
+ my $db_charset
+ = $self->selectrow_arrayref("SHOW VARIABLES LIKE 'character_set_database'");
+
+ # First column holds the variable name, second column holds the value.
+ my $charset = $self->utf8_charset;
+ return $db_charset->[1] eq $charset ? 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;
}
#####################################################################
@@ -882,29 +914,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)>
@@ -918,42 +950,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)>
@@ -966,10 +999,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]})};
}
#####################################################################
@@ -993,34 +1027,33 @@ this code does.
# bz_column_info_real function would be very difficult to create
# properly for any other DB besides MySQL.
sub _bz_build_schema_from_disk {
- my ($self) = @_;
-
- my $schema = $self->_bz_schema->get_empty_schema();
-
- my @tables = $self->bz_table_list_real();
- if (@tables) {
- print "Building Schema object from database...\n";
+ my ($self) = @_;
+
+ my $schema = $self->_bz_schema->get_empty_schema();
+
+ my @tables = $self->bz_table_list_real();
+ if (@tables) {
+ print "Building Schema object from database...\n";
+ }
+ foreach my $table (@tables) {
+ $schema->add_table($table);
+ my @columns = $self->bz_table_columns_real($table);
+ foreach my $column (@columns) {
+ my $type_info = $self->bz_column_info_real($table, $column);
+ $schema->set_column($table, $column, $type_info);
}
- foreach my $table (@tables) {
- $schema->add_table($table);
- my @columns = $self->bz_table_columns_real($table);
- foreach my $column (@columns) {
- my $type_info = $self->bz_column_info_real($table, $column);
- $schema->set_column($table, $column, $type_info);
- }
- my @indexes = $self->bz_index_list_real($table);
- foreach my $index (@indexes) {
- unless ($index eq 'PRIMARY') {
- my $index_info = $self->bz_index_info_real($table, $index);
- ($index_info = $index_info->{FIELDS})
- if (!$index_info->{TYPE});
- $schema->set_index($table, $index, $index_info);
- }
- }
+ my @indexes = $self->bz_index_list_real($table);
+ foreach my $index (@indexes) {
+ unless ($index eq 'PRIMARY') {
+ my $index_info = $self->bz_index_info_real($table, $index);
+ ($index_info = $index_info->{FIELDS}) if (!$index_info->{TYPE});
+ $schema->set_index($table, $index, $index_info);
+ }
}
+ }
- return $schema;
+ return $schema;
}
1;
diff --git a/Bugzilla/DB/Oracle.pm b/Bugzilla/DB/Oracle.pm
index a519bb796..81ca1090f 100644
--- a/Bugzilla/DB/Oracle.pm
+++ b/Bugzilla/DB/Oracle.pm
@@ -37,466 +37,477 @@ 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 ';
our $fulltext_label = 0;
sub BUILDARGS {
- 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)};
- # You can never connect to Oracle without a DB name,
- # and there is no default DB.
- $dbname ||= Bugzilla->localconfig->{db_name};
+ # You can never connect to Oracle without a DB name,
+ # and there is no default DB.
+ $dbname ||= Bugzilla->localconfig->{db_name};
- # Set the language enviroment
- $ENV{'NLS_LANG'} = '.AL32UTF8' if Bugzilla->params->{'utf8'};
+ # Set the language enviroment
+ $ENV{'NLS_LANG'} = '.AL32UTF8' if Bugzilla->params->{'utf8'};
- # construct the DSN from the parameters we got
- my $dsn = "dbi:Oracle:host=$host;sid=$dbname";
- $dsn .= ";port=$port" if $port;
- my $attrs = { FetchHashKeyName => 'NAME_lc',
- LongReadLen => max(Bugzilla->params->{'maxattachmentsize'} || 0,
- MIN_LONG_READ_LEN) * 1024,
- };
- return { dsn => $dsn, user => $user, pass => $pass, attrs => $attrs };
+ # 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,
+ };
+ return {dsn => $dsn, user => $user, pass => $pass, attrs => $attrs};
}
sub on_dbi_connected {
- my ($class, $dbh) = @_;
+ my ($class, $dbh) = @_;
+
+ # Set the session's default date format to match MySQL
+ $dbh->do("ALTER SESSION SET NLS_DATE_FORMAT='YYYY-MM-DD HH24:MI:SS'");
+ $dbh->do("ALTER SESSION SET NLS_TIMESTAMP_FORMAT='YYYY-MM-DD HH24:MI:SS'");
+ $dbh->do("ALTER SESSION SET NLS_LENGTH_SEMANTICS='CHAR'")
+ if Bugzilla->params->{'utf8'};
- # Set the session's default date format to match MySQL
- $dbh->do("ALTER SESSION SET NLS_DATE_FORMAT='YYYY-MM-DD HH24:MI:SS'");
- $dbh->do("ALTER SESSION SET NLS_TIMESTAMP_FORMAT='YYYY-MM-DD HH24:MI:SS'");
- $dbh->do("ALTER SESSION SET NLS_LENGTH_SEMANTICS='CHAR'")
- if Bugzilla->params->{'utf8'};
- # To allow case insensitive query.
- $dbh->do("ALTER SESSION SET NLS_COMP='ANSI'");
- $dbh->do("ALTER SESSION SET NLS_SORT='BINARY_AI'");
+ # To allow case insensitive query.
+ $dbh->do("ALTER SESSION SET NLS_COMP='ANSI'");
+ $dbh->do("ALTER SESSION SET NLS_SORT='BINARY_AI'");
}
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);
+ 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) = @_;
- $text = $self->quote($text);
- trick_taint($text);
- $fulltext_label++;
- return "CONTAINS($column,$text,$fulltext_label) > 0", "SCORE($fulltext_label)";
+ my ($self, $column, $text) = @_;
+ $text = $self->quote($text);
+ trick_taint($text);
+ $fulltext_label++;
+ return "CONTAINS($column,$text,$fulltext_label) > 0", "SCORE($fulltext_label)";
}
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;
- $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.
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;
+ 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 recognizes CURRENT_DATE, but not CURRENT_DATE()
+ # and its CURRENT_DATE is a date+time, so wrap in TRUNC()
+ $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 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;
+ # 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 recognizes CURRENT_DATE, but not CURRENT_DATE()
# and its CURRENT_DATE is a date+time, so wrap in TRUNC()
- $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);
-
- # Oracle recognizes CURRENT_DATE, but not CURRENT_DATE()
- # and its CURRENT_DATE is a date+time, so wrap in TRUNC()
- $nonstring =~ s/\bCURRENT_DATE\b(?:\(\))?/TRUNC(CURRENT_DATE)/io;
-
- # Oracle use SUBSTR instead of SUBSTRING
- $nonstring =~ s/\bSUBSTRING\b/SUBSTR/io;
-
- # Oracle need no 'AS'
- $nonstring =~ s/\bAS\b//ig;
-
- # Take the first 4000 chars for comparison
- $nonstring =~ s/\(\s*(longdescs_\d+\.thetext|attachdata_\d+\.thedata)/
+ # Take the first 4000 chars for comparison
+ $nonstring =~ s/\(\s*(longdescs_\d+\.thetext|attachdata_\d+\.thedata)/
\(DBMS_LOB.SUBSTR\($1, 4000, 1\)/ig;
- # Look for a LIMIT clause
- ($limit) = ($nonstring =~ m(/\* LIMIT (\d*) \*/)o);
+ # Look for a LIMIT clause
+ ($limit) = ($nonstring =~ m(/\* LIMIT (\d*) \*/)o);
- if(!length($string)){
- push @result, EMPTY_STRING;
- push @result, $nonstring;
- } else {
- push @result, $string;
- push @result, $nonstring;
- }
+ if (!length($string)) {
+ push @result, EMPTY_STRING;
+ push @result, $nonstring;
}
+ else {
+ push @result, $string;
+ push @result, $nonstring;
+ }
+ }
- my $new_sql = join "'", @result;
+ 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);
+ # 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
+ # 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;
+ if (defined($limit)) {
+ if ($new_sql !~ /\bWHERE\b/) {
+ $new_sql = $new_sql . " WHERE 1=1";
}
- return $new_sql;
+ 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;
+ }
+ 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;
}
#####################################################################
@@ -504,20 +515,22 @@ sub quote_identifier {
#####################################################################
sub bz_table_columns_real {
- my ($self, $table) = @_;
- $table = uc($table);
- my $cols = $self->selectcol_arrayref(
- "SELECT LOWER(COLUMN_NAME) FROM USER_TAB_COLUMNS WHERE
- TABLE_NAME = ? ORDER BY COLUMN_NAME", undef, $table);
- return @$cols;
+ my ($self, $table) = @_;
+ $table = uc($table);
+ my $cols = $self->selectcol_arrayref(
+ "SELECT LOWER(COLUMN_NAME) FROM USER_TAB_COLUMNS WHERE
+ TABLE_NAME = ? ORDER BY COLUMN_NAME", undef, $table
+ );
+ return @$cols;
}
sub bz_table_list_real {
- my ($self) = @_;
- my $tables = $self->selectcol_arrayref(
- "SELECT LOWER(TABLE_NAME) FROM USER_TABLES WHERE
- TABLE_NAME NOT LIKE ? ORDER BY TABLE_NAME", undef, 'DR$%');
- return @$tables;
+ my ($self) = @_;
+ my $tables = $self->selectcol_arrayref(
+ "SELECT LOWER(TABLE_NAME) FROM USER_TABLES WHERE
+ TABLE_NAME NOT LIKE ? ORDER BY TABLE_NAME", undef, 'DR$%'
+ );
+ return @$tables;
}
#####################################################################
@@ -525,32 +538,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(
@@ -568,9 +586,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
@@ -614,110 +634,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;
}
############################################################################
@@ -726,65 +753,66 @@ package Bugzilla::DB::Oracle::st;
use base 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);
+ }
+ elsif (ref($row) eq 'HASH') {
+ Bugzilla::DB::Oracle::_fix_hashref($row);
}
- return $ref;
+ }
+ return $ref;
}
sub fetchall_hashref {
- my $self = shift;
- my $ref = $self->SUPER::fetchall_hashref(@_);
- return undef if !defined $ref;
- foreach my $row (values %$ref) {
- Bugzilla::DB::Oracle::_fix_hashref($row);
- }
- return $ref;
+ my $self = shift;
+ my $ref = $self->SUPER::fetchall_hashref(@_);
+ return undef if !defined $ref;
+ foreach my $row (values %$ref) {
+ Bugzilla::DB::Oracle::_fix_hashref($row);
+ }
+ return $ref;
}
sub fetch {
- my $self = shift;
- my $row = $self->SUPER::fetch(@_);
- if ($row) {
- Bugzilla::DB::Oracle::_fix_arrayref($row);
- }
- return $row;
+ my $self = shift;
+ my $row = $self->SUPER::fetch(@_);
+ if ($row) {
+ Bugzilla::DB::Oracle::_fix_arrayref($row);
+ }
+ return $row;
}
1;
diff --git a/Bugzilla/DB/Pg.pm b/Bugzilla/DB/Pg.pm
index 0db349412..d1bb0f798 100644
--- a/Bugzilla/DB/Pg.pm
+++ b/Bugzilla/DB/Pg.pm
@@ -31,151 +31,153 @@ use DBD::Pg;
# This module extends the DB interface via inheritance
extends qw(Bugzilla::DB);
-use constant BLOB_TYPE => { pg_type => DBD::Pg::PG_BYTEA };
+use constant BLOB_TYPE => {pg_type => DBD::Pg::PG_BYTEA};
sub BUILDARGS {
- my ($class, $params) = @_;
- my ($user, $pass, $host, $dbname, $port) =
- @$params{qw(db_user db_pass db_host db_name db_port)};
+ my ($class, $params) = @_;
+ my ($user, $pass, $host, $dbname, $port)
+ = @$params{qw(db_user db_pass db_host db_name db_port)};
- # The default database name for PostgreSQL. We have
- # to connect to SOME database, even if we have
- # no $dbname parameter.
- $dbname ||= 'template1';
+ # The default database name for PostgreSQL. We have
+ # to connect to SOME database, even if we have
+ # no $dbname parameter.
+ $dbname ||= 'template1';
- # construct the DSN from the parameters we got
- my $dsn = "dbi:Pg:dbname=$dbname";
- $dsn .= ";host=$host" if $host;
- $dsn .= ";port=$port" if $port;
+ # construct the DSN from the parameters we got
+ my $dsn = "dbi:Pg:dbname=$dbname";
+ $dsn .= ";host=$host" if $host;
+ $dsn .= ";port=$port" if $port;
- # This stops Pg from printing out lots of "NOTICE" messages when
- # creating tables.
- $dsn .= ";options='-c client_min_messages=warning'";
+ # This stops Pg from printing out lots of "NOTICE" messages when
+ # creating tables.
+ $dsn .= ";options='-c client_min_messages=warning'";
- my $attrs = { pg_enable_utf8 => Bugzilla->params->{'utf8'} };
+ my $attrs = {pg_enable_utf8 => Bugzilla->params->{'utf8'}};
- return { dsn => $dsn, user => $user, pass => $pass, attrs => $attrs }
+ return {dsn => $dsn, user => $user, pass => $pass, attrs => $attrs};
}
# 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) = @_;
- $sort = 1 if !defined $sort;
- $separator = $self->quote(', ') if !defined $separator;
- my $sql = "array_accum($text)";
- if ($sort) {
- $sql = "array_sort($sql)";
- }
- return "array_to_string($sql, $separator)";
+ my ($self, $text, $separator, $sort) = @_;
+ $sort = 1 if !defined $sort;
+ $separator = $self->quote(', ') if !defined $separator;
+ my $sql = "array_accum($text)";
+ if ($sort) {
+ $sql = "array_sort($sql)";
+ }
+ return "array_to_string($sql, $separator)";
}
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_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) = @_;
+ my ($self, $date, $format) = @_;
- $format = "%Y.%m.%d %H:%i:%s" if !$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 ($self, $date, $operator, $interval, $units) = @_;
- return "$date $operator $interval * INTERVAL '1 $units'";
+ return "$date $operator $interval * INTERVAL '1 $units'";
}
sub sql_string_concat {
- my ($self, @params) = @_;
+ my ($self, @params) = @_;
- # Postgres 7.3 does not support concatenating of different types, so we
- # need to cast both parameters to text. Version 7.4 seems to handle this
- # properly, so when we stop support 7.3, this can be removed.
- return '(CAST(' . join(' AS text) || CAST(', @params) . ' AS text))';
+ # Postgres 7.3 does not support concatenating of different types, so we
+ # need to cast both parameters to text. Version 7.4 seems to handle this
+ # properly, so when we stop support 7.3, this can be removed.
+ return '(CAST(' . join(' AS text) || CAST(', @params) . ' AS text))';
}
# Tell us whether or not a particular sequence exists in the DB.
sub bz_sequence_exists {
- my ($self, $seq_name) = @_;
- my $exists = $self->selectrow_array(
- 'SELECT 1 FROM pg_statio_user_sequences WHERE relname = ?',
- undef, $seq_name);
- return $exists || 0;
+ my ($self, $seq_name) = @_;
+ my $exists
+ = $self->selectrow_array(
+ 'SELECT 1 FROM pg_statio_user_sequences WHERE relname = ?',
+ undef, $seq_name);
+ return $exists || 0;
}
sub bz_explain {
- my ($self, $sql) = @_;
- my $explain = $self->selectcol_arrayref("EXPLAIN ANALYZE $sql");
- return join("\n", @$explain);
+ my ($self, $sql) = @_;
+ my $explain = $self->selectcol_arrayref("EXPLAIN ANALYZE $sql");
+ return join("\n", @$explain);
}
#####################################################################
@@ -183,38 +185,42 @@ sub bz_explain {
#####################################################################
sub bz_check_server_version {
- my $self = shift;
- my ($db) = @_;
- my $server_version = $self->SUPER::bz_check_server_version(@_);
- my ($major_version, $minor_version) = $server_version =~ /^0*(\d+)\.0*(\d+)/;
- # Pg 9.0 requires DBD::Pg 2.17.2 in order to properly read bytea values.
- # Pg 9.2 requires DBD::Pg 2.19.3 as spclocation no longer exists.
- if ($major_version >= 9) {
- local $db->{dbd}->{version} = ($minor_version >= 2) ? '2.19.3' : '2.17.2';
- local $db->{name} = $db->{name} . " ${major_version}.$minor_version";
- Bugzilla::DB::_bz_check_dbd(@_);
- }
+ my $self = shift;
+ my ($db) = @_;
+ my $server_version = $self->SUPER::bz_check_server_version(@_);
+ my ($major_version, $minor_version) = $server_version =~ /^0*(\d+)\.0*(\d+)/;
+
+ # Pg 9.0 requires DBD::Pg 2.17.2 in order to properly read bytea values.
+ # Pg 9.2 requires DBD::Pg 2.19.3 as spclocation no longer exists.
+ if ($major_version >= 9) {
+ local $db->{dbd}->{version} = ($minor_version >= 2) ? '2.19.3' : '2.17.2';
+ local $db->{name} = $db->{name} . " ${major_version}.$minor_version";
+ Bugzilla::DB::_bz_check_dbd(@_);
+ }
}
sub bz_setup_database {
- my $self = shift;
- $self->SUPER::bz_setup_database(@_);
-
- # Custom Functions
- my $function = 'array_accum';
- my $array_accum = $self->selectrow_array(
- 'SELECT 1 FROM pg_proc WHERE proname = ?', undef, $function);
- if (!$array_accum) {
- print "Creating function $function...\n";
- $self->do("CREATE AGGREGATE array_accum (
+ my $self = shift;
+ $self->SUPER::bz_setup_database(@_);
+
+ # Custom Functions
+ my $function = 'array_accum';
+ my $array_accum
+ = $self->selectrow_array('SELECT 1 FROM pg_proc WHERE proname = ?',
+ undef, $function);
+ if (!$array_accum) {
+ print "Creating function $function...\n";
+ $self->do(
+ "CREATE AGGREGATE array_accum (
SFUNC = array_append,
BASETYPE = anyelement,
STYPE = anyarray,
INITCOND = '{}'
- )");
- }
+ )"
+ );
+ }
- $self->do(<<'END');
+ $self->do(<<'END');
CREATE OR REPLACE FUNCTION array_sort(ANYARRAY)
RETURNS ANYARRAY LANGUAGE SQL
IMMUTABLE STRICT
@@ -228,117 +234,132 @@ SELECT ARRAY(
$$;
END
- # 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);
}
+ }
}
#####################################################################
@@ -348,12 +369,13 @@ sub _fix_case_differences {
# Pg includes the PostgreSQL system tables in table_list_real, so
# we need to remove those.
sub bz_table_list_real {
- my $self = shift;
+ my $self = shift;
+
+ my @full_table_list = $self->SUPER::bz_table_list_real(@_);
- my @full_table_list = $self->SUPER::bz_table_list_real(@_);
- # All PostgreSQL system tables start with "pg_" or "sql_"
- my @table_list = grep(!/(^pg_)|(^sql_)/, @full_table_list);
- return @table_list;
+ # All PostgreSQL system tables start with "pg_" or "sql_"
+ my @table_list = grep(!/(^pg_)|(^sql_)/, @full_table_list);
+ return @table_list;
}
1;
diff --git a/Bugzilla/DB/Schema.pm b/Bugzilla/DB/Schema.pm
index e1c19fa51..f681445b0 100644
--- a/Bugzilla/DB/Schema.pm
+++ b/Bugzilla/DB/Schema.pm
@@ -31,6 +31,7 @@ use List::MoreUtils qw(firstidx natatime);
use Try::Tiny;
use Module::Runtime qw(require_module);
use Safe;
+
# Historical, needed for SCHEMA_VERSION = '1.00'
use Storable qw(dclone freeze thaw);
@@ -199,1644 +200,1593 @@ 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 => 'INT2',
+ NOTNULL => 1,
+ REFERENCES => {TABLE => 'components', COLUMN => 'id'}
+ },
+ resolution => {TYPE => 'varchar(64)', NOTNULL => 1, DEFAULT => "''"},
+ target_milestone => {TYPE => 'varchar(20)', 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'},
+ alias => {TYPE => 'varchar(40)'},
],
- # 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_alias_idx => {FIELDS => ['alias'], TYPE => 'UNIQUE'},
+ 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 => 'INT2', NOTNULL => 1,
- REFERENCES => {TABLE => 'components',
- COLUMN => 'id'}},
- resolution => {TYPE => 'varchar(64)',
- NOTNULL => 1, DEFAULT => "''"},
- target_milestone => {TYPE => 'varchar(20)',
- 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'},
- alias => {TYPE => 'varchar(40)'},
- ],
- INDEXES => [
- bugs_alias_idx => {FIELDS => ['alias'],
- TYPE => 'UNIQUE'},
- 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'],
- ],
- },
-
- 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 => ['bug_id'],
- 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(100)', 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'},
- attach_size => {TYPE => 'INT4', NOTNULL => 1,
- DEFAULT => 0},
- ],
- 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'],
- attachments_ispatch_idx => ['ispatch'],
- ],
- },
- 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 => 'INT2',
- 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 => 'INT2',
- 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},
- 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(20)', NOTNULL => 1},
- sortkey => {TYPE => 'INT2', NOTNULL => 1,
- DEFAULT => 0},
- isactive => {TYPE => 'BOOLEAN', NOTNULL => 1,
- DEFAULT => 'TRUE'},
- ],
- INDEXES => [
- milestones_product_id_idx => {FIELDS => [qw(product_id value)],
- TYPE => 'UNIQUE'},
- ],
- },
-
- # Global Field Values
- # -------------------
-
- bug_status => {
- FIELDS => [
- @{ dclone(FIELD_TABLE_SCHEMA->{FIELDS}) },
- is_open => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'},
-
- ],
- INDEXES => [
- bug_status_value_idx => {FIELDS => ['value'],
- TYPE => 'UNIQUE'},
- bug_status_sortkey_idx => ['sortkey', 'value'],
- bug_status_visibility_value_id_idx => ['visibility_value_id'],
- ],
- },
-
- resolution => {
- FIELDS => dclone(FIELD_TABLE_SCHEMA->{FIELDS}),
- INDEXES => [
- resolution_value_idx => {FIELDS => ['value'],
- TYPE => 'UNIQUE'},
- resolution_sortkey_idx => ['sortkey', 'value'],
- resolution_visibility_value_id_idx => ['visibility_value_id'],
- ],
- },
-
- bug_severity => {
- FIELDS => dclone(FIELD_TABLE_SCHEMA->{FIELDS}),
- INDEXES => [
- bug_severity_value_idx => {FIELDS => ['value'],
- TYPE => 'UNIQUE'},
- bug_severity_sortkey_idx => ['sortkey', 'value'],
- bug_severity_visibility_value_id_idx => ['visibility_value_id'],
- ],
- },
-
- priority => {
- FIELDS => dclone(FIELD_TABLE_SCHEMA->{FIELDS}),
- INDEXES => [
- priority_value_idx => {FIELDS => ['value'],
- TYPE => 'UNIQUE'},
- priority_sortkey_idx => ['sortkey', 'value'],
- priority_visibility_value_id_idx => ['visibility_value_id'],
- ],
- },
-
- rep_platform => {
- FIELDS => dclone(FIELD_TABLE_SCHEMA->{FIELDS}),
- INDEXES => [
- rep_platform_value_idx => {FIELDS => ['value'],
- TYPE => 'UNIQUE'},
- rep_platform_sortkey_idx => ['sortkey', 'value'],
- rep_platform_visibility_value_id_idx => ['visibility_value_id'],
- ],
- },
-
- op_sys => {
- FIELDS => dclone(FIELD_TABLE_SCHEMA->{FIELDS}),
- INDEXES => [
- op_sys_value_idx => {FIELDS => ['value'],
- TYPE => 'UNIQUE'},
- op_sys_sortkey_idx => ['sortkey', 'value'],
- op_sys_visibility_value_id_idx => ['visibility_value_id'],
- ],
- },
-
- status_workflow => {
- FIELDS => [
- # On bug creation, there is no old value.
- old_status => {TYPE => 'INT2',
- REFERENCES => {TABLE => 'bug_status',
- COLUMN => 'id',
- DELETE => 'CASCADE'}},
- new_status => {TYPE => 'INT2', NOTNULL => 1,
- REFERENCES => {TABLE => 'bug_status',
- COLUMN => 'id',
- DELETE => 'CASCADE'}},
- require_comment => {TYPE => 'INT1', NOTNULL => 1, DEFAULT => 0},
- ],
- INDEXES => [
- status_workflow_idx => {FIELDS => ['old_status', 'new_status'],
- TYPE => 'UNIQUE'},
- ],
- },
-
- # USER INFO
- # ---------
-
- # General User Information
- # ------------------------
-
- profiles => {
- FIELDS => [
- userid => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1,
- PRIMARYKEY => 1},
- login_name => {TYPE => 'varchar(255)', NOTNULL => 1},
- cryptpassword => {TYPE => 'varchar(128)'},
- realname => {TYPE => 'varchar(255)', NOTNULL => 1,
- DEFAULT => "''"},
- nickname => {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'},
- password_change_required => { TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE' },
- password_change_reason => { TYPE => 'varchar(64)' },
- mfa => {TYPE => 'varchar(8)', DEFAULT => "''" },
- mfa_required_date => {TYPE => 'DATETIME'},
- ],
- INDEXES => [
- profiles_login_name_idx => {FIELDS => ['login_name'],
- TYPE => 'UNIQUE'},
- profiles_extern_id_idx => {FIELDS => ['extern_id'],
- TYPE => 'UNIQUE'},
- profiles_nickname_idx => ['nickname'],
- profiles_realname_ft_idx => {FIELDS => ['realname'],
- TYPE => 'FULLTEXT'},
- ],
- },
-
- 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'],
- ],
- },
-
- profile_mfa => {
- FIELDS => [
- id => { TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1 },
- user_id => { TYPE => 'INT3', NOTNULL => 1,
- REFERENCES => { TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE' } },
- name => { TYPE => 'varchar(16)', NOTNULL => 1 },
- value => { TYPE => 'varchar(255)' },
- ],
- INDEXES => [
- profile_mfa_userid_name_idx => { FIELDS => [ 'user_id', 'name' ], TYPE => 'UNIQUE' },
- ],
- },
-
- 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'},
- ],
- },
-
- component_cc => {
-
- FIELDS => [
- user_id => {TYPE => 'INT3', NOTNULL => 1,
- REFERENCES => {TABLE => 'profiles',
- COLUMN => 'userid',
- DELETE => 'CASCADE'}},
- component_id => {TYPE => 'INT2', 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},
- userid => {TYPE => 'INT3', NOTNULL => 1,
- REFERENCES => {TABLE => 'profiles',
- COLUMN => 'userid',
- DELETE => 'CASCADE'}},
- ipaddr => {TYPE => 'varchar(40)'},
- lastused => {TYPE => 'DATETIME', NOTNULL => 1},
- id => {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1},
- restrict_ipaddr => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 0},
- ],
- INDEXES => [
- logincookies_lastused_idx => ['lastused'],
- logincookies_cookie_idx => {FIELDS => ['cookie'], TYPE => 'UNIQUE'},
- ],
- },
-
- 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'],
- ],
- },
-
- token_data => {
- FIELDS => [
- id => { TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1 },
- token => { TYPE => 'varchar(22)', NOTNULL => 1,
- REFERENCES => { TABLE => 'tokens', COLUMN => 'token', DELETE => 'CASCADE' }},
- extra_data => { TYPE => 'MEDIUMTEXT', NOTNULL => 1 },
- ],
- INDEXES => [
- token_data_idx => { FIELDS => ['token'], TYPE => 'UNIQUE' },
- ],
- },
-
- # 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'},
- owner_user_id => {TYPE => 'INT3',
- REFERENCES => {
- TABLE => 'profiles',
- COLUMN => 'userid'}},
- idle_member_removal => {TYPE => 'INT2', NOTNULL => 1, DEFAULT => '0'}
- ],
- 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(20)',
- NOTNULL => 1, DEFAULT => "'---'"},
- allows_unconfirmed => {TYPE => 'BOOLEAN', NOTNULL => 1,
- DEFAULT => 'TRUE'},
- ],
- INDEXES => [
- products_name_idx => {FIELDS => ['name'],
- TYPE => 'UNIQUE'},
- ],
- },
-
- components => {
- FIELDS => [
- id => {TYPE => 'SMALLSERIAL', 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'},
- triage_owner_id => {TYPE => 'INT3',
- REFERENCES => {TABLE => 'profiles',
- COLUMN => 'userid',
- DELETE => 'SET NULL'}},
- ],
- 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 => 'MEDIUMTEXT', 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)'},
- category => {TYPE => 'varchar(64)', NOTNULL => 1, DEFAULT => "'General'"}
- ],
- },
-
- 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'},
- ],
- },
-
- 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)],
- email_rates_message_ts_idx => ['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},
- ],
- },
-
- 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'},
- last_used_ip => {TYPE => 'varchar(40)'},
- 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'],
- ],
- },
-
- user_request_log => {
- FIELDS => [
- id => {TYPE => 'INTSERIAL', NOTNULL => 1,
- PRIMARYKEY => 1},
- user_id => {TYPE => 'INT3', NOTNULL => 1 },
- ip_address => {TYPE => 'varchar(40)', NOTNULL => 1},
- user_agent => {TYPE => 'TINYTEXT', NOTNULL => 1},
- timestamp => {TYPE => 'DATETIME', NOTNULL => 1},
- bug_id => {TYPE => 'INT3', NOTNULL => 0},
- attach_id => {TYPE => 'INT4', NOTNULL => 0},
- request_url => {TYPE => 'TINYTEXT', NOTNULL => 1},
- method => {TYPE => 'TINYTEXT', NOTNULL => 1},
- action => {TYPE => 'varchar(20)', NOTNULL => 1},
- server => {TYPE => 'varchar(7)', NOTNULL => 1},
- ],
- INDEXES => [
- user_user_request_log_user_id_idx => ['user_id'],
- ],
- },
-};
+ bugs_activity => {
+ FIELDS => [
+ id => {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1},
+ bug_id => {
+ TYPE => 'INT3',
+ NOTNULL => 1,
+ REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'}
+ },
+ attach_id => {
+ TYPE => 'INT3',
+ REFERENCES =>
+ {TABLE => 'attachments', COLUMN => 'attach_id', DELETE => 'CASCADE'}
+ },
+ who => {
+ TYPE => 'INT3',
+ NOTNULL => 1,
+ REFERENCES => {TABLE => 'profiles', COLUMN => 'userid'}
+ },
+ bug_when => {TYPE => 'DATETIME', NOTNULL => 1},
+ fieldid => {
+ TYPE => 'INT3',
+ NOTNULL => 1,
+ REFERENCES => {TABLE => 'fielddefs', COLUMN => 'id'}
+ },
+ added => {TYPE => 'varchar(255)'},
+ removed => {TYPE => 'varchar(255)'},
+ comment_id => {
+ TYPE => 'INT4',
+ REFERENCES =>
+ {TABLE => 'longdescs', COLUMN => 'comment_id', DELETE => 'CASCADE'}
+ },
+ ],
+ INDEXES => [
+ bugs_activity_bug_id_idx => ['bug_id'],
+ bugs_activity_who_idx => ['who'],
+ bugs_activity_bug_when_idx => ['bug_when'],
+ bugs_activity_fieldid_idx => ['fieldid'],
+ bugs_activity_added_idx => ['added'],
+ bugs_activity_removed_idx => ['removed'],
+ ],
+ },
-# Foreign Keys are added in Bugzilla::DB::bz_add_field_tables
-use constant MULTI_SELECT_VALUE_TABLE => {
+ 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 => ['bug_id'],
+ 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(100)', 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'},
+ attach_size => {TYPE => 'INT4', NOTNULL => 1, DEFAULT => 0},
+ ],
+ 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'],
+ attachments_ispatch_idx => ['ispatch'],
+ ],
+ },
+ 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 => 'INT2',
+ 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 => 'INT2',
+ 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},
+ 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(20)', NOTNULL => 1},
+ sortkey => {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0},
+ isactive => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'},
+ ],
+ INDEXES => [
+ milestones_product_id_idx =>
+ {FIELDS => [qw(product_id value)], TYPE => 'UNIQUE'},
+ ],
+ },
+
+ # Global Field Values
+ # -------------------
+
+ bug_status => {
+ FIELDS => [
+ @{dclone(FIELD_TABLE_SCHEMA->{FIELDS})},
+ is_open => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'},
+
+ ],
+ INDEXES => [
+ bug_status_value_idx => {FIELDS => ['value'], TYPE => 'UNIQUE'},
+ bug_status_sortkey_idx => ['sortkey', 'value'],
+ bug_status_visibility_value_id_idx => ['visibility_value_id'],
+ ],
+ },
+
+ resolution => {
+ FIELDS => dclone(FIELD_TABLE_SCHEMA->{FIELDS}),
+ INDEXES => [
+ resolution_value_idx => {FIELDS => ['value'], TYPE => 'UNIQUE'},
+ resolution_sortkey_idx => ['sortkey', 'value'],
+ resolution_visibility_value_id_idx => ['visibility_value_id'],
+ ],
+ },
+
+ bug_severity => {
+ FIELDS => dclone(FIELD_TABLE_SCHEMA->{FIELDS}),
+ INDEXES => [
+ bug_severity_value_idx => {FIELDS => ['value'], TYPE => 'UNIQUE'},
+ bug_severity_sortkey_idx => ['sortkey', 'value'],
+ bug_severity_visibility_value_id_idx => ['visibility_value_id'],
+ ],
+ },
+
+ priority => {
+ FIELDS => dclone(FIELD_TABLE_SCHEMA->{FIELDS}),
+ INDEXES => [
+ priority_value_idx => {FIELDS => ['value'], TYPE => 'UNIQUE'},
+ priority_sortkey_idx => ['sortkey', 'value'],
+ priority_visibility_value_id_idx => ['visibility_value_id'],
+ ],
+ },
+
+ rep_platform => {
+ FIELDS => dclone(FIELD_TABLE_SCHEMA->{FIELDS}),
+ INDEXES => [
+ rep_platform_value_idx => {FIELDS => ['value'], TYPE => 'UNIQUE'},
+ rep_platform_sortkey_idx => ['sortkey', 'value'],
+ rep_platform_visibility_value_id_idx => ['visibility_value_id'],
+ ],
+ },
+
+ op_sys => {
+ FIELDS => dclone(FIELD_TABLE_SCHEMA->{FIELDS}),
+ INDEXES => [
+ op_sys_value_idx => {FIELDS => ['value'], TYPE => 'UNIQUE'},
+ op_sys_sortkey_idx => ['sortkey', 'value'],
+ op_sys_visibility_value_id_idx => ['visibility_value_id'],
+ ],
+ },
+
+ status_workflow => {
+ FIELDS => [
+
+ # On bug creation, there is no old value.
+ old_status => {
+ TYPE => 'INT2',
+ REFERENCES => {TABLE => 'bug_status', COLUMN => 'id', DELETE => 'CASCADE'}
+ },
+ new_status => {
+ TYPE => 'INT2',
+ NOTNULL => 1,
+ REFERENCES => {TABLE => 'bug_status', COLUMN => 'id', DELETE => 'CASCADE'}
+ },
+ require_comment => {TYPE => 'INT1', NOTNULL => 1, DEFAULT => 0},
+ ],
+ INDEXES => [
+ status_workflow_idx =>
+ {FIELDS => ['old_status', 'new_status'], TYPE => 'UNIQUE'},
+ ],
+ },
+
+ # USER INFO
+ # ---------
+
+ # General User Information
+ # ------------------------
+
+ profiles => {
+ FIELDS => [
+ userid => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1},
+ login_name => {TYPE => 'varchar(255)', NOTNULL => 1},
+ cryptpassword => {TYPE => 'varchar(128)'},
+ realname => {TYPE => 'varchar(255)', NOTNULL => 1, DEFAULT => "''"},
+ nickname => {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'},
+ password_change_required =>
+ {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'},
+ password_change_reason => {TYPE => 'varchar(64)'},
+ mfa => {TYPE => 'varchar(8)', DEFAULT => "''"},
+ mfa_required_date => {TYPE => 'DATETIME'},
+ ],
+ INDEXES => [
+ profiles_login_name_idx => {FIELDS => ['login_name'], TYPE => 'UNIQUE'},
+ profiles_extern_id_idx => {FIELDS => ['extern_id'], TYPE => 'UNIQUE'},
+ profiles_nickname_idx => ['nickname'],
+ profiles_realname_ft_idx => {FIELDS => ['realname'], TYPE => 'FULLTEXT'},
+ ],
+ },
+
+ 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'],
+ ],
+ },
+
+ profile_mfa => {
+ FIELDS => [
+ id => {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1},
+ user_id => {
+ TYPE => 'INT3',
+ NOTNULL => 1,
+ REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'}
+ },
+ name => {TYPE => 'varchar(16)', NOTNULL => 1},
+ value => {TYPE => 'varchar(255)'},
+ ],
+ INDEXES => [
+ profile_mfa_userid_name_idx =>
+ {FIELDS => ['user_id', 'name'], TYPE => 'UNIQUE'},
+ ],
+ },
+
+ 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'},],
+ },
+
+ component_cc => {
+
+ FIELDS => [
+ user_id => {
+ TYPE => 'INT3',
+ NOTNULL => 1,
+ REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'}
+ },
+ component_id => {
+ TYPE => 'INT2',
+ 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},
+ userid => {
+ TYPE => 'INT3',
+ NOTNULL => 1,
+ REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'}
+ },
+ ipaddr => {TYPE => 'varchar(40)'},
+ lastused => {TYPE => 'DATETIME', NOTNULL => 1},
+ id => {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1},
+ restrict_ipaddr => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 0},
+ ],
+ INDEXES => [
+ logincookies_lastused_idx => ['lastused'],
+ logincookies_cookie_idx => {FIELDS => ['cookie'], TYPE => 'UNIQUE'},
+ ],
+ },
+
+ 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'],],
+ },
+
+ token_data => {
+ FIELDS => [
+ id => {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1},
+ token => {
+ TYPE => 'varchar(22)',
+ NOTNULL => 1,
+ REFERENCES => {TABLE => 'tokens', COLUMN => 'token', DELETE => 'CASCADE'}
+ },
+ extra_data => {TYPE => 'MEDIUMTEXT', NOTNULL => 1},
+ ],
+ INDEXES => [token_data_idx => {FIELDS => ['token'], TYPE => 'UNIQUE'},],
+ },
+
+ # 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'},
+ owner_user_id =>
+ {TYPE => 'INT3', REFERENCES => {TABLE => 'profiles', COLUMN => 'userid'}},
+ idle_member_removal => {TYPE => 'INT2', NOTNULL => 1, DEFAULT => '0'}
+ ],
+ 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(20)', NOTNULL => 1, DEFAULT => "'---'"},
+ allows_unconfirmed => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'},
+ ],
+ INDEXES => [products_name_idx => {FIELDS => ['name'], TYPE => 'UNIQUE'},],
+ },
+
+ components => {
+ FIELDS => [
+ id => {TYPE => 'SMALLSERIAL', 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'},
+ triage_owner_id => {
+ TYPE => 'INT3',
+ REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'SET NULL'}
+ },
+ ],
+ 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 => 'MEDIUMTEXT', 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)'},
+ category => {TYPE => 'varchar(64)', NOTNULL => 1, DEFAULT => "'General'"}
+ ],
+ },
+
+ 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'},
+ ],
+ },
+
+ 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)],
+ email_rates_message_ts_idx => ['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 => [
- bug_id => {TYPE => 'INT3', NOTNULL => 1},
- value => {TYPE => 'varchar(64)', NOTNULL => 1},
+ 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 => [
- bug_id_idx => {FIELDS => [qw( bug_id value)], TYPE => 'UNIQUE'},
+ ts_exitstatus_funcid_idx => ['funcid'],
+ ts_exitstatus_delete_after_idx => ['delete_after'],
],
+ },
+
+ # SCHEMA STORAGE
+ # --------------
+
+ bz_schema => {
+ FIELDS => [
+ schema_data => {TYPE => 'LONGBLOB', NOTNULL => 1},
+ version => {TYPE => 'decimal(3,2)', NOTNULL => 1},
+ ],
+ },
+
+ bug_user_last_visit => {
+ FIELDS => [
+ id => {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1},
+ user_id => {
+ TYPE => 'INT3',
+ NOTNULL => 1,
+ REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'}
+ },
+ bug_id => {
+ TYPE => 'INT3',
+ NOTNULL => 1,
+ REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'}
+ },
+ last_visit_ts => {TYPE => 'DATETIME', NOTNULL => 1},
+ ],
+ INDEXES => [
+ bug_user_last_visit_idx => {FIELDS => ['user_id', 'bug_id'], TYPE => 'UNIQUE'},
+ bug_user_last_visit_last_visit_ts_idx => ['last_visit_ts'],
+ ],
+ },
+
+ user_api_keys => {
+ FIELDS => [
+ id => {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1},
+ user_id => {
+ TYPE => 'INT3',
+ NOTNULL => 1,
+ REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'}
+ },
+ api_key => {TYPE => 'varchar(40)', NOTNULL => 1},
+ description => {TYPE => 'varchar(255)'},
+ revoked => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'},
+ last_used => {TYPE => 'DATETIME'},
+ last_used_ip => {TYPE => 'varchar(40)'},
+ 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'],
+ ],
+ },
+
+ user_request_log => {
+ FIELDS => [
+ id => {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1},
+ user_id => {TYPE => 'INT3', NOTNULL => 1},
+ ip_address => {TYPE => 'varchar(40)', NOTNULL => 1},
+ user_agent => {TYPE => 'TINYTEXT', NOTNULL => 1},
+ timestamp => {TYPE => 'DATETIME', NOTNULL => 1},
+ bug_id => {TYPE => 'INT3', NOTNULL => 0},
+ attach_id => {TYPE => 'INT4', NOTNULL => 0},
+ request_url => {TYPE => 'TINYTEXT', NOTNULL => 1},
+ method => {TYPE => 'TINYTEXT', NOTNULL => 1},
+ action => {TYPE => 'varchar(20)', NOTNULL => 1},
+ server => {TYPE => 'varchar(7)', NOTNULL => 1},
+ ],
+ INDEXES => [user_user_request_log_user_id_idx => ['user_id'],],
+ },
+};
+
+# Foreign Keys are added in Bugzilla::DB::bz_add_field_tables
+use constant MULTI_SELECT_VALUE_TABLE => {
+ FIELDS => [
+ bug_id => {TYPE => 'INT3', NOTNULL => 1},
+ value => {TYPE => 'varchar(64)', NOTNULL => 1},
+ ],
+ INDEXES => [bug_id_idx => {FIELDS => [qw( bug_id value)], TYPE => 'UNIQUE'},],
};
#--------------------------------------------------------------------------
@@ -1871,30 +1821,31 @@ 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;
- try {
- require_module($class);
- }
- catch {
- die "The $class class could not be found ($subclass not supported?): $_";
- };
+ if ($driver) {
+ (my $subclass = $driver) =~ s/^(\S)/\U$1/;
+ $class .= '::' . $subclass;
+ try {
+ require_module($class);
}
- die "$class is an abstract base class. Instantiate a subclass instead."
- if ($class eq __PACKAGE__);
+ catch {
+ die "The $class class could not be found ($subclass not supported?): $_";
+ };
+ }
+ 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 {
@@ -1917,33 +1868,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 {
@@ -1959,36 +1911,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 {
@@ -2022,30 +1979,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);
+
+ # DEFAULT attribute must appear before any column constraints
+ # (e.g., NOT NULL), for Oracle
+ $type_ddl .= " DEFAULT $default" if (defined($default));
- my $type_ddl = $self->convert_type($type);
- # DEFAULT attribute must appear before any column constraints
- # (e.g., NOT NULL), for Oracle
- $type_ddl .= " DEFAULT $default" if (defined($default));
- # PRIMARY KEY must appear before NOT NULL for SQLite.
- $type_ddl .= " PRIMARY KEY" if ($finfo->{PRIMARYKEY});
- $type_ddl .= " NOT NULL" if ($finfo->{NOTNULL});
+ # PRIMARY KEY must appear before NOT NULL for SQLite.
+ $type_ddl .= " PRIMARY KEY" if ($finfo->{PRIMARYKEY});
+ $type_ddl .= " NOT NULL" if ($finfo->{NOTNULL});
- return($type_ddl);
+ return ($type_ddl);
-} #eosub--get_type_ddl
+} #eosub--get_type_ddl
sub get_fk_ddl {
@@ -2079,78 +2040,80 @@ is undefined.
=cut
- my ($self, $table, $column, $references) = @_;
- return "" if !$references;
+ my ($self, $table, $column, $references) = @_;
+ return "" if !$references;
- my $update = $references->{UPDATE} || 'CASCADE';
- my $delete = $references->{DELETE} || 'RESTRICT';
- my $to_table = $references->{TABLE} || confess "No table in reference";
- my $to_column = $references->{COLUMN} || confess "No column in reference";
- my $fk_name = $self->_get_fk_name($table, $column, $references);
+ my $update = $references->{UPDATE} || 'CASCADE';
+ my $delete = $references->{DELETE} || 'RESTRICT';
+ my $to_table = $references->{TABLE} || confess "No table in reference";
+ my $to_column = $references->{COLUMN} || confess "No column in reference";
+ my $fk_name = $self->_get_fk_name($table, $column, $references);
- return "\n CONSTRAINT $fk_name FOREIGN KEY ($column)\n"
- . " REFERENCES $to_table($to_column)\n"
- . " ON UPDATE $update ON DELETE $delete";
+ return
+ "\n CONSTRAINT $fk_name FOREIGN KEY ($column)\n"
+ . " REFERENCES $to_table($to_column)\n"
+ . " ON UPDATE $update ON DELETE $delete";
}
# Generates a name for a Foreign Key. It's separate from get_fk_ddl
# so that certain databases can override it (for shorter identifiers or
# other reasons).
sub _get_fk_name {
- my ($self, $table, $column, $references) = @_;
- my $to_table = $references->{TABLE};
- my $to_column = $references->{COLUMN};
- my $name = "fk_${table}_${column}_${to_table}_${to_column}";
+ my ($self, $table, $column, $references) = @_;
+ my $to_table = $references->{TABLE};
+ my $to_column = $references->{COLUMN};
+ my $name = "fk_${table}_${column}_${to_table}_${to_column}";
- if (length($name) > $self->MAX_IDENTIFIER_LEN) {
- $name = 'fk_' . $self->_hash_identifier($name);
- }
+ if (length($name) > $self->MAX_IDENTIFIER_LEN) {
+ $name = 'fk_' . $self->_hash_identifier($name);
+ }
- return $name;
+ return $name;
}
sub _hash_identifier {
- my ($invocant, $value) = @_;
- # We do -7 to allow prefixes like "idx_" or "fk_", or perhaps something
- # longer in the future.
- return substr(md5_hex($value), 0, $invocant->MAX_IDENTIFIER_LEN - 7);
+ my ($invocant, $value) = @_;
+
+ # We do -7 to allow prefixes like "idx_" or "fk_", or perhaps something
+ # longer in the future.
+ return substr(md5_hex($value), 0, $invocant->MAX_IDENTIFIER_LEN - 7);
}
sub get_add_fks_sql {
- my ($self, $table, $column_fks) = @_;
-
- my @add = $self->_column_fks_to_ddl($table, $column_fks);
-
- my @sql;
- if ($self->MULTIPLE_FKS_IN_ALTER) {
- my $alter = "ALTER TABLE $table ADD " . join(', ADD ', @add);
- push(@sql, $alter);
+ my ($self, $table, $column_fks) = @_;
+
+ my @add = $self->_column_fks_to_ddl($table, $column_fks);
+
+ my @sql;
+ if ($self->MULTIPLE_FKS_IN_ALTER) {
+ my $alter = "ALTER TABLE $table ADD " . join(', ADD ', @add);
+ push(@sql, $alter);
+ }
+ else {
+ foreach my $fk_string (@add) {
+ push(@sql, "ALTER TABLE $table ADD $fk_string");
}
- else {
- foreach my $fk_string (@add) {
- push(@sql, "ALTER TABLE $table ADD $fk_string");
- }
- }
- return @sql;
+ }
+ return @sql;
}
sub _column_fks_to_ddl {
- my ($self, $table, $column_fks) = @_;
- my @ddl;
- foreach my $column (keys %$column_fks) {
- my $def = $column_fks->{$column};
- my $fk_string = $self->get_fk_ddl($table, $column, $def);
- push(@ddl, $fk_string);
- }
- return @ddl;
+ my ($self, $table, $column_fks) = @_;
+ my @ddl;
+ foreach my $column (keys %$column_fks) {
+ my $def = $column_fks->{$column};
+ my $fk_string = $self->get_fk_ddl($table, $column, $def);
+ push(@ddl, $fk_string);
+ }
+ return @ddl;
}
sub get_drop_fk_sql {
- my ($self, $table, $column, $references) = @_;
- my $fk_name = $self->_get_fk_name($table, $column, $references);
+ 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 {
@@ -2161,8 +2124,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 {
@@ -2179,16 +2142,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 {
@@ -2203,8 +2166,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 {
@@ -2218,34 +2181,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 {
@@ -2262,30 +2224,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 {
@@ -2298,28 +2259,27 @@ 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;
}
@@ -2337,16 +2297,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 {
@@ -2365,22 +2326,25 @@ sub get_add_column_ddl {
=cut
- my ($self, $table, $column, $definition, $init_value) = @_;
- my @statements;
- push(@statements, "ALTER TABLE $table ". $self->ADD_COLUMN ." $column " .
- $self->get_type_ddl($definition));
-
- # XXX - Note that although this works for MySQL, most databases will fail
- # before this point, if we haven't set a default.
- (push(@statements, "UPDATE $table SET $column = $init_value"))
- if defined $init_value;
-
- if (defined $definition->{REFERENCES}) {
- push(@statements, $self->get_add_fks_sql($table, { $column =>
- $definition->{REFERENCES} }));
- }
-
- return (@statements);
+ my ($self, $table, $column, $definition, $init_value) = @_;
+ my @statements;
+ push(@statements,
+ "ALTER TABLE $table "
+ . $self->ADD_COLUMN
+ . " $column "
+ . $self->get_type_ddl($definition));
+
+ # XXX - Note that although this works for MySQL, most databases will fail
+ # before this point, if we haven't set a default.
+ (push(@statements, "UPDATE $table SET $column = $init_value"))
+ if defined $init_value;
+
+ if (defined $definition->{REFERENCES}) {
+ push(@statements,
+ $self->get_add_fks_sql($table, {$column => $definition->{REFERENCES}}));
+ }
+
+ return (@statements);
}
sub get_add_index_ddl {
@@ -2401,20 +2365,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 = '';
- }
+ my ($index_fields, $index_type);
- return $self->_get_create_index_ddl($table, $name, $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 {
@@ -2437,85 +2402,88 @@ sub get_alter_column_ddl {
=cut
- my $self = shift;
- my ($table, $column, $new_def, $set_nulls_to) = @_;
-
- my @statements;
- my $old_def = $self->get_column_abstract($table, $column);
- my $specific = $self->{db_specific};
-
- # If the types have changed, we have to deal with that.
- if (uc(trim($old_def->{TYPE})) ne uc(trim($new_def->{TYPE}))) {
- push(@statements, $self->_get_alter_type_sql($table, $column,
- $new_def, $old_def));
- }
-
- my $default = $new_def->{DEFAULT};
- my $default_old = $old_def->{DEFAULT};
-
- if (defined $default) {
- $default = $specific->{$default} if exists $specific->{$default};
- }
- # This first condition prevents "uninitialized value" errors.
- if (!defined $default && !defined $default_old) {
- # Do Nothing
- }
- # If we went from having a default to not having one
- elsif (!defined $default && defined $default_old) {
- push(@statements, "ALTER TABLE $table ALTER COLUMN $column"
- . " DROP DEFAULT");
- }
- # If we went from no default to a default, or we changed the default.
- elsif ( (defined $default && !defined $default_old) ||
- ($default ne $default_old) )
- {
- push(@statements, "ALTER TABLE $table ALTER COLUMN $column "
- . " SET DEFAULT $default");
- }
-
- # If we went from NULL to NOT NULL.
- if (!$old_def->{NOTNULL} && $new_def->{NOTNULL}) {
- push(@statements, $self->_set_nulls_sql(@_));
- push(@statements, "ALTER TABLE $table ALTER COLUMN $column"
- . " SET NOT NULL");
- }
- # If we went from NOT NULL to NULL
- elsif ($old_def->{NOTNULL} && !$new_def->{NOTNULL}) {
- push(@statements, "ALTER TABLE $table ALTER COLUMN $column"
- . " DROP NOT NULL");
- }
-
- # If we went from not being a PRIMARY KEY to being a PRIMARY KEY.
- if (!$old_def->{PRIMARYKEY} && $new_def->{PRIMARYKEY}) {
- push(@statements, "ALTER TABLE $table ADD PRIMARY KEY ($column)");
- }
- # If we went from being a PK to not being a PK
- elsif ( $old_def->{PRIMARYKEY} && !$new_def->{PRIMARYKEY} ) {
- push(@statements, "ALTER TABLE $table DROP PRIMARY KEY");
- }
-
- return @statements;
+ my $self = shift;
+ my ($table, $column, $new_def, $set_nulls_to) = @_;
+
+ my @statements;
+ my $old_def = $self->get_column_abstract($table, $column);
+ my $specific = $self->{db_specific};
+
+ # If the types have changed, we have to deal with that.
+ if (uc(trim($old_def->{TYPE})) ne uc(trim($new_def->{TYPE}))) {
+ push(@statements,
+ $self->_get_alter_type_sql($table, $column, $new_def, $old_def));
+ }
+
+ my $default = $new_def->{DEFAULT};
+ my $default_old = $old_def->{DEFAULT};
+
+ if (defined $default) {
+ $default = $specific->{$default} if exists $specific->{$default};
+ }
+
+ # This first condition prevents "uninitialized value" errors.
+ if (!defined $default && !defined $default_old) {
+
+ # Do Nothing
+ }
+
+ # If we went from having a default to not having one
+ elsif (!defined $default && defined $default_old) {
+ push(@statements, "ALTER TABLE $table ALTER COLUMN $column" . " DROP DEFAULT");
+ }
+
+ # If we went from no default to a default, or we changed the default.
+ elsif ((defined $default && !defined $default_old)
+ || ($default ne $default_old))
+ {
+ push(@statements,
+ "ALTER TABLE $table ALTER COLUMN $column " . " SET DEFAULT $default");
+ }
+
+ # If we went from NULL to NOT NULL.
+ if (!$old_def->{NOTNULL} && $new_def->{NOTNULL}) {
+ push(@statements, $self->_set_nulls_sql(@_));
+ push(@statements, "ALTER TABLE $table ALTER COLUMN $column" . " SET NOT NULL");
+ }
+
+ # If we went from NOT NULL to NULL
+ elsif ($old_def->{NOTNULL} && !$new_def->{NOTNULL}) {
+ push(@statements, "ALTER TABLE $table ALTER COLUMN $column" . " DROP NOT NULL");
+ }
+
+ # If we went from not being a PRIMARY KEY to being a PRIMARY KEY.
+ if (!$old_def->{PRIMARYKEY} && $new_def->{PRIMARYKEY}) {
+ push(@statements, "ALTER TABLE $table ADD PRIMARY KEY ($column)");
+ }
+
+ # If we went from being a PK to not being a PK
+ elsif ($old_def->{PRIMARYKEY} && !$new_def->{PRIMARYKEY}) {
+ push(@statements, "ALTER TABLE $table DROP PRIMARY KEY");
+ }
+
+ return @statements;
}
# Helps handle any fields that were NULL before, if we have a default,
# when doing an ALTER COLUMN.
sub _set_nulls_sql {
- my ($self, $table, $column, $new_def, $set_nulls_to) = @_;
- my $default = $new_def->{DEFAULT};
- # If we have a set_nulls_to, that overrides the DEFAULT
- # (although nobody would usually specify both a default and
- # a set_nulls_to.)
- $default = $set_nulls_to if defined $set_nulls_to;
- if (defined $default) {
- my $specific = $self->{db_specific};
- $default = $specific->{$default} if exists $specific->{$default};
- }
- my @sql;
- if (defined $default) {
- push(@sql, "UPDATE $table SET $column = $default"
- . " WHERE $column IS NULL");
- }
- return @sql;
+ my ($self, $table, $column, $new_def, $set_nulls_to) = @_;
+ my $default = $new_def->{DEFAULT};
+
+ # If we have a set_nulls_to, that overrides the DEFAULT
+ # (although nobody would usually specify both a default and
+ # a set_nulls_to.)
+ $default = $set_nulls_to if defined $set_nulls_to;
+ if (defined $default) {
+ my $specific = $self->{db_specific};
+ $default = $specific->{$default} if exists $specific->{$default};
+ }
+ my @sql;
+ if (defined $default) {
+ push(@sql, "UPDATE $table SET $column = $default" . " WHERE $column IS NULL");
+ }
+ return @sql;
}
sub get_drop_index_ddl {
@@ -2529,11 +2497,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 {
@@ -2547,8 +2515,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)>
@@ -2560,8 +2528,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 {
@@ -2579,8 +2547,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.";
}
@@ -2610,8 +2578,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)>
@@ -2624,13 +2592,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 {
@@ -2647,15 +2615,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)>
@@ -2673,29 +2641,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 {
@@ -2711,16 +2681,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)>
@@ -2734,8 +2704,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)>
@@ -2751,22 +2721,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>
@@ -2776,10 +2744,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 {
@@ -2794,17 +2762,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 {
@@ -2820,11 +2789,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 {
@@ -2845,10 +2814,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)>
@@ -2858,19 +2827,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 {
@@ -2891,36 +2861,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)>
@@ -2938,16 +2911,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 {
@@ -2965,24 +2939,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;
}
@@ -3006,18 +2980,18 @@ sub columns_equal {
=cut
sub serialize_abstract {
- my ($self) = @_;
+ my ($self) = @_;
- # Make it ok to eval
- local $Data::Dumper::Purity = 1;
+ # Make it ok to eval
+ local $Data::Dumper::Purity = 1;
- # Avoid cross-refs
- local $Data::Dumper::Deepcopy = 1;
+ # Avoid cross-refs
+ local $Data::Dumper::Deepcopy = 1;
- # Always sort keys to allow textual compare
- local $Data::Dumper::Sortkeys = 1;
+ # Always sort keys to allow textual compare
+ local $Data::Dumper::Sortkeys = 1;
- return Dumper($self->{abstract_schema});
+ return Dumper($self->{abstract_schema});
}
=item C<deserialize_abstract($serialized, $version)>
@@ -3036,36 +3010,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);
}
#####################################################################
@@ -3093,8 +3065,8 @@ object.
=cut
sub get_empty_schema {
- my ($class) = @_;
- return $class->deserialize_abstract(Dumper({}), SCHEMA_VERSION);
+ my ($class) = @_;
+ return $class->deserialize_abstract(Dumper({}), SCHEMA_VERSION);
}
1;
diff --git a/Bugzilla/DB/Schema/Mysql.pm b/Bugzilla/DB/Schema/Mysql.pm
index 79814140a..0b8ee59c3 100644
--- a/Bugzilla/DB/Schema/Mysql.pm
+++ b/Bugzilla/DB/Schema/Mysql.pm
@@ -34,198 +34,218 @@ use base qw(Bugzilla::DB::Schema);
# 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.
};
#------------------------------------------------------------------------------
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',
+ TEXT => 'text',
- TINYTEXT => 'tinytext',
- MEDIUMTEXT => 'mediumtext',
- LONGTEXT => 'mediumtext',
- TEXT => 'text',
+ LONGBLOB => 'longblob',
- LONGBLOB => 'longblob',
+ NATIVE_DATETIME => 'datetime',
+ DATETIME => 'timestamp',
+ DATE => 'date',
+ };
- NATIVE_DATETIME => 'datetime',
- DATETIME => 'timestamp',
- DATE => 'date',
- };
+ $self->_adjust_schema;
- $self->_adjust_schema;
+ return $self;
- return $self;
+} #eosub--_initialize
-} #eosub--_initialize
#------------------------------------------------------------------------------
sub _get_create_table_ddl {
- # Returns a "create table" SQL statement.
- my($self, $table) = @_;
- my $charset = Bugzilla::DB::Mysql->utf8_charset;
- my $collate = Bugzilla::DB::Mysql->utf8_collate;
- my $row_format = Bugzilla::DB::Mysql->default_row_format($table);
- my @parts = (
- $self->SUPER::_get_create_table_ddl($table),
- 'ENGINE = InnoDB',
- "CHARACTER SET $charset COLLATE $collate",
- "ROW_FORMAT=$row_format",
- );
- return join(' ', @parts);
-} #eosub--_get_create_table_ddl
+
+ # Returns a "create table" SQL statement.
+ my ($self, $table) = @_;
+ my $charset = Bugzilla::DB::Mysql->utf8_charset;
+ my $collate = Bugzilla::DB::Mysql->utf8_collate;
+ my $row_format = Bugzilla::DB::Mysql->default_row_format($table);
+ my @parts = (
+ $self->SUPER::_get_create_table_ddl($table), 'ENGINE = InnoDB',
+ "CHARACTER SET $charset COLLATE $collate", "ROW_FORMAT=$row_format",
+ );
+ return join(' ', @parts);
+} #eosub--_get_create_table_ddl
+
#------------------------------------------------------------------------------
sub _get_create_index_ddl {
- # Extend superclass method to create FULLTEXT indexes on text fields.
- # Returns a "create index" SQL statement.
- my($self, $table_name, $index_name, $index_fields, $index_type) = @_;
+ # Extend superclass method to create FULLTEXT indexes on text fields.
+ # Returns a "create index" SQL statement.
+
+ my ($self, $table_name, $index_name, $index_fields, $index_type) = @_;
- my $sql = "CREATE ";
- $sql .= "$index_type " if ($index_type eq 'UNIQUE'
- || $index_type eq 'FULLTEXT');
- $sql .= "INDEX \`$index_name\` ON $table_name \(" .
- join(", ", @$index_fields) . "\)";
+ my $sql = "CREATE ";
+ $sql .= "$index_type "
+ if ($index_type eq 'UNIQUE' || $index_type eq 'FULLTEXT');
+ $sql .= "INDEX \`$index_name\` ON $table_name \("
+ . join(", ", @$index_fields) . "\)";
- return($sql);
+ return ($sql);
+
+} #eosub--_get_create_index_ddl
-} #eosub--_get_create_index_ddl
#--------------------------------------------------------------------
sub get_create_database_sql {
- my ($self, $name) = @_;
- # We only create as utf8 if we have no params (meaning we're doing
- # a new installation) or if the utf8 param is on.
- my $charset = Bugzilla::DB::Mysql->utf8_charset;
- my $collate = Bugzilla::DB::Mysql->utf8_collate;
- return ("CREATE DATABASE $name CHARACTER SET $charset COLLATE $collate");
+ my ($self, $name) = @_;
+
+ # We only create as utf8 if we have no params (meaning we're doing
+ # a new installation) or if the utf8 param is on.
+ my $charset = Bugzilla::DB::Mysql->utf8_charset;
+ my $collate = Bugzilla::DB::Mysql->utf8_collate;
+ return ("CREATE DATABASE $name CHARACTER SET $charset COLLATE $collate");
}
# 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");
+ push(
+ @statements, "ALTER TABLE $table ALTER COLUMN $column
+ SET DEFAULT " . $new_def->{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");
- }
+ 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;
+ 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.
@@ -235,29 +255,31 @@ sub get_drop_index_ddl {
# 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.
@@ -265,145 +287,158 @@ 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+)?$/);
- $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+)?$/);
+ $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");
}
sub get_type_ddl {
- my $self = shift;
- my $type_ddl = $self->SUPER::get_type_ddl(@_);
-
- # TIMESTAMPS as of 5.6.6 still default to
- # 'NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'
- # unless explicitly setup in the table definition. This will change in future releases
- # and can be disabled by using 'explicit_defaults_for_timestamp = 1' in my.cnf.
- # So instead, we explicitly setup TIMESTAMP types to not be automatic.
- if ($type_ddl =~ /^timestamp/i) {
- if ($type_ddl !~ /NOT NULL/) {
- $type_ddl .= ' NULL DEFAULT NULL';
- }
- if ($type_ddl =~ /NOT NULL/ && $type_ddl !~ /DEFAULT/) {
- $type_ddl .= ' DEFAULT CURRENT_TIMESTAMP';
- }
+ my $self = shift;
+ my $type_ddl = $self->SUPER::get_type_ddl(@_);
+
+# TIMESTAMPS as of 5.6.6 still default to
+# 'NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'
+# unless explicitly setup in the table definition. This will change in future releases
+# and can be disabled by using 'explicit_defaults_for_timestamp = 1' in my.cnf.
+# So instead, we explicitly setup TIMESTAMP types to not be automatic.
+ if ($type_ddl =~ /^timestamp/i) {
+ if ($type_ddl !~ /NOT NULL/) {
+ $type_ddl .= ' NULL DEFAULT NULL';
+ }
+ if ($type_ddl =~ /NOT NULL/ && $type_ddl !~ /DEFAULT/) {
+ $type_ddl .= ' DEFAULT CURRENT_TIMESTAMP';
}
+ }
- return $type_ddl;
+ return $type_ddl;
}
1;
diff --git a/Bugzilla/DB/Schema/Oracle.pm b/Bugzilla/DB/Schema/Oracle.pm
index b67ddfd59..36f957820 100644
--- a/Bugzilla/DB/Schema/Oracle.pm
+++ b/Bugzilla/DB/Schema/Oracle.pm
@@ -21,8 +21,9 @@ use base 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,91 +32,95 @@ 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;
+
+} #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
@@ -124,30 +129,31 @@ sub get_drop_index_ddl {
# - 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,350 +164,370 @@ 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);
+ 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;
+ return $fk_name;
}
sub get_add_column_ddl {
- my $self = shift;
- my ($table, $column, $definition, $init_value) = @_;
- my @sql;
-
- # Create sequences and triggers to emulate SERIAL datatypes.
- if ($definition->{TYPE} =~ /SERIAL/i) {
- # Clone the definition to not alter the original one.
- my %def = %$definition;
- # Oracle requires to define the column is several steps.
- my $pk = delete $def{PRIMARYKEY};
- my $notnull = delete $def{NOTNULL};
- @sql = $self->SUPER::get_add_column_ddl($table, $column, \%def, $init_value);
- push(@sql, $self->_get_create_seq_ddl($table, $column));
- push(@sql, "UPDATE $table SET $column = ${table}_${column}_SEQ.NEXTVAL");
- push(@sql, "ALTER TABLE $table MODIFY $column NOT NULL") if $notnull;
- push(@sql, "ALTER TABLE $table ADD PRIMARY KEY ($column)") if $pk;
- }
- else {
- @sql = $self->SUPER::get_add_column_ddl(@_);
- # Create triggers to deal with empty string.
- if ($definition->{TYPE} =~ /varchar|TEXT/i && $definition->{NOTNULL}) {
- push(@sql, _get_notnull_trigger_ddl($table, $column));
- }
+ my $self = shift;
+ my ($table, $column, $definition, $init_value) = @_;
+ my @sql;
+
+ # Create sequences and triggers to emulate SERIAL datatypes.
+ if ($definition->{TYPE} =~ /SERIAL/i) {
+
+ # Clone the definition to not alter the original one.
+ my %def = %$definition;
+
+ # Oracle requires to define the column is several steps.
+ my $pk = delete $def{PRIMARYKEY};
+ my $notnull = delete $def{NOTNULL};
+ @sql = $self->SUPER::get_add_column_ddl($table, $column, \%def, $init_value);
+ push(@sql, $self->_get_create_seq_ddl($table, $column));
+ push(@sql, "UPDATE $table SET $column = ${table}_${column}_SEQ.NEXTVAL");
+ push(@sql, "ALTER TABLE $table MODIFY $column NOT NULL") if $notnull;
+ push(@sql, "ALTER TABLE $table ADD PRIMARY KEY ($column)") if $pk;
+ }
+ else {
+ @sql = $self->SUPER::get_add_column_ddl(@_);
+
+ # Create triggers to deal with empty string.
+ if ($definition->{TYPE} =~ /varchar|TEXT/i && $definition->{NOTNULL}) {
+ push(@sql, _get_notnull_trigger_ddl($table, $column));
}
+ }
- return @sql;
+ return @sql;
}
sub get_alter_column_ddl {
- my ($self, $table, $column, $new_def, $set_nulls_to) = @_;
-
- my @statements;
- my $old_def = $self->get_column_abstract($table, $column);
- my $specific = $self->{db_specific};
-
- # If the types have changed, we have to deal with that.
- if (uc(trim($old_def->{TYPE})) ne uc(trim($new_def->{TYPE}))) {
- push(@statements, $self->_get_alter_type_sql($table, $column,
- $new_def, $old_def));
- }
-
- my $default = $new_def->{DEFAULT};
- my $default_old = $old_def->{DEFAULT};
-
- if (defined $default) {
- $default = $specific->{$default} if exists $specific->{$default};
- }
- # This first condition prevents "uninitialized value" errors.
- if (!defined $default && !defined $default_old) {
- # Do Nothing
- }
- # If we went from having a default to not having one
- elsif (!defined $default && defined $default_old) {
- push(@statements, "ALTER TABLE $table MODIFY $column"
- . " DEFAULT NULL");
- }
- # If we went from no default to a default, or we changed the default.
- elsif ( (defined $default && !defined $default_old) ||
- ($default ne $default_old) )
- {
- push(@statements, "ALTER TABLE $table MODIFY $column "
- . " DEFAULT $default");
- }
-
- # If we went from NULL to NOT NULL.
- if (!$old_def->{NOTNULL} && $new_def->{NOTNULL}) {
- my $setdefault;
- # Handle any fields that were NULL before, if we have a default,
- $setdefault = $default if defined $default;
- # But if we have a set_nulls_to, that overrides the DEFAULT
- # (although nobody would usually specify both a default and
- # a set_nulls_to.)
- $setdefault = $set_nulls_to if defined $set_nulls_to;
- if (defined $setdefault) {
- push(@statements, "UPDATE $table SET $column = $setdefault"
- . " WHERE $column IS NULL");
- }
- push(@statements, "ALTER TABLE $table MODIFY $column"
- . " NOT NULL");
- push (@statements, _get_notnull_trigger_ddl($table, $column))
- if $old_def->{TYPE} =~ /varchar|text/i
- && $new_def->{TYPE} =~ /varchar|text/i;
- }
- # If we went from NOT NULL to NULL
- elsif ($old_def->{NOTNULL} && !$new_def->{NOTNULL}) {
- push(@statements, "ALTER TABLE $table MODIFY $column"
- . " NULL");
- push(@statements, "DROP TRIGGER ${table}_${column}")
- if $new_def->{TYPE} =~ /varchar|text/i
- && $old_def->{TYPE} =~ /varchar|text/i;
- }
-
- # If we went from not being a PRIMARY KEY to being a PRIMARY KEY.
- if (!$old_def->{PRIMARYKEY} && $new_def->{PRIMARYKEY}) {
- push(@statements, "ALTER TABLE $table ADD PRIMARY KEY ($column)");
- }
- # 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");
+ my ($self, $table, $column, $new_def, $set_nulls_to) = @_;
+
+ my @statements;
+ my $old_def = $self->get_column_abstract($table, $column);
+ my $specific = $self->{db_specific};
+
+ # If the types have changed, we have to deal with that.
+ if (uc(trim($old_def->{TYPE})) ne uc(trim($new_def->{TYPE}))) {
+ push(@statements,
+ $self->_get_alter_type_sql($table, $column, $new_def, $old_def));
+ }
+
+ my $default = $new_def->{DEFAULT};
+ my $default_old = $old_def->{DEFAULT};
+
+ if (defined $default) {
+ $default = $specific->{$default} if exists $specific->{$default};
+ }
+
+ # This first condition prevents "uninitialized value" errors.
+ if (!defined $default && !defined $default_old) {
+
+ # Do Nothing
+ }
+
+ # If we went from having a default to not having one
+ elsif (!defined $default && defined $default_old) {
+ push(@statements, "ALTER TABLE $table MODIFY $column" . " DEFAULT NULL");
+ }
+
+ # If we went from no default to a default, or we changed the default.
+ elsif ((defined $default && !defined $default_old)
+ || ($default ne $default_old))
+ {
+ push(@statements, "ALTER TABLE $table MODIFY $column " . " DEFAULT $default");
+ }
+
+ # If we went from NULL to NOT NULL.
+ if (!$old_def->{NOTNULL} && $new_def->{NOTNULL}) {
+ my $setdefault;
+
+ # Handle any fields that were NULL before, if we have a default,
+ $setdefault = $default if defined $default;
+
+ # But if we have a set_nulls_to, that overrides the DEFAULT
+ # (although nobody would usually specify both a default and
+ # a set_nulls_to.)
+ $setdefault = $set_nulls_to if defined $set_nulls_to;
+ if (defined $setdefault) {
+ push(@statements,
+ "UPDATE $table SET $column = $setdefault" . " WHERE $column IS NULL");
}
-
- 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};
- }
-
- 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));
+ 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 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;
+ my ($self, $table, $column, $value) = @_;
+ my @sql;
+ my $seq_name = "${table}_${column}_SEQ";
+ push(@sql, "DROP SEQUENCE ${seq_name}");
+ push(@sql, $self->_get_create_seq_ddl($table, $column, $value));
+ return @sql;
}
1;
diff --git a/Bugzilla/DB/Schema/Pg.pm b/Bugzilla/DB/Schema/Pg.pm
index 7606faa3d..8af1af8c0 100644
--- a/Bugzilla/DB/Schema/Pg.pm
+++ b/Bugzilla/DB/Schema/Pg.pm
@@ -23,169 +23,191 @@ use Storable qw(dclone);
#------------------------------------------------------------------------------
sub _initialize {
- my $self = shift;
-
- $self = $self->SUPER::_initialize(@_);
-
- # Remove FULLTEXT index types from the schemas.
- foreach my $table (keys %{ $self->{schema} }) {
- if ($self->{schema}{$table}{INDEXES}) {
- foreach my $index (@{ $self->{schema}{$table}{INDEXES} }) {
- if (ref($index) eq 'HASH') {
- delete($index->{TYPE}) if (exists $index->{TYPE}
- && $index->{TYPE} eq 'FULLTEXT');
- }
- }
- foreach my $index (@{ $self->{abstract_schema}{$table}{INDEXES} }) {
- if (ref($index) eq 'HASH') {
- delete($index->{TYPE}) if (exists $index->{TYPE}
- && $index->{TYPE} eq 'FULLTEXT');
- }
- }
+ my $self = shift;
+
+ $self = $self->SUPER::_initialize(@_);
+
+ # Remove FULLTEXT index types from the schemas.
+ foreach my $table (keys %{$self->{schema}}) {
+ if ($self->{schema}{$table}{INDEXES}) {
+ foreach my $index (@{$self->{schema}{$table}{INDEXES}}) {
+ if (ref($index) eq 'HASH') {
+ delete($index->{TYPE})
+ if (exists $index->{TYPE} && $index->{TYPE} eq 'FULLTEXT');
+ }
+ }
+ foreach my $index (@{$self->{abstract_schema}{$table}{INDEXES}}) {
+ if (ref($index) eq 'HASH') {
+ delete($index->{TYPE})
+ if (exists $index->{TYPE} && $index->{TYPE} eq 'FULLTEXT');
}
+ }
}
+ }
- $self->{db_specific} = {
+ $self->{db_specific} = {
- BOOLEAN => 'smallint',
- FALSE => '0',
- TRUE => '1',
+ BOOLEAN => 'smallint',
+ FALSE => '0',
+ TRUE => '1',
- INT1 => 'integer',
- INT2 => 'integer',
- INT3 => 'integer',
- INT4 => 'integer',
+ INT1 => 'integer',
+ INT2 => 'integer',
+ INT3 => 'integer',
+ INT4 => 'integer',
- SMALLSERIAL => 'serial unique',
- MEDIUMSERIAL => 'serial unique',
- INTSERIAL => 'serial unique',
+ SMALLSERIAL => 'serial unique',
+ MEDIUMSERIAL => 'serial unique',
+ INTSERIAL => 'serial unique',
- TINYTEXT => 'varchar(255)',
- MEDIUMTEXT => 'text',
- LONGTEXT => 'text',
+ TINYTEXT => 'varchar(255)',
+ MEDIUMTEXT => 'text',
+ LONGTEXT => 'text',
- LONGBLOB => 'bytea',
+ LONGBLOB => 'bytea',
- DATETIME => 'timestamp(0) without time zone',
- DATE => 'date',
- };
+ DATETIME => 'timestamp(0) without time zone',
+ DATE => 'date',
+ };
- $self->_adjust_schema;
+ $self->_adjust_schema;
- return $self;
+ return $self;
+
+} #eosub--_initialize
-} #eosub--_initialize
#--------------------------------------------------------------------
sub get_create_database_sql {
- my ($self, $name) = @_;
- # We only create as utf8 if we have no params (meaning we're doing
- # a new installation) or if the utf8 param is on.
- my $create_utf8 = Bugzilla->params->{'utf8'}
- || !defined Bugzilla->params->{'utf8'};
- my $charset = $create_utf8 ? "ENCODING 'UTF8' TEMPLATE template0" : '';
- return ("CREATE DATABASE $name $charset");
+ my ($self, $name) = @_;
+
+ # We only create as utf8 if we have no params (meaning we're doing
+ # a new installation) or if the utf8 param is on.
+ my $create_utf8
+ = Bugzilla->params->{'utf8'} || !defined Bugzilla->params->{'utf8'};
+ my $charset = $create_utf8 ? "ENCODING 'UTF8' TEMPLATE template0" : '';
+ return ("CREATE DATABASE $name $charset");
}
sub get_rename_column_ddl {
- my ($self, $table, $old_name, $new_name) = @_;
- if (lc($old_name) eq lc($new_name)) {
- # if the only change is a case change, return an empty list, since Pg
- # is case-insensitive and will return an error about a duplicate name
- return ();
- }
- my @sql = ("ALTER TABLE $table RENAME COLUMN $old_name TO $new_name");
- my $def = $self->get_column_abstract($table, $old_name);
- if ($def->{TYPE} =~ /SERIAL/i) {
- # We have to rename the series also.
- push(@sql, "ALTER SEQUENCE ${table}_${old_name}_seq
- RENAME TO ${table}_${new_name}_seq");
- }
- return @sql;
+ my ($self, $table, $old_name, $new_name) = @_;
+ if (lc($old_name) eq lc($new_name)) {
+
+ # if the only change is a case change, return an empty list, since Pg
+ # is case-insensitive and will return an error about a duplicate name
+ return ();
+ }
+ my @sql = ("ALTER TABLE $table RENAME COLUMN $old_name TO $new_name");
+ my $def = $self->get_column_abstract($table, $old_name);
+ if ($def->{TYPE} =~ /SERIAL/i) {
+
+ # We have to rename the series also.
+ push(
+ @sql, "ALTER SEQUENCE ${table}_${old_name}_seq
+ RENAME TO ${table}_${new_name}_seq"
+ );
+ }
+ return @sql;
}
sub get_rename_table_sql {
- my ($self, $old_name, $new_name) = @_;
- if (lc($old_name) eq lc($new_name)) {
- # if the only change is a case change, return an empty list, since Pg
- # is case-insensitive and will return an error about a duplicate name
- return ();
+ my ($self, $old_name, $new_name) = @_;
+ if (lc($old_name) eq lc($new_name)) {
+
+ # if the only change is a case change, return an empty list, since Pg
+ # is case-insensitive and will return an error about a duplicate name
+ return ();
+ }
+
+ my @sql = ("ALTER TABLE $old_name RENAME TO $new_name");
+
+ # If there's a SERIAL column on this table, we also need to rename the
+ # sequence.
+ # If there is a PRIMARY KEY, we need to rename it too.
+ my @columns = $self->get_table_columns($old_name);
+ foreach my $column (@columns) {
+ my $def = $self->get_column_abstract($old_name, $column);
+ if ($def->{TYPE} =~ /SERIAL/i) {
+ my $old_seq = "${old_name}_${column}_seq";
+ my $new_seq = "${new_name}_${column}_seq";
+ push(@sql, "ALTER SEQUENCE $old_seq RENAME TO $new_seq");
+ push(
+ @sql, "ALTER TABLE $new_name ALTER COLUMN $column
+ SET DEFAULT NEXTVAL('$new_seq')"
+ );
}
-
- my @sql = ("ALTER TABLE $old_name RENAME TO $new_name");
-
- # If there's a SERIAL column on this table, we also need to rename the
- # sequence.
- # If there is a PRIMARY KEY, we need to rename it too.
- my @columns = $self->get_table_columns($old_name);
- foreach my $column (@columns) {
- my $def = $self->get_column_abstract($old_name, $column);
- if ($def->{TYPE} =~ /SERIAL/i) {
- my $old_seq = "${old_name}_${column}_seq";
- my $new_seq = "${new_name}_${column}_seq";
- push(@sql, "ALTER SEQUENCE $old_seq RENAME TO $new_seq");
- push(@sql, "ALTER TABLE $new_name ALTER COLUMN $column
- SET DEFAULT NEXTVAL('$new_seq')");
- }
- if ($def->{PRIMARYKEY}) {
- my $old_pk = "${old_name}_pkey";
- my $new_pk = "${new_name}_pkey";
- push(@sql, "ALTER INDEX $old_pk RENAME to $new_pk");
- }
+ if ($def->{PRIMARYKEY}) {
+ my $old_pk = "${old_name}_pkey";
+ my $new_pk = "${new_name}_pkey";
+ push(@sql, "ALTER INDEX $old_pk RENAME to $new_pk");
}
+ }
- return @sql;
+ return @sql;
}
sub get_set_serial_sql {
- my ($self, $table, $column, $value) = @_;
- return ("SELECT setval('${table}_${column}_seq', $value, false)
- FROM $table");
+ my ($self, $table, $column, $value) = @_;
+ return (
+ "SELECT setval('${table}_${column}_seq', $value, false)
+ FROM $table"
+ );
}
sub _get_alter_type_sql {
- my ($self, $table, $column, $new_def, $old_def) = @_;
- my @statements;
-
- my $type = $new_def->{TYPE};
- $type = $self->{db_specific}->{$type}
- if exists $self->{db_specific}->{$type};
-
- if ($type =~ /serial/i && $old_def->{TYPE} !~ /serial/i) {
- die("You cannot specify a DEFAULT on a SERIAL-type column.")
- if $new_def->{DEFAULT};
- }
-
- $type =~ s/\bserial\b/integer/i;
-
- # On Pg, you don't need UNIQUE if you're a PK--it creates
- # two identical indexes otherwise.
- $type =~ s/unique//i if $new_def->{PRIMARYKEY};
-
- push(@statements, "ALTER TABLE $table ALTER COLUMN $column
- TYPE $type");
-
- if ($new_def->{TYPE} =~ /serial/i && $old_def->{TYPE} !~ /serial/i) {
- push(@statements, "CREATE SEQUENCE ${table}_${column}_seq
- OWNED BY $table.$column");
- push(@statements, "SELECT setval('${table}_${column}_seq',
+ my ($self, $table, $column, $new_def, $old_def) = @_;
+ my @statements;
+
+ my $type = $new_def->{TYPE};
+ $type = $self->{db_specific}->{$type} if exists $self->{db_specific}->{$type};
+
+ if ($type =~ /serial/i && $old_def->{TYPE} !~ /serial/i) {
+ die("You cannot specify a DEFAULT on a SERIAL-type column.")
+ if $new_def->{DEFAULT};
+ }
+
+ $type =~ s/\bserial\b/integer/i;
+
+ # On Pg, you don't need UNIQUE if you're a PK--it creates
+ # two identical indexes otherwise.
+ $type =~ s/unique//i if $new_def->{PRIMARYKEY};
+
+ push(
+ @statements, "ALTER TABLE $table ALTER COLUMN $column
+ TYPE $type"
+ );
+
+ if ($new_def->{TYPE} =~ /serial/i && $old_def->{TYPE} !~ /serial/i) {
+ push(
+ @statements, "CREATE SEQUENCE ${table}_${column}_seq
+ OWNED BY $table.$column"
+ );
+ push(
+ @statements, "SELECT setval('${table}_${column}_seq',
MAX($table.$column))
- FROM $table");
- push(@statements, "ALTER TABLE $table ALTER COLUMN $column
- SET DEFAULT nextval('${table}_${column}_seq')");
- }
-
- # If this column is no longer SERIAL, we need to drop the sequence
- # that went along with it.
- if ($old_def->{TYPE} =~ /serial/i && $new_def->{TYPE} !~ /serial/i) {
- push(@statements, "ALTER TABLE $table ALTER COLUMN $column
- DROP DEFAULT");
- push(@statements, "ALTER SEQUENCE ${table}_${column}_seq
- OWNED BY NONE");
- push(@statements, "DROP SEQUENCE ${table}_${column}_seq");
- }
-
- return @statements;
+ FROM $table"
+ );
+ push(
+ @statements, "ALTER TABLE $table ALTER COLUMN $column
+ SET DEFAULT nextval('${table}_${column}_seq')"
+ );
+ }
+
+ # If this column is no longer SERIAL, we need to drop the sequence
+ # that went along with it.
+ if ($old_def->{TYPE} =~ /serial/i && $new_def->{TYPE} !~ /serial/i) {
+ push(
+ @statements, "ALTER TABLE $table ALTER COLUMN $column
+ DROP DEFAULT"
+ );
+ push(
+ @statements, "ALTER SEQUENCE ${table}_${column}_seq
+ OWNED BY NONE"
+ );
+ push(@statements, "DROP SEQUENCE ${table}_${column}_seq");
+ }
+
+ return @statements;
}
1;
diff --git a/Bugzilla/DB/Schema/Sqlite.pm b/Bugzilla/DB/Schema/Sqlite.pm
index 6d524db59..1bd53515a 100644
--- a/Bugzilla/DB/Schema/Sqlite.pm
+++ b/Bugzilla/DB/Schema/Sqlite.pm
@@ -22,37 +22,37 @@ use constant FK_ON_CREATE => 1;
sub _initialize {
- my $self = shift;
+ my $self = shift;
- $self = $self->SUPER::_initialize(@_);
+ $self = $self->SUPER::_initialize(@_);
- $self->{db_specific} = {
- BOOLEAN => 'integer',
- FALSE => '0',
- TRUE => '1',
+ $self->{db_specific} = {
+ BOOLEAN => 'integer',
+ FALSE => '0',
+ TRUE => '1',
- INT1 => 'integer',
- INT2 => 'integer',
- INT3 => 'integer',
- INT4 => 'integer',
+ INT1 => 'integer',
+ INT2 => 'integer',
+ INT3 => 'integer',
+ INT4 => 'integer',
- SMALLSERIAL => 'SERIAL',
- MEDIUMSERIAL => 'SERIAL',
- INTSERIAL => 'SERIAL',
+ SMALLSERIAL => 'SERIAL',
+ MEDIUMSERIAL => 'SERIAL',
+ INTSERIAL => 'SERIAL',
- TINYTEXT => 'text',
- MEDIUMTEXT => 'text',
- LONGTEXT => 'text',
+ TINYTEXT => 'text',
+ MEDIUMTEXT => 'text',
+ LONGTEXT => 'text',
- LONGBLOB => 'blob',
+ LONGBLOB => 'blob',
- DATETIME => 'DATETIME',
- DATE => 'DATETIME',
- };
+ DATETIME => 'DATETIME',
+ DATE => 'DATETIME',
+ };
- $self->_adjust_schema;
+ $self->_adjust_schema;
- return $self;
+ return $self;
}
@@ -61,83 +61,86 @@ sub _initialize {
#################################
sub _sqlite_create_table {
- my ($self, $table) = @_;
- return scalar Bugzilla->dbh->selectrow_array(
- "SELECT sql FROM sqlite_master WHERE name = ? AND type = 'table'",
- undef, $table);
+ my ($self, $table) = @_;
+ return
+ scalar Bugzilla->dbh->selectrow_array(
+ "SELECT sql FROM sqlite_master WHERE name = ? AND type = 'table'",
+ undef, $table);
}
sub _sqlite_table_lines {
- my $self = shift;
- my $table_sql = $self->_sqlite_create_table(@_);
- $table_sql =~ s/\n*\)$//s;
- # The $ makes this work even if people some day add crazy stuff to their
- # schema like multi-column foreign keys.
- return split(/,\s*$/m, $table_sql);
+ my $self = shift;
+ my $table_sql = $self->_sqlite_create_table(@_);
+ $table_sql =~ s/\n*\)$//s;
+
+ # The $ makes this work even if people some day add crazy stuff to their
+ # schema like multi-column foreign keys.
+ return split(/,\s*$/m, $table_sql);
}
# This does most of the "heavy lifting" of the schema-altering functions.
sub _sqlite_alter_schema {
- my ($self, $table, $create_table, $options) = @_;
-
- # $create_table is sometimes an array in the form that _sqlite_table_lines
- # returns.
- if (ref $create_table) {
- $create_table = join(',', @$create_table) . "\n)";
- }
-
- my $dbh = Bugzilla->dbh;
-
- my $random = generate_random_password(5);
- my $rename_to = "${table}_$random";
-
- my @columns = $dbh->bz_table_columns_real($table);
- push(@columns, $options->{extra_column}) if $options->{extra_column};
- if (my $exclude = $options->{exclude_column}) {
- @columns = grep { $_ ne $exclude } @columns;
+ my ($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_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";
}
+ 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(@_);
+ 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 ($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;
+ 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);
+ return $self->_sqlite_alter_schema($table, \@clauses);
}
diff --git a/Bugzilla/DB/Sqlite.pm b/Bugzilla/DB/Sqlite.pm
index 81ee7d888..7a97ad06a 100644
--- a/Bugzilla/DB/Sqlite.pm
+++ b/Bugzilla/DB/Sqlite.pm
@@ -45,23 +45,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));
}
###############
@@ -69,73 +69,80 @@ sub _sqlite_position_ci {
###############
sub BUILDARGS {
- my ($class, $params) = @_;
- my $db_name = $params->{db_name};
-
- # Let people specify paths intead of data/ for the DB.
- if ($db_name && $db_name ne ':memory:' && $db_name !~ m{[\\/]}) {
- # When the DB is first created, there's a chance that the
- # data directory doesn't exist at all, because the Install::Filesystem
- # code happens after DB creation. So we create the directory ourselves
- # if it doesn't exist.
- my $datadir = bz_locations()->{datadir};
- if (!-d $datadir) {
- mkdir $datadir or warn "$datadir: $!";
- }
- if (!-d "$datadir/db/") {
- mkdir "$datadir/db/" or warn "$datadir/db: $!";
- }
- $db_name = bz_locations()->{datadir} . "/db/$db_name";
+ my ($class, $params) = @_;
+ my $db_name = $params->{db_name};
+
+ # Let people specify paths intead of data/ for the DB.
+ if ($db_name && $db_name ne ':memory:' && $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";
+ }
+
+ # construct the DSN from the parameters we got
+ my $dsn = "dbi:SQLite:dbname=$db_name";
- # construct the DSN from the parameters we got
- my $dsn = "dbi:SQLite:dbname=$db_name";
+ my $attrs = {
- my $attrs = {
- # XXX Should we just enforce this to be always on?
- sqlite_unicode => Bugzilla->params->{'utf8'},
- };
+ # XXX Should we just enforce this to be always on?
+ sqlite_unicode => Bugzilla->params->{'utf8'},
+ };
- return { dsn => $dsn, user => '', pass => '', attrs => $attrs };
+ return {dsn => $dsn, user => '', pass => '', attrs => $attrs};
}
sub on_dbi_connected {
- my ($class, $dbh) = @_;
-
- 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) {
- $dbh->do("PRAGMA $name = $value");
- }
-
- $dbh->sqlite_create_collation('bugzilla', \&_sqlite_collate_ci);
- $dbh->sqlite_create_function('position', 2, \&_sqlite_position);
- $dbh->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".
- $dbh->sqlite_create_function('substring', 3, \&CORE::substr);
- $dbh->sqlite_create_function('mod', 2, \&_sqlite_mod);
- $dbh->sqlite_create_function('now', 0, \&_sqlite_now);
- $dbh->sqlite_create_function('localtimestamp', 1, \&_sqlite_now);
- $dbh->sqlite_create_function('floor', 1, \&POSIX::floor);
+ my ($class, $dbh) = @_;
+
+ 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) {
+ $dbh->do("PRAGMA $name = $value");
+ }
+
+ $dbh->sqlite_create_collation('bugzilla', \&_sqlite_collate_ci);
+ $dbh->sqlite_create_function('position', 2, \&_sqlite_position);
+ $dbh->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".
+ $dbh->sqlite_create_function('substring', 3, \&CORE::substr);
+ $dbh->sqlite_create_function('mod', 2, \&_sqlite_mod);
+ $dbh->sqlite_create_function('now', 0, \&_sqlite_now);
+ $dbh->sqlite_create_function('localtimestamp', 1, \&_sqlite_now);
+ $dbh->sqlite_create_function('floor', 1, \&POSIX::floor);
}
###############
@@ -143,85 +150,88 @@ sub on_dbi_connected {
###############
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;
- return "STRFTIME(" . $self->quote($format) . ", $date)";
+ my ($self, $date, $format) = @_;
+ $format = "%Y.%m.%d %H:%M:%S" if !$format;
+ $format =~ s/\%i/\%M/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')";
}
###############
@@ -229,56 +239,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;
diff --git a/Bugzilla/DaemonControl.pm b/Bugzilla/DaemonControl.pm
index 5cb32973f..4f3c624af 100644
--- a/Bugzilla/DaemonControl.pm
+++ b/Bugzilla/DaemonControl.pm
@@ -29,256 +29,250 @@ use POSIX qw(WEXITSTATUS);
use base qw(Exporter);
our @EXPORT_OK = qw(
- run_httpd run_cereal run_jobqueue
- run_cereal_and_httpd run_cereal_and_jobqueue
- catch_signal on_finish on_exception
- assert_httpd assert_database assert_selenium
+ run_httpd run_cereal run_jobqueue
+ run_cereal_and_httpd run_cereal_and_jobqueue
+ catch_signal on_finish on_exception
+ assert_httpd assert_database assert_selenium
);
our %EXPORT_TAGS = (
- all => \@EXPORT_OK,
- run => [grep { /^run_/ } @EXPORT_OK],
- utils => [qw(catch_signal on_exception on_finish)],
+ all => \@EXPORT_OK,
+ run => [grep {/^run_/} @EXPORT_OK],
+ utils => [qw(catch_signal on_exception on_finish)],
);
my $BUGZILLA_DIR = bz_locations->{cgi_path};
-my $JOBQUEUE_BIN = catfile( $BUGZILLA_DIR, 'jobqueue.pl' );
-my $CEREAL_BIN = catfile( $BUGZILLA_DIR, 'scripts', 'cereal.pl' );
-my $BUGZILLA_BIN = catfile( $BUGZILLA_DIR, 'bugzilla.pl' );
-my $HYPNOTOAD_BIN = catfile( $BUGZILLA_DIR, 'local', 'bin', 'hypnotoad' );
-my @PERL5LIB = ( $BUGZILLA_DIR, catdir($BUGZILLA_DIR, 'lib'), catdir($BUGZILLA_DIR, 'local', 'lib', 'perl5') );
+my $JOBQUEUE_BIN = catfile($BUGZILLA_DIR, 'jobqueue.pl');
+my $CEREAL_BIN = catfile($BUGZILLA_DIR, 'scripts', 'cereal.pl');
+my $BUGZILLA_BIN = catfile($BUGZILLA_DIR, 'bugzilla.pl');
+my $HYPNOTOAD_BIN = catfile($BUGZILLA_DIR, 'local', 'bin', 'hypnotoad');
+my @PERL5LIB = (
+ $BUGZILLA_DIR,
+ catdir($BUGZILLA_DIR, 'lib'),
+ catdir($BUGZILLA_DIR, 'local', 'lib', 'perl5')
+);
my %HTTP_BACKENDS = (
- hypnotoad => [ $HYPNOTOAD_BIN, $BUGZILLA_BIN, '-f' ],
- simple => [ $BUGZILLA_BIN, 'daemon' ],
+ hypnotoad => [$HYPNOTOAD_BIN, $BUGZILLA_BIN, '-f'],
+ simple => [$BUGZILLA_BIN, 'daemon'],
);
sub catch_signal {
- my ($name, @done) = @_;
- my $loop = IO::Async::Loop->new;
- my $signal_f = $loop->new_future;
- my $signal = IO::Async::Signal->new(
- name => $name,
- on_receipt => sub {
- my ($self) = @_;
- my $l = IO::Async::Loop->new;
- $signal_f->done(@done);
- $l->remove($self);
- }
- );
- $signal_f->on_cancel(
- sub {
- my $l = IO::Async::Loop->new;
- $l->remove($signal);
- },
- );
+ my ($name, @done) = @_;
+ my $loop = IO::Async::Loop->new;
+ my $signal_f = $loop->new_future;
+ my $signal = IO::Async::Signal->new(
+ name => $name,
+ on_receipt => sub {
+ my ($self) = @_;
+ my $l = IO::Async::Loop->new;
+ $signal_f->done(@done);
+ $l->remove($self);
+ }
+ );
+ $signal_f->on_cancel(
+ sub {
+ my $l = IO::Async::Loop->new;
+ $l->remove($signal);
+ },
+ );
- $loop->add($signal);
+ $loop->add($signal);
- return $signal_f;
+ return $signal_f;
}
sub run_cereal {
- my $loop = IO::Async::Loop->new;
- my $exit_f = $loop->new_future;
- my $cereal = IO::Async::Process->new(
- command => [$CEREAL_BIN],
- on_finish => on_finish($exit_f),
- on_exception => on_exception( 'cereal', $exit_f ),
- );
- $exit_f->on_cancel( sub { $cereal->kill('TERM') } );
- $exit_f->on_ready(
- sub {
- delete $ENV{LOG4PERL_STDERR_DISABLE};
- }
- );
- $loop->add($cereal);
- $ENV{LOG4PERL_STDERR_DISABLE} = 1;
-
- return $exit_f;
+ my $loop = IO::Async::Loop->new;
+ my $exit_f = $loop->new_future;
+ my $cereal = IO::Async::Process->new(
+ command => [$CEREAL_BIN],
+ on_finish => on_finish($exit_f),
+ on_exception => on_exception('cereal', $exit_f),
+ );
+ $exit_f->on_cancel(sub { $cereal->kill('TERM') });
+ $exit_f->on_ready(sub {
+ delete $ENV{LOG4PERL_STDERR_DISABLE};
+ });
+ $loop->add($cereal);
+ $ENV{LOG4PERL_STDERR_DISABLE} = 1;
+
+ return $exit_f;
}
sub run_httpd {
- my (@args) = @_;
-
- my $loop = IO::Async::Loop->new;
- my $exit_f = $loop->new_future;
- my $httpd = IO::Async::Process->new(
- code => sub {
- $ENV{BUGZILLA_HTTPD_ARGS} = encode_json(\@args);
- $ENV{PERL5LIB} = join(':', @PERL5LIB);
- my $backend = $ENV{HTTP_BACKEND} // 'hypnotoad';
- my $command = $HTTP_BACKENDS{ $backend };
- exec @$command
- or die "failed to exec $command->[0] $!";
- },
- on_finish => on_finish($exit_f),
- on_exception => on_exception( 'httpd', $exit_f ),
- );
- $exit_f->on_cancel( sub { $httpd->kill('TERM') } );
- $loop->add($httpd);
-
- return $exit_f;
+ my (@args) = @_;
+
+ my $loop = IO::Async::Loop->new;
+ my $exit_f = $loop->new_future;
+ my $httpd = IO::Async::Process->new(
+ code => sub {
+ $ENV{BUGZILLA_HTTPD_ARGS} = encode_json(\@args);
+ $ENV{PERL5LIB} = join(':', @PERL5LIB);
+ my $backend = $ENV{HTTP_BACKEND} // 'hypnotoad';
+ my $command = $HTTP_BACKENDS{$backend};
+ exec @$command or die "failed to exec $command->[0] $!";
+ },
+ on_finish => on_finish($exit_f),
+ on_exception => on_exception('httpd', $exit_f),
+ );
+ $exit_f->on_cancel(sub { $httpd->kill('TERM') });
+ $loop->add($httpd);
+
+ return $exit_f;
}
sub run_jobqueue {
- my (@args) = @_;
-
- my $loop = IO::Async::Loop->new;
- my $exit_f = $loop->new_future;
- my $jobqueue = IO::Async::Process->new(
- command => [ $JOBQUEUE_BIN, 'start', '-f', '-d', @args ],
- on_finish => on_finish($exit_f),
- on_exception => on_exception( 'httpd', $exit_f ),
- );
- $exit_f->on_cancel( sub { $jobqueue->kill('TERM') } );
- $loop->add($jobqueue);
-
- return $exit_f;
+ my (@args) = @_;
+
+ my $loop = IO::Async::Loop->new;
+ my $exit_f = $loop->new_future;
+ my $jobqueue = IO::Async::Process->new(
+ command => [$JOBQUEUE_BIN, 'start', '-f', '-d', @args],
+ on_finish => on_finish($exit_f),
+ on_exception => on_exception('httpd', $exit_f),
+ );
+ $exit_f->on_cancel(sub { $jobqueue->kill('TERM') });
+ $loop->add($jobqueue);
+
+ return $exit_f;
}
sub run_cereal_and_jobqueue {
- my (@jobqueue_args) = @_;
+ my (@jobqueue_args) = @_;
- my $signal_f = catch_signal('TERM', 0);
- my $cereal_exit_f = run_cereal();
+ my $signal_f = catch_signal('TERM', 0);
+ my $cereal_exit_f = run_cereal();
- return assert_cereal()->then(
- sub {
- my $jobqueue_exit_f = run_jobqueue(@jobqueue_args);
- return Future->wait_any($cereal_exit_f, $jobqueue_exit_f, $signal_f);
- }
- );
+ return assert_cereal()->then(sub {
+ my $jobqueue_exit_f = run_jobqueue(@jobqueue_args);
+ return Future->wait_any($cereal_exit_f, $jobqueue_exit_f, $signal_f);
+ });
}
sub run_cereal_and_httpd {
- my @httpd_args = @_;
-
- my $signal_f = catch_signal('TERM', 0);
- my $cereal_exit_f = run_cereal();
-
- return assert_cereal()->then(
- sub {
- push @httpd_args, '-DNETCAT_LOGS';
-
- my $lc = Bugzilla::Install::Localconfig::read_localconfig();
- if ( ($lc->{inbound_proxies} // '') eq '*' && $lc->{urlbase} =~ /^https/) {
- push @httpd_args, '-DHTTPS';
- }
- elsif ($lc->{urlbase} =~ /^https/) {
- WARN('HTTPS urlbase but inbound_proxies is not "*"');
- }
- my $httpd_exit_f = run_httpd(@httpd_args);
-
- return Future->wait_any($cereal_exit_f, $httpd_exit_f, $signal_f);
- }
- );
+ my @httpd_args = @_;
+
+ my $signal_f = catch_signal('TERM', 0);
+ my $cereal_exit_f = run_cereal();
+
+ return assert_cereal()->then(sub {
+ push @httpd_args, '-DNETCAT_LOGS';
+
+ my $lc = Bugzilla::Install::Localconfig::read_localconfig();
+ if (($lc->{inbound_proxies} // '') eq '*' && $lc->{urlbase} =~ /^https/) {
+ push @httpd_args, '-DHTTPS';
+ }
+ elsif ($lc->{urlbase} =~ /^https/) {
+ WARN('HTTPS urlbase but inbound_proxies is not "*"');
+ }
+ my $httpd_exit_f = run_httpd(@httpd_args);
+
+ return Future->wait_any($cereal_exit_f, $httpd_exit_f, $signal_f);
+ });
}
sub assert_httpd {
- my $loop = IO::Async::Loop->new;
- my $port = $ENV{PORT} // 8000;
- my $repeat = repeat {
- $loop->delay_future(after => 0.25)->then(
- sub {
- Future->wrap(get("http://localhost:$port/__lbheartbeat__") // '');
- },
- );
- } until => sub {
- my $f = shift;
- ( $f->get =~ /^httpd OK/ );
- };
- my $timeout = $loop->timeout_future(after => 20)->else_fail('assert_httpd timeout');
- return Future->wait_any($repeat, $timeout);
+ my $loop = IO::Async::Loop->new;
+ my $port = $ENV{PORT} // 8000;
+ my $repeat = repeat {
+ $loop->delay_future(after => 0.25)->then(
+ sub {
+ Future->wrap(get("http://localhost:$port/__lbheartbeat__") // '');
+ },
+ );
+ }
+ until => sub {
+ my $f = shift;
+ ($f->get =~ /^httpd OK/);
+ };
+ my $timeout
+ = $loop->timeout_future(after => 20)->else_fail('assert_httpd timeout');
+ return Future->wait_any($repeat, $timeout);
}
sub assert_selenium {
- my ($host, $port) = @_;
- $host //= 'localhost';
- $port //= 4444;
+ my ($host, $port) = @_;
+ $host //= 'localhost';
+ $port //= 4444;
- return assert_connect($host, $port, 'assert_selenium');
+ return assert_connect($host, $port, 'assert_selenium');
}
sub assert_cereal {
- return assert_connect(
- 'localhost',
- $ENV{LOGGING_PORT} // 5880,
- 'assert_cereal'
- );
+ return assert_connect('localhost', $ENV{LOGGING_PORT} // 5880, 'assert_cereal');
}
sub assert_connect {
- my ($host, $port, $name) = @_;
- my $loop = IO::Async::Loop->new;
- my $repeat = repeat {
- $loop->delay_future(after => 1)->then(
- sub {
- my $sock = IO::Socket::INET->new( PeerAddr => $host, PeerPort => $port );
- Future->wrap($sock ? 1 : 0);
- },
- );
- } until => sub { shift->get };
- my $timeout = $loop->timeout_future(after => 60)->else_fail("$name timeout");
- return Future->wait_any($repeat, $timeout);
+ my ($host, $port, $name) = @_;
+ my $loop = IO::Async::Loop->new;
+ my $repeat = repeat {
+ $loop->delay_future(after => 1)->then(
+ sub {
+ my $sock = IO::Socket::INET->new(PeerAddr => $host, PeerPort => $port);
+ Future->wrap($sock ? 1 : 0);
+ },
+ );
+ }
+ until => sub { shift->get };
+ my $timeout = $loop->timeout_future(after => 60)->else_fail("$name timeout");
+ return Future->wait_any($repeat, $timeout);
}
sub assert_database {
- my $loop = IO::Async::Loop->new;
- my $lc = Bugzilla::Install::Localconfig::read_localconfig();
-
- for my $var (qw(db_name db_host db_user db_pass)) {
- return $loop->new_future->die("$var is not set!") unless $lc->{$var};
- }
-
- my $dsn = "dbi:mysql:database=$lc->{db_name};host=$lc->{db_host}";
- my $repeat = repeat {
- $loop->delay_future( after => 0.25 )->then(
- sub {
- my $dbh = DBI->connect(
- $dsn,
- $lc->{db_user},
- $lc->{db_pass},
- { RaiseError => 0, PrintError => 0 },
- );
- Future->wrap($dbh);
- }
+ my $loop = IO::Async::Loop->new;
+ my $lc = Bugzilla::Install::Localconfig::read_localconfig();
+
+ for my $var (qw(db_name db_host db_user db_pass)) {
+ return $loop->new_future->die("$var is not set!") unless $lc->{$var};
+ }
+
+ my $dsn = "dbi:mysql:database=$lc->{db_name};host=$lc->{db_host}";
+ my $repeat = repeat {
+ $loop->delay_future(after => 0.25)->then(sub {
+ my $dbh
+ = DBI->connect($dsn, $lc->{db_user}, $lc->{db_pass},
+ {RaiseError => 0, PrintError => 0},
);
- } until => sub { defined shift->get };
-
- my $timeout = $loop->timeout_future( after => 20 )->else_fail('assert_database timeout');
- my $any_f = Future->wait_any( $repeat, $timeout );
- return $any_f->transform(
- done => sub { return },
- fail => sub { "unable to connect to $dsn as $lc->{db_user}" },
- );
+ Future->wrap($dbh);
+ });
+ }
+ until => sub { defined shift->get };
+
+ my $timeout
+ = $loop->timeout_future(after => 20)->else_fail('assert_database timeout');
+ my $any_f = Future->wait_any($repeat, $timeout);
+ return $any_f->transform(
+ done => sub {return},
+ fail => sub {"unable to connect to $dsn as $lc->{db_user}"},
+ );
}
sub on_finish {
- my ($f) = @_;
+ my ($f) = @_;
- return sub {
- my ($self, $exitcode) = @_;
- $f->done(WEXITSTATUS($exitcode));
- };
+ return sub {
+ my ($self, $exitcode) = @_;
+ $f->done(WEXITSTATUS($exitcode));
+ };
}
sub on_exception {
- my ( $name, $f ) = @_;
-
- return sub {
- my ( $self, $exception, $errno, $exitcode ) = @_;
-
- if ( length $exception ) {
- $f->fail( "$name died with the exception $exception (errno was $errno)\n" );
- }
- elsif ( ( my $status = WEXITSTATUS($exitcode) ) == 255 ) {
- $f->fail("$name failed to exec() - $errno\n");
- }
- else {
- $f->fail("$name exited with exit status $status\n");
- }
- };
+ my ($name, $f) = @_;
+
+ return sub {
+ my ($self, $exception, $errno, $exitcode) = @_;
+
+ if (length $exception) {
+ $f->fail("$name died with the exception $exception (errno was $errno)\n");
+ }
+ elsif ((my $status = WEXITSTATUS($exitcode)) == 255) {
+ $f->fail("$name failed to exec() - $errno\n");
+ }
+ else {
+ $f->fail("$name exited with exit status $status\n");
+ }
+ };
}
1;
diff --git a/Bugzilla/DuoAPI.pm b/Bugzilla/DuoAPI.pm
index ab50a61e2..fb2b4ba38 100644
--- a/Bugzilla/DuoAPI.pm
+++ b/Bugzilla/DuoAPI.pm
@@ -68,94 +68,82 @@ use MIME::Base64 qw(encode_base64);
use POSIX qw(strftime);
sub new {
- my($proto, $ikey, $skey, $host) = @_;
- my $class = ref($proto) || $proto;
- my $self = {
- 'ikey' => $ikey,
- 'skey' => $skey,
- 'host' => $host,
- };
- bless($self, $class);
- return $self;
+ my ($proto, $ikey, $skey, $host) = @_;
+ my $class = ref($proto) || $proto;
+ my $self = {'ikey' => $ikey, 'skey' => $skey, 'host' => $host,};
+ bless($self, $class);
+ return $self;
}
sub canonicalize_params {
- my ($self, $params) = @_;
+ my ($self, $params) = @_;
- my @ret;
- while (my ($k, $v) = each(%{$params})) {
- push(@ret, join('=', CGI::escape($k), CGI::escape($v)));
- }
- return join('&', sort(@ret));
+ my @ret;
+ while (my ($k, $v) = each(%{$params})) {
+ push(@ret, join('=', CGI::escape($k), CGI::escape($v)));
+ }
+ return join('&', sort(@ret));
}
sub sign {
- my ($self, $method, $path, $canon_params, $date) = @_;
- my $canon = join("\n",
- $date,
- uc($method),
- lc($self->{'host'}),
- $path,
- $canon_params);
- my $sig = hmac_sha1_hex($canon, $self->{'skey'});
- my $auth = join(':',
- $self->{'ikey'},
- $sig);
- $auth = 'Basic ' . encode_base64($auth, '');
- return $auth;
+ my ($self, $method, $path, $canon_params, $date) = @_;
+ my $canon
+ = join("\n", $date, uc($method), lc($self->{'host'}), $path, $canon_params);
+ my $sig = hmac_sha1_hex($canon, $self->{'skey'});
+ my $auth = join(':', $self->{'ikey'}, $sig);
+ $auth = 'Basic ' . encode_base64($auth, '');
+ return $auth;
}
sub api_call {
- my ($self, $method, $path, $params) = @_;
- $params ||= {};
-
- my $canon_params = $self->canonicalize_params($params);
- my $date = strftime('%a, %d %b %Y %H:%M:%S -0000',
- gmtime(time()));
- my $auth = $self->sign($method, $path, $canon_params, $date);
-
- my $ua = LWP::UserAgent->new();
- my $req = HTTP::Request->new();
- $req->method($method);
- $req->protocol('HTTP/1.1');
- $req->header('If-SSL-Cert-Subject' => qr{CN=[^=]+\.duosecurity.com$});
- $req->header('Authorization' => $auth);
- $req->header('Date' => $date);
- $req->header('Host' => $self->{'host'});
-
- if (grep(/^$method$/, qw(POST PUT))) {
- $req->header('Content-type' => 'application/x-www-form-urlencoded');
- $req->content($canon_params);
- }
- else {
- $path .= '?' . $canon_params;
- }
-
- $req->uri('https://' . $self->{'host'} . $path);
- if ($ENV{'DEBUG'}) {
- print STDERR $req->as_string();
- }
- my $res = $ua->request($req);
- return $res;
+ my ($self, $method, $path, $params) = @_;
+ $params ||= {};
+
+ my $canon_params = $self->canonicalize_params($params);
+ my $date = strftime('%a, %d %b %Y %H:%M:%S -0000', gmtime(time()));
+ my $auth = $self->sign($method, $path, $canon_params, $date);
+
+ my $ua = LWP::UserAgent->new();
+ my $req = HTTP::Request->new();
+ $req->method($method);
+ $req->protocol('HTTP/1.1');
+ $req->header('If-SSL-Cert-Subject' => qr{CN=[^=]+\.duosecurity.com$});
+ $req->header('Authorization' => $auth);
+ $req->header('Date' => $date);
+ $req->header('Host' => $self->{'host'});
+
+ if (grep(/^$method$/, qw(POST PUT))) {
+ $req->header('Content-type' => 'application/x-www-form-urlencoded');
+ $req->content($canon_params);
+ }
+ else {
+ $path .= '?' . $canon_params;
+ }
+
+ $req->uri('https://' . $self->{'host'} . $path);
+ if ($ENV{'DEBUG'}) {
+ print STDERR $req->as_string();
+ }
+ my $res = $ua->request($req);
+ return $res;
}
sub json_api_call {
- my $self = shift;
- my $res = $self->api_call(@_);
- my $json = $res->content();
- if ($json !~ /^{/) {
- croak($json);
- }
- my $ret = decode_json($json);
- if (($ret->{'stat'} || '') ne 'OK') {
- my $msg = join('',
- 'Error ', $ret->{'code'}, ': ', $ret->{'message'});
- if (defined($ret->{'message_detail'})) {
- $msg .= ' (' . $ret->{'message_detail'} . ')';
- }
- croak($msg);
+ my $self = shift;
+ my $res = $self->api_call(@_);
+ my $json = $res->content();
+ if ($json !~ /^{/) {
+ croak($json);
+ }
+ my $ret = decode_json($json);
+ if (($ret->{'stat'} || '') ne 'OK') {
+ my $msg = join('', 'Error ', $ret->{'code'}, ': ', $ret->{'message'});
+ if (defined($ret->{'message_detail'})) {
+ $msg .= ' (' . $ret->{'message_detail'} . ')';
}
- return $ret->{'response'};
+ croak($msg);
+ }
+ return $ret->{'response'};
}
1;
diff --git a/Bugzilla/DuoWeb.pm b/Bugzilla/DuoWeb.pm
index 722c032e3..9dbfc46ad 100644
--- a/Bugzilla/DuoWeb.pm
+++ b/Bugzilla/DuoWeb.pm
@@ -47,67 +47,69 @@ my $SKEY_LEN = 40;
my $AKEY_LEN = 40;
our $ERR_USER = 'ERR|The username passed to sign_request() is invalid.';
-our $ERR_IKEY = 'ERR|The Duo integration key passed to sign_request() is invalid.';
+our $ERR_IKEY
+ = 'ERR|The Duo integration key passed to sign_request() is invalid.';
our $ERR_SKEY = 'ERR|The Duo secret key passed to sign_request() is invalid.';
-our $ERR_AKEY = "ERR|The application secret key passed to sign_request() must be at least $AKEY_LEN characters.";
+our $ERR_AKEY
+ = "ERR|The application secret key passed to sign_request() must be at least $AKEY_LEN characters.";
our $ERR_UNKNOWN = 'ERR|An unknown error has occurred.';
sub _sign_vals {
- my ($key, $vals, $prefix, $expire) = @_;
+ my ($key, $vals, $prefix, $expire) = @_;
- my $exp = time + $expire;
+ my $exp = time + $expire;
- my $val = join '|', @{$vals}, $exp;
- my $b64 = encode_base64($val, '');
- my $cookie = "$prefix|$b64";
+ my $val = join '|', @{$vals}, $exp;
+ my $b64 = encode_base64($val, '');
+ my $cookie = "$prefix|$b64";
- my $sig = hmac_sha1_hex($cookie, $key);
+ my $sig = hmac_sha1_hex($cookie, $key);
- return "$cookie|$sig";
+ return "$cookie|$sig";
}
sub _parse_vals {
- my ($key, $val, $prefix, $ikey) = @_;
+ my ($key, $val, $prefix, $ikey) = @_;
- my $ts = time;
+ my $ts = time;
- if (not defined $val) {
- return '';
- }
+ if (not defined $val) {
+ return '';
+ }
- my @parts = split /\|/, $val;
- if (scalar(@parts) != 3) {
- return '';
- }
- my ($u_prefix, $u_b64, $u_sig) = @parts;
+ my @parts = split /\|/, $val;
+ if (scalar(@parts) != 3) {
+ return '';
+ }
+ my ($u_prefix, $u_b64, $u_sig) = @parts;
- my $sig = hmac_sha1_hex("$u_prefix|$u_b64", $key);
+ my $sig = hmac_sha1_hex("$u_prefix|$u_b64", $key);
- if (hmac_sha1_hex($sig, $key) ne hmac_sha1_hex($u_sig, $key)) {
- return '';
- }
+ if (hmac_sha1_hex($sig, $key) ne hmac_sha1_hex($u_sig, $key)) {
+ return '';
+ }
- if ($u_prefix ne $prefix) {
- return '';
- }
+ if ($u_prefix ne $prefix) {
+ return '';
+ }
- my @cookie_parts = split /\|/, decode_base64($u_b64);
- if (scalar(@cookie_parts) != 3) {
- return '';
- }
- my ($user, $u_ikey, $exp) = @cookie_parts;
+ my @cookie_parts = split /\|/, decode_base64($u_b64);
+ if (scalar(@cookie_parts) != 3) {
+ return '';
+ }
+ my ($user, $u_ikey, $exp) = @cookie_parts;
- if ($u_ikey ne $ikey) {
- return '';
- }
+ if ($u_ikey ne $ikey) {
+ return '';
+ }
- if ($ts >= $exp) {
- return '';
- }
+ if ($ts >= $exp) {
+ return '';
+ }
- return $user;
+ return $user;
}
=pod
@@ -124,38 +126,38 @@ sub _parse_vals {
=cut
sub sign_request {
- my ($ikey, $skey, $akey, $username) = @_;
+ my ($ikey, $skey, $akey, $username) = @_;
- if (not $username) {
- return $ERR_USER;
- }
+ if (not $username) {
+ return $ERR_USER;
+ }
- if (index($username, '|') != -1) {
- return $ERR_USER;
- }
+ if (index($username, '|') != -1) {
+ return $ERR_USER;
+ }
- if (not $ikey or length $ikey != $IKEY_LEN) {
- return $ERR_IKEY;
- }
+ if (not $ikey or length $ikey != $IKEY_LEN) {
+ return $ERR_IKEY;
+ }
- if (not $skey or length $skey != $SKEY_LEN) {
- return $ERR_SKEY;
- }
+ if (not $skey or length $skey != $SKEY_LEN) {
+ return $ERR_SKEY;
+ }
- if (not $akey or length $akey < $AKEY_LEN) {
- return $ERR_AKEY;
- }
+ if (not $akey or length $akey < $AKEY_LEN) {
+ return $ERR_AKEY;
+ }
- my $vals = [ $username, $ikey ];
+ my $vals = [$username, $ikey];
- my $duo_sig = _sign_vals($skey, $vals, $DUO_PREFIX, $DUO_EXPIRE);
- my $app_sig = _sign_vals($akey, $vals, $APP_PREFIX, $APP_EXPIRE);
+ my $duo_sig = _sign_vals($skey, $vals, $DUO_PREFIX, $DUO_EXPIRE);
+ my $app_sig = _sign_vals($akey, $vals, $APP_PREFIX, $APP_EXPIRE);
- if (not $duo_sig or not $app_sig) {
- return $ERR_UNKNOWN;
- }
+ if (not $duo_sig or not $app_sig) {
+ return $ERR_UNKNOWN;
+ }
- return "$duo_sig:$app_sig";
+ return "$duo_sig:$app_sig";
}
=pod
@@ -175,20 +177,20 @@ sub sign_request {
=cut
sub verify_response {
- my ($ikey, $skey, $akey, $sig_response) = @_;
+ my ($ikey, $skey, $akey, $sig_response) = @_;
- if (not defined $sig_response) {
- return '';
- }
+ if (not defined $sig_response) {
+ return '';
+ }
- my ($auth_sig, $app_sig) = split /:/, $sig_response;
- my $auth_user = _parse_vals($skey, $auth_sig, $AUTH_PREFIX, $ikey);
- my $app_user = _parse_vals($akey, $app_sig, $APP_PREFIX, $ikey);
+ my ($auth_sig, $app_sig) = split /:/, $sig_response;
+ my $auth_user = _parse_vals($skey, $auth_sig, $AUTH_PREFIX, $ikey);
+ my $app_user = _parse_vals($akey, $app_sig, $APP_PREFIX, $ikey);
- if ($auth_user ne $app_user) {
- return '';
- }
+ if ($auth_user ne $app_user) {
+ return '';
+ }
- return $auth_user;
+ return $auth_user;
}
1;
diff --git a/Bugzilla/Elastic.pm b/Bugzilla/Elastic.pm
index a01d1be42..805094f03 100644
--- a/Bugzilla/Elastic.pm
+++ b/Bugzilla/Elastic.pm
@@ -14,44 +14,42 @@ use Bugzilla::Util qw(trick_taint);
with 'Bugzilla::Elastic::Role::HasClient';
sub suggest_users {
- my ($self, $text) = @_;
-
- unless (Bugzilla->params->{elasticsearch}) {
- # optimization: faster than a regular method call.
- goto &_suggest_users_fallback;
- }
-
- my $field = 'suggest_user';
- if ($text =~ /^:(.+)$/) {
- $text = $1;
- $field = 'suggest_nick';
- }
-
- my $result = eval {
- $self->client->suggest(
- index => Bugzilla::User->ES_INDEX,
- body => {
- $field => {
- text => $text,
- completion => { field => $field, size => 25 },
- }
- }
- );
- };
- if (defined $result) {
- return [ map { $_->{payload} } @{$result->{$field}[0]{options}} ];
- }
- else {
- warn "suggest_users error: $@";
- # optimization: faster than a regular method call.
- goto &_suggest_users_fallback;
- }
+ my ($self, $text) = @_;
+
+ unless (Bugzilla->params->{elasticsearch}) {
+
+ # optimization: faster than a regular method call.
+ goto &_suggest_users_fallback;
+ }
+
+ my $field = 'suggest_user';
+ if ($text =~ /^:(.+)$/) {
+ $text = $1;
+ $field = 'suggest_nick';
+ }
+
+ my $result = eval {
+ $self->client->suggest(
+ index => Bugzilla::User->ES_INDEX,
+ body =>
+ {$field => {text => $text, completion => {field => $field, size => 25},}}
+ );
+ };
+ if (defined $result) {
+ return [map { $_->{payload} } @{$result->{$field}[0]{options}}];
+ }
+ else {
+ warn "suggest_users error: $@";
+
+ # optimization: faster than a regular method call.
+ goto &_suggest_users_fallback;
+ }
}
sub _suggest_users_fallback {
- my ($self, $text) = @_;
- my $users = Bugzilla::User::match($text, 25, 1);
- return [ map { { real_name => $_->name, name => $_->login } } @$users];
+ my ($self, $text) = @_;
+ my $users = Bugzilla::User::match($text, 25, 1);
+ return [map { {real_name => $_->name, name => $_->login} } @$users];
}
1;
diff --git a/Bugzilla/Elastic/Indexer.pm b/Bugzilla/Elastic/Indexer.pm
index a9d796ae7..579829014 100644
--- a/Bugzilla/Elastic/Indexer.pm
+++ b/Bugzilla/Elastic/Indexer.pm
@@ -16,206 +16,197 @@ use namespace::clean;
with 'Bugzilla::Elastic::Role::HasClient';
-has 'shadow_dbh' => ( is => 'lazy' );
+has 'shadow_dbh' => (is => 'lazy');
-has 'debug_sql' => (
- is => 'ro',
- default => 0,
-);
+has 'debug_sql' => (is => 'ro', default => 0,);
-has 'progress_bar' => (
- is => 'ro',
- predicate => 'has_progress_bar',
-);
+has 'progress_bar' => (is => 'ro', predicate => 'has_progress_bar',);
sub _create_index {
- my ($self, $class) = @_;
- my $indices = $self->client->indices;
- my $index_name = $class->ES_INDEX;
-
- unless ($indices->exists(index => $index_name)) {
- $indices->create(
- index => $index_name,
- body => { settings => $class->ES_SETTINGS },
- );
- }
+ my ($self, $class) = @_;
+ my $indices = $self->client->indices;
+ my $index_name = $class->ES_INDEX;
+
+ unless ($indices->exists(index => $index_name)) {
+ $indices->create(
+ index => $index_name,
+ body => {settings => $class->ES_SETTINGS},
+ );
+ }
}
sub _bulk_helper {
- my ($self, $class) = @_;
+ my ($self, $class) = @_;
- return $self->client->bulk_helper(
- index => $class->ES_INDEX,
- type => $class->ES_TYPE,
- );
+ return $self->client->bulk_helper(
+ index => $class->ES_INDEX,
+ type => $class->ES_TYPE,
+ );
}
sub _find_largest {
- my ($self, $class, $field) = @_;
-
- my $result = $self->client->search(
- index => $class->ES_INDEX,
- type => $class->ES_TYPE,
- body => {
- aggs => { $field => { extended_stats => { field => $field } } },
- size => 0
- }
- );
-
- my $max = $result->{aggregations}{$field}{max};
- if (not defined $max) {
- return 0;
- }
- elsif (looks_like_number($max)) {
- return $max;
- }
- else {
- die "largest value for '$field' is not a number: $max";
- }
+ my ($self, $class, $field) = @_;
+
+ my $result = $self->client->search(
+ index => $class->ES_INDEX,
+ type => $class->ES_TYPE,
+ body => {aggs => {$field => {extended_stats => {field => $field}}}, size => 0}
+ );
+
+ my $max = $result->{aggregations}{$field}{max};
+ if (not defined $max) {
+ return 0;
+ }
+ elsif (looks_like_number($max)) {
+ return $max;
+ }
+ else {
+ die "largest value for '$field' is not a number: $max";
+ }
}
sub _find_largest_mtime {
- my ($self, $class) = @_;
+ my ($self, $class) = @_;
- return $self->_find_largest($class, 'es_mtime');
+ return $self->_find_largest($class, 'es_mtime');
}
sub _find_largest_id {
- my ($self, $class) = @_;
+ my ($self, $class) = @_;
- return $self->_find_largest($class, $class->ID_FIELD);
+ return $self->_find_largest($class, $class->ID_FIELD);
}
sub _put_mapping {
- my ($self, $class) = @_;
-
- my %body = ( properties => scalar $class->ES_PROPERTIES );
- if ($class->does('Bugzilla::Elastic::Role::ChildObject')) {
- $body{_parent} = { type => $class->ES_PARENT_TYPE };
- }
-
- $self->client->indices->put_mapping(
- index => $class->ES_INDEX,
- type => $class->ES_TYPE,
- body => \%body,
- );
+ my ($self, $class) = @_;
+
+ my %body = (properties => scalar $class->ES_PROPERTIES);
+ if ($class->does('Bugzilla::Elastic::Role::ChildObject')) {
+ $body{_parent} = {type => $class->ES_PARENT_TYPE};
+ }
+
+ $self->client->indices->put_mapping(
+ index => $class->ES_INDEX,
+ type => $class->ES_TYPE,
+ body => \%body,
+ );
}
sub _debug_sql {
- my ($self, $sql, $params) = @_;
- if ($self->debug_sql) {
- my ($out, @args) = ($sql, $params ? (@$params) : ());
- $out =~ s/^\n//gs;
- $out =~ s/^\s{8}//gm;
- $out =~ s/\?/Bugzilla->dbh->quote(shift @args)/ge;
- warn $out, "\n";
- }
-
- return ($sql, $params)
+ my ($self, $sql, $params) = @_;
+ if ($self->debug_sql) {
+ my ($out, @args) = ($sql, $params ? (@$params) : ());
+ $out =~ s/^\n//gs;
+ $out =~ s/^\s{8}//gm;
+ $out =~ s/\?/Bugzilla->dbh->quote(shift @args)/ge;
+ warn $out, "\n";
+ }
+
+ return ($sql, $params);
}
sub bulk_load {
- my ( $self, $class ) = @_;
+ my ($self, $class) = @_;
- $self->_create_index($class);
+ $self->_create_index($class);
- my $bulk = $self->_bulk_helper($class);
- my $last_mtime = $self->_find_largest_mtime($class);
- my $last_id = $self->_find_largest_id($class);
- my $new_ids = $self->_select_all_ids($class, $last_id);
- my $updated_ids = $self->_select_updated_ids($class, $last_mtime);
+ my $bulk = $self->_bulk_helper($class);
+ my $last_mtime = $self->_find_largest_mtime($class);
+ my $last_id = $self->_find_largest_id($class);
+ my $new_ids = $self->_select_all_ids($class, $last_id);
+ my $updated_ids = $self->_select_updated_ids($class, $last_mtime);
- $self->_put_mapping($class);
- $self->_bulk_load_ids($bulk, $class, $new_ids) if @$new_ids;
- $self->_bulk_load_ids($bulk, $class, $updated_ids) if @$updated_ids;
+ $self->_put_mapping($class);
+ $self->_bulk_load_ids($bulk, $class, $new_ids) if @$new_ids;
+ $self->_bulk_load_ids($bulk, $class, $updated_ids) if @$updated_ids;
- return {
- new => scalar @$new_ids,
- updated => scalar @$updated_ids,
- };
+ return {new => scalar @$new_ids, updated => scalar @$updated_ids,};
}
sub _select_all_ids {
- my ($self, $class, $last_id) = @_;
+ my ($self, $class, $last_id) = @_;
- my $dbh = Bugzilla->dbh;
- my ($sql, $params) = $self->_debug_sql($class->ES_SELECT_ALL_SQL($last_id));
- return $dbh->selectcol_arrayref($sql, undef, @$params);
+ my $dbh = Bugzilla->dbh;
+ my ($sql, $params) = $self->_debug_sql($class->ES_SELECT_ALL_SQL($last_id));
+ return $dbh->selectcol_arrayref($sql, undef, @$params);
}
sub _select_updated_ids {
- my ($self, $class, $last_mtime) = @_;
+ my ($self, $class, $last_mtime) = @_;
- my $dbh = Bugzilla->dbh;
- my ($updated_sql, $updated_params) = $self->_debug_sql($class->ES_SELECT_UPDATED_SQL($last_mtime));
- return $dbh->selectcol_arrayref($updated_sql, undef, @$updated_params);
+ my $dbh = Bugzilla->dbh;
+ my ($updated_sql, $updated_params)
+ = $self->_debug_sql($class->ES_SELECT_UPDATED_SQL($last_mtime));
+ return $dbh->selectcol_arrayref($updated_sql, undef, @$updated_params);
}
sub bulk_load_ids {
- my ($self, $class, $ids) = @_;
+ my ($self, $class, $ids) = @_;
- $self->_create_index($class);
- $self->_put_mapping($class);
- $self->_bulk_load_ids($self->_bulk_helper($class), $class, $ids);
+ $self->_create_index($class);
+ $self->_put_mapping($class);
+ $self->_bulk_load_ids($self->_bulk_helper($class), $class, $ids);
}
sub _bulk_load_ids {
- my ($self, $bulk, $class, $all_ids) = @_;
-
- my $iter = natatime $class->ES_OBJECTS_AT_ONCE, @$all_ids;
- my $mtime = $self->_current_mtime;
- my $progress_bar;
- my $next_update;
-
- if ($self->has_progress_bar) {
- my $name = (split(/::/, $class))[-1];
- $progress_bar = $self->progress_bar->new({
- name => $name,
- count => scalar @$all_ids,
- ETA => 'linear'
- });
- $progress_bar->message(sprintf "loading %d $class objects, %d at a time", scalar @$all_ids, $class->ES_OBJECTS_AT_ONCE);
- $next_update = $progress_bar->update(0);
- $progress_bar->max_update_rate(1);
+ my ($self, $bulk, $class, $all_ids) = @_;
+
+ my $iter = natatime $class->ES_OBJECTS_AT_ONCE, @$all_ids;
+ my $mtime = $self->_current_mtime;
+ my $progress_bar;
+ my $next_update;
+
+ if ($self->has_progress_bar) {
+ my $name = (split(/::/, $class))[-1];
+ $progress_bar
+ = $self->progress_bar->new({
+ name => $name, count => scalar @$all_ids, ETA => 'linear'
+ });
+ $progress_bar->message(
+ sprintf "loading %d $class objects, %d at a time",
+ scalar @$all_ids,
+ $class->ES_OBJECTS_AT_ONCE
+ );
+ $next_update = $progress_bar->update(0);
+ $progress_bar->max_update_rate(1);
+ }
+
+ my $total = 0;
+ my $start = time;
+ while (my @ids = $iter->()) {
+ if ($progress_bar) {
+ $total += @ids;
+ if ($total >= $next_update) {
+ $next_update = $progress_bar->update($total);
+ my $duration = time - $start || 1;
+ }
}
- my $total = 0;
- my $start = time;
- while (my @ids = $iter->()) {
- if ($progress_bar) {
- $total += @ids;
- if ($total >= $next_update) {
- $next_update = $progress_bar->update($total);
- my $duration = time - $start || 1;
- }
- }
-
- my $objects = $class->new_from_list(\@ids);
- foreach my $object (@$objects) {
- my %doc = (
- id => $object->es_id,
- source => scalar $object->es_document($mtime),
- );
-
- if ($class->does('Bugzilla::Elastic::Role::ChildObject')) {
- $doc{parent} = $object->es_parent_id;
- }
-
- $bulk->index(\%doc);
- }
- Bugzilla->_cleanup();
+ my $objects = $class->new_from_list(\@ids);
+ foreach my $object (@$objects) {
+ my %doc
+ = (id => $object->es_id, source => scalar $object->es_document($mtime),);
+
+ if ($class->does('Bugzilla::Elastic::Role::ChildObject')) {
+ $doc{parent} = $object->es_parent_id;
+ }
+
+ $bulk->index(\%doc);
}
+ Bugzilla->_cleanup();
+ }
- $bulk->flush;
+ $bulk->flush;
}
sub _build_shadow_dbh { Bugzilla->switch_to_shadow_db }
sub _current_mtime {
- my ($self) = @_;
- my ($mtime) = $self->shadow_dbh->selectrow_array("SELECT UNIX_TIMESTAMP(NOW())");
- return $mtime;
+ my ($self) = @_;
+ my ($mtime)
+ = $self->shadow_dbh->selectrow_array("SELECT UNIX_TIMESTAMP(NOW())");
+ return $mtime;
}
1;
diff --git a/Bugzilla/Elastic/Role/HasClient.pm b/Bugzilla/Elastic/Role/HasClient.pm
index a971392e0..41d8e7647 100644
--- a/Bugzilla/Elastic/Role/HasClient.pm
+++ b/Bugzilla/Elastic/Role/HasClient.pm
@@ -13,13 +13,13 @@ use Moo::Role;
has 'client' => (is => 'lazy');
sub _build_client {
- my ($self) = @_;
+ my ($self) = @_;
- require Search::Elasticsearch;
- return Search::Elasticsearch->new(
- nodes => [ split(/\s+/, Bugzilla->params->{elasticsearch_nodes}) ],
- cxn_pool => 'Sniff',
- );
+ require Search::Elasticsearch;
+ return Search::Elasticsearch->new(
+ nodes => [split(/\s+/, Bugzilla->params->{elasticsearch_nodes})],
+ cxn_pool => 'Sniff',
+ );
}
1;
diff --git a/Bugzilla/Elastic/Role/Object.pm b/Bugzilla/Elastic/Role/Object.pm
index 674545d04..6974d9087 100644
--- a/Bugzilla/Elastic/Role/Object.pm
+++ b/Bugzilla/Elastic/Role/Object.pm
@@ -12,45 +12,44 @@ use Role::Tiny;
requires qw(ES_TYPE ES_INDEX ES_SETTINGS ES_PROPERTIES es_document);
requires qw(ID_FIELD DB_TABLE);
-sub ES_OBJECTS_AT_ONCE { 100 }
+sub ES_OBJECTS_AT_ONCE {100}
sub ES_SELECT_ALL_SQL {
- my ($class, $last_id) = @_;
+ my ($class, $last_id) = @_;
- my $id = $class->ID_FIELD;
- my $table = $class->DB_TABLE;
+ my $id = $class->ID_FIELD;
+ my $table = $class->DB_TABLE;
- return ("SELECT $id FROM $table WHERE $id > ? ORDER BY $id", [$last_id // 0]);
+ return ("SELECT $id FROM $table WHERE $id > ? ORDER BY $id", [$last_id // 0]);
}
requires qw(ES_SELECT_UPDATED_SQL);
sub es_id {
- my ($self) = @_;
- return join('_', $self->ES_TYPE, $self->id);
+ my ($self) = @_;
+ return join('_', $self->ES_TYPE, $self->id);
}
around 'ES_PROPERTIES' => sub {
- my $orig = shift;
- my $self = shift;
- my $properties = $orig->($self, @_);
- $properties->{es_mtime} = { type => 'long' };
- $properties->{$self->ID_FIELD} = { type => 'long', analyzer => 'keyword' };
+ my $orig = shift;
+ my $self = shift;
+ my $properties = $orig->($self, @_);
+ $properties->{es_mtime} = {type => 'long'};
+ $properties->{$self->ID_FIELD} = {type => 'long', analyzer => 'keyword'};
- return $properties;
+ return $properties;
};
around 'es_document' => sub {
- my ($orig, $self, $mtime) = @_;
- my $doc = $orig->($self);
+ my ($orig, $self, $mtime) = @_;
+ my $doc = $orig->($self);
- $doc->{es_mtime} = $mtime;
- $doc->{$self->ID_FIELD} = $self->id;
- $doc->{_id} = $self->es_id;
+ $doc->{es_mtime} = $mtime;
+ $doc->{$self->ID_FIELD} = $self->id;
+ $doc->{_id} = $self->es_id;
- return $doc;
+ return $doc;
};
-
1;
diff --git a/Bugzilla/Elastic/Search.pm b/Bugzilla/Elastic/Search.pm
index 26ab71bec..032f9b03a 100644
--- a/Bugzilla/Elastic/Search.pm
+++ b/Bugzilla/Elastic/Search.pm
@@ -16,63 +16,62 @@ use namespace::clean;
use Bugzilla::Elastic::Search::FakeCGI;
-has 'quicksearch' => ( is => 'ro' );
-has 'limit' => ( is => 'ro', predicate => 'has_limit' );
-has 'offset' => ( is => 'ro', predicate => 'has_offset' );
-has 'fields' => ( is => 'ro', isa => \&_arrayref_of_fields, default => sub { [] } );
-has 'params' => ( is => 'lazy' );
-has 'clause' => ( is => 'lazy' );
-has 'es_query' => ( is => 'lazy' );
+has 'quicksearch' => (is => 'ro');
+has 'limit' => (is => 'ro', predicate => 'has_limit');
+has 'offset' => (is => 'ro', predicate => 'has_offset');
+has 'fields' =>
+ (is => 'ro', isa => \&_arrayref_of_fields, default => sub { [] });
+has 'params' => (is => 'lazy');
+has 'clause' => (is => 'lazy');
+has 'es_query' => (is => 'lazy');
has 'search_description' => (is => 'lazy');
-has 'query_time' => ( is => 'rwp' );
+has 'query_time' => (is => 'rwp');
-has '_input_order' => ( is => 'ro', init_arg => 'order', required => 1);
-has '_order' => ( is => 'lazy', init_arg => undef );
-has 'invalid_order_columns' => ( is => 'lazy' );
+has '_input_order' => (is => 'ro', init_arg => 'order', required => 1);
+has '_order' => (is => 'lazy', init_arg => undef);
+has 'invalid_order_columns' => (is => 'lazy');
with 'Bugzilla::Elastic::Role::HasClient';
with 'Bugzilla::Elastic::Role::Search';
my @SUPPORTED_FIELDS = qw(
- bug_id product component short_desc
- priority status_whiteboard bug_status resolution
- keywords alias assigned_to reporter delta_ts
- longdesc cf_crash_signature classification bug_severity
- commenter
+ bug_id product component short_desc
+ priority status_whiteboard bug_status resolution
+ keywords alias assigned_to reporter delta_ts
+ longdesc cf_crash_signature classification bug_severity
+ commenter
);
my %IS_SUPPORTED_FIELD = map { $_ => 1 } @SUPPORTED_FIELDS;
$IS_SUPPORTED_FIELD{relevance} = 1;
my @NORMAL_FIELDS = qw(
- priority
- bug_severity
- bug_status
- resolution
- product
- component
- classification
- short_desc
- assigned_to
- reporter
+ priority
+ bug_severity
+ bug_status
+ resolution
+ product
+ component
+ classification
+ short_desc
+ assigned_to
+ reporter
);
my %SORT_MAP = (
- bug_id => '_id',
- relevance => '_score',
- map { $_ => "$_.eq" } @NORMAL_FIELDS,
+ bug_id => '_id',
+ relevance => '_score',
+ map { $_ => "$_.eq" } @NORMAL_FIELDS,
);
-my %EQUALS_MAP = (
- map { $_ => "$_.eq" } @NORMAL_FIELDS,
-);
+my %EQUALS_MAP = (map { $_ => "$_.eq" } @NORMAL_FIELDS,);
sub _arrayref_of_fields {
- my $f = $_;
- foreach my $field (@$f) {
- Bugzilla::Elastic::Search::UnsupportedField->throw(field => $field)
- unless $IS_SUPPORTED_FIELD{$field};
- }
+ my $f = $_;
+ foreach my $field (@$f) {
+ Bugzilla::Elastic::Search::UnsupportedField->throw(field => $field)
+ unless $IS_SUPPORTED_FIELD{$field};
+ }
}
@@ -82,360 +81,342 @@ sub _arrayref_of_fields {
# But the DB column stayed the same... and elasticsearch uses the db name
# However search likes to use the "new" name.
# for now we hack a fix in here.
-my %REMAP_NAME = (
- changeddate => 'delta_ts',
-);
+my %REMAP_NAME = (changeddate => 'delta_ts',);
sub data {
- my ($self) = @_;
- my $body = $self->es_query;
- my $result = eval {
- $self->client->search(
- index => Bugzilla::Bug->ES_INDEX,
- type => Bugzilla::Bug->ES_TYPE,
- body => $body,
- );
- };
- die $@ unless $result;
- $self->_set_query_time($result->{took} / 1000);
-
- my @fields = map { $REMAP_NAME{$_} // $_ } @{ $self->fields };
- my (@ids, %hits);
- foreach my $hit (@{ $result->{hits}{hits} }) {
- push @ids, $hit->{_id};
- my $source = $hit->{_source};
- $source->{relevance} = $hit->{_score};
- foreach my $val (values %$source) {
- next unless defined $val;
- trick_taint($val);
- }
- trick_taint($hit->{_id});
- if ($source) {
- $hits{$hit->{_id}} = [ @$source{@fields} ];
- }
- else {
- $hits{$hit->{_id}} = $hit->{_id};
- }
+ my ($self) = @_;
+ my $body = $self->es_query;
+ my $result = eval {
+ $self->client->search(
+ index => Bugzilla::Bug->ES_INDEX,
+ type => Bugzilla::Bug->ES_TYPE,
+ body => $body,
+ );
+ };
+ die $@ unless $result;
+ $self->_set_query_time($result->{took} / 1000);
+
+ my @fields = map { $REMAP_NAME{$_} // $_ } @{$self->fields};
+ my (@ids, %hits);
+ foreach my $hit (@{$result->{hits}{hits}}) {
+ push @ids, $hit->{_id};
+ my $source = $hit->{_source};
+ $source->{relevance} = $hit->{_score};
+ foreach my $val (values %$source) {
+ next unless defined $val;
+ trick_taint($val);
+ }
+ trick_taint($hit->{_id});
+ if ($source) {
+ $hits{$hit->{_id}} = [@$source{@fields}];
+ }
+ else {
+ $hits{$hit->{_id}} = $hit->{_id};
}
- my $visible_ids = Bugzilla->user->visible_bugs(\@ids);
+ }
+ my $visible_ids = Bugzilla->user->visible_bugs(\@ids);
- return [ map { $hits{$_} } @$visible_ids ];
+ return [map { $hits{$_} } @$visible_ids];
}
sub _valid_order {
- my ($self) = @_;
+ my ($self) = @_;
- return grep { $IS_SUPPORTED_FIELD{$_->[0]} } @{$self->_order};
+ return grep { $IS_SUPPORTED_FIELD{$_->[0]} } @{$self->_order};
}
sub order {
- my ($self) = @_;
+ my ($self) = @_;
- return map { $_->[0] } $self->_valid_order;
+ return map { $_->[0] } $self->_valid_order;
}
sub _quicksearch_to_params {
- my ($quicksearch) = @_;
- no warnings 'redefine';
- my $cgi = Bugzilla::Elastic::Search::FakeCGI->new;
- local *Bugzilla::cgi = sub { $cgi };
- local $Bugzilla::Search::Quicksearch::ELASTIC = 1;
- quicksearch($quicksearch);
-
- return $cgi->params;
+ my ($quicksearch) = @_;
+ no warnings 'redefine';
+ my $cgi = Bugzilla::Elastic::Search::FakeCGI->new;
+ local *Bugzilla::cgi = sub {$cgi};
+ local $Bugzilla::Search::Quicksearch::ELASTIC = 1;
+ quicksearch($quicksearch);
+
+ return $cgi->params;
}
sub _build_fields { return \@SUPPORTED_FIELDS }
sub _build__order {
- my ($self) = @_;
-
- my @order;
- foreach my $order (@{$self->_input_order}) {
- if ($order =~ /^(.+)\s+(asc|desc)$/i) {
- push @order, [ $1, lc $2 ];
- }
- else {
- push @order, [ $order ];
- }
+ my ($self) = @_;
+
+ my @order;
+ foreach my $order (@{$self->_input_order}) {
+ if ($order =~ /^(.+)\s+(asc|desc)$/i) {
+ push @order, [$1, lc $2];
+ }
+ else {
+ push @order, [$order];
}
- return \@order;
+ }
+ return \@order;
}
sub _build_invalid_order_columns {
- my ($self) = @_;
+ my ($self) = @_;
- return [ map { $_->[0] } grep { !$IS_SUPPORTED_FIELD{$_->[0]} } @{ $self->_order } ];
+ return [map { $_->[0] }
+ grep { !$IS_SUPPORTED_FIELD{$_->[0]} } @{$self->_order}];
}
sub _build_params {
- my ($self) = @_;
+ my ($self) = @_;
- return _quicksearch_to_params($self->quicksearch);
+ return _quicksearch_to_params($self->quicksearch);
}
sub _build_clause {
- my ($self) = @_;
- my $search = Bugzilla::Search->new(params => $self->params);
+ my ($self) = @_;
+ my $search = Bugzilla::Search->new(params => $self->params);
- return $search->_params_to_data_structure;
+ return $search->_params_to_data_structure;
}
sub _build_search_description {
- my ($self) = @_;
+ my ($self) = @_;
- return [_describe($self->clause)];
+ return [_describe($self->clause)];
}
sub _describe {
- my ($thing) = @_;
+ my ($thing) = @_;
- state $class_to_func = {
- 'Bugzilla::Search::Condition' => \&_describe_condition,
- 'Bugzilla::Search::Clause' => \&_describe_clause
- };
+ state $class_to_func = {
+ 'Bugzilla::Search::Condition' => \&_describe_condition,
+ 'Bugzilla::Search::Clause' => \&_describe_clause
+ };
- my $func = $class_to_func->{ref $thing} or die "nothing for $thing\n";
+ my $func = $class_to_func->{ref $thing} or die "nothing for $thing\n";
- return $func->($thing);
+ return $func->($thing);
}
sub _describe_clause {
- my ($clause) = @_;
+ my ($clause) = @_;
- return map { _describe($_) } @{$clause->children};
+ return map { _describe($_) } @{$clause->children};
}
sub _describe_condition {
- my ($cond) = @_;
+ my ($cond) = @_;
- return { field => $cond->field, type => $cond->operator, value => _describe_value($cond->value) };
+ return {
+ field => $cond->field,
+ type => $cond->operator,
+ value => _describe_value($cond->value)
+ };
}
sub _describe_value {
- my ($val) = @_;
+ my ($val) = @_;
- return ref($val) ? join(", ", @$val) : $val;
+ return ref($val) ? join(", ", @$val) : $val;
}
sub _build_es_query {
- my ($self) = @_;
- my @extra;
-
- if ($self->_valid_order) {
- my @sort = map {
- my $f = $SORT_MAP{$_->[0]} // $_->[0];
- @$_ > 1 ? { $f => lc $_[1] } : $f;
- } $self->_valid_order;
- push @extra, sort => \@sort;
- }
- if ($self->has_offset) {
- push @extra, from => $self->offset;
+ my ($self) = @_;
+ my @extra;
+
+ if ($self->_valid_order) {
+ my @sort = map {
+ my $f = $SORT_MAP{$_->[0]} // $_->[0];
+ @$_ > 1 ? {$f => lc $_[1]} : $f;
+ } $self->_valid_order;
+ push @extra, sort => \@sort;
+ }
+ if ($self->has_offset) {
+ push @extra, from => $self->offset;
+ }
+ my $max_limit = Bugzilla->params->{max_search_results};
+ my $limit = Bugzilla->params->{default_search_limit};
+ if ($self->has_limit) {
+ if ($self->limit) {
+ my $l = $self->limit;
+ $limit = $l < $max_limit ? $l : $max_limit;
}
- my $max_limit = Bugzilla->params->{max_search_results};
- my $limit = Bugzilla->params->{default_search_limit};
- if ($self->has_limit) {
- if ($self->limit) {
- my $l = $self->limit;
- $limit = $l < $max_limit ? $l : $max_limit;
- }
- else {
- $limit = $max_limit;
- }
+ else {
+ $limit = $max_limit;
}
- push @extra, size => $limit;
- return {
- _source => @{$self->fields} ? \1 : \0,
- query => _query($self->clause),
- @extra,
- };
+ }
+ push @extra, size => $limit;
+ return {
+ _source => @{$self->fields} ? \1 : \0,
+ query => _query($self->clause),
+ @extra,
+ };
}
sub _query {
- my ($thing) = @_;
- state $class_to_func = {
- 'Bugzilla::Search::Condition' => \&_query_condition,
- 'Bugzilla::Search::Clause' => \&_query_clause,
- };
+ my ($thing) = @_;
+ state $class_to_func = {
+ 'Bugzilla::Search::Condition' => \&_query_condition,
+ 'Bugzilla::Search::Clause' => \&_query_clause,
+ };
- my $func = $class_to_func->{ref $thing} or die "nothing for $thing\n";
+ my $func = $class_to_func->{ref $thing} or die "nothing for $thing\n";
- return $func->($thing);
+ return $func->($thing);
}
sub _query_condition {
- my ($cond) = @_;
- state $operator_to_es = {
- equals => \&_operator_equals,
- substring => \&_operator_substring,
- anyexact => \&_operator_anyexact,
- anywords => \&_operator_anywords,
- allwords => \&_operator_allwords,
- };
-
- my $field = $cond->field;
- my $operator = $cond->operator;
- my $value = $cond->value;
-
- if ($field eq 'resolution') {
- $value = [ map { $_ eq '---' ? '' : $_ } ref $value ? @$value : $value ];
- }
-
- unless ($IS_SUPPORTED_FIELD{$field}) {
- Bugzilla::Elastic::Search::UnsupportedField->throw(field => $field);
- }
-
- my $op = $operator_to_es->{$operator}
- or Bugzilla::Elastic::Search::UnsupportedOperator->throw(operator => $operator);
-
- my $result;
- if (ref $op) {
- $result = $op->($field, $value);
- } else {
- $result = { $op => { $field => $value } };
- }
-
- return $result;
+ my ($cond) = @_;
+ state $operator_to_es = {
+ equals => \&_operator_equals,
+ substring => \&_operator_substring,
+ anyexact => \&_operator_anyexact,
+ anywords => \&_operator_anywords,
+ allwords => \&_operator_allwords,
+ };
+
+ my $field = $cond->field;
+ my $operator = $cond->operator;
+ my $value = $cond->value;
+
+ if ($field eq 'resolution') {
+ $value = [map { $_ eq '---' ? '' : $_ } ref $value ? @$value : $value];
+ }
+
+ unless ($IS_SUPPORTED_FIELD{$field}) {
+ Bugzilla::Elastic::Search::UnsupportedField->throw(field => $field);
+ }
+
+ my $op = $operator_to_es->{$operator}
+ or
+ Bugzilla::Elastic::Search::UnsupportedOperator->throw(operator => $operator);
+
+ my $result;
+ if (ref $op) {
+ $result = $op->($field, $value);
+ }
+ else {
+ $result = {$op => {$field => $value}};
+ }
+
+ return $result;
}
# is equal to any of the strings
sub _operator_anyexact {
- my ($field, $value) = @_;
- my @values = ref $value ? @$value : split(/\s*,\s*/, $value);
- if (@values == 1) {
- return _operator_equals($field, $values[0]);
- }
- else {
- return {
- terms => {
- $EQUALS_MAP{$field} // $field => [map { lc } @values],
- minimum_should_match => 1,
- },
- };
- }
+ my ($field, $value) = @_;
+ my @values = ref $value ? @$value : split(/\s*,\s*/, $value);
+ if (@values == 1) {
+ return _operator_equals($field, $values[0]);
+ }
+ else {
+ return {
+ terms => {
+ $EQUALS_MAP{$field} // $field => [map {lc} @values],
+ minimum_should_match => 1,
+ },
+ };
+ }
}
# contains any of the words
sub _operator_anywords {
- my ($field, $value) = @_;
- return {
- match => {
- $field => { query => $value, operator => "or" }
- },
- };
+ my ($field, $value) = @_;
+ return {match => {$field => {query => $value, operator => "or"}},};
}
# contains all of the words
sub _operator_allwords {
- my ($field, $value) = @_;
- return {
- match => {
- $field => { query => $value, operator => "and" }
- },
- };
+ my ($field, $value) = @_;
+ return {match => {$field => {query => $value, operator => "and"}},};
}
sub _operator_equals {
- my ($field, $value) = @_;
- return {
- match => {
- $EQUALS_MAP{$field} // $field => $value,
- },
- };
+ my ($field, $value) = @_;
+ return {match => {$EQUALS_MAP{$field} // $field => $value,},};
}
sub _operator_substring {
- my ($field, $value) = @_;
- my $is_insider = Bugzilla->user->is_insider;
-
- if ($field eq 'longdesc') {
- return {
- has_child => {
- type => 'comment',
- query => {
- bool => {
- must => [
- { match => { body => { query => $value, operator => "and" } } },
- $is_insider ? () : { term => { is_private => \0 } },
- ],
- },
- },
- },
- }
- }
- elsif ($field eq 'reporter' or $field eq 'assigned_to') {
- return {
- prefix => {
- $EQUALS_MAP{$field} // $field => lc $value,
- }
- }
- }
- elsif ($field eq 'status_whiteboard' && $value =~ /[\[\]]/) {
- return {
- match => {
- $EQUALS_MAP{$field} // $field => $value,
- }
- };
- }
- else {
- return {
- wildcard => {
- $EQUALS_MAP{$field} // $field => lc "*$value*",
- }
- };
- }
+ my ($field, $value) = @_;
+ my $is_insider = Bugzilla->user->is_insider;
+
+ if ($field eq 'longdesc') {
+ return {
+ has_child => {
+ type => 'comment',
+ query => {
+ bool => {
+ must => [
+ {match => {body => {query => $value, operator => "and"}}},
+ $is_insider ? () : {term => {is_private => \0}},
+ ],
+ },
+ },
+ },
+ };
+ }
+ elsif ($field eq 'reporter' or $field eq 'assigned_to') {
+ return {prefix => {$EQUALS_MAP{$field} // $field => lc $value,}};
+ }
+ elsif ($field eq 'status_whiteboard' && $value =~ /[\[\]]/) {
+ return {match => {$EQUALS_MAP{$field} // $field => $value,}};
+ }
+ else {
+ return {wildcard => {$EQUALS_MAP{$field} // $field => lc "*$value*",}};
+ }
}
sub _query_clause {
- my ($clause) = @_;
+ my ($clause) = @_;
- state $joiner_to_func = {
- AND => \&_join_and,
- OR => \&_join_or,
- };
+ state $joiner_to_func = {AND => \&_join_and, OR => \&_join_or,};
- my @children = grep { !$_->isa('Bugzilla::Search::Clause') || @{$_->children} } @{$clause->children};
- if (@children == 1) {
- return _query($children[0]);
- }
+ my @children = grep { !$_->isa('Bugzilla::Search::Clause') || @{$_->children} }
+ @{$clause->children};
+ if (@children == 1) {
+ return _query($children[0]);
+ }
- return $joiner_to_func->{$clause->joiner}->([ map { _query($_) } @children ]);
+ return $joiner_to_func->{$clause->joiner}->([map { _query($_) } @children]);
}
sub _join_and {
- my ($children) = @_;
- return { bool => { must => $children } },
+ my ($children) = @_;
+ return {bool => {must => $children}},;
}
sub _join_or {
- my ($children) = @_;
- return { bool => { should => $children } };
+ my ($children) = @_;
+ return {bool => {should => $children}};
}
# Exceptions
BEGIN {
- package Bugzilla::Elastic::Search::Redirect;
- use Moo;
- with 'Throwable';
+ package Bugzilla::Elastic::Search::Redirect;
+ use Moo;
+
+ with 'Throwable';
- has 'redirect_args' => (is => 'ro', required => 1);
+ has 'redirect_args' => (is => 'ro', required => 1);
- package Bugzilla::Elastic::Search::UnsupportedField;
- use Moo;
- use overload q{""} => sub { "Unsupported field: ", $_[0]->field }, fallback => 1;
+ package Bugzilla::Elastic::Search::UnsupportedField;
+ use Moo;
+ use overload
+ q{""} => sub { "Unsupported field: ", $_[0]->field },
+ fallback => 1;
- with 'Throwable';
+ with 'Throwable';
- has 'field' => (is => 'ro', required => 1);
+ has 'field' => (is => 'ro', required => 1);
- package Bugzilla::Elastic::Search::UnsupportedOperator;
- use Moo;
+ package Bugzilla::Elastic::Search::UnsupportedOperator;
+ use Moo;
- with 'Throwable';
+ with 'Throwable';
- has 'operator' => (is => 'ro', required => 1);
+ has 'operator' => (is => 'ro', required => 1);
}
1;
diff --git a/Bugzilla/Elastic/Search/FakeCGI.pm b/Bugzilla/Elastic/Search/FakeCGI.pm
index 827c96c52..9772d798c 100644
--- a/Bugzilla/Elastic/Search/FakeCGI.pm
+++ b/Bugzilla/Elastic/Search/FakeCGI.pm
@@ -13,31 +13,34 @@ has 'params' => (is => 'ro', default => sub { {} });
# we pretend to be Bugzilla::CGI at times.
sub canonicalise_query {
- return Bugzilla::CGI::canonicalise_query(@_);
+ return Bugzilla::CGI::canonicalise_query(@_);
}
sub delete {
- my ($self, $key) = @_;
- delete $self->params->{$key};
+ my ($self, $key) = @_;
+ delete $self->params->{$key};
}
sub redirect {
- my ($self, @args) = @_;
+ my ($self, @args) = @_;
- Bugzilla::Elastic::Search::Redirect->throw(redirect_args => \@args);
+ Bugzilla::Elastic::Search::Redirect->throw(redirect_args => \@args);
}
sub param {
- my ($self, $key, $val, @rest) = @_;
- if (@_ > 3) {
- $self->params->{$key} = [$val, @rest];
- } elsif (@_ == 3) {
- $self->params->{$key} = $val;
- } elsif (@_ == 2) {
- return $self->params->{$key};
- } else {
- return $self->params
- }
+ my ($self, $key, $val, @rest) = @_;
+ if (@_ > 3) {
+ $self->params->{$key} = [$val, @rest];
+ }
+ elsif (@_ == 3) {
+ $self->params->{$key} = $val;
+ }
+ elsif (@_ == 2) {
+ return $self->params->{$key};
+ }
+ else {
+ return $self->params;
+ }
}
1;
diff --git a/Bugzilla/Error.pm b/Bugzilla/Error.pm
index 70430d40d..4a6ab6865 100644
--- a/Bugzilla/Error.pm
+++ b/Bugzilla/Error.pm
@@ -14,7 +14,8 @@ use warnings;
use base qw(Exporter);
## no critic (Modules::ProhibitAutomaticExportation)
-our @EXPORT = qw( ThrowCodeError ThrowTemplateError ThrowUserError ThrowErrorPage);
+our @EXPORT
+ = qw( ThrowCodeError ThrowTemplateError ThrowUserError ThrowErrorPage);
## use critic
use Bugzilla::Constants;
@@ -31,200 +32,207 @@ use Scalar::Util qw(blessed);
# We cannot use $^S to detect if we are in an eval(), because mod_perl
# already eval'uates everything, so $^S = 1 in all cases under mod_perl!
sub _in_eval {
- my $in_eval = 0;
- for (my $stack = 1; my $sub = (caller($stack))[3]; $stack++) {
- last if $sub =~ /^Bugzilla::Quantum::CGI::try/;
- $in_eval = 1 if $sub =~ /^\(eval\)/;
- }
- return $in_eval;
+ my $in_eval = 0;
+ for (my $stack = 1; my $sub = (caller($stack))[3]; $stack++) {
+ last if $sub =~ /^Bugzilla::Quantum::CGI::try/;
+ $in_eval = 1 if $sub =~ /^\(eval\)/;
+ }
+ return $in_eval;
}
sub _throw_error {
- my ($name, $error, $vars, $logfunc) = @_;
- $vars ||= {};
-
- # Make sure any transaction is rolled back (if supported).
- # If we are within an eval(), do not roll back transactions as we are
- # eval'uating some test on purpose.
- my $dbh = eval { Bugzilla->dbh };
- $dbh->bz_rollback_transaction() if ($dbh && $dbh->bz_in_transaction() && !_in_eval());
-
- if (Bugzilla->error_mode == ERROR_MODE_MOJO) {
- my ($type) = $name =~ /^global\/(user|code)-error/;
- my $class = $type ? 'Bugzilla::Error::' . ucfirst($type) : 'Mojo::Exception';
- my $e = $class->new($error)->trace(2);
- $e->vars($vars) if $e->can('vars');
- CORE::die $e->inspect;
- }
-
- $vars->{error} = $error;
- 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.
- require Bugzilla::Hook;
- Bugzilla::Hook::process('error_catch', { error => $error, vars => $vars,
- message => \$message });
-
- if ($Bugzilla::Template::is_processing) {
- my ($type) = $name =~ /^global\/(user|code)-error/;
- $type //= 'unknown';
- die Template::Exception->new("bugzilla.$type.$error", $vars);
+ my ($name, $error, $vars, $logfunc) = @_;
+ $vars ||= {};
+
+ # Make sure any transaction is rolled back (if supported).
+ # If we are within an eval(), do not roll back transactions as we are
+ # eval'uating some test on purpose.
+ my $dbh = eval { Bugzilla->dbh };
+ $dbh->bz_rollback_transaction()
+ if ($dbh && $dbh->bz_in_transaction() && !_in_eval());
+
+ if (Bugzilla->error_mode == ERROR_MODE_MOJO) {
+ my ($type) = $name =~ /^global\/(user|code)-error/;
+ my $class = $type ? 'Bugzilla::Error::' . ucfirst($type) : 'Mojo::Exception';
+ my $e = $class->new($error)->trace(2);
+ $e->vars($vars) if $e->can('vars');
+ CORE::die $e->inspect;
+ }
+
+ $vars->{error} = $error;
+ 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.
+ require Bugzilla::Hook;
+ Bugzilla::Hook::process('error_catch',
+ {error => $error, vars => $vars, message => \$message});
+
+ if ($Bugzilla::Template::is_processing) {
+ my ($type) = $name =~ /^global\/(user|code)-error/;
+ $type //= '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');
+ $template->process($name, $vars) || ThrowTemplateError($template->error());
+ print $cgi->multipart_final() if $cgi->{_multipart_in_progress};
+ $logfunc->("webpage error: $error");
+ }
+ 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');
- $template->process($name, $vars)
- || ThrowTemplateError($template->error());
- print $cgi->multipart_final() if $cgi->{_multipart_in_progress};
- $logfunc->("webpage error: $error");
- }
- elsif (Bugzilla->error_mode == ERROR_MODE_TEST) {
- die Dumper($vars);
- }
- elsif (Bugzilla->error_mode == ERROR_MODE_DIE) {
- die "$message\n";
+ if (Bugzilla->error_mode == ERROR_MODE_DIE_SOAP_FAULT) {
+ $logfunc->("XMLRPC error: $error ($code)");
+ die SOAP::Fault->faultcode($code)->faultstring($message);
}
- elsif (Bugzilla->error_mode == ERROR_MODE_DIE_SOAP_FAULT
- || Bugzilla->error_mode == ERROR_MODE_JSON_RPC
- || Bugzilla->error_mode == ERROR_MODE_REST)
- {
- # Clone the hash so we aren't modifying the constant.
- my %error_map = %{ WS_ERROR_CODE() };
- Bugzilla::Hook::process('webservice_error_codes',
- { error_map => \%error_map });
- my $code = $error_map{$error};
- if (!$code) {
- $code = ERROR_UNKNOWN_FATAL if $name =~ /code/i;
- $code = ERROR_UNKNOWN_TRANSIENT if $name =~ /user/i;
- }
-
- if (Bugzilla->error_mode == ERROR_MODE_DIE_SOAP_FAULT) {
- $logfunc->("XMLRPC error: $error ($code)");
- die SOAP::Fault->faultcode($code)->faultstring($message);
- }
- else {
- my $server = Bugzilla->_json_server;
-
- my $status_code = 0;
- if (Bugzilla->error_mode == ERROR_MODE_REST) {
- my %status_code_map = %{ REST_STATUS_CODE_MAP() };
- $status_code = $status_code_map{$code} || $status_code_map{'_default'};
- $logfunc->("REST error: $error (HTTP $status_code, internal code $code)");
- }
- else {
- my $fake_code = 100000 + $code;
- $logfunc->("JSONRPC error: $error ($fake_code)");
- }
- # Technically JSON-RPC isn't allowed to have error numbers
- # higher than 999, but we do this to avoid conflicts with
- # the internal JSON::RPC error codes.
- $server->raise_error(code => 100000 + $code,
- status_code => $status_code,
- message => $message,
- id => $server->{_bz_request_id},
- version => $server->version);
- # Most JSON-RPC Throw*Error calls happen within an eval inside
- # of JSON::RPC. So, in that circumstance, instead of exiting,
- # we die with no message. JSON::RPC checks raise_error before
- # it checks $@, so it returns the proper error.
- die if _in_eval();
- $server->response($server->error_response_header);
- }
+ else {
+ my $server = Bugzilla->_json_server;
+
+ my $status_code = 0;
+ if (Bugzilla->error_mode == ERROR_MODE_REST) {
+ my %status_code_map = %{REST_STATUS_CODE_MAP()};
+ $status_code = $status_code_map{$code} || $status_code_map{'_default'};
+ $logfunc->("REST error: $error (HTTP $status_code, internal code $code)");
+ }
+ else {
+ my $fake_code = 100000 + $code;
+ $logfunc->("JSONRPC error: $error ($fake_code)");
+ }
+
+ # Technically JSON-RPC isn't allowed to have error numbers
+ # higher than 999, but we do this to avoid conflicts with
+ # the internal JSON::RPC error codes.
+ $server->raise_error(
+ code => 100000 + $code,
+ status_code => $status_code,
+ message => $message,
+ id => $server->{_bz_request_id},
+ version => $server->version
+ );
+
+ # Most JSON-RPC Throw*Error calls happen within an eval inside
+ # of JSON::RPC. So, in that circumstance, instead of exiting,
+ # we die with no message. JSON::RPC checks raise_error before
+ # it checks $@, so it returns the proper error.
+ die if _in_eval();
+ $server->response($server->error_response_header);
}
+ }
- exit;
+ exit;
}
sub _add_vars_to_logging_fields {
- my ($vars) = @_;
+ my ($vars) = @_;
- foreach my $key (keys %$vars) {
- Bugzilla::Logging->fields->{"var_$key"} = $vars->{$key};
- }
+ foreach my $key (keys %$vars) {
+ Bugzilla::Logging->fields->{"var_$key"} = $vars->{$key};
+ }
}
sub _make_logfunc {
- my ($type) = @_;
- my $logger = Log::Log4perl->get_logger("Bugzilla.Error.$type");
- return sub {
- local $Log::Log4perl::caller_depth = $Log::Log4perl::caller_depth + 3;
- if ($type eq 'User') {
- $logger->warn(@_);
- }
- else {
- $logger->error(@_);
- }
- };
+ my ($type) = @_;
+ my $logger = Log::Log4perl->get_logger("Bugzilla.Error.$type");
+ return sub {
+ local $Log::Log4perl::caller_depth = $Log::Log4perl::caller_depth + 3;
+ if ($type eq 'User') {
+ $logger->warn(@_);
+ }
+ else {
+ $logger->error(@_);
+ }
+ };
}
sub ThrowUserError {
- my ($error, $vars) = @_;
- my $logfunc = _make_logfunc('User');
- _add_vars_to_logging_fields($vars);
+ my ($error, $vars) = @_;
+ my $logfunc = _make_logfunc('User');
+ _add_vars_to_logging_fields($vars);
- _throw_error( 'global/user-error.html.tmpl', $error, $vars, $logfunc);
+ _throw_error('global/user-error.html.tmpl', $error, $vars, $logfunc);
}
sub ThrowCodeError {
- my ($error, $vars) = @_;
- my $logfunc = _make_logfunc('Code');
- _add_vars_to_logging_fields($vars);
+ my ($error, $vars) = @_;
+ my $logfunc = _make_logfunc('Code');
+ _add_vars_to_logging_fields($vars);
- _throw_error( 'global/code-error.html.tmpl', $error, $vars, $logfunc );
+ _throw_error('global/code-error.html.tmpl', $error, $vars, $logfunc);
}
sub ThrowTemplateError {
- my ($template_err) = @_;
- my $dbh = eval { Bugzilla->dbh };
- # Make sure the transaction is rolled back (if supported).
- $dbh->bz_rollback_transaction() if $dbh && $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 = eval { Bugzilla->dbh };
+
+ # Make sure the transaction is rolled back (if supported).
+ $dbh->bz_rollback_transaction() if $dbh && $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;
}
+ }
- # mod_perl overrides exit to call die with this string
- # we never want to display this to the user
- die $template_err if ref($template_err) eq 'ARRAY' && $template_err->[0] eq "EXIT\n";
+ my $vars = {};
+ if (Bugzilla->error_mode == ERROR_MODE_DIE) {
+ die("error: template error: $template_err");
+ }
- state $logger = Log::Log4perl->get_logger('Bugzilla.Error.Template');
- $logger->error($template_err);
+ # mod_perl overrides exit to call die with this string
+ # we never want to display this to the user
+ die $template_err
+ if ref($template_err) eq 'ARRAY' && $template_err->[0] eq "EXIT\n";
- $vars->{'template_error_msg'} = $template_err;
- $vars->{'error'} = "template_error";
+ state $logger = Log::Log4perl->get_logger('Bugzilla.Error.Template');
+ $logger->error($template_err);
- $vars->{'template_error_msg'} =~ s/ at \S+ line \d+\.\s*$//;
+ $vars->{'template_error_msg'} = $template_err;
+ $vars->{'error'} = "template_error";
- my $template = Bugzilla->template;
+ $vars->{'template_error_msg'} =~ s/ at \S+ line \d+\.\s*$//;
+
+ my $template = Bugzilla->template;
- # Try a template first; but if this one fails too, fall back
- # on plain old print statements.
- if (!$template->process("global/code-error.html.tmpl", $vars)) {
- my $maintainer = html_quote(Bugzilla->params->{'maintainer'});
- my $error = html_quote($vars->{'template_error_msg'});
- my $error2 = html_quote($template->error());
- 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)) {
+ my $maintainer = html_quote(Bugzilla->params->{'maintainer'});
+ my $error = html_quote($vars->{'template_error_msg'});
+ my $error2 = html_quote($template->error());
+ print <<END;
<tt>
<p>
Bugzilla has suffered an internal error:
@@ -241,46 +249,51 @@ sub ThrowTemplateError {
</p>
</tt>
END
- }
- exit;
+ }
+ exit;
}
sub ThrowErrorPage {
- # BMO customisation for bug 659231
- my ($template_name, $message) = @_;
- my $dbh = Bugzilla->dbh;
- $dbh->bz_rollback_transaction() if $dbh->bz_in_transaction();
+ # BMO customisation for bug 659231
+ my ($template_name, $message) = @_;
- if (Bugzilla->error_mode == ERROR_MODE_DIE) {
- die("error: $message");
- }
+ my $dbh = Bugzilla->dbh;
+ $dbh->bz_rollback_transaction() if $dbh->bz_in_transaction();
+
+ if (Bugzilla->error_mode == ERROR_MODE_DIE) {
+ die("error: $message");
+ }
- if (Bugzilla->error_mode == ERROR_MODE_DIE_SOAP_FAULT
- || Bugzilla->error_mode == ERROR_MODE_JSON_RPC)
- {
- my $code = ERROR_UNKNOWN_TRANSIENT;
- if (Bugzilla->error_mode == ERROR_MODE_DIE_SOAP_FAULT) {
- die SOAP::Fault->faultcode($code)->faultstring($message);
- } else {
- my $server = Bugzilla->_json_server;
- $server->raise_error(code => 100000 + $code,
- message => $message,
- id => $server->{_bz_request_id},
- version => $server->version);
- die if _in_eval();
- $server->response($server->error_response_header);
- }
- } else {
- my $cgi = Bugzilla->cgi;
- my $template = Bugzilla->template;
- my $vars = {};
- $vars->{message} = $message;
- print $cgi->header();
- $template->process($template_name, $vars)
- || ThrowTemplateError($template->error());
- exit;
+ if ( Bugzilla->error_mode == ERROR_MODE_DIE_SOAP_FAULT
+ || Bugzilla->error_mode == ERROR_MODE_JSON_RPC)
+ {
+ my $code = ERROR_UNKNOWN_TRANSIENT;
+ if (Bugzilla->error_mode == ERROR_MODE_DIE_SOAP_FAULT) {
+ die SOAP::Fault->faultcode($code)->faultstring($message);
}
+ else {
+ my $server = Bugzilla->_json_server;
+ $server->raise_error(
+ code => 100000 + $code,
+ message => $message,
+ id => $server->{_bz_request_id},
+ version => $server->version
+ );
+ die if _in_eval();
+ $server->response($server->error_response_header);
+ }
+ }
+ else {
+ my $cgi = Bugzilla->cgi;
+ my $template = Bugzilla->template;
+ my $vars = {};
+ $vars->{message} = $message;
+ print $cgi->header();
+ $template->process($template_name, $vars)
+ || ThrowTemplateError($template->error());
+ exit;
+ }
}
1;
diff --git a/Bugzilla/Error/Base.pm b/Bugzilla/Error/Base.pm
index ea44c272a..ed3bb2cd5 100644
--- a/Bugzilla/Error/Base.pm
+++ b/Bugzilla/Error/Base.pm
@@ -13,9 +13,9 @@ use Mojo::Base 'Mojo::Exception';
has 'vars' => sub { {} };
has 'template' => sub {
- my $self = shift;
- my $type = lc( (split(/::/, ref $self))[-1] );
- return "global/$type-error";
+ my $self = shift;
+ my $type = lc((split(/::/, ref $self))[-1]);
+ return "global/$type-error";
};
1;
diff --git a/Bugzilla/Error/Template.pm b/Bugzilla/Error/Template.pm
index a3afa7e4d..5cae8f5bc 100644
--- a/Bugzilla/Error/Template.pm
+++ b/Bugzilla/Error/Template.pm
@@ -12,15 +12,9 @@ use strict;
use warnings;
use Moo;
-has 'file' => (
- is => 'ro',
- required => 1,
-);
+has 'file' => (is => 'ro', required => 1,);
-has 'vars' => (
- is => 'ro',
- default => sub { {} },
-);
+has 'vars' => (is => 'ro', default => sub { {} },);
-1; \ No newline at end of file
+1;
diff --git a/Bugzilla/Extension.pm b/Bugzilla/Extension.pm
index 8e173c711..901999978 100644
--- a/Bugzilla/Extension.pm
+++ b/Bugzilla/Extension.pm
@@ -22,52 +22,54 @@ use Taint::Util qw(untaint);
BEGIN { push @INC, \&INC_HOOK }
sub INC_HOOK {
- my (undef, $fake_file) = @_;
- state $bz_locations = bz_locations();
- my ($vol, $dir, $file) = File::Spec->splitpath($fake_file);
- my @dirs = grep { length $_ } File::Spec->splitdir($dir);
-
- if (@dirs > 2 && $dirs[0] eq 'Bugzilla' && $dirs[1] eq 'Extension') {
- my $extension = $dirs[2];
- splice @dirs, 0, 3, File::Spec->splitdir($bz_locations->{extensionsdir}), $extension, "lib";
- my $real_file = Cwd::realpath(File::Spec->catpath($vol, File::Spec->catdir(@dirs), $file));
-
- my $first = 1;
- untaint($real_file);
- $INC{$fake_file} = $real_file;
- my $found = open my $fh, '<', $real_file;
- unless ($found) {
- require Carp;
- Carp::croak "Can't locate $fake_file while looking for $real_file in \@INC (\@INC contains: @INC)";
- }
- return sub {
- no warnings;
- if ( !$first ) {
- return 0 if eof $fh;
- $_ = readline $fh
- or return 0;
- untaint($_);
- return 1;
- }
- else {
- $_ = qq{# line 1 "$real_file"\n};
- $first = 0;
- return 1;
- }
- };
+ my (undef, $fake_file) = @_;
+ state $bz_locations = bz_locations();
+ my ($vol, $dir, $file) = File::Spec->splitpath($fake_file);
+ my @dirs = grep { length $_ } File::Spec->splitdir($dir);
+
+ if (@dirs > 2 && $dirs[0] eq 'Bugzilla' && $dirs[1] eq 'Extension') {
+ my $extension = $dirs[2];
+ splice @dirs, 0, 3, File::Spec->splitdir($bz_locations->{extensionsdir}),
+ $extension, "lib";
+ my $real_file
+ = Cwd::realpath(File::Spec->catpath($vol, File::Spec->catdir(@dirs), $file));
+
+ my $first = 1;
+ untaint($real_file);
+ $INC{$fake_file} = $real_file;
+ my $found = open my $fh, '<', $real_file;
+ unless ($found) {
+ require Carp;
+ Carp::croak
+ "Can't locate $fake_file while looking for $real_file in \@INC (\@INC contains: @INC)";
}
- return;
-};
+ return sub {
+ no warnings;
+ if (!$first) {
+ return 0 if eof $fh;
+ $_ = readline $fh or return 0;
+ untaint($_);
+ return 1;
+ }
+ else {
+ $_ = qq{# line 1 "$real_file"\n};
+ $first = 0;
+ return 1;
+ }
+ };
+ }
+ return;
+}
####################
# Subclass Methods #
####################
sub new {
- my ($class, $params) = @_;
- $params ||= {};
- bless $params, $class;
- return $params;
+ my ($class, $params) = @_;
+ $params ||= {};
+ bless $params, $class;
+ return $params;
}
#######################################
@@ -75,83 +77,80 @@ 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";
- }
- }
-
- if ($map and defined $map->{$extension_file}) {
- $package = $map->{$extension_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";
+ my $name = require $config_file;
+ if ($name =~ /^\d+$/) {
+ ThrowCodeError('extension_must_return_name',
+ {extension => $config_file, returned => $name});
+ }
+ $package = "${class}::$name";
}
+ }
+
+ if ($map and defined $map->{$extension_file}) {
+ $package = $map->{$extension_file};
+ }
+ else {
+ my $name = require $extension_file;
+ if ($name =~ /^\d+$/) {
+ ThrowCodeError('extension_must_return_name',
+ {extension => $extension_file, returned => $name});
+ }
+ $package = "${class}::$name";
+ }
- $class->_validate_package($package, $extension_file);
- return $package;
+ $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';
- }
-
- 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 });
- }
+ 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;
- state $EXTENSIONS = [];
- return $EXTENSIONS if @$EXTENSIONS;
-
- my ($file_sets) = extension_code_files();
- foreach my $file_set (@$file_sets) {
- my $package = $class->load(@$file_set);
- push(@$EXTENSIONS, $package);
- }
+ my $class = shift;
+ state $EXTENSIONS = [];
+ return $EXTENSIONS if @$EXTENSIONS;
+
+ my ($file_sets) = extension_code_files();
+ foreach my $file_set (@$file_sets) {
+ my $package = $class->load(@$file_set);
+ push(@$EXTENSIONS, $package);
+ }
- return $EXTENSIONS;
+ return $EXTENSIONS;
}
####################
@@ -160,21 +159,21 @@ sub load_all {
use constant enabled => 1;
-sub package_dir {
- my ($class) = @_;
- state $bz_locations = bz_locations();
- my (undef, undef, $name) = split(/::/, $class);
- return File::Spec->catdir($bz_locations->{extensionsdir}, $name);
+sub package_dir {
+ my ($class) = @_;
+ state $bz_locations = bz_locations();
+ my (undef, undef, $name) = split(/::/, $class);
+ return File::Spec->catdir($bz_locations->{extensionsdir}, $name);
}
sub template_dir {
- my ($class) = @_;
- return File::Spec->catdir($class->package_dir, "template");
+ my ($class) = @_;
+ return File::Spec->catdir($class->package_dir, "template");
}
sub web_dir {
- my ($class) = @_;
- return File::Spec->catdir($class->package_dir, "web");
+ my ($class) = @_;
+ return File::Spec->catdir($class->package_dir, "web");
}
1;
diff --git a/Bugzilla/Field.pm b/Bugzilla/Field.pm
index 837e1c0de..b0b7224dd 100644
--- a/Bugzilla/Field.pm
+++ b/Bugzilla/Field.pm
@@ -83,80 +83,79 @@ use constant DB_TABLE => 'fielddefs';
use constant LIST_ORDER => 'sortkey, name';
use constant DB_COLUMNS => qw(
- id
- name
- description
- type
- custom
- mailhead
- sortkey
- obsolete
- enter_bug
- buglist
- visibility_field_id
- value_field_id
- reverse_desc
- is_mandatory
- is_numeric
+ id
+ name
+ description
+ 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,
- 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,
+ 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
- mailhead
- sortkey
- obsolete
- enter_bug
- buglist
- visibility_field_id
- value_field_id
- reverse_desc
- is_mandatory
- is_numeric
- type
+ description
+ 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' },
- # BMO : allow integer fields to be NULL
- FIELD_TYPE_INTEGER, { TYPE => 'INT4' },
+
+ # 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'},
+
+ # BMO : allow integer fields to be NULL
+ FIELD_TYPE_INTEGER, {TYPE => 'INT4'},
};
# Field definitions for the fields that ship with Bugzilla.
@@ -164,108 +163,217 @@ 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 => 'component', desc => 'Component', 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 => '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 => '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 => 'assignee_last_login', desc => 'Assignee Last Login Date', 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',
- 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 => 'Tags'},
- {name => 'last_visit_ts', desc => 'Last Visit', buglist => 1,
- type => FIELD_TYPE_DATETIME},
- {name => 'bug_interest_ts', desc => 'Bug Interest', buglist => 1,
- type => FIELD_TYPE_DATETIME},
- {name => 'comment_tag', desc => 'Comment Tag'},
- {name => 'triage_owner', desc => 'Triage Owner', buglist => 1},
+ {
+ 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 => 'component',
+ desc => 'Component',
+ 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 => '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 => '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 => 'assignee_last_login',
+ desc => 'Assignee Last Login Date',
+ 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', 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 => 'Tags'},
+ {
+ name => 'last_visit_ts',
+ desc => 'Last Visit',
+ buglist => 1,
+ type => FIELD_TYPE_DATETIME
+ },
+ {
+ name => 'bug_interest_ts',
+ desc => 'Bug Interest',
+ buglist => 1,
+ type => FIELD_TYPE_DATETIME
+ },
+ {name => 'comment_tag', desc => 'Comment Tag'},
+ {name => 'triage_owner', desc => 'Triage Owner', buglist => 1},
);
################
@@ -274,15 +382,15 @@ 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];
- }
- if (delete $params->{skip_extensions}) {
- $params->{WHERE}{'type != ?'} = FIELD_TYPE_EXTENSION;
- }
- return $self->SUPER::match(@_);
+ my $self = shift;
+ my ($params) = @_;
+ if (delete $params->{is_select}) {
+ $params->{type} = [FIELD_TYPE_SINGLE_SELECT, FIELD_TYPE_MULTI_SELECT];
+ }
+ if (delete $params->{skip_extensions}) {
+ $params->{WHERE}{'type != ?'} = FIELD_TYPE_EXTENSION;
+ }
+ return $self->SUPER::match(@_);
}
##############
@@ -292,142 +400,144 @@ 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_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
- }
+ 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;
+ $reverse_desc = clean_text($reverse_desc);
+ return $reverse_desc;
}
sub _check_is_mandatory { return $_[1] ? 1 : 0; }
@@ -563,11 +673,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
@@ -588,19 +700,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
@@ -617,8 +729,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
@@ -637,12 +749,12 @@ 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({ id => $self->{visibility_field_id}, cache => 1 });
- }
- return $self->{visibility_field};
+ my $self = shift;
+ if ($self->{visibility_field_id}) {
+ $self->{visibility_field}
+ ||= $self->new({id => $self->{visibility_field_id}, cache => 1});
+ }
+ return $self->{visibility_field};
}
=pod
@@ -660,22 +772,23 @@ or undef if there is no C<visibility_field> set.
=cut
sub visibility_values {
- my $self = shift;
- my $dbh = Bugzilla->dbh;
+ my $self = shift;
+ my $dbh = Bugzilla->dbh;
- return [] if !$self->{visibility_field_id};
+ 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);
+ 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);
- }
+ $self->{visibility_values}
+ = Bugzilla::Field::Choice->type($self->visibility_field)
+ ->new_from_list($visibility_value_ids);
+ }
- return $self->{visibility_values};
+ return $self->{visibility_values};
}
=pod
@@ -692,10 +805,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
@@ -713,11 +826,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
@@ -734,10 +847,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
@@ -751,15 +864,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
@@ -775,13 +888,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
@@ -866,28 +979,31 @@ 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_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_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]); }
@@ -911,83 +1027,87 @@ 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;
+
+ if (!$self->custom) {
+ ThrowCodeError('field_not_custom', {'name' => $name});
+ }
+
+ if (!$self->obsolete) {
+ ThrowUserError('customfield_not_obsolete', {'name' => $self->name});
+ }
+
+ # BMO: disable bug updates during field creation
+ # using an eval as try/finally
+ eval {
+ SetParam('disable_bug_updates', 1);
+ write_params();
+
+ $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});
+ }
- my $name = $self->name;
+ # Check to see if bugs table has records (slow)
+ my $bugs_query = "";
- if (!$self->custom) {
- ThrowCodeError('field_not_custom', {'name' => $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 != ''";
+ }
+
+ # Ignore the default single select value
+ if ($self->type == FIELD_TYPE_SINGLE_SELECT) {
+ $bugs_query .= " AND $name != '---'";
+ }
}
- if (!$self->obsolete) {
- ThrowUserError('customfield_not_obsolete', {'name' => $self->name });
+ my $has_bugs = $dbh->selectrow_array($bugs_query);
+ if ($has_bugs) {
+ ThrowUserError('customfield_has_contents', {'name' => $name});
}
- # BMO: disable bug updates during field creation
- # using an eval as try/finally
- eval {
- SetParam('disable_bug_updates', 1);
- write_params();
-
- $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 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 != '---'";
- }
- }
-
- my $has_bugs = $dbh->selectrow_array($bugs_query);
- if ($has_bugs) {
- ThrowUserError('customfield_has_contents', {'name' => $name });
- }
-
- # Once we reach here, we should be OK to delete.
- $dbh->do('DELETE FROM fielddefs WHERE id = ?', undef, $self->id);
-
- my $type = $self->type;
-
- # the values for multi-select are stored in a seperate table
- if ($type != FIELD_TYPE_MULTI_SELECT) {
- $dbh->bz_drop_column('bugs', $name);
- }
-
- if ($self->is_select) {
- # Delete the table that holds the legal values for this field.
- $dbh->bz_drop_field_tables($self);
- }
-
- Bugzilla->memcached->clear({ table => 'fielddefs', id => $self->id });
- Bugzilla->memcached->clear_config();
-
- $dbh->bz_commit_transaction();
- };
- my $error = "$@";
- SetParam('disable_bug_updates', 0);
- write_params();
- die $error if $error;
+ # Once we reach here, we should be OK to delete.
+ $dbh->do('DELETE FROM fielddefs WHERE id = ?', undef, $self->id);
+
+ my $type = $self->type;
+
+ # the values for multi-select are stored in a seperate table
+ if ($type != FIELD_TYPE_MULTI_SELECT) {
+ $dbh->bz_drop_column('bugs', $name);
+ }
+
+ if ($self->is_select) {
+
+ # Delete the table that holds the legal values for this field.
+ $dbh->bz_drop_field_tables($self);
+ }
+
+ Bugzilla->memcached->clear({table => 'fielddefs', id => $self->id});
+ Bugzilla->memcached->clear_config();
+
+ $dbh->bz_commit_transaction();
+ };
+ my $error = "$@";
+ SetParam('disable_bug_updates', 0);
+ write_params();
+ die $error if $error;
}
=pod
@@ -1031,109 +1151,114 @@ C<is_mandatory> - boolean - Whether this field is mandatory. Defaults to 0.
=cut
sub create {
- my $class = shift;
- my ($params) = @_;
- my $dbh = Bugzilla->dbh;
-
- # BMO: disable bug updates during field creation
- # using an eval as try/finally
- my $field;
- eval {
- if ($params->{'custom'}) {
- SetParam('disable_bug_updates', 1);
- write_params();
- }
-
- # 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};
- $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) {
- # 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();
- }
- };
-
- my $error = "$@";
+ my $class = shift;
+ my ($params) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ # BMO: disable bug updates during field creation
+ # using an eval as try/finally
+ my $field;
+ eval {
if ($params->{'custom'}) {
- SetParam('disable_bug_updates', 0);
- write_params();
+ SetParam('disable_bug_updates', 1);
+ write_params();
}
- die $error if $error;
- Bugzilla::Hook::process("field_end_of_create", { field => $field });
+ # 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;
- return $field;
+ # 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};
+ $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) {
+
+ # 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();
+ }
+ };
+
+ my $error = "$@";
+ if ($params->{'custom'}) {
+ SetParam('disable_bug_updates', 0);
+ write_params();
+ }
+ die $error if $error;
+
+ Bugzilla::Hook::process("field_end_of_create", {field => $field});
+
+ 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;
+ 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
@@ -1156,13 +1281,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
@@ -1181,107 +1307,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)) {
- print "SQL fragment found in the 'fielddefs' table...\n";
- print "Old field name: " . $old_field_name . "\n";
- # 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.
- print "Fixing the 'fielddefs' table...\n";
- print "New field name: " . $new_field_name . "\n";
- $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)) {
+ print "SQL fragment found in the 'fielddefs' table...\n";
+ print "Old field name: " . $old_field_name . "\n";
+
+ # 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.
+ print "Fixing the 'fielddefs' table...\n";
+ print "New field name: " . $new_field_name . "\n";
+ $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
@@ -1313,32 +1447,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
@@ -1360,15 +1494,17 @@ Returns: the corresponding field ID or an error if the field name
=cut
sub get_field_id {
- my ($name) = @_;
- my $dbh = Bugzilla->dbh;
+ my ($name) = @_;
+ my $dbh = Bugzilla->dbh;
- trick_taint($name);
- my $id = $dbh->selectrow_array('SELECT id FROM fielddefs
- WHERE name = ?', undef, $name);
+ trick_taint($name);
+ my $id = $dbh->selectrow_array(
+ 'SELECT id FROM fielddefs
+ WHERE name = ?', undef, $name
+ );
- ThrowCodeError('invalid_field_name', {field => $name}) unless $id;
- return $id
+ ThrowCodeError('invalid_field_name', {field => $name}) unless $id;
+ return $id;
}
1;
diff --git a/Bugzilla/Field/Choice.pm b/Bugzilla/Field/Choice.pm
index 10f8f38e6..eab2c20f3 100644
--- a/Bugzilla/Field/Choice.pm
+++ b/Bugzilla/Field/Choice.pm
@@ -28,42 +28,42 @@ use Scalar::Util qw(blessed);
use constant IS_CONFIG => 1;
use constant DB_COLUMNS => qw(
- id
- value
- sortkey
- isactive
- visibility_value_id
+ id
+ value
+ sortkey
+ isactive
+ visibility_value_id
);
use constant UPDATE_COLUMNS => qw(
- value
- sortkey
- isactive
- visibility_value_id
+ value
+ sortkey
+ isactive
+ visibility_value_id
);
use constant NAME_FIELD => 'value';
use constant LIST_ORDER => 'sortkey, value';
use constant VALIDATORS => {
- value => \&_check_value,
- sortkey => \&_check_sortkey,
- visibility_value_id => \&_check_visibility_value_id,
- isactive => \&_check_isactive,
+ value => \&_check_value,
+ sortkey => \&_check_sortkey,
+ visibility_value_id => \&_check_visibility_value_id,
+ isactive => \&_check_isactive,
};
use constant CLASS_MAP => {
- bug_status => 'Bugzilla::Status',
- classification => 'Bugzilla::Classification',
- component => 'Bugzilla::Component',
- product => 'Bugzilla::Product',
+ bug_status => 'Bugzilla::Status',
+ classification => 'Bugzilla::Classification',
+ component => 'Bugzilla::Component',
+ product => 'Bugzilla::Product',
};
use constant DEFAULT_MAP => {
- op_sys => 'defaultopsys',
- rep_platform => 'defaultplatform',
- priority => 'defaultpriority',
- bug_severity => 'defaultseverity',
+ op_sys => 'defaultopsys',
+ rep_platform => 'defaultplatform',
+ priority => 'defaultpriority',
+ bug_severity => 'defaultseverity',
};
#################
@@ -76,32 +76,32 @@ 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 ($class->CLASS_MAP->{$field_name}) {
- return $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;
+ my ($class, $field) = @_;
+ my $field_obj = blessed $field ? $field : Bugzilla::Field->check($field);
+ my $field_name = $field_obj->name;
+
+ if ($class->CLASS_MAP->{$field_name}) {
+ return $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;
package $package;
use base qw(Bugzilla::Field::Choice);
use constant DB_TABLE => '$field_name';
EOC
- }
+ }
- return $package;
+ return $package;
}
################
@@ -112,11 +112,11 @@ EOC
# 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(@_);
}
#########################
@@ -128,64 +128,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);
}
- $dbh->bz_commit_transaction();
- return wantarray ? ($changes, $old_self) : $changes;
+ 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;
}
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();
}
############
@@ -193,12 +195,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};
}
##############
@@ -206,73 +209,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)
- || 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)
+ || ThrowUserError('fieldvalue_sortkey_invalid',
+ {sortkey => $orig_value, field => $invocant->field});
+ return $value;
}
sub _check_visibility_value_id {
- my ($invocant, $value_id) = @_;
- $value_id = trim($value_id);
- my $field = $invocant->field->value_field;
- return undef if !$field || !$value_id;
- my $value_obj = Bugzilla::Field::Choice->type($field)
- ->check({ id => $value_id });
- return $value_obj->id;
+ my ($invocant, $value_id) = @_;
+ $value_id = trim($value_id);
+ my $field = $invocant->field->value_field;
+ return undef if !$field || !$value_id;
+ my $value_obj = Bugzilla::Field::Choice->type($field)->check({id => $value_id});
+ return $value_obj->id;
}
1;
diff --git a/Bugzilla/Field/ChoiceInterface.pm b/Bugzilla/Field/ChoiceInterface.pm
index bcfd75578..207ac9cb5 100644
--- a/Bugzilla/Field/ChoiceInterface.pm
+++ b/Bugzilla/Field/ChoiceInterface.pm
@@ -26,14 +26,19 @@ sub FIELD_NAME { return $_[0]->DB_TABLE; }
####################
sub _check_if_controller {
- my $self = shift;
- my $vis_fields = $self->controls_visibility_of_fields;
- my $values = $self->controlled_values_array;
- if (@$vis_fields || @$values) {
- ThrowUserError('fieldvalue_is_controller',
- { value => $self, fields => [map($_->name, @$vis_fields)],
- vals => $self->controlled_values });
- }
+ my $self = shift;
+ my $vis_fields = $self->controls_visibility_of_fields;
+ my $values = $self->controlled_values_array;
+ if (@$vis_fields || @$values) {
+ ThrowUserError(
+ 'fieldvalue_is_controller',
+ {
+ value => $self,
+ fields => [map($_->name, @$vis_fields)],
+ vals => $self->controlled_values
+ }
+ );
+ }
}
@@ -42,145 +47,149 @@ sub _check_if_controller {
#############
sub is_active { return $_[0]->{'isactive'}; }
-sub sortkey { return $_[0]->{'sortkey'}; }
+sub sortkey { return $_[0]->{'sortkey'}; }
sub bug_count {
- my $self = shift;
- return $self->{bug_count} if defined $self->{bug_count};
- my $dbh = Bugzilla->dbh;
- my $fname = $self->field->name;
- my $count;
- if ($self->field->type == FIELD_TYPE_MULTI_SELECT) {
- $count = $dbh->selectrow_array("SELECT COUNT(*) FROM bug_$fname
- WHERE value = ?", undef, $self->name);
- }
- else {
- $count = $dbh->selectrow_array("SELECT COUNT(*) FROM bugs
- WHERE $fname = ?",
- undef, $self->name);
- }
- $self->{bug_count} = $count;
- return $count;
+ my $self = shift;
+ return $self->{bug_count} if defined $self->{bug_count};
+ my $dbh = Bugzilla->dbh;
+ my $fname = $self->field->name;
+ my $count;
+ if ($self->field->type == FIELD_TYPE_MULTI_SELECT) {
+ $count = $dbh->selectrow_array(
+ "SELECT COUNT(*) FROM bug_$fname
+ WHERE value = ?", undef, $self->name
+ );
+ }
+ else {
+ $count = $dbh->selectrow_array(
+ "SELECT COUNT(*) FROM bugs
+ WHERE $fname = ?", undef, $self->name
+ );
+ }
+ $self->{bug_count} = $count;
+ return $count;
}
sub field {
- my $invocant = shift;
- my $class = ref $invocant || $invocant;
- my $cache = Bugzilla->request_cache;
- # This is just to make life easier for subclasses. Our auto-generated
- # subclasses from Bugzilla::Field::Choice->type() already have this set.
- $cache->{"field_$class"} ||=
- new Bugzilla::Field({ name => $class->FIELD_NAME });
- return $cache->{"field_$class"};
+ my $invocant = shift;
+ my $class = ref $invocant || $invocant;
+ my $cache = Bugzilla->request_cache;
+
+ # This is just to make life easier for subclasses. Our auto-generated
+ # subclasses from Bugzilla::Field::Choice->type() already have this set.
+ $cache->{"field_$class"} ||= new Bugzilla::Field({name => $class->FIELD_NAME});
+ return $cache->{"field_$class"};
}
sub is_default {
- my $self = shift;
- my $name = $self->DEFAULT_MAP->{$self->field->name};
- # If it doesn't exist in DEFAULT_MAP, then there is no parameter
- # related to this field.
- return 0 unless $name;
- return ($self->name eq Bugzilla->params->{$name}) ? 1 : 0;
+ my $self = shift;
+ my $name = $self->DEFAULT_MAP->{$self->field->name};
+
+ # If it doesn't exist in DEFAULT_MAP, then there is no parameter
+ # related to this field.
+ return 0 unless $name;
+ return ($self->name eq Bugzilla->params->{$name}) ? 1 : 0;
}
sub is_static {
- my $self = shift;
- # If we need to special-case Resolution for *anything* else, it should
- # get its own subclass.
- if ($self->field->name eq 'resolution') {
- return grep($_ eq $self->name, ('', 'FIXED', 'DUPLICATE'))
- ? 1 : 0;
- }
- elsif ($self->field->custom) {
- return $self->name eq '---' ? 1 : 0;
- }
- return 0;
+ my $self = shift;
+
+ # If we need to special-case Resolution for *anything* else, it should
+ # get its own subclass.
+ if ($self->field->name eq 'resolution') {
+ return grep($_ eq $self->name, ('', 'FIXED', 'DUPLICATE')) ? 1 : 0;
+ }
+ elsif ($self->field->custom) {
+ return $self->name eq '---' ? 1 : 0;
+ }
+ return 0;
}
sub controls_visibility_of_fields {
- my $self = shift;
- my $dbh = Bugzilla->dbh;
+ my $self = shift;
+ my $dbh = Bugzilla->dbh;
- if (!$self->{controls_visibility_of_fields}) {
- my $ids = $dbh->selectcol_arrayref(
- "SELECT id FROM fielddefs
+ if (!$self->{controls_visibility_of_fields}) {
+ my $ids = $dbh->selectcol_arrayref(
+ "SELECT id FROM fielddefs
INNER JOIN field_visibility
ON fielddefs.id = field_visibility.field_id
- WHERE value_id = ? AND visibility_field_id = ?", undef,
- $self->id, $self->field->id);
+ WHERE value_id = ? AND visibility_field_id = ?", undef, $self->id,
+ $self->field->id
+ );
- $self->{controls_visibility_of_fields} =
- Bugzilla::Field->new_from_list($ids);
- }
+ $self->{controls_visibility_of_fields} = Bugzilla::Field->new_from_list($ids);
+ }
- return $self->{controls_visibility_of_fields};
+ return $self->{controls_visibility_of_fields};
}
sub visibility_value {
- my $self = shift;
- if ($self->{visibility_value_id}) {
- require Bugzilla::Field::Choice;
- $self->{visibility_value} ||=
- Bugzilla::Field::Choice->type($self->field->value_field)->new(
- $self->{visibility_value_id});
- }
- return $self->{visibility_value};
+ my $self = shift;
+ if ($self->{visibility_value_id}) {
+ require Bugzilla::Field::Choice;
+ $self->{visibility_value}
+ ||= Bugzilla::Field::Choice->type($self->field->value_field)
+ ->new($self->{visibility_value_id});
+ }
+ return $self->{visibility_value};
}
sub controlled_values {
- my $self = shift;
- return $self->{controlled_values} if defined $self->{controlled_values};
- my $fields = $self->field->controls_values_of;
- my %controlled_values;
- require Bugzilla::Field::Choice;
- foreach my $field (@$fields) {
- $controlled_values{$field->name} =
- Bugzilla::Field::Choice->type($field)
- ->match({ visibility_value_id => $self->id });
- }
- $self->{controlled_values} = \%controlled_values;
- return $self->{controlled_values};
+ my $self = shift;
+ return $self->{controlled_values} if defined $self->{controlled_values};
+ my $fields = $self->field->controls_values_of;
+ my %controlled_values;
+ require Bugzilla::Field::Choice;
+ foreach my $field (@$fields) {
+ $controlled_values{$field->name} = Bugzilla::Field::Choice->type($field)
+ ->match({visibility_value_id => $self->id});
+ }
+ $self->{controlled_values} = \%controlled_values;
+ return $self->{controlled_values};
}
sub controlled_values_array {
- my ($self) = @_;
- my $values = $self->controlled_values;
- return [map { @{ $values->{$_} } } keys %$values];
+ my ($self) = @_;
+ my $values = $self->controlled_values;
+ return [map { @{$values->{$_}} } keys %$values];
}
sub is_visible_on_bug {
- my ($self, $bug) = @_;
+ my ($self, $bug) = @_;
- # Values currently set on the bug are always shown.
- return 1 if $self->is_set_on_bug($bug);
+ # Values currently set on the bug are always shown.
+ return 1 if $self->is_set_on_bug($bug);
- # Inactive values are, otherwise, never shown.
- return 0 if !$self->is_active;
+ # Inactive values are, otherwise, never shown.
+ return 0 if !$self->is_active;
- # Values without a visibility value are, otherwise, always shown.
- my $visibility_value = $self->visibility_value;
- return 1 if !$visibility_value;
+ # Values without a visibility value are, otherwise, always shown.
+ my $visibility_value = $self->visibility_value;
+ return 1 if !$visibility_value;
- # Values with a visibility value are only shown if the visibility
- # value is set on the bug.
- return $visibility_value->is_set_on_bug($bug);
+ # Values with a visibility value are only shown if the visibility
+ # value is set on the bug.
+ return $visibility_value->is_set_on_bug($bug);
}
sub is_set_on_bug {
- my ($self, $bug) = @_;
- my $field_name = $self->FIELD_NAME;
- # This allows bug/create/create.html.tmpl to pass in a hashref that
- # looks like a bug object.
- my $value = blessed($bug) ? $bug->$field_name : $bug->{$field_name};
- $value = $value->name if blessed($value);
- return 0 if !defined $value;
-
- if ($self->field->type == FIELD_TYPE_BUG_URLS
- or $self->field->type == FIELD_TYPE_MULTI_SELECT)
- {
- return grep($_ eq $self->name, @$value) ? 1 : 0;
- }
- return $value eq $self->name ? 1 : 0;
+ my ($self, $bug) = @_;
+ my $field_name = $self->FIELD_NAME;
+
+ # This allows bug/create/create.html.tmpl to pass in a hashref that
+ # looks like a bug object.
+ my $value = blessed($bug) ? $bug->$field_name : $bug->{$field_name};
+ $value = $value->name if blessed($value);
+ return 0 if !defined $value;
+
+ if ( $self->field->type == FIELD_TYPE_BUG_URLS
+ or $self->field->type == FIELD_TYPE_MULTI_SELECT)
+ {
+ return grep($_ eq $self->name, @$value) ? 1 : 0;
+ }
+ return $value eq $self->name ? 1 : 0;
}
1;
diff --git a/Bugzilla/Flag.pm b/Bugzilla/Flag.pm
index 625794974..3ed055b3d 100644
--- a/Bugzilla/Flag.pm
+++ b/Bugzilla/Flag.pm
@@ -58,8 +58,9 @@ use base 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;
@@ -70,35 +71,32 @@ use constant SKIP_REQUESTEE_ON_ERROR => 1;
our $disable_flagmail = 0;
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 ######
@@ -140,15 +138,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'}; }
###############################
@@ -182,44 +180,43 @@ is an attachment flag, else undefined.
=cut
sub type {
- my $self = shift;
+ my $self = shift;
- return $self->{'type'}
- ||= new Bugzilla::FlagType($self->{'type_id'}, cache => 1 );
+ return $self->{'type'}
+ ||= new Bugzilla::FlagType($self->{'type_id'}, cache => 1);
}
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});
}
################################
@@ -241,26 +238,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
@@ -278,8 +276,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(@_)};
}
######################################################################
@@ -287,145 +285,157 @@ 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->reporter->id != $obj_flag->requestee->id)
- {
- $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->reporter->id != $obj_flag->requestee->id)
+ {
+ $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;
-
- $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});
- }
+ my ($class, $flag, $flag_type, $params, $bug, $attachment) = @_;
- # 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
@@ -441,153 +451,163 @@ 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 });
- }
+ my $self = shift;
+ my $dbh = Bugzilla->dbh;
+ my $timestamp = shift || $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
+
+ my $changes = $self->SUPER::update(@_);
- # BMO - provide a hook which passes the flag object
- Bugzilla::Hook::process('flag_updated', {flag => $self, changes => $changes, timestamp => $timestamp});
+ 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;
+ # BMO - provide a hook which passes the flag object
+ Bugzilla::Hook::process('flag_updated',
+ {flag => $self, changes => $changes, timestamp => $timestamp});
+
+ 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 @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;
- $new_flag->{creation_date} = format_time($timestamp, '%Y.%m.%d %H:%i:%s');
- $new_flag->{modification_date} = format_time($timestamp, '%Y.%m.%d %H:%i:%s');
- $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};
- }
+ my ($class, $self, $old_self, $timestamp) = @_;
+
+ 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;
+ $new_flag->{creation_date} = format_time($timestamp, '%Y.%m.%d %H:%i:%s');
+ $new_flag->{modification_date} = format_time($timestamp, '%Y.%m.%d %H:%i:%s');
+ $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);
-
- # BMO - provide a hook which passes the timestamp,
- # because that isn't passed to remove_from_db().
- Bugzilla::Hook::process('flag_deleted', {flag => $old_flag, timestamp => $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);
+
+ # BMO - provide a hook which passes the timestamp,
+ # because that isn't passed to remove_from_db().
+ Bugzilla::Hook::process('flag_deleted',
+ {flag => $old_flag, timestamp => $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
@@ -595,53 +615,56 @@ 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);
-
- # BMO - provide a hook which passes the timestamp,
- # because that isn't passed to remove_from_db().
- my ($timestamp) = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
- Bugzilla::Hook::process('flag_deleted', {flag => $flag, timestamp => $timestamp});
- $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);
+
+ # BMO - provide a hook which passes the timestamp,
+ # because that isn't passed to remove_from_db().
+ my ($timestamp) = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
+ Bugzilla::Hook::process('flag_deleted',
+ {flag => $flag, timestamp => $timestamp});
+ $flag->remove_from_db();
}
- return @removed;
+ }
+ return @removed;
}
###############################
@@ -649,153 +672,168 @@ 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 him or leave him alone.
+ ThrowCodeError('flag_type_requestee_disabled', {type => $self->type})
+ if !$self->type->is_requesteeble;
+
+ # BMO customisation:
+ # 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 him or leave him alone.
- ThrowCodeError('flag_type_requestee_disabled', { type => $self->type })
- if !$self->type->is_requesteeble;
-
- # BMO customisation:
- # 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)
- || ThrowCodeError('invalid_user');
-
- # set_status() has already been called. So this refers
- # to the new flag status.
- my $status = $self->status;
-
- ThrowUserError('flag_update_denied',
- { name => $self->type->name,
- status => $status,
- old_status => $self->{_old_status} })
- unless $setter->can_change_flag($self->type, $self->{_old_status} || 'X', $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;
+ my ($self, $setter) = @_;
+
+ # By default, the currently logged in user is the setter.
+ $setter ||= Bugzilla->user;
+ (blessed($setter) && $setter->isa('Bugzilla::User') && $setter->id)
+ || ThrowCodeError('invalid_user');
+
+ # set_status() has already been called. So this refers
+ # to the new flag status.
+ my $status = $self->status;
+
+ ThrowUserError(
+ 'flag_update_denied',
+ {
+ name => $self->type->name,
+ status => $status,
+ old_status => $self->{_old_status}
}
- return $setter;
+ )
+ unless $setter->can_change_flag($self->type, $self->{_old_status} || 'X',
+ $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;
}
######################################################################
@@ -816,128 +854,146 @@ array of hashes. This array is then passed to Flag::create().
=cut
sub extract_flags_from_cgi {
- my ($class, $bug, $attachment, $vars, $skip) = @_;
- my $cgi = Bugzilla->cgi;
-
- my $match_status = Bugzilla::User::match_field({
- '^requestee(_type)?-(\d+)$' => { 'type' => 'multi' },
- }, undef, $skip);
-
- $vars->{'match_field'} = 'requestee';
- if ($match_status == USER_MATCH_FAILED) {
- $vars->{'message'} = 'user_match_failed';
- }
- elsif ($match_status == USER_MATCH_MULTIPLE) {
- $vars->{'message'} = 'user_match_multiple';
+ my ($class, $bug, $attachment, $vars, $skip) = @_;
+ my $cgi = Bugzilla->cgi;
+
+ my $match_status
+ = Bugzilla::User::match_field(
+ {'^requestee(_type)?-(\d+)$' => {'type' => 'multi'},},
+ undef, $skip);
+
+ $vars->{'match_field'} = 'requestee';
+ if ($match_status == USER_MATCH_FAILED) {
+ $vars->{'message'} = 'user_match_failed';
+ }
+ elsif ($match_status == USER_MATCH_MULTIPLE) {
+ $vars->{'message'} = 'user_match_multiple';
+ }
+
+ # Extract a list of flag type IDs from field names.
+ my @flagtype_ids = map(/^flag_type-(\d+)$/ ? $1 : (), $cgi->param());
+ @flagtype_ids = grep($cgi->param("flag_type-$_") ne 'X', @flagtype_ids);
+
+ # Extract a list of existing flag IDs.
+ my @flag_ids = map(/^flag-(\d+)$/ ? $1 : (), $cgi->param());
+
+ return ([], []) unless (scalar(@flagtype_ids) || scalar(@flag_ids));
+
+ my (@new_flags, @flags);
+ foreach my $flag_id (@flag_ids) {
+ my $flag = $class->new($flag_id);
+
+ # If the flag no longer exists, ignore it.
+ next unless $flag;
+
+ my $status = $cgi->param("flag-$flag_id");
+
+ # If the user entered more than one name into the requestee field
+ # (i.e. they want more than one person to set the flag) we can reuse
+ # the existing flag for the first person (who may well be the existing
+ # requestee), but we have to create new flags for each additional requestee.
+ my @requestees = $cgi->param("requestee-$flag_id");
+ my $requestee_email;
+ if ($status eq "?" && scalar(@requestees) > 1 && $flag->type->is_multiplicable)
+ {
+ # The first person, for which we'll reuse the existing flag.
+ $requestee_email = shift(@requestees);
+
+ # Create new flags like the existing one for each additional person.
+ foreach my $login (@requestees) {
+ push(
+ @new_flags,
+ {
+ type_id => $flag->type_id,
+ status => "?",
+ requestee => $login,
+ skip_roe => $skip
+ }
+ );
+ }
}
+ elsif ($status eq "?" && scalar(@requestees)) {
- # Extract a list of flag type IDs from field names.
- my @flagtype_ids = map(/^flag_type-(\d+)$/ ? $1 : (), $cgi->param());
- @flagtype_ids = grep($cgi->param("flag_type-$_") ne 'X', @flagtype_ids);
-
- # Extract a list of existing flag IDs.
- my @flag_ids = map(/^flag-(\d+)$/ ? $1 : (), $cgi->param());
-
- return ([], []) unless (scalar(@flagtype_ids) || scalar(@flag_ids));
-
- my (@new_flags, @flags);
- foreach my $flag_id (@flag_ids) {
- my $flag = $class->new($flag_id);
- # If the flag no longer exists, ignore it.
- next unless $flag;
-
- my $status = $cgi->param("flag-$flag_id");
-
- # If the user entered more than one name into the requestee field
- # (i.e. they want more than one person to set the flag) we can reuse
- # the existing flag for the first person (who may well be the existing
- # requestee), but we have to create new flags for each additional requestee.
- my @requestees = $cgi->param("requestee-$flag_id");
- my $requestee_email;
- if ($status eq "?"
- && scalar(@requestees) > 1
- && $flag->type->is_multiplicable)
- {
- # The first person, for which we'll reuse the existing flag.
- $requestee_email = shift(@requestees);
-
- # Create new flags like the existing one for each additional person.
- foreach my $login (@requestees) {
- push(@new_flags, { type_id => $flag->type_id,
- status => "?",
- requestee => $login,
- skip_roe => $skip });
- }
- }
- elsif ($status eq "?" && scalar(@requestees)) {
- # If there are several requestees and the flag type is not multiplicable,
- # this will fail. But that's the job of the validator to complain. All
- # we do here is to extract and convert data from the CGI.
- $requestee_email = trim($cgi->param("requestee-$flag_id") || '');
- }
-
- push(@flags, { id => $flag_id,
- status => $status,
- requestee => $requestee_email,
- skip_roe => $skip });
+ # If there are several requestees and the flag type is not multiplicable,
+ # this will fail. But that's the job of the validator to complain. All
+ # we do here is to extract and convert data from the CGI.
+ $requestee_email = trim($cgi->param("requestee-$flag_id") || '');
}
- # Get a list of active flag types available for this product/component.
- my $flag_types = Bugzilla::FlagType::match(
- { 'product_id' => $bug->{'product_id'},
- 'component_id' => $bug->{'component_id'},
- 'is_active' => 1 });
-
- foreach my $flagtype_id (@flagtype_ids) {
- # Checks if there are unexpected flags for the product/component.
- if (!scalar(grep { $_->id == $flagtype_id } @$flag_types)) {
- $vars->{'message'} = 'unexpected_flag_types';
- last;
- }
+ push(
+ @flags,
+ {
+ id => $flag_id,
+ status => $status,
+ requestee => $requestee_email,
+ skip_roe => $skip
+ }
+ );
+ }
+
+ # Get a list of active flag types available for this product/component.
+ my $flag_types = Bugzilla::FlagType::match({
+ 'product_id' => $bug->{'product_id'},
+ 'component_id' => $bug->{'component_id'},
+ 'is_active' => 1
+ });
+
+ foreach my $flagtype_id (@flagtype_ids) {
+
+ # Checks if there are unexpected flags for the product/component.
+ if (!scalar(grep { $_->id == $flagtype_id } @$flag_types)) {
+ $vars->{'message'} = 'unexpected_flag_types';
+ last;
}
-
- foreach my $flag_type (@$flag_types) {
- my $type_id = $flag_type->id;
-
- # Bug flags are only valid for bugs, and attachment flags are
- # only valid for attachments. So don't mix both.
- next unless ($flag_type->target_type eq 'bug' xor $attachment);
-
- # We are only interested in flags the user tries to create.
- next unless scalar(grep { $_ == $type_id } @flagtype_ids);
-
- # Get the number of flags of this type already set for this target.
- my $has_flags = $class->count(
- { 'type_id' => $type_id,
- 'target_type' => $attachment ? 'attachment' : 'bug',
- 'bug_id' => $bug->bug_id,
- 'attach_id' => $attachment ? $attachment->id : undef });
-
- # Do not create a new flag of this type if this flag type is
- # not multiplicable and already has a flag set.
- next if (!$flag_type->is_multiplicable && $has_flags);
-
- my $status = $cgi->param("flag_type-$type_id");
- trick_taint($status);
-
- my @logins = $cgi->param("requestee_type-$type_id");
- if ($status eq "?" && scalar(@logins)) {
- foreach my $login (@logins) {
- push (@new_flags, { type_id => $type_id,
- status => $status,
- requestee => $login,
- skip_roe => $skip });
- last unless $flag_type->is_multiplicable;
- }
- }
- else {
- push (@new_flags, { type_id => $type_id,
- status => $status });
- }
+ }
+
+ foreach my $flag_type (@$flag_types) {
+ my $type_id = $flag_type->id;
+
+ # Bug flags are only valid for bugs, and attachment flags are
+ # only valid for attachments. So don't mix both.
+ next unless ($flag_type->target_type eq 'bug' xor $attachment);
+
+ # We are only interested in flags the user tries to create.
+ next unless scalar(grep { $_ == $type_id } @flagtype_ids);
+
+ # Get the number of flags of this type already set for this target.
+ my $has_flags = $class->count({
+ 'type_id' => $type_id,
+ 'target_type' => $attachment ? 'attachment' : 'bug',
+ 'bug_id' => $bug->bug_id,
+ 'attach_id' => $attachment ? $attachment->id : undef
+ });
+
+ # Do not create a new flag of this type if this flag type is
+ # not multiplicable and already has a flag set.
+ next if (!$flag_type->is_multiplicable && $has_flags);
+
+ my $status = $cgi->param("flag_type-$type_id");
+ trick_taint($status);
+
+ my @logins = $cgi->param("requestee_type-$type_id");
+ if ($status eq "?" && scalar(@logins)) {
+ foreach my $login (@logins) {
+ push(
+ @new_flags,
+ {
+ type_id => $type_id,
+ status => $status,
+ requestee => $login,
+ skip_roe => $skip
+ }
+ );
+ last unless $flag_type->is_multiplicable;
+ }
+ }
+ else {
+ push(@new_flags, {type_id => $type_id, status => $status});
}
+ }
- # Return the list of flags to update and/or to create.
- return (\@flags, \@new_flags);
+ # Return the list of flags to update and/or to create.
+ return (\@flags, \@new_flags);
}
=pod
@@ -954,118 +1010,126 @@ or deleted.
=cut
sub notify {
- my ($class, $flag, $old_flag, $obj, $timestamp) = @_;
-
- if ($disable_flagmail) {
- return;
- }
-
- my ($bug, $attachment);
- if (blessed($obj) && $obj->isa('Bugzilla::Attachment')) {
- $attachment = $obj;
- $bug = $attachment->bug;
+ my ($class, $flag, $old_flag, $obj, $timestamp) = @_;
+
+ if ($disable_flagmail) {
+ return;
+ }
+
+ 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;
- $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, cache => 1 });
- next if (scalar(@bug_in_groups) && (!$ccuser || !$ccuser->can_see_bug($bug->bug_id)));
- next if $attachment_is_private && (!$ccuser || !$ccuser->is_insider);
- # Prevent duplicated entries due to case sensitivity.
- $cc = $ccuser ? $ccuser->email : $cc;
- $recipients{$cc} = $ccuser;
- }
-
- # Only notify if the addressee is allowed to receive the email.
- if ($addressee && $addressee->email_enabled) {
- $recipients{$addressee->email} = $addressee;
- }
- # Process and send notification for each recipient.
- # If there are users in the CC list who don't have an account,
- # use the default language for email notifications.
- my $default_lang;
- if (grep { !$_ } values %recipients) {
- $default_lang = Bugzilla::User->new()->setting('lang');
- }
-
- # Get comments on the bug
- my $all_comments = $bug->comments({ after => $bug->lastdiffed });
- @$all_comments = grep { $_->type || $_->body =~ /\S/ } @$all_comments;
-
- # Get public only comments
- my $public_comments = [ grep { !$_->is_private } @$all_comments ];
-
- foreach my $to (keys %recipients) {
- # Add threadingmarker to allow flag notification emails to be the
- # threaded similar to normal bug change emails.
- my $thread_user_id = $recipients{$to} ? $recipients{$to}->id : 0;
-
- # We only want to show private comments to users in the is_insider group
- my $comments = $recipients{$to} && $recipients{$to}->is_insider
- ? $all_comments : $public_comments;
-
- my $vars = {
- flag => $flag,
- old_flag => $old_flag,
- to => $to,
- date => $timestamp,
- bug => $bug,
- attachment => $attachment,
- threadingmarker => build_thread_marker($bug->id, $thread_user_id),
- new_comments => $comments,
- };
-
- my $lang = $recipients{$to} ?
- $recipients{$to}->setting('lang') : $default_lang;
-
- my $template = Bugzilla->template_inner($lang);
- my $message;
- $template->process("request/email.txt.tmpl", $vars, \$message)
- || ThrowTemplateError($template->error());
-
- MessageToMTA($message);
+ }
+ elsif ($old_flag
+ && $old_flag->status eq '?'
+ && (!$flag || $flag->status ne '?'))
+ {
+ if ($old_flag->setter->wants_mail([EVT_REQUESTED_FLAG])) {
+ $addressee = $old_flag->setter;
}
+ }
+
+ my $cc_list = $flag ? $flag->type->cc_list : $old_flag->type->cc_list;
+ $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, cache => 1});
+ next
+ if (scalar(@bug_in_groups)
+ && (!$ccuser || !$ccuser->can_see_bug($bug->bug_id)));
+ next if $attachment_is_private && (!$ccuser || !$ccuser->is_insider);
+
+ # Prevent duplicated entries due to case sensitivity.
+ $cc = $ccuser ? $ccuser->email : $cc;
+ $recipients{$cc} = $ccuser;
+ }
+
+ # Only notify if the addressee is allowed to receive the email.
+ if ($addressee && $addressee->email_enabled) {
+ $recipients{$addressee->email} = $addressee;
+ }
+
+ # Process and send notification for each recipient.
+ # If there are users in the CC list who don't have an account,
+ # use the default language for email notifications.
+ my $default_lang;
+ if (grep { !$_ } values %recipients) {
+ $default_lang = Bugzilla::User->new()->setting('lang');
+ }
+
+ # Get comments on the bug
+ my $all_comments = $bug->comments({after => $bug->lastdiffed});
+ @$all_comments = grep { $_->type || $_->body =~ /\S/ } @$all_comments;
+
+ # Get public only comments
+ my $public_comments = [grep { !$_->is_private } @$all_comments];
+
+ foreach my $to (keys %recipients) {
+
+ # Add threadingmarker to allow flag notification emails to be the
+ # threaded similar to normal bug change emails.
+ my $thread_user_id = $recipients{$to} ? $recipients{$to}->id : 0;
+
+ # We only want to show private comments to users in the is_insider group
+ my $comments = $recipients{$to}
+ && $recipients{$to}->is_insider ? $all_comments : $public_comments;
+
+ my $vars = {
+ flag => $flag,
+ old_flag => $old_flag,
+ to => $to,
+ date => $timestamp,
+ bug => $bug,
+ attachment => $attachment,
+ threadingmarker => build_thread_marker($bug->id, $thread_user_id),
+ new_comments => $comments,
+ };
+
+ my $lang = $recipients{$to} ? $recipients{$to}->setting('lang') : $default_lang;
+
+ my $template = Bugzilla->template_inner($lang);
+ my $message;
+ $template->process("request/email.txt.tmpl", $vars, \$message)
+ || ThrowTemplateError($template->error());
+
+ MessageToMTA($message);
+ }
}
# This is an internal function used by $bug->flag_types
@@ -1073,39 +1137,42 @@ sub notify {
# flag types and existing flags set on them. You should never
# call this function directly.
sub _flag_types {
- my ($class, $vars) = @_;
-
- my $target_type = $vars->{target_type};
- my $flags;
-
- # Retrieve all existing flags for this bug/attachment.
- if ($target_type eq 'bug') {
- my $bug_id = delete $vars->{bug_id};
- $flags = $class->match({target_type => 'bug', bug_id => $bug_id});
- }
- elsif ($target_type eq 'attachment') {
- my $attach_id = delete $vars->{attach_id};
- $flags = $class->match({attach_id => $attach_id});
- }
- else {
- ThrowCodeError('bad_arg', {argument => 'target_type',
- function => $class . '->_flag_types'});
- }
-
- # Get all available flag types for the given product and component.
- my $cache = Bugzilla->request_cache->{flag_types_per_component}->{$vars->{target_type}} ||= {};
- my $flag_data = $cache->{$vars->{component_id}} ||= Bugzilla::FlagType::match($vars);
- my $flag_types = dclone($flag_data);
-
- $_->{flags} = [] foreach @$flag_types;
- my %flagtypes = map { $_->id => $_ } @$flag_types;
-
- # Group existing flags per type, and skip those becoming invalid
- # (which can happen when a bug is being moved into a new product
- # or component).
- @$flags = grep { exists $flagtypes{$_->type_id} } @$flags;
- push(@{$flagtypes{$_->type_id}->{flags}}, $_) foreach @$flags;
- return $flag_types;
+ my ($class, $vars) = @_;
+
+ my $target_type = $vars->{target_type};
+ my $flags;
+
+ # Retrieve all existing flags for this bug/attachment.
+ if ($target_type eq 'bug') {
+ my $bug_id = delete $vars->{bug_id};
+ $flags = $class->match({target_type => 'bug', bug_id => $bug_id});
+ }
+ elsif ($target_type eq 'attachment') {
+ my $attach_id = delete $vars->{attach_id};
+ $flags = $class->match({attach_id => $attach_id});
+ }
+ else {
+ ThrowCodeError('bad_arg',
+ {argument => 'target_type', function => $class . '->_flag_types'});
+ }
+
+ # Get all available flag types for the given product and component.
+ my $cache
+ = Bugzilla->request_cache->{flag_types_per_component}->{$vars->{target_type}}
+ ||= {};
+ my $flag_data = $cache->{$vars->{component_id}}
+ ||= Bugzilla::FlagType::match($vars);
+ my $flag_types = dclone($flag_data);
+
+ $_->{flags} = [] foreach @$flag_types;
+ my %flagtypes = map { $_->id => $_ } @$flag_types;
+
+ # Group existing flags per type, and skip those becoming invalid
+ # (which can happen when a bug is being moved into a new product
+ # or component).
+ @$flags = grep { exists $flagtypes{$_->type_id} } @$flags;
+ push(@{$flagtypes{$_->type_id}->{flags}}, $_) foreach @$flags;
+ return $flag_types;
}
1;
diff --git a/Bugzilla/FlagType.pm b/Bugzilla/FlagType.pm
index c973ea662..06159be5d 100644
--- a/Bugzilla/FlagType.pm
+++ b/Bugzilla/FlagType.pm
@@ -47,115 +47,116 @@ use base 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();
- Bugzilla::Hook::process('flagtype_end_of_create', { type => $flagtype });
+ Bugzilla::Hook::process('flagtype_end_of_create', {type => $flagtype});
- $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,29 +184,32 @@ 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});
+ }
}
+ }
- Bugzilla::Hook::process('flagtype_end_of_update',
- { type => $self, changed => $changes });
+ Bugzilla::Hook::process('flagtype_end_of_update',
+ {type => $self, changed => $changes});
- $dbh->bz_commit_transaction();
- return $changes;
+ $dbh->bz_commit_transaction();
+ return $changes;
}
###############################
@@ -262,162 +268,164 @@ 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);
- # We do not call Util::validate_email_syntax because these
- # addresses do not require to match 'emailregexp' and do not
- # depend on 'emailsuffix'. So we limit ourselves to a simple
- # sanity check:
- # - match the syntax of a fully qualified email address;
- # - do not contain any illegal character.
- foreach my $address (@addresses) {
- ($address =~ /^[\w\.\+\-=]+@[\w\.\-]+\.[\w\-]+$/
- && $address !~ /[\\\(\)<>&,;:"\[\] \t\r\n\P{ASCII}]/)
- || 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);
+
+ # We do not call Util::validate_email_syntax because these
+ # addresses do not require to match 'emailregexp' and do not
+ # depend on 'emailsuffix'. So we limit ourselves to a simple
+ # sanity check:
+ # - match the syntax of a fully qualified email address;
+ # - do not contain any illegal character.
+ foreach my $address (@addresses) {
+ ( $address =~ /^[\w\.\+\-=]+@[\w\.\-]+\.[\w\-]+$/
+ && $address !~ /[\\\(\)<>&,;:"\[\] \t\r\n\P{ASCII}]/)
+ || 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);
- $user->in_group('editcomponents', $prod_id)
- || ThrowUserError('product_access_denied', $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);
+ $user->in_group('editcomponents', $prod_id)
+ || ThrowUserError('product_access_denied', $params);
}
- $self->{$category} = \%clusions;
- $self->{"${category}_as_hash"} = \%clusions_as_hash;
- $self->{"_update_$category"} = 1;
+ $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;
}
+ $self->{$category} = \%clusions;
+ $self->{"${category}_as_hash"} = \%clusions_as_hash;
+ $self->{"_update_$category"} = 1;
+ }
}
=pod
@@ -458,77 +466,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}, cache => 1 });
- 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}, cache => 1});
+ 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};
}
######################################################################
@@ -552,11 +562,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
@@ -565,19 +575,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
@@ -594,18 +604,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 flagtypes.id FROM $tables WHERE $criteria");
+ my $flagtype_ids = $dbh->selectcol_arrayref(
+ "SELECT flagtypes.id FROM $tables WHERE $criteria");
- return Bugzilla::FlagType->new_from_list($flagtype_ids);
+ return Bugzilla::FlagType->new_from_list($flagtype_ids);
}
=pod
@@ -621,18 +632,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;
}
######################################################################
@@ -659,83 +672,91 @@ by the query.
=cut
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}) {
- 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}) {
+ my $name = $dbh->quote($criteria->{name});
+ trick_taint($name); # Detaint data as we have quoted it.
+ push(@criteria, "flagtypes.name = $name");
+ }
+ 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 (exists($criteria->{active_or_has_flags})
+ && $criteria->{active_or_has_flags} =~ /^\d+$/)
+ {
+ push(@$tables,
+ "LEFT JOIN flags AS f ON flagtypes.id = f.type_id "
+ . "AND f.bug_id = "
+ . $criteria->{active_or_has_flags});
+ push(@criteria, "(flagtypes.is_active = 1 OR f.id IS NOT NULL)");
+ }
+ 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->{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 {
+ $addl_join_clause
+ = "AND e.component_id IS NULL OR (i.component_id = e.component_id) ";
}
- if (exists($criteria->{is_active})) {
- my $is_active = $criteria->{is_active} ? "1" : "0";
- push(@criteria, "flagtypes.is_active = $is_active");
- }
- if (exists($criteria->{active_or_has_flags}) && $criteria->{active_or_has_flags} =~ /^\d+$/) {
- push(@$tables, "LEFT JOIN flags AS f ON flagtypes.id = f.type_id " .
- "AND f.bug_id = " . $criteria->{active_or_has_flags});
- push(@criteria, "(flagtypes.is_active = 1 OR f.id IS NOT NULL)");
- }
- 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->{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;
+ $join_clause
+ .= "AND ((e.product_id = $product_id $addl_join_clause) OR e.product_id IS NULL)";
+
+ push(@$tables, "LEFT JOIN flagexclusions AS e ON ($join_clause)");
+ push(@criteria, "e.type_id IS NULL");
+ }
+ if ($criteria->{group}) {
+ my $gid = $criteria->{group};
+ detaint_natural($gid)
+ || ThrowCodeError('bad_arg',
+ {argument => 'group', function => 'Bugzilla::FlagType::sqlify_criteria'});
+
+ push(@criteria,
+ "(flagtypes.grant_group_id = $gid " . " OR flagtypes.request_group_id = $gid)");
+ }
+
+ return @criteria;
}
1;
diff --git a/Bugzilla/Group.pm b/Bugzilla/Group.pm
index 7f684ea15..df8bc5899 100644
--- a/Bugzilla/Group.pm
+++ b/Bugzilla/Group.pm
@@ -27,22 +27,22 @@ use Scalar::Util qw(blessed);
use constant IS_CONFIG => 1;
sub DB_COLUMNS {
- my $class = shift;
- my @columns = qw(
- id
- name
- description
- isbuggroup
- userregexp
- isactive
- icon_url
- owner_user_id
- idle_member_removal
- );
- my $dbh = Bugzilla->dbh;
- my $table = $class->DB_TABLE;
+ my $class = shift;
+ my @columns = qw(
+ id
+ name
+ description
+ isbuggroup
+ userregexp
+ isactive
+ icon_url
+ owner_user_id
+ idle_member_removal
+ );
+ my $dbh = Bugzilla->dbh;
+ my $table = $class->DB_TABLE;
- return map { "$table.$_" } grep { $dbh->bz_column_info($table, $_) } @columns;
+ return map {"$table.$_"} grep { $dbh->bz_column_info($table, $_) } @columns;
}
use constant DB_TABLE => 'groups';
@@ -50,174 +50,178 @@ 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,
- owner_user_id => \&_check_owner,
- idle_member_removal => \&_check_idle_member_removal
+ name => \&_check_name,
+ description => \&_check_description,
+ userregexp => \&_check_user_regexp,
+ isactive => \&_check_is_active,
+ isbuggroup => \&_check_is_bug_group,
+ icon_url => \&_check_icon_url,
+ owner_user_id => \&_check_owner,
+ idle_member_removal => \&_check_idle_member_removal
};
use constant UPDATE_COLUMNS => qw(
- name
- description
- userregexp
- isactive
- icon_url
- owner_user_id
- idle_member_removal
+ name
+ description
+ userregexp
+ isactive
+ icon_url
+ owner_user_id
+ idle_member_removal
);
# Parameters that are lists of groups.
use constant GROUP_PARAMS => qw(chartgroup insidergroup timetrackinggroup
- querysharegroup);
+ querysharegroup);
sub DYNAMIC_COLUMNS {
- return Bugzilla->usage_mode == USAGE_MODE_CMDLINE;
+ return Bugzilla->usage_mode == USAGE_MODE_CMDLINE;
}
###############################
#### 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 idle_member_removal { return $_[0]->{'idle_member_removal'}; }
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};
}
# returns all possible members of groups, keyed by the group name or _direct
# a user present in multiple groups will be returned multiple times
sub members_complete {
- my ($self) = @_;
- my $dbh = Bugzilla->dbh;
- require Bugzilla::User;
+ my ($self) = @_;
+ my $dbh = Bugzilla->dbh;
+ require Bugzilla::User;
- my $sth = $dbh->prepare(
- "SELECT DISTINCT user_id FROM user_group_map WHERE isbless = 0 AND group_id = ?"
+ my $sth
+ = $dbh->prepare(
+ "SELECT DISTINCT user_id FROM user_group_map WHERE isbless = 0 AND group_id = ?"
);
- my $result = { _direct => $self->members_direct() };
- foreach my $group_id (@{ $self->flatten_group_membership($self->id) }) {
- next if $group_id == $self->id;
- my $group_name = Bugzilla::Group->new({ id => $group_id, cache => 1 })->name;
- my $user_ids = $dbh->selectcol_arrayref($sth, undef, $group_id);
- $result->{$group_name} = Bugzilla::User->new_from_list($user_ids);
- }
- return $result;
+ my $result = {_direct => $self->members_direct()};
+ foreach my $group_id (@{$self->flatten_group_membership($self->id)}) {
+ next if $group_id == $self->id;
+ my $group_name = Bugzilla::Group->new({id => $group_id, cache => 1})->name;
+ my $user_ids = $dbh->selectcol_arrayref($sth, undef, $group_id);
+ $result->{$group_name} = Bugzilla::User->new_from_list($user_ids);
+ }
+ return $result;
}
# A helper for members_direct and members_non_inherited
sub _get_members {
- my ($self, $grant_type) = @_;
- my $dbh = Bugzilla->dbh;
- my $grant_clause = $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 = $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, $params) = @_;
- $params ||= {};
- require Bugzilla::FlagType;
- $self->{flag_types} ||= Bugzilla::FlagType::match({ group => $self->id, %$params });
- return $self->{flag_types};
+ my ($self, $params) = @_;
+ $params ||= {};
+ require Bugzilla::FlagType;
+ $self->{flag_types}
+ ||= Bugzilla::FlagType::match({group => $self->id, %$params});
+ 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};
}
sub owner {
- my $self = shift;
- return $self->{owner} if exists $self->{owner};
- if ($self->{owner_user_id}) {
- $self->{owner} = Bugzilla::User->check({ id => $self->{owner_user_id}, cache => 1 });
- }
- return $self->{owner} || undef;
+ my $self = shift;
+ return $self->{owner} if exists $self->{owner};
+ if ($self->{owner_user_id}) {
+ $self->{owner}
+ = Bugzilla::User->check({id => $self->{owner_user_id}, cache => 1});
+ }
+ return $self->{owner} || undef;
}
###############################
@@ -225,134 +229,136 @@ sub owner {
###############################
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_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_idle_member_removal { $_[0]->set('idle_member_removal', $_[1]); }
sub set_owner {
- my ($self, $owner_id) = @_;
- $self->set('owner_user_id', $owner_id);
- # Reset the default owner object.
- delete $self->{owner};
+ my ($self, $owner_id) = @_;
+ $self->set('owner_user_id', $owner_id);
+
+ # Reset the default owner object.
+ delete $self->{owner};
}
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
@@ -361,148 +367,161 @@ sub _enforce_mandatory {
AND bug_group_map.group_id = group_control_map.group_id
WHERE group_control_map.group_id = ?
AND group_control_map.membercontrol = ?
- AND bug_group_map.group_id IS NULL',
- undef, ($gid, CONTROLMAPMANDATORY));
-
- my $sth = $dbh->prepare('INSERT INTO bug_group_map (bug_id, group_id) VALUES (?, ?)');
- foreach my $bug_id (@$bug_ids) {
- $sth->execute($bug_id, $gid);
- }
+ AND bug_group_map.group_id IS NULL', undef,
+ ($gid, CONTROLMAPMANDATORY)
+ );
+
+ my $sth
+ = $dbh->prepare('INSERT INTO bug_group_map (bug_id, group_id) VALUES (?, ?)');
+ foreach my $bug_id (@$bug_ids) {
+ $sth->execute($bug_id, $gid);
+ }
}
sub is_active_bug_group {
- my $self = shift;
- return $self->is_active && $self->is_bug_group;
+ my $self = shift;
+ return $self->is_active && $self->is_bug_group;
}
sub _rederive_regexp {
- my ($self) = @_;
+ my ($self) = @_;
- my $dbh = Bugzilla->dbh;
- my $sth = $dbh->prepare("SELECT userid, login_name, group_id
+ my $dbh = Bugzilla->dbh;
+ my $sth = $dbh->prepare(
+ "SELECT userid, login_name, group_id
FROM profiles
LEFT JOIN user_group_map
ON user_group_map.user_id = profiles.userid
AND group_id = ?
AND grant_type = ?
- AND isbless = 0");
- my $sthadd = $dbh->prepare("INSERT INTO user_group_map
+ AND isbless = 0"
+ );
+ my $sthadd = $dbh->prepare(
+ "INSERT INTO user_group_map
(user_id, group_id, grant_type, isbless)
- VALUES (?, ?, ?, 0)");
- my $sthdel = $dbh->prepare("DELETE FROM user_group_map
+ VALUES (?, ?, ?, 0)"
+ );
+ my $sthdel = $dbh->prepare(
+ "DELETE FROM user_group_map
WHERE user_id = ? AND group_id = ?
- AND grant_type = ? and isbless = 0");
- $sth->execute($self->id, GRANT_REGEXP);
- my $regexp = $self->user_regexp;
- while (my ($uid, $login, $present) = $sth->fetchrow_array) {
- if ($regexp ne '' and $login =~ /$regexp/i) {
- $sthadd->execute($uid, $self->id, GRANT_REGEXP) unless $present;
- } else {
- $sthdel->execute($uid, $self->id, GRANT_REGEXP) if $present;
- }
+ AND grant_type = ? and isbless = 0"
+ );
+ $sth->execute($self->id, GRANT_REGEXP);
+ my $regexp = $self->user_regexp;
+
+ while (my ($uid, $login, $present) = $sth->fetchrow_array) {
+ if ($regexp ne '' and $login =~ /$regexp/i) {
+ $sthadd->execute($uid, $self->id, GRANT_REGEXP) unless $present;
}
+ else {
+ $sthdel->execute($uid, $self->id, GRANT_REGEXP) if $present;
+ }
+ }
}
sub flatten_group_membership {
- my ($self, @groups) = @_;
-
- my $dbh = Bugzilla->dbh;
- my $sth;
- my @groupidstocheck = @groups;
- my %groupidschecked = ();
- $sth = $dbh->prepare("SELECT member_id FROM group_group_map
+ my ($self, @groups) = @_;
+
+ my $dbh = Bugzilla->dbh;
+ my $sth;
+ my @groupidstocheck = @groups;
+ my %groupidschecked = ();
+ $sth = $dbh->prepare(
+ "SELECT member_id FROM group_group_map
WHERE grantor_id = ?
- AND grant_type = " . GROUP_MEMBERSHIP);
- while (my $node = shift @groupidstocheck) {
- $sth->execute($node);
- my $member;
- while (($member) = $sth->fetchrow_array) {
- if (!$groupidschecked{$member}) {
- $groupidschecked{$member} = 1;
- push @groupidstocheck, $member;
- push @groups, $member unless grep $_ == $member, @groups;
- }
- }
+ AND grant_type = " . GROUP_MEMBERSHIP
+ );
+ while (my $node = shift @groupidstocheck) {
+ $sth->execute($node);
+ my $member;
+ while (($member) = $sth->fetchrow_array) {
+ if (!$groupidschecked{$member}) {
+ $groupidschecked{$member} = 1;
+ push @groupidstocheck, $member;
+ push @groups, $member unless grep $_ == $member, @groups;
+ }
}
- return \@groups;
+ }
+ return \@groups;
}
-
-
################################
##### Module Subroutines ###
################################
sub create {
- my $class = shift;
- my ($params) = @_;
- my $dbh = Bugzilla->dbh;
-
- my $silently = delete $params->{silently};
- 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};
+ 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
+ my $group = $class->SUPER::create(@_);
+
+ # 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);
- }
+ 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);
+ }
- $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();
- Bugzilla->memcached->clear_config();
- return $group;
+ Bugzilla::Hook::process('group_end_of_create', {group => $group});
+ $dbh->bz_commit_transaction();
+ Bugzilla->memcached->clear_config();
+ 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;
}
###############################
@@ -510,53 +529,57 @@ sub check_no_disclose {
###############################
sub _check_name {
- my ($invocant, $name) = @_;
- $name = trim($name);
- $name || ThrowUserError("empty_group_name");
- # If we're creating a Group or changing the name...
- if (!ref($invocant) || lc($invocant->name) ne lc($name)) {
- my $exists = new Bugzilla::Group({name => $name });
- ThrowUserError("group_exists", { name => $name }) if $exists;
- }
- return $name;
+ my ($invocant, $name) = @_;
+ $name = trim($name);
+ $name || ThrowUserError("empty_group_name");
+
+ # If we're creating a Group or changing the name...
+ if (!ref($invocant) || lc($invocant->name) ne lc($name)) {
+ my $exists = new Bugzilla::Group({name => $name});
+ ThrowUserError("group_exists", {name => $name}) if $exists;
+ }
+ return $name;
}
sub _check_description {
- my ($invocant, $desc) = @_;
- $desc = trim($desc);
- $desc || ThrowUserError("empty_group_description");
- return $desc;
+ my ($invocant, $desc) = @_;
+ $desc = trim($desc);
+ $desc || ThrowUserError("empty_group_description");
+ return $desc;
}
sub _check_user_regexp {
- my ($invocant, $regex) = @_;
- $regex = trim($regex) || '';
- ThrowUserError("invalid_regexp") unless (eval {qr/$regex/});
- return $regex;
+ my ($invocant, $regex) = @_;
+ $regex = trim($regex) || '';
+ ThrowUserError("invalid_regexp") unless (eval {qr/$regex/});
+ return $regex;
}
sub _check_is_active { return $_[1] ? 1 : 0; }
+
sub _check_is_bug_group {
- return $_[1] ? 1 : 0;
+ return $_[1] ? 1 : 0;
}
sub _check_icon_url { return $_[1] ? clean_text($_[1]) : undef; }
sub _check_owner {
- my ($invocant, $owner, undef, $params) = @_;
- return Bugzilla::User->check({ name => $owner, cache => 1 })->id if $owner;
- # We require an owner if the group is a not a system group
- if (blessed($invocant) && !$invocant->is_bug_group) {
- return undef;
- }
- ThrowUserError('group_needs_owner');
+ my ($invocant, $owner, undef, $params) = @_;
+ return Bugzilla::User->check({name => $owner, cache => 1})->id if $owner;
+
+ # We require an owner if the group is a not a system group
+ if (blessed($invocant) && !$invocant->is_bug_group) {
+ return undef;
+ }
+ ThrowUserError('group_needs_owner');
}
sub _check_idle_member_removal {
- my ($invocant, $value) = @_;
- detaint_natural($value)
- || ThrowUserError('invalid_parameter', { name => 'idle member removal', err => 'must be numeric' });
- return $value <= 0 ? 0 : $value ;
+ my ($invocant, $value) = @_;
+ detaint_natural($value)
+ || ThrowUserError('invalid_parameter',
+ {name => 'idle member removal', err => 'must be numeric'});
+ return $value <= 0 ? 0 : $value;
}
1;
diff --git a/Bugzilla/Hook.pm b/Bugzilla/Hook.pm
index bed6a53b0..18335b907 100644
--- a/Bugzilla/Hook.pm
+++ b/Bugzilla/Hook.pm
@@ -12,33 +12,33 @@ use strict;
use warnings;
sub process {
- my ($name, $args) = @_;
+ my ($name, $args) = @_;
- _entering($name);
+ _entering($name);
- foreach my $extension (@{ Bugzilla->extensions }) {
- if ($extension->can($name)) {
- $extension->$name($args);
- }
+ foreach my $extension (@{Bugzilla->extensions}) {
+ if ($extension->can($name)) {
+ $extension->$name($args);
}
+ }
- _leaving($name);
+ _leaving($name);
}
sub in {
- my $hook_name = shift;
- my $currently_in = Bugzilla->request_cache->{hook_stack}->[-1] || '';
- return $hook_name eq $currently_in ? 1 : 0;
+ my $hook_name = shift;
+ my $currently_in = Bugzilla->request_cache->{hook_stack}->[-1] || '';
+ return $hook_name eq $currently_in ? 1 : 0;
}
sub _entering {
- my ($hook_name) = @_;
- my $hook_stack = Bugzilla->request_cache->{hook_stack} ||= [];
- push(@$hook_stack, $hook_name);
+ my ($hook_name) = @_;
+ my $hook_stack = Bugzilla->request_cache->{hook_stack} ||= [];
+ push(@$hook_stack, $hook_name);
}
sub _leaving {
- pop @{ Bugzilla->request_cache->{hook_stack} };
+ pop @{Bugzilla->request_cache->{hook_stack}};
}
1;
diff --git a/Bugzilla/Install.pm b/Bugzilla/Install.pm
index 8bce9b5e7..705a8396c 100644
--- a/Bugzilla/Install.pm
+++ b/Bugzilla/Install.pm
@@ -31,509 +31,512 @@ 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
{
- name => 'display_quips',
- options => ["on", "off"],
- default => "on",
- category => 'Searching'
+ name => 'display_quips',
+ options => ["on", "off"],
+ default => "on",
+ category => 'Searching'
},
+
# 2005-03-10 travis@sedsystems.ca -- Bug 199048
{
- name => 'comment_sort_order',
- options => ["oldest_to_newest", "newest_to_oldest", "newest_to_oldest_desc_first"],
- default => "oldest_to_newest",
- category => 'Bug Editing'
+ name => 'comment_sort_order',
+ options =>
+ ["oldest_to_newest", "newest_to_oldest", "newest_to_oldest_desc_first"],
+ default => "oldest_to_newest",
+ category => 'Bug Editing'
},
+
# 2005-05-12 bugzilla@glob.com.au -- Bug 63536
{
- name => 'post_bug_submit_action',
- options => ["next_bug", "same_bug", "nothing"],
- default => "next_bug",
- category => 'Bug Editing'
+ name => 'post_bug_submit_action',
+ options => ["next_bug", "same_bug", "nothing"],
+ default => "next_bug",
+ category => 'Bug Editing'
},
+
# 2005-06-29 wurblzap@gmail.com -- Bug 257767
{
- name => 'csv_colsepchar',
- options => [',',';'],
- default => ',',
- category => 'Searching'
+ name => 'csv_colsepchar',
+ options => [',', ';'],
+ default => ',',
+ category => 'Searching'
},
+
# 2005-10-26 wurblzap@gmail.com -- Bug 291459
{
- name => 'zoom_textareas',
- options => ["on", "off"],
- default => "on",
- category => 'Bug Editing'
+ name => 'zoom_textareas',
+ options => ["on", "off"],
+ default => "on",
+ category => 'Bug Editing'
},
+
# 2005-10-21 LpSolit@gmail.com -- Bug 313020
{
- name => 'per_bug_queries',
- options => ['on', 'off'],
- default => 'off',
- category => 'Searching'
+ name => 'per_bug_queries',
+ options => ['on', 'off'],
+ default => 'off',
+ category => 'Searching'
},
+
# 2006-05-01 olav@bkor.dhs.org -- Bug 7710
{
- name => 'state_addselfcc',
- options => ['always', 'never', 'cc_unless_role'],
- default => 'cc_unless_role',
- category => 'Bug Editing'
+ name => 'state_addselfcc',
+ options => ['always', 'never', 'cc_unless_role'],
+ default => 'cc_unless_role',
+ category => 'Bug Editing'
},
+
# 2006-08-04 wurblzap@gmail.com -- Bug 322693
{
- name => 'skin',
- subclass => 'Skin',
- default => 'Dusk',
- category => 'User Interface'
+ name => 'skin',
+ subclass => 'Skin',
+ default => 'Dusk',
+ category => 'User Interface'
},
+
# 2006-12-10 LpSolit@gmail.com -- Bug 297186
{
- name => 'lang',
- subclass => 'Lang',
- default => ${Bugzilla->languages}[0],
- category => 'User Interface'
+ name => 'lang',
+ subclass => 'Lang',
+ default => ${Bugzilla->languages}[0],
+ category => 'User Interface'
},
+
# 2007-07-02 altlist@gmail.com -- Bug 225731
{
- name => 'quote_replies',
- options => ['quoted_reply', 'simple_reply', 'off'],
- default => "quoted_reply",
- category => 'Bug Editing'
+ name => 'quote_replies',
+ options => ['quoted_reply', 'simple_reply', 'off'],
+ default => "quoted_reply",
+ category => 'Bug Editing'
},
+
# 2009-02-01 mozilla@matt.mchenryfamily.org -- Bug 398473
{
- name => 'comment_box_position',
- options => ['before_comments', 'after_comments'],
- default => 'before_comments',
- category => 'Bug Editing'
+ name => 'comment_box_position',
+ options => ['before_comments', 'after_comments'],
+ default => 'before_comments',
+ category => 'Bug Editing'
},
+
# 2008-08-27 LpSolit@gmail.com -- Bug 182238
{
- name => 'timezone',
- subclass => 'Timezone',
- default => 'local',
- category => 'User Interface'
+ name => 'timezone',
+ subclass => 'Timezone',
+ default => 'local',
+ category => 'User Interface'
},
+
# 2011-02-07 dkl@mozilla.com -- Bug 580490
{
- name => 'quicksearch_fulltext',
- options => ['on', 'off'],
- default => 'on',
- category => 'Searching'
+ name => 'quicksearch_fulltext',
+ options => ['on', 'off'],
+ default => 'on',
+ category => 'Searching'
},
+
# 2011-06-21 glob@mozilla.com -- Bug 589128
{
- name => 'email_format',
- options => ['html', 'text_only'],
- default => 'html',
- category => 'Email Notifications'
+ name => 'email_format',
+ options => ['html', 'text_only'],
+ default => 'html',
+ category => 'Email Notifications'
},
+
# 2011-06-16 glob@mozilla.com -- Bug 663747
{
- name => 'bugmail_new_prefix',
- options => ['on', 'off'],
- default => 'on',
- category => 'Email Notifications'
+ name => 'bugmail_new_prefix',
+ options => ['on', 'off'],
+ default => 'on',
+ category => 'Email Notifications'
},
+
# 2013-07-26 joshi_sunil@in.com -- Bug 669535
{
- name => 'possible_duplicates',
- options => ['on', 'off'],
- default => 'on',
- category => 'User Interface'
+ name => 'possible_duplicates',
+ options => ['on', 'off'],
+ default => 'on',
+ category => 'User Interface'
},
+
# 2011-10-11 glob@mozilla.com -- Bug 301656
{
- name => 'requestee_cc',
- options => ['on', 'off'],
- default => 'on',
- category => 'Reviews and Needinfo'
+ name => 'requestee_cc',
+ options => ['on', 'off'],
+ default => 'on',
+ category => 'Reviews and Needinfo'
},
{
- name => 'api_key_only',
- options => ['on', 'off'],
- default => 'off',
- category => 'API'
+ name => 'api_key_only',
+ options => ['on', 'off'],
+ default => 'off',
+ category => 'API'
},
{
- name => 'use_elasticsearch',
- options => ['on', 'off'],
- default => 'off',
- category => 'Searching'
+ name => 'use_elasticsearch',
+ options => ['on', 'off'],
+ default => 'off',
+ category => 'Searching'
},
{
- name => 'autosize_comments',
- options => ['on', 'off'],
- default => 'on',
- category => 'User Interface'
+ name => 'autosize_comments',
+ options => ['on', 'off'],
+ default => 'on',
+ category => 'User Interface'
},
- ];
-};
+ ];
+}
use constant SYSTEM_GROUPS => (
- {
- name => 'admin',
- description => 'Administrators'
- },
- {
- name => 'tweakparams',
- description => 'Can change Parameters'
- },
- {
- name => 'editusers',
- description => 'Can edit or disable users'
- },
- {
- name => 'disableusers',
- description => 'Can disable users'
- },
- {
- name => 'creategroups',
- description => 'Can create and destroy groups'
- },
- {
- name => 'editclassifications',
- description => 'Can create, destroy, and edit classifications'
- },
- {
- name => 'editcomponents',
- description => 'Can create, destroy, and edit components'
- },
- {
- name => 'editkeywords',
- description => 'Can create, destroy, and edit keywords'
- },
- {
- name => 'editbugs',
- description => 'Can edit all bug fields',
- userregexp => '.*'
- },
- {
- name => 'canconfirm',
- description => 'Can confirm a bug or mark it a duplicate'
- },
- {
- name => 'bz_canusewhineatothers',
- description => 'Can configure whine reports for other users',
- },
- {
- name => 'bz_canusewhines',
- description => 'User can configure whine reports for self',
- # inherited_by means that users in the groups listed below are
- # automatically members of bz_canusewhines.
- inherited_by => ['editbugs', 'bz_canusewhineatothers'],
- },
- {
- name => 'bz_sudoers',
- description => 'Can perform actions as other users',
- },
- {
- name => 'bz_sudo_protect',
- description => 'Can not be impersonated by other users',
- inherited_by => ['bz_sudoers'],
- },
- {
- name => 'bz_quip_moderators',
- description => 'Can moderate quips',
- },
- {
- name => 'bz_can_disable_mfa',
- description => 'Can disable MFA when editing users',
- },
+ {name => 'admin', description => 'Administrators'},
+ {name => 'tweakparams', description => 'Can change Parameters'},
+ {name => 'editusers', description => 'Can edit or disable users'},
+ {name => 'disableusers', description => 'Can disable users'},
+ {name => 'creategroups', description => 'Can create and destroy groups'},
+ {
+ name => 'editclassifications',
+ description => 'Can create, destroy, and edit classifications'
+ },
+ {
+ name => 'editcomponents',
+ description => 'Can create, destroy, and edit components'
+ },
+ {
+ name => 'editkeywords',
+ description => 'Can create, destroy, and edit keywords'
+ },
+ {
+ name => 'editbugs',
+ description => 'Can edit all bug fields',
+ userregexp => '.*'
+ },
+ {
+ name => 'canconfirm',
+ description => 'Can confirm a bug or mark it a duplicate'
+ },
+ {
+ name => 'bz_canusewhineatothers',
+ description => 'Can configure whine reports for other users',
+ },
+ {
+ name => 'bz_canusewhines',
+ description => 'User can configure whine reports for self',
+
+ # inherited_by means that users in the groups listed below are
+ # automatically members of bz_canusewhines.
+ inherited_by => ['editbugs', 'bz_canusewhineatothers'],
+ },
+ {name => 'bz_sudoers', description => 'Can perform actions as other users',},
+ {
+ name => 'bz_sudo_protect',
+ description => 'Can not be impersonated by other users',
+ inherited_by => ['bz_sudoers'],
+ },
+ {name => 'bz_quip_moderators', description => 'Can moderate quips',},
+ {
+ name => 'bz_can_disable_mfa',
+ description => 'Can disable MFA when editing users',
+ },
);
-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) {
- print get_text('install_setting_setup'), "\n";
- }
-
- my @settings = @{SETTINGS()};
- foreach my $params (@settings) {
- add_setting($params);
- }
+ 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) {
+ print get_text('install_setting_setup'), "\n";
+ }
+
+ my @settings = @{SETTINGS()};
+ foreach my $params (@settings) {
+ add_setting($params);
+ }
}
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) {
- print get_text('install_groups_setup'), "\n";
- }
-
- # Create most of the system groups
- foreach my $definition (SYSTEM_GROUPS) {
- my $exists = new Bugzilla::Group({ name => $definition->{name} });
- if (!$exists) {
- $definition->{isbuggroup} = 0;
- $definition->{silently} = !$editbugs_exists;
- my $inherited_by = delete $definition->{inherited_by};
- my $created = Bugzilla::Group->create($definition);
- # Each group in inherited_by is automatically a member of this
- # group.
- if ($inherited_by) {
- foreach my $name (@$inherited_by) {
- my $member = Bugzilla::Group->check($name);
- $dbh->do('INSERT INTO group_group_map (grantor_id,
- member_id) VALUES (?,?)',
- undef, $created->id, $member->id);
- }
- }
+ my $dbh = Bugzilla->dbh;
+
+ $dbh->bz_start_transaction();
+
+ # If there is no editbugs group, this is the first time we're
+ # adding groups.
+ my $editbugs_exists = new Bugzilla::Group({name => 'editbugs'});
+ if (!$editbugs_exists) {
+ print get_text('install_groups_setup'), "\n";
+ }
+
+ # Create most of the system groups
+ foreach my $definition (SYSTEM_GROUPS) {
+ my $exists = new Bugzilla::Group({name => $definition->{name}});
+ if (!$exists) {
+ $definition->{isbuggroup} = 0;
+ $definition->{silently} = !$editbugs_exists;
+ my $inherited_by = delete $definition->{inherited_by};
+ my $created = Bugzilla::Group->create($definition);
+
+ # Each group in inherited_by is automatically a member of this
+ # group.
+ if ($inherited_by) {
+ foreach my $name (@$inherited_by) {
+ my $member = Bugzilla::Group->check($name);
+ $dbh->do(
+ 'INSERT INTO group_group_map (grantor_id,
+ member_id) VALUES (?,?)', undef, $created->id,
+ $member->id
+ );
}
+ }
}
+ }
- $dbh->bz_commit_transaction();
+ $dbh->bz_commit_transaction();
}
sub create_default_classification {
- my $dbh = Bugzilla->dbh;
-
- # Make the default Classification if it doesn't already exist.
- if (!$dbh->selectrow_array('SELECT 1 FROM classifications')) {
- print get_text('install_default_classification',
- { name => DEFAULT_CLASSIFICATION->{name} }) . "\n";
- Bugzilla::Classification->create(DEFAULT_CLASSIFICATION);
- }
+ my $dbh = Bugzilla->dbh;
+
+ # Make the default Classification if it doesn't already exist.
+ if (!$dbh->selectrow_array('SELECT 1 FROM classifications')) {
+ print get_text('install_default_classification',
+ {name => DEFAULT_CLASSIFICATION->{name}})
+ . "\n";
+ Bugzilla::Classification->create(DEFAULT_CLASSIFICATION);
+ }
}
# This function should be called only after creating the admin user.
sub create_default_product {
- my $dbh = Bugzilla->dbh;
-
- # And same for the default product/component.
- if (!$dbh->selectrow_array('SELECT 1 FROM products')) {
- print get_text('install_default_product',
- { name => DEFAULT_PRODUCT->{name} }) . "\n";
-
- my $product = Bugzilla::Product->create(DEFAULT_PRODUCT);
-
- # Get the user who will be the owner of the Component.
- # We pick the admin with the lowest id, which is probably the
- # admin checksetup.pl just created.
- my $admin_group = new Bugzilla::Group({name => 'admin'});
- my ($admin_id) = $dbh->selectrow_array(
- 'SELECT user_id FROM user_group_map WHERE group_id = ?
- ORDER BY user_id ' . $dbh->sql_limit(1),
- undef, $admin_group->id);
- my $admin = Bugzilla::User->new($admin_id);
-
- Bugzilla::Component->create({
- %{ DEFAULT_COMPONENT() }, product => $product,
- initialowner => $admin->login });
- }
+ my $dbh = Bugzilla->dbh;
+
+ # And same for the default product/component.
+ if (!$dbh->selectrow_array('SELECT 1 FROM products')) {
+ print get_text('install_default_product', {name => DEFAULT_PRODUCT->{name}})
+ . "\n";
+
+ my $product = Bugzilla::Product->create(DEFAULT_PRODUCT);
+
+ # Get the user who will be the owner of the Component.
+ # We pick the admin with the lowest id, which is probably the
+ # admin checksetup.pl just created.
+ my $admin_group = new Bugzilla::Group({name => 'admin'});
+ my ($admin_id) = $dbh->selectrow_array(
+ 'SELECT user_id FROM user_group_map WHERE group_id = ?
+ ORDER BY user_id ' . $dbh->sql_limit(1), undef, $admin_group->id
+ );
+ my $admin = Bugzilla::User->new($admin_id);
+
+ Bugzilla::Component->create({
+ %{DEFAULT_COMPONENT()}, product => $product, initialowner => $admin->login
+ });
+ }
}
sub init_workflow {
- my $dbh = Bugzilla->dbh;
- my $has_workflow = $dbh->selectrow_array('SELECT 1 FROM status_workflow');
- return if $has_workflow;
-
- print get_text('install_workflow_init'), "\n";
-
- 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;
+
+ print get_text('install_workflow_init'), "\n";
+
+ my %status_ids = @{
+ $dbh->selectcol_arrayref('SELECT value, id FROM bug_status',
+ {Columns => [1, 2]})
+ };
+
+ foreach my $pair (STATUS_WORKFLOW) {
+ my $old_id = $pair->[0] ? $status_ids{$pair->[0]} : undef;
+ my $new_id = $status_ids{$pair->[1]};
+ $dbh->do(
+ 'INSERT INTO status_workflow (old_status, new_status)
+ VALUES (?,?)', undef, $old_id, $new_id
+ );
+ }
}
sub create_admin {
- my ($params) = @_;
- my $dbh = Bugzilla->dbh;
- my $template = Bugzilla->template;
-
- my $admin_group = new Bugzilla::Group({ name => 'admin' });
- my $admin_inheritors =
- Bugzilla::Group->flatten_group_membership($admin_group->id);
- my $admin_group_ids = join(',', @$admin_inheritors);
-
- my ($admin_count) = $dbh->selectrow_array(
- "SELECT COUNT(*) FROM user_group_map
- WHERE group_id IN ($admin_group_ids)");
-
- return if $admin_count;
-
- my %answer = %{Bugzilla->installation_answers};
- my $login = $answer{'ADMIN_EMAIL'};
- my $password = $answer{'ADMIN_PASSWORD'};
- my $full_name = $answer{'ADMIN_REALNAME'};
-
- if (!$login || !$password || !$full_name) {
- print "\n" . get_text('install_admin_setup') . "\n\n";
- }
-
- while (!$login) {
- print get_text('install_admin_get_email') . ' ';
- $login = <STDIN>;
- chomp $login;
- eval { Bugzilla::User->check_login_name_for_creation($login); };
- if ($@) {
- print $@ . "\n";
- undef $login;
- }
- }
-
- while (!defined $full_name) {
- print get_text('install_admin_get_name') . ' ';
- $full_name = <STDIN>;
- chomp($full_name);
+ my ($params) = @_;
+ my $dbh = Bugzilla->dbh;
+ my $template = Bugzilla->template;
+
+ my $admin_group = new Bugzilla::Group({name => 'admin'});
+ my $admin_inheritors
+ = Bugzilla::Group->flatten_group_membership($admin_group->id);
+ my $admin_group_ids = join(',', @$admin_inheritors);
+
+ my ($admin_count) = $dbh->selectrow_array(
+ "SELECT COUNT(*) FROM user_group_map
+ WHERE group_id IN ($admin_group_ids)"
+ );
+
+ return if $admin_count;
+
+ my %answer = %{Bugzilla->installation_answers};
+ my $login = $answer{'ADMIN_EMAIL'};
+ my $password = $answer{'ADMIN_PASSWORD'};
+ my $full_name = $answer{'ADMIN_REALNAME'};
+
+ if (!$login || !$password || !$full_name) {
+ print "\n" . get_text('install_admin_setup') . "\n\n";
+ }
+
+ while (!$login) {
+ print get_text('install_admin_get_email') . ' ';
+ $login = <STDIN>;
+ chomp $login;
+ eval { Bugzilla::User->check_login_name_for_creation($login); };
+ if ($@) {
+ print $@ . "\n";
+ undef $login;
}
-
- if (!$password) {
- $password = _prompt_for_password(
- get_text('install_admin_get_password'));
- }
-
- my $admin = Bugzilla::User->create({ login_name => $login,
- realname => $full_name,
- cryptpassword => $password });
- make_admin($admin);
+ }
+
+ while (!defined $full_name) {
+ print get_text('install_admin_get_name') . ' ';
+ $full_name = <STDIN>;
+ chomp($full_name);
+ }
+
+ if (!$password) {
+ $password = _prompt_for_password(get_text('install_admin_get_password'));
+ }
+
+ my $admin
+ = Bugzilla::User->create({
+ login_name => $login, realname => $full_name, cryptpassword => $password
+ });
+ make_admin($admin);
}
sub make_admin {
- my ($user) = @_;
- my $dbh = Bugzilla->dbh;
-
- $user = ref($user) ? $user
- : new Bugzilla::User(login_to_id($user, THROW_ERROR));
-
- my $group_insert = $dbh->prepare(
- 'INSERT INTO user_group_map (user_id, group_id, isbless, grant_type)
- VALUES (?, ?, ?, ?)');
-
- # Admins get explicit membership and bless capability for the admin group
- my $admin_group = new Bugzilla::Group({ name => 'admin' });
- # These are run in an eval so that we can ignore the error of somebody
- # already being granted these things.
- eval {
- $group_insert->execute($user->id, $admin_group->id, 0, GRANT_DIRECT);
- };
- eval {
- $group_insert->execute($user->id, $admin_group->id, 1, GRANT_DIRECT);
- };
-
- # Admins should also have editusers directly, even though they'll usually
- # inherit it. People could have changed their inheritance structure.
- my $editusers = new Bugzilla::Group({ name => 'editusers' });
- eval {
- $group_insert->execute($user->id, $editusers->id, 0, GRANT_DIRECT);
- };
-
- # If there is no maintainer set, make this user the maintainer.
- if (!Bugzilla->params->{'maintainer'}) {
- SetParam('maintainer', $user->email);
- write_params();
- }
-
- if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) {
- print "\n", get_text('install_admin_created', { user => $user }), "\n";
- }
+ 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();
+ }
+
+ if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) {
+ print "\n", get_text('install_admin_created', {user => $user}), "\n";
+ }
}
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;
- my $pwqc = Bugzilla->passwdqc;
- my $ok = $pwqc->validate_password($password);
- if (!$ok) {
- print "\n", $pwqc->reason, "\n";
- undef $password;
- }
- elsif ($password ne $pass2) {
- print "\n", "passwords do not match\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;
+ my $pwqc = Bugzilla->passwdqc;
+ my $ok = $pwqc->validate_password($password);
+ if (!$ok) {
+ print "\n", $pwqc->reason, "\n";
+ undef $password;
}
- return $password;
+ elsif ($password ne $pass2) {
+ print "\n", "passwords do not match\n";
+ undef $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();
- print "\n", get_text('install_reset_password_done'), "\n";
+ 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();
+ print "\n", get_text('install_reset_password_done'), "\n";
}
1;
diff --git a/Bugzilla/Install/DB.pm b/Bugzilla/Install/DB.pm
index 8b3d4b8cc..a8db6bb75 100644
--- a/Bugzilla/Install/DB.pm
+++ b/Bugzilla/Install/DB.pm
@@ -33,95 +33,100 @@ 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'});
+
+ # 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);
+ }
- #2008-08-26 elliotte_martin@yahoo.com - Bug 251556
- $dbh->bz_add_column('fielddefs', 'reverse_desc', {TYPE => 'TINYTEXT'});
+ #2008-08-26 elliotte_martin@yahoo.com - Bug 251556
+ $dbh->bz_add_column('fielddefs', 'reverse_desc', {TYPE => 'TINYTEXT'});
- $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
+ );
- $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_mandatory',
+ {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'});
+ $dbh->bz_add_index('fielddefs', 'fielddefs_is_mandatory_idx', ['is_mandatory']);
- # 2010-04-05 dkl@redhat.com - Bug 479400
- _migrate_field_visibility_value();
+ # 2010-04-05 dkl@redhat.com - Bug 479400
+ _migrate_field_visibility_value();
- $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('fielddefs', 'is_numeric',
+ {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'});
+ $dbh->do(
+ 'UPDATE fielddefs SET is_numeric = 1 WHERE type = ' . FIELD_TYPE_BUG_ID);
- Bugzilla::Hook::process('install_update_db_fielddefs');
+ 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.
+ # 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.
@@ -147,1110 +152,1136 @@ sub update_fielddefs_definition {
# the purpose of a column.
#
sub update_table_definitions {
- my $old_params = shift;
- my $dbh = Bugzilla->dbh;
- _update_pre_checksetup_bugzillas();
-
- $dbh->bz_add_column('attachments', 'submitter_id',
- {TYPE => 'INT3', NOTNULL => 1}, 0);
-
- $dbh->bz_rename_column('bugs_activity', 'when', 'bug_when');
-
- _add_bug_vote_cache();
- _update_product_name_definition();
-
- $dbh->bz_add_column('profiles', 'disabledtext',
- {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, '');
-
- _populate_longdescs();
- _update_bugs_activity_field_to_fieldid();
-
- if (!$dbh->bz_column_info('bugs', 'lastdiffed')) {
- $dbh->bz_add_column('bugs', 'lastdiffed', {TYPE =>'DATETIME'});
- $dbh->do('UPDATE bugs SET lastdiffed = NOW()');
- }
-
- _add_unique_login_name_index_to_profiles();
-
- $dbh->bz_add_column('profiles', 'mybugslink',
- {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'});
-
- _update_component_user_fields_to_ids();
-
- $dbh->bz_add_column('bugs', 'everconfirmed',
- {TYPE => 'BOOLEAN', NOTNULL => 1}, 1);
-
- _populate_milestones_table();
-
- # 2000-03-22 Changed the default value for target_milestone to be "---"
- # (which is still not quite correct, but much better than what it was
- # doing), and made the size of the value field in the milestones table match
- # the size of the target_milestone field in the bugs table.
- $dbh->bz_alter_column('bugs', 'target_milestone',
- {TYPE => 'varchar(20)', NOTNULL => 1, DEFAULT => "'---'"});
- $dbh->bz_alter_column('milestones', 'value',
- {TYPE => 'varchar(20)', NOTNULL => 1});
-
- _add_products_defaultmilestone();
-
- # 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)]});
- }
-
- _copy_from_comments_to_longdescs();
- _populate_duplicates_table();
-
- if (!$dbh->bz_column_info('email_setting', 'user_id')) {
- $dbh->bz_add_column('profiles', 'emailflags', {TYPE => 'MEDIUMTEXT'});
- }
-
- if (!$dbh->bz_index_info('email_rates', 'email_rates_message_ts_idx')) {
- $dbh->bz_add_index( 'email_rates', 'email_rates_message_ts_idx', ['message_ts'] );
- }
-
- $dbh->bz_add_column('groups', 'isactive',
- {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'});
-
- $dbh->bz_add_column('attachments', 'isobsolete',
- {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'});
-
- $dbh->bz_drop_column("profiles", "emailnotification");
- $dbh->bz_drop_column("profiles", "newemailtech");
-
- # 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});
-
- _update_bugs_activity_to_only_record_changes();
-
- # 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", "reporter_accessible",
- {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'});
- $dbh->bz_add_column("bugs", "cclist_accessible",
- {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'});
-
- $dbh->bz_add_column("bugs_activity", "attach_id", {TYPE => 'INT5'});
-
- _delete_logincookies_cryptpassword_and_handle_invalid_cookies();
-
- # 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");
-
- # 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'});
-
- _use_ip_instead_of_hostname_in_logincookies();
-
- $dbh->bz_add_column('longdescs', 'isprivate',
- {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'});
- $dbh->bz_add_column('attachments', 'isprivate',
- {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'});
-
- $dbh->bz_add_column("bugs", "alias", {TYPE => "varchar(20)"});
- $dbh->bz_add_index('bugs', 'bugs_alias_idx',
- {TYPE => 'UNIQUE', FIELDS => [qw(alias)]});
-
- _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();
-
- # 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}, '');
- }
+ my $old_params = shift;
+ my $dbh = Bugzilla->dbh;
+ _update_pre_checksetup_bugzillas();
- $dbh->bz_add_column('products', 'classification_id',
- {TYPE => 'INT2', NOTNULL => 1, DEFAULT => '1'});
+ $dbh->bz_add_column('attachments', 'submitter_id',
+ {TYPE => 'INT3', NOTNULL => 1}, 0);
- _fix_group_with_empty_name();
+ $dbh->bz_rename_column('bugs_activity', 'when', 'bug_when');
- $dbh->bz_add_index('bugs_activity', 'bugs_activity_who_idx', [qw(who)]);
+ _add_bug_vote_cache();
+ _update_product_name_definition();
- # 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_add_column('profiles', 'disabledtext',
+ {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, '');
- $dbh->bz_alter_column('bugs', 'lastdiffed', {TYPE => 'DATETIME'});
+ _populate_longdescs();
+ _update_bugs_activity_field_to_fieldid();
- # 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");
- }
+ if (!$dbh->bz_column_info('bugs', 'lastdiffed')) {
+ $dbh->bz_add_column('bugs', 'lastdiffed', {TYPE => 'DATETIME'});
+ $dbh->do('UPDATE bugs SET lastdiffed = NOW()');
+ }
- # 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");
+ _add_unique_login_name_index_to_profiles();
- _migrate_email_prefs_to_new_table();
- _initialize_new_email_prefs();
- _change_all_mysql_booleans_to_tinyint();
+ $dbh->bz_add_column('profiles', 'mybugslink',
+ {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'});
- # make classification_id field type be consistent with DB:Schema
- $dbh->bz_alter_column('products', 'classification_id',
- {TYPE => 'INT2', NOTNULL => 1, DEFAULT => '1'});
+ _update_component_user_fields_to_ids();
- # 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);
+ $dbh->bz_add_column('bugs', 'everconfirmed', {TYPE => 'BOOLEAN', NOTNULL => 1},
+ 1);
- # 2005-03-28 - bug 238800 - index flags.type_id for editflagtypes.cgi
- $dbh->bz_add_index('flags', 'flags_type_id_idx', [qw(type_id)]);
+ _populate_milestones_table();
- # For a short time, the flags_type_id_idx was misnamed in upgraded installs.
- $dbh->bz_drop_index('flags', 'type_id');
+ # 2000-03-22 Changed the default value for target_milestone to be "---"
+ # (which is still not quite correct, but much better than what it was
+ # doing), and made the size of the value field in the milestones table match
+ # the size of the target_milestone field in the bugs table.
+ $dbh->bz_alter_column('bugs', 'target_milestone',
+ {TYPE => 'varchar(20)', NOTNULL => 1, DEFAULT => "'---'"});
+ $dbh->bz_alter_column('milestones', 'value',
+ {TYPE => 'varchar(20)', NOTNULL => 1});
- # 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();
+ _add_products_defaultmilestone();
- if (!exists $dbh->bz_column_info('milestones', 'sortkey')->{DEFAULT}) {
- $dbh->bz_alter_column('milestones', 'sortkey',
- {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0});
- }
+ # 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)]});
+ }
- # 2005-06-14 - LpSolit@gmail.com - Bug 292544
- $dbh->bz_alter_column('bugs', 'creation_ts', {TYPE => 'DATETIME'});
+ _copy_from_comments_to_longdescs();
+ _populate_duplicates_table();
- _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);
+ if (!$dbh->bz_column_info('email_setting', 'user_id')) {
+ $dbh->bz_add_column('profiles', 'emailflags', {TYPE => 'MEDIUMTEXT'});
+ }
- _rederive_regex_groups();
+ if (!$dbh->bz_index_info('email_rates', 'email_rates_message_ts_idx')) {
+ $dbh->bz_add_index('email_rates', 'email_rates_message_ts_idx', ['message_ts']);
+ }
- # PUBLIC is a reserved word in Oracle.
- $dbh->bz_rename_column('series', 'public', 'is_public');
+ $dbh->bz_add_column('groups', 'isactive',
+ {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'});
- # 2005-11-04 LpSolit@gmail.com - Bug 305927
- $dbh->bz_alter_column('groups', 'userregexp',
- {TYPE => 'TINYTEXT', NOTNULL => 1, DEFAULT => "''"});
+ $dbh->bz_add_column('attachments', 'isobsolete',
+ {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'});
- _clean_control_characters_from_short_desc();
+ $dbh->bz_drop_column("profiles", "emailnotification");
+ $dbh->bz_drop_column("profiles", "newemailtech");
+
+ # 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});
+
+ _update_bugs_activity_to_only_record_changes();
+
+ # 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", "reporter_accessible",
+ {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'});
+ $dbh->bz_add_column("bugs", "cclist_accessible",
+ {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'});
+
+ $dbh->bz_add_column("bugs_activity", "attach_id", {TYPE => 'INT5'});
+
+ _delete_logincookies_cryptpassword_and_handle_invalid_cookies();
+
+ # 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");
+
+ # 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'});
+
+ _use_ip_instead_of_hostname_in_logincookies();
+
+ $dbh->bz_add_column('longdescs', 'isprivate',
+ {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'});
+ $dbh->bz_add_column('attachments', 'isprivate',
+ {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'});
+
+ $dbh->bz_add_column("bugs", "alias", {TYPE => "varchar(20)"});
+ $dbh->bz_add_index('bugs', 'bugs_alias_idx',
+ {TYPE => 'UNIQUE', FIELDS => [qw(alias)]});
+
+ _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)'});
- # 2005-12-07 altlst@sonic.net -- Bug 225221
- $dbh->bz_add_column('longdescs', 'comment_id',
- {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1});
+ $dbh->bz_add_column('flagtypes', 'grant_group_id', {TYPE => 'INT3'});
+ $dbh->bz_add_column('flagtypes', 'request_group_id', {TYPE => 'INT3'});
- _stop_storing_inactive_flags();
- _change_short_desc_from_mediumtext_to_varchar();
+ # 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();
- # 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();
+ # 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}, '');
+ }
+
+ $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();
- _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 => "''"});
+ # 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});
}
- 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});
- }
- }
+ # 2006-08-04 LpSolit@gmail.com - Bug 305941
+ $dbh->bz_drop_column('profiles', 'refreshed_when');
+ $dbh->bz_drop_column('groups', 'last_changed');
- # 2006-08-04 LpSolit@gmail.com - Bug 305941
- $dbh->bz_drop_column('profiles', 'refreshed_when');
- $dbh->bz_drop_column('groups', 'last_changed');
+ # 2006-08-06 LpSolit@gmail.com - Bug 347521
+ $dbh->bz_alter_column('flagtypes', 'id',
+ {TYPE => 'SMALLSERIAL', NOTNULL => 1, PRIMARYKEY => 1});
- # 2006-08-06 LpSolit@gmail.com - Bug 347521
- $dbh->bz_alter_column('flagtypes', 'id',
- {TYPE => 'SMALLSERIAL', NOTNULL => 1, PRIMARYKEY => 1});
+ $dbh->bz_alter_column('keyworddefs', 'id',
+ {TYPE => 'SMALLSERIAL', NOTNULL => 1, PRIMARYKEY => 1});
- $dbh->bz_alter_column('keyworddefs', 'id',
- {TYPE => 'SMALLSERIAL', NOTNULL => 1, PRIMARYKEY => 1});
+ # 2006-08-19 LpSolit@gmail.com - Bug 87795
+ $dbh->bz_alter_column('tokens', 'userid', {TYPE => 'INT3'});
- # 2006-08-19 LpSolit@gmail.com - Bug 87795
- $dbh->bz_alter_column('tokens', 'userid', {TYPE => 'INT3'});
+ $dbh->bz_drop_index('bugs', 'bugs_short_desc_idx');
- $dbh->bz_drop_index('bugs', 'bugs_short_desc_idx');
+ # 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 => "''"});
- # 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 => "''"});
+ _update_longdescs_who_index();
- _update_longdescs_who_index();
+ $dbh->bz_add_column('setting', 'subclass', {TYPE => 'varchar(32)'});
- $dbh->bz_add_column('setting', 'subclass', {TYPE => 'varchar(32)'});
+ $dbh->bz_alter_column('longdescs', 'thetext',
+ {TYPE => 'LONGTEXT', NOTNULL => 1}, '');
- $dbh->bz_alter_column('longdescs', 'thetext',
- {TYPE => 'LONGTEXT', NOTNULL => 1}, '');
+ # 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'});
- # 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'});
+ # 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)'});
- # 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)'});
+ $dbh->bz_add_column('versions', 'id',
+ {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1});
+ $dbh->bz_add_column('milestones', 'id',
+ {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1});
- $dbh->bz_add_column('versions', 'id',
- {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1});
- $dbh->bz_add_column('milestones', 'id',
- {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1});
+ _fix_uppercase_custom_field_names();
+ _fix_uppercase_index_names();
- _fix_uppercase_custom_field_names();
- _fix_uppercase_index_names();
+ # 2007-05-17 LpSolit@gmail.com - Bug 344965
+ _initialize_workflow_for_upgrade($old_params);
- # 2007-05-17 LpSolit@gmail.com - Bug 344965
- _initialize_workflow_for_upgrade($old_params);
+ # 2007-08-08 LpSolit@gmail.com - Bug 332149
+ $dbh->bz_add_column('groups', 'icon_url', {TYPE => 'TINYTEXT'});
- # 2007-08-08 LpSolit@gmail.com - Bug 332149
- $dbh->bz_add_column('groups', 'icon_url', {TYPE => 'TINYTEXT'});
+ # 2007-08-21 wurblzap@gmail.com - Bug 365378
+ _make_lang_setting_dynamic();
- # 2007-08-21 wurblzap@gmail.com - Bug 365378
- _make_lang_setting_dynamic();
+ # 2007-11-29 xiaoou.wu@oracle.com - Bug 153129
+ _change_text_types();
- # 2007-11-29 xiaoou.wu@oracle.com - Bug 153129
- _change_text_types();
+ # 2007-09-09 LpSolit@gmail.com - Bug 99215
+ _fix_attachment_modification_date();
- # 2007-09-09 LpSolit@gmail.com - Bug 99215
- _fix_attachment_modification_date();
+ $dbh->bz_drop_index('longdescs', 'longdescs_thetext_idx');
+ _populate_bugs_fulltext();
- $dbh->bz_drop_index('longdescs', 'longdescs_thetext_idx');
- _populate_bugs_fulltext();
+ # 2008-01-18 xiaoou.wu@oracle.com - Bug 414292
+ $dbh->bz_alter_column('series', 'query', {TYPE => 'MEDIUMTEXT', NOTNULL => 1});
- # 2008-01-18 xiaoou.wu@oracle.com - Bug 414292
- $dbh->bz_alter_column('series', 'query',
- { TYPE => 'MEDIUMTEXT', NOTNULL => 1 });
+ # Add FK to multi select field tables
+ _add_foreign_keys_to_multiselects();
- # Add FK to multi select field tables
- _add_foreign_keys_to_multiselects();
+ # 2008-07-28 tfu@redhat.com - Bug 431669
+ $dbh->bz_alter_column('group_control_map', 'product_id',
+ {TYPE => 'INT2', NOTNULL => 1});
- # 2008-07-28 tfu@redhat.com - Bug 431669
- $dbh->bz_alter_column('group_control_map', 'product_id',
- { TYPE => 'INT2', NOTNULL => 1 });
+ # 2008-09-07 LpSolit@gmail.com - Bug 452893
+ _fix_illegal_flag_modification_dates();
- # 2008-09-07 LpSolit@gmail.com - Bug 452893
- _fix_illegal_flag_modification_dates();
+ _add_visiblity_value_to_value_tables();
- _add_visiblity_value_to_value_tables();
+ # 2009-03-02 arbingersys@gmail.com - Bug 423613
+ _add_extern_id_index();
- # 2009-03-02 arbingersys@gmail.com - Bug 423613
- _add_extern_id_index();
+ # 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'});
- # 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'});
+ # 2009-01-16 oreomike@gmail.com - Bug 302420
+ $dbh->bz_add_column('whine_events', 'mailifnobugs',
+ {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'});
- # 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();
- _convert_disallownew_to_isactive();
+ $dbh->bz_alter_column('bugs_activity', 'added', {TYPE => 'varchar(255)'});
+ $dbh->bz_add_index('bugs_activity', 'bugs_activity_added_idx', ['added']);
- $dbh->bz_alter_column('bugs_activity', 'added',
- { TYPE => 'varchar(255)' });
- $dbh->bz_add_index('bugs_activity', 'bugs_activity_added_idx', ['added']);
+ # 2009-09-28 LpSolit@gmail.com - Bug 519032
+ $dbh->bz_drop_column('series', 'last_viewed');
- # 2009-09-28 LpSolit@gmail.com - Bug 519032
- $dbh->bz_drop_column('series', 'last_viewed');
+ # 2009-09-28 LpSolit@gmail.com - Bug 399073
+ _fix_logincookies_ipaddr();
- # 2009-09-28 LpSolit@gmail.com - Bug 399073
- _fix_logincookies_ipaddr();
+ # 2009-11-01 LpSolit@gmail.com - Bug 525025
+ _fix_invalid_custom_field_names();
- # 2009-11-01 LpSolit@gmail.com - Bug 525025
- _fix_invalid_custom_field_names();
+ _set_attachment_comment_types();
- _set_attachment_comment_types();
+ $dbh->bz_drop_column('products', 'milestoneurl');
- $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();
- _add_allows_unconfirmed_to_product_table();
- _convert_flagtypes_fks_to_set_null();
- _fix_decimal_types();
- _fix_series_creator_fk();
+ # 2009-11-14 dkl@redhat.com - Bug 310450
+ $dbh->bz_add_column('bugs_activity', 'comment_id', {TYPE => 'INT4'});
- # 2009-11-14 dkl@redhat.com - Bug 310450
- $dbh->bz_add_column('bugs_activity', 'comment_id', {TYPE => 'INT4'});
+ # 2010-04-07 LpSolit@gmail.com - Bug 69621
+ $dbh->bz_drop_column('bugs', 'keywords');
- # 2010-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});
- # 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});
+ # 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}, '');
- # 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 }, '');
+ # 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'});
- # 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' });
+ # 2010-07-18 LpSolit@gmail.com - Bug 119703
+ _remove_attachment_isurl();
- # 2010-07-18 LpSolit@gmail.com - Bug 119703
- _remove_attachment_isurl();
+ # 2009-05-07 ghendricks@novell.com - Bug 77193
+ _add_isactive_to_product_fields();
- # 2009-05-07 ghendricks@novell.com - Bug 77193
- _add_isactive_to_product_fields();
+ # 2010-10-09 LpSolit@gmail.com - Bug 505165
+ $dbh->bz_alter_column('flags', 'setter_id', {TYPE => 'INT3', NOTNULL => 1});
- # 2010-10-09 LpSolit@gmail.com - Bug 505165
- $dbh->bz_alter_column('flags', 'setter_id', {TYPE => 'INT3', NOTNULL => 1});
+ # 2010-10-09 LpSolit@gmail.com - Bug 451735
+ _fix_series_indexes();
- # 2010-10-09 LpSolit@gmail.com - Bug 451735
- _fix_series_indexes();
+ $dbh->bz_add_column('bug_see_also', 'id',
+ {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1});
- $dbh->bz_add_column('bug_see_also', 'id',
- {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1});
+ _rename_tags_to_tag();
- _rename_tags_to_tag();
+ # 2011-01-29 LpSolit@gmail.com - Bug 616185
+ _migrate_user_tags();
- # 2011-01-29 LpSolit@gmail.com - Bug 616185
- _migrate_user_tags();
+ _populate_bug_see_also_class();
- _populate_bug_see_also_class();
+ # 2011-06-15 dkl@mozilla.com - Bug 658929
+ _migrate_disabledtext_boolean();
- # 2011-06-15 dkl@mozilla.com - Bug 658929
- _migrate_disabledtext_boolean();
+ # 2011-10-11 miketosh - Bug 690173
+ _on_delete_set_null_for_audit_log_userid();
- # 2011-10-11 miketosh - Bug 690173
- _on_delete_set_null_for_audit_log_userid();
+ # 2011-11-01 glob@mozilla.com - Bug 240437
+ $dbh->bz_add_column('profiles', 'last_seen_date', {TYPE => 'DATETIME'});
- # 2011-11-01 glob@mozilla.com - Bug 240437
- $dbh->bz_add_column('profiles', 'last_seen_date', {TYPE => 'DATETIME'});
+ # 2011-11-28 dkl@mozilla.com - Bug 685611
+ _fix_notnull_defaults();
- # 2011-11-28 dkl@mozilla.com - Bug 685611
- _fix_notnull_defaults();
+ # 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)]);
+ }
- # 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)]);
- }
+ # 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-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 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-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
+ # BMO - we change this to BIGSERIAL further down
+ #$dbh->bz_alter_column('bugs_activity', 'id',
+ # {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1});
- # 2012-07-24 dkl@mozilla.com - Bug 776972
- # BMO - we change this to BIGSERIAL further down
- #$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-07-24 dkl@mozilla.com - Bug 776982
- _fix_longdescs_primary_key();
+ # 2012-08-02 dkl@mozilla.com - Bug 756953
+ _fix_dependencies_dupes();
- # 2012-08-02 dkl@mozilla.com - Bug 756953
- _fix_dependencies_dupes();
+ # 2013-02-04 dkl@mozilla.com - Bug 824346
+ _fix_flagclusions_indexes();
- # 2013-02-04 dkl@mozilla.com - Bug 824346
- _fix_flagclusions_indexes();
+ # 2012-04-15 Frank@Frank-Becker.de - Bug 740536
+ $dbh->bz_add_index('audit_log', 'audit_log_class_idx', ['class', 'at_time']);
- # 2012-04-15 Frank@Frank-Becker.de - Bug 740536
- $dbh->bz_add_index('audit_log', 'audit_log_class_idx', ['class', 'at_time']);
+ # 2013-08-16 glob@mozilla.com - Bug 905925
+ $dbh->bz_add_index('attachments', 'attachments_ispatch_idx', ['ispatch']);
- # 2013-08-16 glob@mozilla.com - Bug 905925
- $dbh->bz_add_index('attachments', 'attachments_ispatch_idx', ['ispatch']);
+ # 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-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-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-07-27 LpSolit@gmail.com - Bug 1044561
- _fix_user_api_keys_indexes();
+ # 2018-06-14 dylan@mozilla.com - Bug 1468818
+ $dbh->bz_add_column('longdescs', 'is_markdown',
+ {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'});
- # 2018-06-14 dylan@mozilla.com - Bug 1468818
- $dbh->bz_add_column('longdescs', 'is_markdown',
- {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'});
+ # 2014-10-?? dkl@mozilla.com - Bug 1062940
+ $dbh->bz_alter_column('bugs', 'alias', {TYPE => 'varchar(40)'});
- # 2014-10-?? dkl@mozilla.com - Bug 1062940
- $dbh->bz_alter_column('bugs', 'alias', { TYPE => 'varchar(40)' });
+ # 2015-05-13 dylan@mozilla.com - Bug 1160430
+ $dbh->bz_add_column('keyworddefs', 'is_active',
+ {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'});
- # 2015-05-13 dylan@mozilla.com - Bug 1160430
- $dbh->bz_add_column('keyworddefs', 'is_active',
- {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'});
+ $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)]);
- $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)]);
+ _add_attach_size();
- _add_attach_size();
+ _fix_disable_mail();
- _fix_disable_mail();
+ # 2015-07-25 dylan@mozilla.com - Bug 1179856
+ $dbh->bz_alter_column('tokens', 'token',
+ {TYPE => 'varchar(22)', NOTNULL => 1, PRIMARYKEY => 1});
- # 2015-07-25 dylan@mozilla.com - Bug 1179856
- $dbh->bz_alter_column('tokens', 'token',
- {TYPE => 'varchar(22)', NOTNULL => 1, PRIMARYKEY => 1});
+ # 2015-08-20 dylan@mozilla.com - Bug 1196092
+ $dbh->bz_alter_column('logincookies', 'cookie',
+ {TYPE => 'varchar(22)', NOTNULL => 1});
+ $dbh->bz_add_index('logincookies', 'logincookies_cookie_idx',
+ {TYPE => 'UNIQUE', FIELDS => ['cookie']});
+ $dbh->bz_add_column('logincookies', 'id',
+ {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1});
- # 2015-08-20 dylan@mozilla.com - Bug 1196092
- $dbh->bz_alter_column('logincookies', 'cookie',
- {TYPE => 'varchar(22)', NOTNULL => 1});
- $dbh->bz_add_index('logincookies', 'logincookies_cookie_idx',
- {TYPE => 'UNIQUE', FIELDS => ['cookie']});
- $dbh->bz_add_column('logincookies', 'id',
- {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1});
+ $dbh->bz_add_column('user_api_keys', 'last_used_ip', {TYPE => 'varchar(40)'});
- $dbh->bz_add_column('user_api_keys', 'last_used_ip',
- {TYPE => 'varchar(40)'});
+ _add_restrict_ipaddr();
- _add_restrict_ipaddr();
+ $dbh->bz_add_column('profiles', 'password_change_required',
+ {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'});
+ $dbh->bz_add_column('profiles', 'password_change_reason',
+ {TYPE => 'varchar(64)'});
- $dbh->bz_add_column('profiles', 'password_change_required',
- { TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE' });
- $dbh->bz_add_column('profiles', 'password_change_reason',
- { TYPE => 'varchar(64)' });
+ $dbh->bz_add_column('profiles', 'mfa',
+ {TYPE => 'varchar(8)',, DEFAULT => "''"});
- $dbh->bz_add_column('profiles', 'mfa', { TYPE => 'varchar(8)', , DEFAULT => "''" });
+ $dbh->bz_add_column('profiles', 'mfa_required_date', {TYPE => 'DATETIME'});
+ _migrate_group_owners();
- $dbh->bz_add_column('profiles', 'mfa_required_date', { TYPE => 'DATETIME' });
- _migrate_group_owners();
+ $dbh->bz_add_column('groups', 'idle_member_removal',
+ {TYPE => 'INT2', NOTNULL => 1, DEFAULT => '0'});
- $dbh->bz_add_column('groups', 'idle_member_removal',
- {TYPE => 'INT2', NOTNULL => 1, DEFAULT => '0'});
+ _migrate_preference_categories();
- _migrate_preference_categories();
+ # 2016-09-01 dkl@mozilla.com - Bug 1268317
+ $dbh->bz_add_column('components', 'triage_owner_id', {TYPE => 'INT3'});
- # 2016-09-01 dkl@mozilla.com - Bug 1268317
- $dbh->bz_add_column('components', 'triage_owner_id',
- {TYPE => 'INT3'});
+ $dbh->bz_add_column('profiles', 'nickname',
+ {TYPE => 'varchar(255)', NOTNULL => 1, DEFAULT => "''"});
+ $dbh->bz_add_index('profiles', 'profiles_nickname_idx', [qw(nickname)]);
- $dbh->bz_add_column('profiles', 'nickname',
- {TYPE => 'varchar(255)', NOTNULL => 1, DEFAULT => "''"});
- $dbh->bz_add_index('profiles', 'profiles_nickname_idx', [qw(nickname)]);
+ $dbh->bz_add_index('profiles', 'profiles_realname_ft_idx',
+ {TYPE => 'FULLTEXT', FIELDS => ['realname']});
+ _migrate_nicknames();
- $dbh->bz_add_index('profiles', 'profiles_realname_ft_idx',
- {TYPE => 'FULLTEXT', FIELDS => ['realname']});
- _migrate_nicknames();
+ ################################################################
+ # New --TABLE-- changes should go *** A B O V E *** this point #
+ ################################################################
- ################################################################
- # New --TABLE-- changes should go *** A B O V E *** this point #
- ################################################################
+ Bugzilla::Hook::process('install_update_db');
- 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();
- # 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)
+ 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);
+ 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 him or something. Invent a new profile
- # entry disabled, just to represent him.
- $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 him or something. Invent a new profile
+ # entry disabled, just to represent him.
+ $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}, '');
+ }
+
+ # Populate the milestone table with all existing values in the database
+ my $sth = $dbh->prepare(
+ "SELECT DISTINCT target_milestone, product
+ FROM bugs"
+ );
+ $sth->execute();
- print "Populating milestones table...\n";
+ print "Populating milestones table...\n";
- 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);
+ while (my ($value, $product) = $sth->fetchrow_array()) {
- if (!$ms_exists) {
- $dbh->do("INSERT INTO milestones(value, product, sortkey)
- VALUES (?,?,?)", undef, $value, $product, $sortkey);
- }
- }
+ # 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
@@ -1259,299 +1290,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 => 'SMALLSERIAL', NOTNULL => 1, PRIMARYKEY => 1});
- $dbh->bz_add_column("bugs", "component_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");
+ }
- 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 => 'SMALLSERIAL', NOTNULL => 1, PRIMARYKEY => 1});
+ $dbh->bz_add_column("bugs", "component_id", {TYPE => 'INT2', 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.
@@ -1562,1211 +1609,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)') {
- 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)') {
+ 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 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 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
@@ -2776,520 +2953,577 @@ 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
# 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') {
+
+ # 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'
+ );
- $dbh->bz_alter_column('bugs', 'short_desc', {TYPE => 'varchar(255)',
- NOTNULL => 1});
+ 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('quips', 'quip',
- { 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('quips', 'quip', {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'});
+ 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'});
- }
+ $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
@@ -3297,593 +3531,630 @@ 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 $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";
+ }
- # As recommended by Monty Widenius for GNOME's upgrade.
- # mkanat and justdave concur it'll be helpful for bmo, too.
- $dbh->do('SET SESSION myisam_sort_buffer_size = 3221225472');
+ # As recommended by Monty Widenius for GNOME's upgrade.
+ # mkanat and justdave concur it'll be helpful for bmo, too.
+ $dbh->do('SET SESSION myisam_sort_buffer_size = 3221225472');
- my $newline = $dbh->quote("\n");
- $dbh->do(
- qq{$command INTO bugs_fulltext (bug_id, short_desc, comments,
+ my $newline = $dbh->quote("\n");
+ $dbh->do(
+ qq{$command INTO bugs_fulltext (bug_id, short_desc, comments,
comments_noprivate)
SELECT bugs.bug_id, bugs.short_desc, }
- . $dbh->sql_group_concat('longdescs.thetext', $newline, 0)
- . ', ' . $dbh->sql_group_concat('nopriv.thetext', $newline, 0) .
- qq{ FROM bugs
+ . $dbh->sql_group_concat('longdescs.thetext', $newline, 0) . ', '
+ . $dbh->sql_group_concat('nopriv.thetext', $newline, 0)
+ . qq{ FROM bugs
LEFT JOIN longdescs
ON bugs.bug_id = longdescs.bug_id
LEFT JOIN longdescs AS nopriv
ON longdescs.comment_id = nopriv.comment_id
AND nopriv.isprivate = 0
$where }
- . $dbh->sql_group_by('bugs.bug_id', 'bugs.short_desc'));
- }
+ . $dbh->sql_group_by('bugs.bug_id', 'bugs.short_desc')
+ );
+ }
}
sub _fix_illegal_flag_modification_dates {
- my $dbh = Bugzilla->dbh;
+ my $dbh = Bugzilla->dbh;
+
+ my $rows = $dbh->do(
+ 'UPDATE flags SET modification_date = creation_date
+ WHERE modification_date < creation_date'
+ );
- my $rows = $dbh->do('UPDATE flags SET modification_date = creation_date
- WHERE modification_date < creation_date');
- # If no rows are affected, $dbh->do returns 0E0 instead of 0.
- print "$rows flags had an illegal modification date. Fixed!\n" if ($rows =~ /^\d+$/);
+ # If no rows are affected, $dbh->do returns 0E0 instead of 0.
+ print "$rows flags had an illegal modification date. Fixed!\n"
+ if ($rows =~ /^\d+$/);
}
sub _add_visiblity_value_to_value_tables {
- my $dbh = Bugzilla->dbh;
- my @standard_fields =
- qw(bug_status resolution priority bug_severity op_sys rep_platform);
- my $custom_fields = $dbh->selectcol_arrayref(
- 'SELECT name FROM fielddefs WHERE custom = 1 AND type IN(?,?)',
- undef, FIELD_TYPE_SINGLE_SELECT, FIELD_TYPE_MULTI_SELECT);
- foreach my $field (@standard_fields, @$custom_fields) {
- $dbh->bz_add_column($field, 'visibility_value_id', {TYPE => 'INT2'});
- $dbh->bz_add_index($field, "${field}_visibility_value_id_idx",
- ['visibility_value_id']);
- }
+ my $dbh = Bugzilla->dbh;
+ my @standard_fields
+ = qw(bug_status resolution priority bug_severity op_sys rep_platform);
+ my $custom_fields
+ = $dbh->selectcol_arrayref(
+ 'SELECT name FROM fielddefs WHERE custom = 1 AND type IN(?,?)',
+ undef, FIELD_TYPE_SINGLE_SELECT, FIELD_TYPE_MULTI_SELECT);
+ foreach my $field (@standard_fields, @$custom_fields) {
+ $dbh->bz_add_column($field, 'visibility_value_id', {TYPE => 'INT2'});
+ $dbh->bz_add_index($field, "${field}_visibility_value_id_idx",
+ ['visibility_value_id']);
+ }
}
sub _add_extern_id_index {
- my $dbh = Bugzilla->dbh;
- if (!$dbh->bz_index_info('profiles', 'profiles_extern_id_idx')) {
- # Some Bugzillas have a multiple empty strings in extern_id,
- # which need to be converted to NULLs before we add the index.
- $dbh->do("UPDATE profiles SET extern_id = NULL WHERE extern_id = ''");
- $dbh->bz_add_index('profiles', 'profiles_extern_id_idx',
- {TYPE => 'UNIQUE', FIELDS => [qw(extern_id)]});
- }
+ my $dbh = Bugzilla->dbh;
+ if (!$dbh->bz_index_info('profiles', 'profiles_extern_id_idx')) {
+
+ # Some Bugzillas have a multiple empty strings in extern_id,
+ # which need to be converted to NULLs before we add the index.
+ $dbh->do("UPDATE profiles SET extern_id = NULL WHERE extern_id = ''");
+ $dbh->bz_add_index('profiles', 'profiles_extern_id_idx',
+ {TYPE => 'UNIQUE', FIELDS => [qw(extern_id)]});
+ }
}
sub _convert_disallownew_to_isactive {
- my $dbh = Bugzilla->dbh;
- if ($dbh->bz_column_info('products', 'disallownew')){
- $dbh->bz_add_column('products', 'isactive',
- { TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'});
+ 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');
+ # 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');
- }
+ $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);
-
- my $tag_id = $dbh->selectrow_array($sth_tag_id,
- undef, $user_id, $tag_name);
-
- indicate_progress({ current => ++$current, total => $total,
- every => 25 });
+ # Make sure that things like "001" get converted to "1"
+ @bug_ids = map { int($_) } @bug_ids;
- my $uri = URI->new("buglist.cgi?$query", 'http');
- my $bug_id_list = $uri->query_param_delete('bug_id');
- if (!$bug_id_list) {
- warn "No bug_id param for tag $name from user $user_id: $query";
- next;
- }
- my @bug_ids = split(/[\s,]+/, $bug_id_list);
- # Make sure that things like "001" get converted to "1"
- @bug_ids = map { int($_) } @bug_ids;
- # And remove duplicates
- @bug_ids = uniq @bug_ids;
- foreach my $bug_id (@bug_ids) {
- # If "int" above failed this might be undef. We also
- # don't want to accept bug 0.
- next if !$bug_id;
- $sth_bug_tag->execute($bug_id, $tag_id);
- }
+ # And remove duplicates
+ @bug_ids = uniq @bug_ids;
+ foreach my $bug_id (@bug_ids) {
- # 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 = ?");
+ my $result = $dbh->selectall_arrayref("SELECT id, value FROM bug_see_also");
- $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 $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_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 _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 => {} });
- print "Removing duplicated entries from the '$table' table...\n" 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 => {}});
+ print "Removing duplicated entries from the '$table' table...\n" 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_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 _add_attach_size {
- my $dbh = Bugzilla->dbh;
+ my $dbh = Bugzilla->dbh;
- return if $dbh->bz_column_info('attachments', 'attach_size');
+ return if $dbh->bz_column_info('attachments', 'attach_size');
- $dbh->bz_add_column('attachments', 'attach_size',
- {TYPE => 'INT4', NOTNULL => 1, DEFAULT => 0});
+ $dbh->bz_add_column('attachments', 'attach_size',
+ {TYPE => 'INT4', NOTNULL => 1, DEFAULT => 0});
- print "Setting attach_size...\n";
- $dbh->do("
+ print "Setting attach_size...\n";
+ $dbh->do("
UPDATE attachments
INNER JOIN attach_data ON attach_data.id = attachments.attach_id
SET attachments.attach_size = LENGTH(attach_data.thedata)
@@ -3891,57 +4162,61 @@ sub _add_attach_size {
}
sub _fix_disable_mail {
- # you can no longer have disabled accounts with enabled mail
- Bugzilla->dbh->do("UPDATE profiles SET disable_mail = 1 WHERE is_enabled = 0");
+
+ # you can no longer have disabled accounts with enabled mail
+ Bugzilla->dbh->do("UPDATE profiles SET disable_mail = 1 WHERE is_enabled = 0");
}
sub _add_restrict_ipaddr {
- my $dbh = Bugzilla->dbh;
- return if $dbh->bz_column_info('logincookies', 'restrict_ipaddr');
+ my $dbh = Bugzilla->dbh;
+ return if $dbh->bz_column_info('logincookies', 'restrict_ipaddr');
- $dbh->bz_add_column('logincookies', 'restrict_ipaddr',
- {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 0});
- $dbh->do("UPDATE logincookies SET restrict_ipaddr = 1 WHERE ipaddr IS NOT NULL");
+ $dbh->bz_add_column('logincookies', 'restrict_ipaddr',
+ {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 0});
+ $dbh->do(
+ "UPDATE logincookies SET restrict_ipaddr = 1 WHERE ipaddr IS NOT NULL");
}
sub _migrate_group_owners {
- my $dbh = Bugzilla->dbh;
- return if $dbh->bz_column_info('groups', 'owner_user_id');
- $dbh->bz_add_column('groups', 'owner_user_id', {TYPE => 'INT3'});
- my $nobody = Bugzilla::User->new({ name => Bugzilla->params->{'nobody_user'}, cache => 1 });
- unless ($nobody) {
- $nobody = Bugzilla::User->create(
- {
- login_name => Bugzilla->params->{'nobody_user'},
- realname => 'Nobody (ok to assign bugs to)',
- cryptpassword => '*',
- }
- );
- }
- $dbh->do('UPDATE groups SET owner_user_id = ?', undef, $nobody->id);
+ my $dbh = Bugzilla->dbh;
+ return if $dbh->bz_column_info('groups', 'owner_user_id');
+ $dbh->bz_add_column('groups', 'owner_user_id', {TYPE => 'INT3'});
+ my $nobody = Bugzilla::User->new(
+ {name => Bugzilla->params->{'nobody_user'}, cache => 1});
+ unless ($nobody) {
+ $nobody = Bugzilla::User->create({
+ login_name => Bugzilla->params->{'nobody_user'},
+ realname => 'Nobody (ok to assign bugs to)',
+ cryptpassword => '*',
+ });
+ }
+ $dbh->do('UPDATE groups SET owner_user_id = ?', undef, $nobody->id);
}
sub _migrate_nicknames {
- my $dbh = Bugzilla->dbh;
- my $sth = $dbh->prepare('SELECT userid FROM profiles WHERE realname LIKE "%:%" AND is_enabled = 1 AND NOT nickname');
- $sth->execute();
- while (my ($user_id) = $sth->fetchrow_array) {
- my $user = Bugzilla::User->new($user_id);
- $user->set_name($user->name);
- $user->update();
- }
+ my $dbh = Bugzilla->dbh;
+ my $sth
+ = $dbh->prepare(
+ 'SELECT userid FROM profiles WHERE realname LIKE "%:%" AND is_enabled = 1 AND NOT nickname'
+ );
+ $sth->execute();
+ while (my ($user_id) = $sth->fetchrow_array) {
+ my $user = Bugzilla::User->new($user_id);
+ $user->set_name($user->name);
+ $user->update();
+ }
}
sub _migrate_preference_categories {
- my $dbh = Bugzilla->dbh;
- return if $dbh->bz_column_info('setting', 'category');
- $dbh->bz_add_column('setting', 'category',
- {TYPE => 'varchar(64)', NOTNULL => 1, DEFAULT => "'General'"});
- my @settings = @{ Bugzilla::Install::SETTINGS() };
- foreach my $params (@settings) {
- $dbh->do('UPDATE setting SET category = ? WHERE name = ?',
- undef, $params->{category}, $params->{name});
- }
+ my $dbh = Bugzilla->dbh;
+ return if $dbh->bz_column_info('setting', 'category');
+ $dbh->bz_add_column('setting', 'category',
+ {TYPE => 'varchar(64)', NOTNULL => 1, DEFAULT => "'General'"});
+ my @settings = @{Bugzilla::Install::SETTINGS()};
+ foreach my $params (@settings) {
+ $dbh->do('UPDATE setting SET category = ? WHERE name = ?',
+ undef, $params->{category}, $params->{name});
+ }
}
1;
diff --git a/Bugzilla/Install/Filesystem.pm b/Bugzilla/Install/Filesystem.pm
index cb1b1ad15..da019b760 100644
--- a/Bugzilla/Install/Filesystem.pm
+++ b/Bugzilla/Install/Filesystem.pm
@@ -40,10 +40,10 @@ use English qw(-no_match_vars $OSNAME);
use base qw(Exporter);
our @EXPORT = qw(
- update_filesystem
- fix_all_file_permissions
- fix_dir_permissions
- fix_file_permissions
+ update_filesystem
+ fix_all_file_permissions
+ fix_dir_permissions
+ fix_file_permissions
);
use constant INDEX_HTML => <<'EOT';
@@ -59,12 +59,12 @@ use constant INDEX_HTML => <<'EOT';
EOT
use constant HTTPD_ENV => qw(
- LOCALCONFIG_ENV
- BUGZILLA_UNSAFE_AUTH_DELEGATION
- LOG4PERL_CONFIG_FILE
- LOG4PERL_STDERR_DISABLE
- USE_NYTPROF
- NYTPROF_DIR
+ LOCALCONFIG_ENV
+ BUGZILLA_UNSAFE_AUTH_DELEGATION
+ LOG4PERL_CONFIG_FILE
+ LOG4PERL_STDERR_DISABLE
+ USE_NYTPROF
+ NYTPROF_DIR
);
###############
@@ -72,47 +72,55 @@ use constant HTTPD_ENV => qw(
###############
# 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
# 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 }
sub DIR_ALSO_WS_STICKY { $OSNAME eq 'linux' ? 02000 : 0 }
@@ -127,469 +135,437 @@ sub DIR_ALSO_WS_STICKY { $OSNAME eq 'linux' ? 02000 : 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 $confdir = bz_locations()->{'confdir'};
- 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'};
- my $logsdir = bz_locations()->{'logsdir'};
-
- # 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 },
- '*.psgi' => { perms => CGI_READ },
- 'whineatnews.pl' => { perms => WS_EXECUTE },
- 'collectstats.pl' => { perms => WS_EXECUTE },
- 'importxml.pl' => { perms => WS_EXECUTE },
- 'testserver.pl' => { perms => WS_EXECUTE },
- 'whine.pl' => { perms => WS_EXECUTE },
- 'email_in.pl' => { perms => WS_EXECUTE },
- 'sanitycheck.pl' => { perms => WS_EXECUTE },
- 'checksetup.pl' => { perms => OWNER_EXECUTE },
- 'runtests.pl' => { perms => OWNER_EXECUTE },
- 'jobqueue.pl' => { perms => OWNER_EXECUTE },
- 'migrate.pl' => { perms => OWNER_EXECUTE },
- 'Makefile.PL' => { perms => OWNER_EXECUTE },
- 'gen-cpanfile.pl' => { perms => OWNER_EXECUTE },
- 'jobqueue-worker.pl' => { perms => OWNER_EXECUTE },
- 'clean-bug-user-last-visit.pl' => { perms => WS_EXECUTE },
-
- 'bugzilla.pl' => { perms => OWNER_EXECUTE },
- '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 },
- 'cvs-update.log' => { perms => WS_SERVE },
- 'scripts/sendunsentbugmail.pl' => { perms => WS_EXECUTE },
- '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" => { perms => CGI_WRITE },
- "$datadir/old-params.txt" => { perms => OWNER_WRITE },
- "$extensionsdir/create.pl" => { perms => OWNER_EXECUTE },
- "$extensionsdir/*/*.pl" => { perms => WS_EXECUTE },
- "$extensionsdir/*/bin/*" => { perms => WS_EXECUTE },
-
- # google webmaster tools verification files
- 'google*.html' => { perms => WS_SERVE },
- 'contribute.json' => { perms => WS_SERVE },
+ my $datadir = bz_locations()->{'datadir'};
+ my $confdir = bz_locations()->{'confdir'};
+ 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'};
+ my $logsdir = bz_locations()->{'logsdir'};
+
+ # 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},
+ '*.psgi' => {perms => CGI_READ},
+ 'whineatnews.pl' => {perms => WS_EXECUTE},
+ 'collectstats.pl' => {perms => WS_EXECUTE},
+ 'importxml.pl' => {perms => WS_EXECUTE},
+ 'testserver.pl' => {perms => WS_EXECUTE},
+ 'whine.pl' => {perms => WS_EXECUTE},
+ 'email_in.pl' => {perms => WS_EXECUTE},
+ 'sanitycheck.pl' => {perms => WS_EXECUTE},
+ 'checksetup.pl' => {perms => OWNER_EXECUTE},
+ 'runtests.pl' => {perms => OWNER_EXECUTE},
+ 'jobqueue.pl' => {perms => OWNER_EXECUTE},
+ 'migrate.pl' => {perms => OWNER_EXECUTE},
+ 'Makefile.PL' => {perms => OWNER_EXECUTE},
+ 'gen-cpanfile.pl' => {perms => OWNER_EXECUTE},
+ 'jobqueue-worker.pl' => {perms => OWNER_EXECUTE},
+ 'clean-bug-user-last-visit.pl' => {perms => WS_EXECUTE},
+
+ 'bugzilla.pl' => {perms => OWNER_EXECUTE},
+ '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},
+ 'cvs-update.log' => {perms => WS_SERVE},
+ 'scripts/sendunsentbugmail.pl' => {perms => WS_EXECUTE},
+ '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" => {perms => CGI_WRITE},
+ "$datadir/old-params.txt" => {perms => OWNER_WRITE},
+ "$extensionsdir/create.pl" => {perms => OWNER_EXECUTE},
+ "$extensionsdir/*/*.pl" => {perms => WS_EXECUTE},
+ "$extensionsdir/*/bin/*" => {perms => WS_EXECUTE},
+
+ # google webmaster tools verification files
+ 'google*.html' => {perms => WS_SERVE},
+ 'contribute.json' => {perms => WS_SERVE},
+ );
+
+ # Directories that we want to set the perms on, but not
+ # recurse through. These are directories we didn't create
+ # in checkesetup.pl.
+ #
+ # Purpose of BMO change: unknown.
+ my %non_recurse_dirs = ('.' => 0755, 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},
+ $logsdir => {files => CGI_WRITE, dirs => DIR_CGI_WRITE | DIR_ALSO_WS_STICKY},
+ $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},
+ static => {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},
+ $confdir => {files => WS_SERVE, dirs => DIR_WS_SERVE,},
+
+ # Purpose: allow webserver to read .bzr so we execute bzr commands
+ # in backticks and look at the result over the web. Used to show
+ # bzr history.
+ '.bzr' => {files => WS_SERVE, dirs => DIR_WS_SERVE},
+
+ # Directories only for the owner, not for the webserver.
+ 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,},
+ 'scripts' => {files => OWNER_EXECUTE, dirs => DIR_WS_SERVE,},
+ );
+
+ # --- 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,
+ $template_cache => DIR_CGI_WRITE,
+ $logsdir => DIR_CGI_WRITE | DIR_ALSO_WS_STICKY,
+
+ # Directories that contain content served directly by the web server.
+ "$skinsdir/custom" => DIR_WS_SERVE,
+ "$skinsdir/contrib" => DIR_WS_SERVE,
+ $confdir => DIR_CGI_READ,
+ );
+
+ my $yui_all_css = sub {
+ return join(
+ "\n",
+ map {
+ my $css = read_file($_);
+ _css_url_fix($css, $_, "skins/yui.css.list")
+ } read_file("skins/yui.css.list", {chomp => 1})
);
-
- # Directories that we want to set the perms on, but not
- # recurse through. These are directories we didn't create
- # in checkesetup.pl.
- #
- # Purpose of BMO change: unknown.
- my %non_recurse_dirs = (
- '.' => 0755,
- 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 },
- $logsdir => { files => CGI_WRITE,
- dirs => DIR_CGI_WRITE | DIR_ALSO_WS_STICKY },
- $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 },
- static => { 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 },
- $confdir => { files => WS_SERVE,
- dirs => DIR_WS_SERVE, },
-
- # Purpose: allow webserver to read .bzr so we execute bzr commands
- # in backticks and look at the result over the web. Used to show
- # bzr history.
- '.bzr' => { files => WS_SERVE,
- dirs => DIR_WS_SERVE },
- # Directories only for the owner, not for the webserver.
- 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, },
- 'scripts' => { files => OWNER_EXECUTE,
- dirs => DIR_WS_SERVE, },
- );
-
- # --- 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,
- $template_cache => DIR_CGI_WRITE,
- $logsdir => DIR_CGI_WRITE | DIR_ALSO_WS_STICKY,
- # Directories that contain content served directly by the web server.
- "$skinsdir/custom" => DIR_WS_SERVE,
- "$skinsdir/contrib" => DIR_WS_SERVE,
- $confdir => DIR_CGI_READ,
- );
-
- my $yui_all_css = sub {
- return join("\n",
- map {
- my $css = read_file($_);
- _css_url_fix($css, $_, "skins/yui.css.list")
- } read_file("skins/yui.css.list", { chomp => 1 })
- );
- };
-
- my $yui_all_js = sub {
- return join("\n",
- map { scalar read_file($_) } read_file("js/yui.js.list", { chomp => 1 })
- );
- };
-
- my $yui3_all_css = sub {
- return join("\n",
- map {
- my $css = read_file($_);
- _css_url_fix($css, $_, "skins/yui3.css.list")
- } read_file("skins/yui3.css.list", { chomp => 1 })
- );
- };
-
- my $yui3_all_js = sub {
- return join("\n",
- map { scalar read_file($_) } read_file("js/yui3.js.list", { chomp => 1 })
- );
- };
-
- # 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 => '' },
- "js/yui.js" => { perms => CGI_READ,
- overwrite => 1,
- contents => $yui_all_js },
- "skins/yui.css" => { perms => CGI_READ,
- overwrite => 1,
- contents => $yui_all_css },
- "js/yui3.js" => { perms => CGI_READ,
- overwrite => 1,
- contents => $yui3_all_js },
- "skins/yui3.css" => { perms => CGI_READ,
- overwrite => 1,
- contents => $yui3_all_css },
+ };
+
+ my $yui_all_js = sub {
+ return join("\n",
+ map { scalar read_file($_) } read_file("js/yui.js.list", {chomp => 1}));
+ };
+
+ my $yui3_all_css = sub {
+ return join(
+ "\n",
+ map {
+ my $css = read_file($_);
+ _css_url_fix($css, $_, "skins/yui3.css.list")
+ } read_file("skins/yui3.css.list", {chomp => 1})
);
+ };
+
+ my $yui3_all_js = sub {
+ return join("\n",
+ map { scalar read_file($_) } read_file("js/yui3.js.list", {chomp => 1}));
+ };
+
+ # 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 => ''},
+ "js/yui.js" => {perms => CGI_READ, overwrite => 1, contents => $yui_all_js},
+ "skins/yui.css" =>
+ {perms => CGI_READ, overwrite => 1, contents => $yui_all_css},
+ "js/yui3.js" => {perms => CGI_READ, overwrite => 1, contents => $yui3_all_js},
+ "skins/yui3.css" =>
+ {perms => CGI_READ, overwrite => 1, contents => $yui3_all_css},
+ );
+
+ # Create static error pages.
+ $create_dirs{"errors"} = DIR_CGI_READ;
+
+ # 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 => INDEX_HTML});
+
+ 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,
+ }
+ );
- # Create static error pages.
- $create_dirs{"errors"} = DIR_CGI_READ;
+ my %all_files = (%create_files, %index_html, %files);
+ my %all_dirs = (%create_dirs, %non_recurse_dirs);
- # 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 => INDEX_HTML }
- );
+ return {
+ create_dirs => \%create_dirs,
+ recurse_dirs => \%recurse_dirs,
+ all_dirs => \%all_dirs,
- 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,
- });
-
- my %all_files = (%create_files, %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,
- index_html => \%index_html,
- all_files => \%all_files,
- };
+ create_files => \%create_files,
+ 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);
+ 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);
+ }
+
+ # 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: $!";
}
-
- # 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: $!";
- }
- }
-
- # 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");
- }
-
- _create_files(%files);
- if ($params->{index_html}) {
- _create_files(%{$fs->{index_html}});
- }
- elsif (-e 'index.html') {
- my $templatedir = bz_locations()->{'templatedir'};
- print "*** It appears that you still have an old index.html hanging around.\n",
- "Either the contents of this file should be moved into a template and\n",
- "placed in the '$templatedir/en/custom' directory, or you should delete\n",
- "the file.\n";
- }
-
- # Delete old files that no longer need to exist
-
- # 2001-04-29 jake@bugzilla.org - Remove oldemailtech
- # http://bugzilla.mozilla.org/show_bug.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();
+ }
+
+ # 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");
+ }
+
+ _create_files(%files);
+ if ($params->{index_html}) {
+ _create_files(%{$fs->{index_html}});
+ }
+ elsif (-e 'index.html') {
+ my $templatedir = bz_locations()->{'templatedir'};
+ print "*** It appears that you still have an old index.html hanging around.\n",
+ "Either the contents of this file should be moved into a template and\n",
+ "placed in the '$templatedir/en/custom' directory, or you should delete\n",
+ "the file.\n";
+ }
+
+ # Delete old files that no longer need to exist
+
+ # 2001-04-29 jake@bugzilla.org - Remove oldemailtech
+ # http://bugzilla.mozilla.org/show_bug.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();
}
sub _css_url_fix {
- my ($content, $from, $to) = @_;
- my $from_dir = dirname(File::Spec->rel2abs($from, bz_locations()->{libpath}));
- my $to_dir = dirname(File::Spec->rel2abs($to, bz_locations()->{libpath}));
-
- return css_url_rewrite(
- $content,
- sub {
- my ($url) = @_;
- if ( $url =~ m{^(?:/|data:)} ) {
- return sprintf 'url(%s)', $url;
- }
- else {
- my $new_url = File::Spec->abs2rel(
- Cwd::realpath(
- File::Spec->rel2abs( $url, $from_dir )
- ),
- $to_dir
- );
- return sprintf "url(%s)", $new_url;
- }
- }
- );
+ my ($content, $from, $to) = @_;
+ my $from_dir = dirname(File::Spec->rel2abs($from, bz_locations()->{libpath}));
+ my $to_dir = dirname(File::Spec->rel2abs($to, bz_locations()->{libpath}));
+
+ return css_url_rewrite(
+ $content,
+ sub {
+ my ($url) = @_;
+ if ($url =~ m{^(?:/|data:)}) {
+ return sprintf 'url(%s)', $url;
+ }
+ else {
+ my $new_url
+ = File::Spec->abs2rel(Cwd::realpath(File::Spec->rel2abs($url, $from_dir)),
+ $to_dir);
+ return sprintf "url(%s)", $new_url;
+ }
+ }
+ );
}
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 = "/* Custom rules for $basename.\n"
- . " * The rules you put here override rules in that stylesheet. */";
- 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: $!";
- }
- };
+ my ($file) = @_;
+ my $basename = basename($file);
+ my $empty_contents = "/* Custom rules for $basename.\n"
+ . " * The rules you put here override rules in that stylesheet. */";
+ 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");
+ }
}
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) {
- my $info = $files{$file};
- if ($info->{overwrite} or not -f $file) {
- print "Creating $file...\n";
- my $fh = IO::File->new( $file, O_WRONLY | O_CREAT | O_TRUNC, $info->{perms} )
- or die "unable to write $file: $!";
- my $contents = $info->{contents};
- if (defined $contents && ref($contents) eq 'CODE') {
- print $fh $contents->();
- }
- elsif (defined $contents) {
- print $fh $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) {
+ my $info = $files{$file};
+ if ($info->{overwrite} or not -f $file) {
+ print "Creating $file...\n";
+ my $fh = IO::File->new($file, O_WRONLY | O_CREAT | O_TRUNC, $info->{perms})
+ or die "unable to write $file: $!";
+ my $contents = $info->{contents};
+ if (defined $contents && ref($contents) eq 'CODE') {
+ print $fh $contents->();
+ }
+ elsif (defined $contents) {
+ print $fh $contents;
+ }
+ $fh->close;
}
+ }
}
# If you ran a REALLY old version of Bugzilla, your chart files are in the
@@ -597,243 +573,266 @@ sub _create_files {
# 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) {
- close(IN);
- close(OUT);
+ # 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;
+ }
}
+
+ close(IN);
+ close(OUT);
+ }
}
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;
}
diff --git a/Bugzilla/Install/Localconfig.pm b/Bugzilla/Install/Localconfig.pm
index 6650eca27..fad7404d8 100644
--- a/Bugzilla/Install/Localconfig.pm
+++ b/Bugzilla/Install/Localconfig.pm
@@ -36,276 +36,206 @@ use Sys::Hostname qw(hostname);
use parent qw(Exporter);
our @EXPORT_OK = qw(
- read_localconfig
- update_localconfig
- ENV_KEYS
+ read_localconfig
+ update_localconfig
+ ENV_KEYS
);
# might want to change this for upstream
-use constant ENV_PREFIX => 'BMO_';
-use constant PARAM_OVERRIDE => qw( use_mailer_queue mail_delivery_method shadowdb shadowdbhost shadowdbport shadowdbsock );
+use constant ENV_PREFIX => 'BMO_';
+use constant PARAM_OVERRIDE =>
+ qw( use_mailer_queue mail_delivery_method shadowdb shadowdbhost shadowdbport shadowdbsock );
sub _sensible_group {
- return '' if ON_WINDOWS;
- return scalar getgrgid($EGID);
+ return '' if ON_WINDOWS;
+ return scalar getgrgid($EGID);
}
sub _migrate_param {
- my ( $name, $fallback_value ) = @_;
-
- return sub {
- if ( Bugzilla->can('params') ) {
- return Bugzilla->params->{$name} // $fallback_value;
- }
- else {
- return $fallback_value;
- }
- };
+ my ($name, $fallback_value) = @_;
+
+ return sub {
+ if (Bugzilla->can('params')) {
+ return Bugzilla->params->{$name} // $fallback_value;
+ }
+ else {
+ return $fallback_value;
+ }
+ };
}
use constant LOCALCONFIG_VARS => (
- {
- name => 'create_htaccess',
- default => 1,
- },
- {
- name => 'webservergroup',
- default => \&_sensible_group,
- },
- {
- name => 'use_suexec',
- default => 0,
- },
- {
- name => 'db_driver',
- default => 'mysql',
- },
- {
- name => 'db_host',
- default => 'localhost',
- },
- {
- name => 'db_name',
- default => 'bugs',
- },
- {
-
- name => 'db_user',
- default => 'bugs',
- },
- {
- name => 'db_pass',
- default => '',
- },
- {
- name => 'db_port',
- default => 0,
- },
- {
- name => 'db_sock',
- default => '',
- },
- {
- name => 'db_check',
- default => 1,
- },
- {
- name => 'index_html',
- default => 0,
- },
- {
- name => 'cvsbin',
- default => sub { bin_loc('cvs') },
- },
- {
- name => 'interdiffbin',
- default => sub { bin_loc('interdiff') },
- },
- {
- name => 'diffpath',
- default => sub { dirname( bin_loc('diff') ) },
- },
- {
- name => 'tct_bin',
- default => sub { bin_loc('tct') },
- },
- {
- 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 => 'param_override',
- default => {
- use_mailer_queue => undef,
- mail_delivery_method => undef,
- shadowdb => undef,
- shadowdbhost => undef,
- shadowdbport => undef,
- shadowdbsock => undef,
- },
- },
- {
- name => 'apache_size_limit',
- default => 600000,
- },
- {
- name => 'memcached_servers',
- default => _migrate_param( "memcached_servers", "" ),
- },
- {
- name => 'memcached_namespace',
- default => _migrate_param( "memcached_namespace", "bugzilla:" ),
- },
- {
- name => 'urlbase',
- default => _migrate_param( "urlbase", "" ),
- },
- {
- name => 'canonical_urlbase',
- default => '',
- },
- {
- name => 'attachment_base',
- default => _migrate_param( "attachment_base", '' ),
- },
- {
- name => 'ses_username',
- default => '',
- },
- {
- name => 'ses_password',
- default => '',
- },
- {
- name => 'inbound_proxies',
- default => _migrate_param( 'inbound_proxies', '' ),
- },
- {
- name => 'shadowdb_user',
- default => '',
- },
- {
- name => 'shadowdb_pass',
- default => '',
- },
- {
- name => 'datadog_host',
- default => '',
- },
- {
- name => 'datadog_port',
- default => 8125,
+ {name => 'create_htaccess', default => 1,},
+ {name => 'webservergroup', default => \&_sensible_group,},
+ {name => 'use_suexec', default => 0,},
+ {name => 'db_driver', default => 'mysql',},
+ {name => 'db_host', default => 'localhost',},
+ {name => 'db_name', default => 'bugs',},
+ {
+
+ name => 'db_user',
+ default => 'bugs',
+ },
+ {name => 'db_pass', default => '',},
+ {name => 'db_port', default => 0,},
+ {name => 'db_sock', default => '',},
+ {name => 'db_check', default => 1,},
+ {name => 'index_html', default => 0,},
+ {name => 'cvsbin', default => sub { bin_loc('cvs') },},
+ {name => 'interdiffbin', default => sub { bin_loc('interdiff') },},
+ {name => 'diffpath', default => sub { dirname(bin_loc('diff')) },},
+ {name => 'tct_bin', default => sub { bin_loc('tct') },},
+ {
+ 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 => 'param_override',
+ default => {
+ use_mailer_queue => undef,
+ mail_delivery_method => undef,
+ shadowdb => undef,
+ shadowdbhost => undef,
+ shadowdbport => undef,
+ shadowdbsock => undef,
},
+ },
+ {name => 'apache_size_limit', default => 600000,},
+ {
+ name => 'memcached_servers',
+ default => _migrate_param("memcached_servers", ""),
+ },
+ {
+ name => 'memcached_namespace',
+ default => _migrate_param("memcached_namespace", "bugzilla:"),
+ },
+ {name => 'urlbase', default => _migrate_param("urlbase", ""),},
+ {name => 'canonical_urlbase', default => '',},
+ {name => 'attachment_base', default => _migrate_param("attachment_base", ''),},
+ {name => 'ses_username', default => '',},
+ {name => 'ses_password', default => '',},
+ {name => 'inbound_proxies', default => _migrate_param('inbound_proxies', ''),},
+ {name => 'shadowdb_user', default => '',},
+ {name => 'shadowdb_pass', default => '',},
+ {name => 'datadog_host', default => '',},
+ {name => 'datadog_port', default => 8125,},
);
use constant ENV_KEYS => (
- (map { ENV_PREFIX . $_->{name} } LOCALCONFIG_VARS),
- (map { ENV_PREFIX . $_ } PARAM_OVERRIDE),
+ (map { ENV_PREFIX . $_->{name} } LOCALCONFIG_VARS),
+ (map { ENV_PREFIX . $_ } PARAM_OVERRIDE),
);
sub _read_localconfig_from_env {
- my %localconfig;
-
- foreach my $var ( LOCALCONFIG_VARS ) {
- my $name = $var->{name};
- my $key = ENV_PREFIX . $name;
- if ($name eq 'param_override') {
- foreach my $override (PARAM_OVERRIDE) {
- my $o_key = ENV_PREFIX . $override;
- $localconfig{param_override}{$override} = $ENV{$o_key};
- untaint($localconfig{param_override}{$override});
- }
- }
- elsif (exists $ENV{$key}) {
- $localconfig{$name} = $ENV{$key};
- untaint($localconfig{$name});
- }
- else {
- my $default = $var->{default};
- $localconfig{$name} = ref($default) eq 'CODE' ? $default->() : $default;
- untaint($localconfig{$name});
- }
+ my %localconfig;
+
+ foreach my $var (LOCALCONFIG_VARS) {
+ my $name = $var->{name};
+ my $key = ENV_PREFIX . $name;
+ if ($name eq 'param_override') {
+ foreach my $override (PARAM_OVERRIDE) {
+ my $o_key = ENV_PREFIX . $override;
+ $localconfig{param_override}{$override} = $ENV{$o_key};
+ untaint($localconfig{param_override}{$override});
+ }
+ }
+ elsif (exists $ENV{$key}) {
+ $localconfig{$name} = $ENV{$key};
+ untaint($localconfig{$name});
}
+ else {
+ my $default = $var->{default};
+ $localconfig{$name} = ref($default) eq 'CODE' ? $default->() : $default;
+ untaint($localconfig{$name});
+ }
+ }
- return \%localconfig;
+ return \%localconfig;
}
sub _read_localconfig_from_file {
- 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 ($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;
+ }
+ }
+ }
- return \%localconfig;
+ return \%localconfig;
}
sub read_localconfig {
- my ($include_deprecated) = @_;
- my $config = $ENV{LOCALCONFIG_ENV}
- ? _read_localconfig_from_env()
- : _read_localconfig_from_file($include_deprecated);
+ my ($include_deprecated) = @_;
+ my $config
+ = $ENV{LOCALCONFIG_ENV}
+ ? _read_localconfig_from_env()
+ : _read_localconfig_from_file($include_deprecated);
- # Use the site's URL as the default Canonical URL
- $config->{canonical_urlbase} //= $config->{urlbase};
+ # Use the site's URL as the default Canonical URL
+ $config->{canonical_urlbase} //= $config->{urlbase};
- return $config;
+ return $config;
}
#
@@ -333,96 +263,99 @@ sub read_localconfig {
# Cute, ey?
#
sub update_localconfig {
- my ($params) = @_;
-
- if ($ENV{LOCALCONFIG_ENV}) {
- require Carp;
- Carp::croak("update_localconfig() called with LOCALCONFIG_ENV enabled");
+ my ($params) = @_;
+
+ if ($ENV{LOCALCONFIG_ENV}) {
+ require Carp;
+ Carp::croak("update_localconfig() called with LOCALCONFIG_ENV enabled");
+ }
+
+ my $output = $params->{output} || 0;
+ my $answer = Bugzilla->installation_answers;
+ my $localconfig = read_localconfig('include deprecated');
+
+ my @new_vars;
+ foreach my $var (LOCALCONFIG_VARS) {
+ my $name = $var->{name};
+ my $value = $localconfig->{$name};
+
+ # Regenerate site_wide_secret if it was made by our old, weak
+ # generate_random_password. Previously we used to generate
+ # a 256-character string for site_wide_secret.
+ $value = undef
+ if ($name eq 'site_wide_secret' and defined $value and length($value) == 256);
+
+ if (!defined $value) {
+ push(@new_vars, $name);
+ $var->{default} = &{$var->{default}} if ref($var->{default}) eq 'CODE';
+ if (exists $answer->{$name}) {
+ $localconfig->{$name} = $answer->{$name};
+ }
+ else {
+ $localconfig->{$name} = $var->{default};
+ }
}
+ }
- my $output = $params->{output} || 0;
- my $answer = Bugzilla->installation_answers;
- my $localconfig = read_localconfig('include deprecated');
-
- my @new_vars;
- foreach my $var (LOCALCONFIG_VARS) {
- my $name = $var->{name};
- my $value = $localconfig->{$name};
- # Regenerate site_wide_secret if it was made by our old, weak
- # generate_random_password. Previously we used to generate
- # a 256-character string for site_wide_secret.
- $value = undef if ($name eq 'site_wide_secret' and defined $value
- and length($value) == 256);
-
- if (!defined $value) {
- push(@new_vars, $name);
- $var->{default} = &{$var->{default}} if ref($var->{default}) eq 'CODE';
- if (exists $answer->{$name}) {
- $localconfig->{$name} = $answer->{$name};
- }
- else {
- $localconfig->{$name} = $var->{default};
- }
- }
- }
+ if (!$localconfig->{'interdiffbin'} && $output) {
+ print "\n", install_string('patchutils_missing'), "\n";
+ }
- if (!$localconfig->{'interdiffbin'} && $output) {
- print "\n", install_string('patchutils_missing'), "\n";
- }
+ my @old_vars;
+ foreach my $var (keys %$localconfig) {
+ push(@old_vars, $var) if !grep($_->{name} eq $var, LOCALCONFIG_VARS);
+ }
- my @old_vars;
- foreach my $var (keys %$localconfig) {
- push(@old_vars, $var) if !grep($_->{name} eq $var, LOCALCONFIG_VARS);
- }
+ my $filename = bz_locations->{'localconfig'};
- my $filename = bz_locations->{'localconfig'};
-
- # Ensure output is sorted and deterministic
- local $Data::Dumper::Sortkeys = 1;
-
- # 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";
- }
+ # Ensure output is sorted and deterministic
+ local $Data::Dumper::Sortkeys = 1;
- # Re-write localconfig
- open(my $fh, ">:utf8", $filename) or die "$filename: $!";
- foreach my $var (LOCALCONFIG_VARS) {
- my $name = $var->{name};
- my $desc = install_string("localconfig_$name", { root => ROOT_USER });
- chomp($desc);
- # Make the description into a comment.
- $desc =~ s/^/# /mg;
- print $fh $desc, "\n",
- Data::Dumper->Dump([$localconfig->{$name}],
- ["*$name"]), "\n";
- }
-
- if (@new_vars) {
- my $newstuff = join(', ', @new_vars);
- print "\n";
- print colored(install_string('lc_new_vars', { localconfig => $filename,
- new_vars => wrap_hard($newstuff, 70) }),
- COLOR_ERROR), "\n";
- exit unless $params->{use_defaults};
+ # 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";
}
-
- # Reset the cache for Bugzilla->localconfig so that it will be re-read
- delete Bugzilla->process_cache->{localconfig};
-
- return { old_vars => \@old_vars, new_vars => \@new_vars };
+ close $old_file;
+ my $oldstuff = join(', ', @old_vars);
+ print install_string('lc_old_vars',
+ {localconfig => $filename, old_file => $filename_old, vars => $oldstuff}),
+ "\n";
+ }
+
+ # Re-write localconfig
+ open(my $fh, ">:utf8", $filename) or die "$filename: $!";
+ foreach my $var (LOCALCONFIG_VARS) {
+ my $name = $var->{name};
+ my $desc = install_string("localconfig_$name", {root => ROOT_USER});
+ chomp($desc);
+
+ # Make the description into a comment.
+ $desc =~ s/^/# /mg;
+ print $fh $desc, "\n", Data::Dumper->Dump([$localconfig->{$name}], ["*$name"]),
+ "\n";
+ }
+
+ if (@new_vars) {
+ my $newstuff = join(', ', @new_vars);
+ print "\n";
+ print colored(
+ install_string(
+ 'lc_new_vars', {localconfig => $filename, new_vars => wrap_hard($newstuff, 70)}
+ ),
+ COLOR_ERROR
+ ),
+ "\n";
+ exit unless $params->{use_defaults};
+ }
+
+ # Reset the cache for Bugzilla->localconfig so that it will be re-read
+ delete Bugzilla->process_cache->{localconfig};
+
+ return {old_vars => \@old_vars, new_vars => \@new_vars};
}
1;
diff --git a/Bugzilla/Install/Requirements.pm b/Bugzilla/Install/Requirements.pm
index beed721f3..4b11d83a1 100644
--- a/Bugzilla/Install/Requirements.pm
+++ b/Bugzilla/Install/Requirements.pm
@@ -30,14 +30,14 @@ use parent qw(Exporter);
use autodie;
our @EXPORT = qw(
- FEATURE_FILES
-
- check_cpan_requirements
- check_cpan_feature
- check_all_cpan_features
- check_webdotbase
- check_font_file
- map_files_to_features
+ FEATURE_FILES
+
+ check_cpan_requirements
+ check_cpan_feature
+ check_all_cpan_features
+ check_webdotbase
+ check_font_file
+ map_files_to_features
);
our $checking_for_indent = 0;
@@ -52,11 +52,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
@@ -69,235 +69,255 @@ 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'],
- rest => ['Bugzilla/API/Server.pm', 'rest.cgi', 'Bugzilla/API/*/*.pm',
- 'Bugzilla/API/*/Server.pm', 'Bugzilla/API/*/Resource/*.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'],
- mfa => ['Bugzilla/MFA/*.pm'],
- memcached => ['Bugzilla/Memcache.pm'],
- s3 => ['Bugzilla/S3.pm', 'Bugzilla/S3/Bucket.pm', 'Bugzilla/Attachment/S3.pm']
+ jsonrpc => ['Bugzilla/WebService/Server/JSONRPC.pm', 'jsonrpc.cgi'],
+ xmlrpc => [
+ 'Bugzilla/WebService/Server/XMLRPC.pm', 'xmlrpc.cgi',
+ 'Bugzilla/WebService.pm', 'Bugzilla/WebService/*.pm'
+ ],
+ rest => [
+ 'Bugzilla/API/Server.pm', 'rest.cgi',
+ 'Bugzilla/API/*/*.pm', 'Bugzilla/API/*/Server.pm',
+ 'Bugzilla/API/*/Resource/*.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'],
+ mfa => ['Bugzilla/MFA/*.pm'],
+ memcached => ['Bugzilla/Memcache.pm'],
+ s3 => ['Bugzilla/S3.pm', 'Bugzilla/S3/Bucket.pm', 'Bugzilla/Attachment/S3.pm']
);
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,
- };
- }
+ 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;
- return \%report;
+ $report{$feature->identifier}
+ = {description => $feature->description, result => $result,};
+ }
+
+ 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 = Bugzilla::CPAN->cpan_requirements($prereqs);
- 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 = Bugzilla::CPAN->cpan_requirements($prereqs);
+ 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'};
- my $webdotdir = bz_locations()->{'webdotdir'};
- # Check .htaccess allows access to generated images
- if (-e "$webdotdir/.htaccess") {
- my $htaccess = new IO::File("$webdotdir/.htaccess", 'r')
- || die "$webdotdir/.htaccess: " . $!;
- if (!grep(/ \\\.png\$/, $htaccess->getlines)) {
- print STDERR install_string('webdot_bad_htaccess',
- { dir => $webdotdir }), "\n";
- }
- $htaccess->close;
+ # Check .htaccess allows access to generated images
+ if (-e "$webdotdir/.htaccess") {
+ my $htaccess = new IO::File("$webdotdir/.htaccess", 'r')
+ || die "$webdotdir/.htaccess: " . $!;
+ if (!grep(/ \\\.png\$/, $htaccess->getlines)) {
+ print STDERR install_string('webdot_bad_htaccess', {dir => $webdotdir}), "\n";
}
+ $htaccess->close;
+ }
- return $return;
+ return $return;
}
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;
diff --git a/Bugzilla/Install/Util.pm b/Bugzilla/Install/Util.pm
index b6d28d9c7..a5522e134 100644
--- a/Bugzilla/Install/Util.pm
+++ b/Bugzilla/Install/Util.pm
@@ -27,367 +27,391 @@ use PerlIO;
use base qw(Exporter);
our @EXPORT_OK = qw(
- bin_loc
- get_version_and_os
- extension_code_files
- i_am_persistent
- indicate_progress
- install_string
- include_languages
- success
- template_include_path
- init_console
+ bin_loc
+ get_version_and_os
+ extension_code_files
+ 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];
- if (ON_WINDOWS) {
- require Win32;
- $os_name = Win32::GetOSName();
- }
- # $os_details[3] is the minor version.
- return { bz_ver => BUGZILLA_VERSION,
- perl_ver => sprintf('%vd', $^V),
- os_name => $os_name,
- os_ver => $os_details[3] };
+
+ # Display version information
+ my @os_details = POSIX::uname;
+
+ # 0 is the name of the OS, 2 is the major version,
+ my $os_name = $os_details[0] . ' ' . $os_details[2];
+ if (ON_WINDOWS) {
+ require Win32;
+ $os_name = Win32::GetOSName();
+ }
+
+ # $os_details[3] is the minor version.
+ return {
+ bz_ver => BUGZILLA_VERSION,
+ perl_ver => sprintf('%vd', $^V),
+ os_name => $os_name,
+ os_ver => $os_details[3]
+ };
}
sub _extension_paths {
- my $dir = bz_locations()->{'extensionsdir'};
- my @extension_items = glob("$dir/*");
- my @paths;
- foreach my $item (@extension_items) {
- my $basename = basename($item);
- # Skip CVS directories and any hidden files/dirs.
- next if ($basename eq 'CVS' or $basename =~ /^\./);
- if (-d $item) {
- if (!-e "$item/disabled") {
- push(@paths, $item);
- }
- }
- elsif ($item =~ /\.pm$/i) {
- push(@paths, $item);
- }
- }
- return @paths;
+ my $dir = bz_locations()->{'extensionsdir'};
+ my @extension_items = glob("$dir/*");
+ my @paths;
+ foreach my $item (@extension_items) {
+ my $basename = basename($item);
+
+ # Skip CVS directories and any hidden files/dirs.
+ next if ($basename eq 'CVS' or $basename =~ /^\./);
+ if (-d $item) {
+ if (!-e "$item/disabled") {
+ push(@paths, $item);
+ }
+ }
+ elsif ($item =~ /\.pm$/i) {
+ push(@paths, $item);
+ }
+ }
+ return @paths;
}
sub extension_code_files {
- my ($requirements_only) = @_;
- my @files;
- foreach my $path (_extension_paths()) {
- my @load_files;
- if (-d $path) {
- my $extension_file = "$path/Extension.pm";
- my $config_file = "$path/Config.pm";
- if (-e $extension_file) {
- push(@load_files, $extension_file);
- }
- if (-e $config_file) {
- push(@load_files, $config_file);
- }
-
- # Don't load Extension.pm if we just want Config.pm and
- # we found both.
- if ($requirements_only and scalar(@load_files) == 2) {
- shift(@load_files);
- }
- }
- else {
- push(@load_files, $path);
- }
- next if !scalar(@load_files);
- # We know that these paths are safe, because they came from
- # extensionsdir and we checked them specifically for their format.
- # Also, the only thing we ever do with them is pass them to "require".
- trick_taint($_) foreach @load_files;
- push(@files, \@load_files);
+ my ($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);
+ }
- return (\@files);
+ return (\@files);
}
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 = Bugzilla::CPAN->cpan_meta;
+ my ($feature_name) = @_;
+ eval {
+ my $meta = Bugzilla::CPAN->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 {
- my @template_dirs;
+ my @template_dirs;
- foreach my $path (_extension_paths()) {
- next if !-d $path;
- if ( -d "$path/template") {
- push(@template_dirs, "$path/template");
- }
+ foreach my $path (_extension_paths()) {
+ next if !-d $path;
+ if (-d "$path/template") {
+ push(@template_dirs, "$path/template");
}
+ }
- state $bz_locations = bz_locations();
- push(@template_dirs, $bz_locations->{'templatedir'});
- return \@template_dirs;
+ state $bz_locations = bz_locations();
+ 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;
}
# 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;
}
- @A <=> @B;
+ }
+ @A <=> @B;
}
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;
}
######################
@@ -396,160 +420,169 @@ 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 and eval { require Win32::Console }) {
- # Although setlocale() works on Windows, it doesn't always return
- # the current *console's* encoding. So we use OutputCP here instead,
- # when we can.
- $encoding = Win32::Console::OutputCP();
- }
- else {
- my $locale = setlocale(LC_CTYPE);
- if ($locale =~ /\.([^\.]+)$/) {
- $encoding = $1;
- }
- }
- $encoding = "cp$encoding" if ON_WINDOWS;
- $encoding = Encode::resolve_alias($encoding) if $encoding;
- if ($encoding and $encoding !~ /utf-8/i) {
- binmode STDOUT, ":encoding($encoding)";
- binmode STDERR, ":encoding($encoding)";
- }
- else {
- binmode STDOUT, ':utf8';
- binmode STDERR, ':utf8';
- }
+ # If we've already set an encoding layer on STDOUT, don't
+ # add another one.
+ my @stdout_layers = PerlIO::get_layers(STDOUT);
+ return if grep(/^encoding/, @stdout_layers);
+
+ my $encoding;
+ if (ON_WINDOWS and eval { require Win32::Console }) {
+
+ # Although setlocale() works on Windows, it doesn't always return
+ # the current *console's* encoding. So we use OutputCP here instead,
+ # when we can.
+ $encoding = Win32::Console::OutputCP();
+ }
+ else {
+ my $locale = setlocale(LC_CTYPE);
+ if ($locale =~ /\.([^\.]+)$/) {
+ $encoding = $1;
+ }
+ }
+ $encoding = "cp$encoding" if ON_WINDOWS;
+
+ $encoding = Encode::resolve_alias($encoding) if $encoding;
+ if ($encoding and $encoding !~ /utf-8/i) {
+ binmode STDOUT, ":encoding($encoding)";
+ binmode STDERR, ":encoding($encoding)";
+ }
+ else {
+ binmode STDOUT, ':utf8';
+ binmode STDERR, ':utf8';
+ }
}
sub init_console {
- eval { ON_WINDOWS && require Win32::Console::ANSI; };
- $ENV{'ANSI_COLORS_DISABLED'} = 1 if ($@ || !-t *STDOUT);
- $SIG{__DIE__} = \&_console_die;
- prevent_windows_dialog_boxes();
- set_output_encoding();
+ eval { ON_WINDOWS && require Win32::Console::ANSI; };
+ $ENV{'ANSI_COLORS_DISABLED'} = 1 if ($@ || !-t *STDOUT);
+ $SIG{__DIE__} = \&_console_die;
+ prevent_windows_dialog_boxes();
+ set_output_encoding();
}
sub _console_die {
- my ($message) = @_;
- # $^S means "we are in an eval"
- if ($^S) {
- die $message;
- }
- # Remove newlines from the message before we color it, and then
- # add them back in on display. Otherwise the ANSI escape code
- # for resetting the color comes after the newline, and Perl thinks
- # that it should put "at Bugzilla/Install.pm line 1234" after the
- # message.
- $message =~ s/\n+$//;
- # We put quotes around the message to stringify any object exceptions,
- # like Template::Exception.
- die colored("$message", COLOR_ERROR) . "\n";
+ my ($message) = @_;
+
+ # $^S means "we are in an eval"
+ if ($^S) {
+ die $message;
+ }
+
+ # Remove newlines from the message before we color it, and then
+ # add them back in on display. Otherwise the ANSI escape code
+ # for resetting the color comes after the newline, and Perl thinks
+ # that it should put "at Bugzilla/Install.pm line 1234" after the
+ # message.
+ $message =~ s/\n+$//;
+
+ # We put quotes around the message to stringify any object exceptions,
+ # like Template::Exception.
+ die colored("$message", COLOR_ERROR) . "\n";
}
sub success {
- my ($message) = @_;
- print colored($message, COLOR_SUCCESS), "\n";
+ my ($message) = @_;
+ print colored($message, COLOR_SUCCESS), "\n";
}
sub prevent_windows_dialog_boxes {
- # This code comes from http://bugs.activestate.com/show_bug.cgi?id=82183
- # and prevents Perl modules from popping up dialog boxes, particularly
- # during checksetup (since loading DBD::Oracle during checksetup when
- # Oracle isn't installed causes a scary popup and pauses checksetup).
- #
- # Win32::API ships with ActiveState by default, though there could
- # theoretically be a Windows installation without it, I suppose.
- if (ON_WINDOWS and eval { require Win32::API }) {
- # Call kernel32.SetErrorMode with arguments that mean:
- # "The system does not display the critical-error-handler message box.
- # Instead, the system sends the error to the calling process." and
- # "A child process inherits the error mode of its parent process."
- my $SetErrorMode = Win32::API->new('kernel32', 'SetErrorMode',
- 'I', 'I');
- my $SEM_FAILCRITICALERRORS = 0x0001;
- my $SEM_NOGPFAULTERRORBOX = 0x0002;
- $SetErrorMode->Call($SEM_FAILCRITICALERRORS | $SEM_NOGPFAULTERRORBOX);
- }
+
+ # This code comes from http://bugs.activestate.com/show_bug.cgi?id=82183
+ # and prevents Perl modules from popping up dialog boxes, particularly
+ # during checksetup (since loading DBD::Oracle during checksetup when
+ # Oracle isn't installed causes a scary popup and pauses checksetup).
+ #
+ # Win32::API ships with ActiveState by default, though there could
+ # theoretically be a Windows installation without it, I suppose.
+ if (ON_WINDOWS and eval { require Win32::API }) {
+
+ # Call kernel32.SetErrorMode with arguments that mean:
+ # "The system does not display the critical-error-handler message box.
+ # Instead, the system sends the error to the calling process." and
+ # "A child process inherits the error mode of its parent process."
+ my $SetErrorMode = Win32::API->new('kernel32', 'SetErrorMode', 'I', 'I');
+ my $SEM_FAILCRITICALERRORS = 0x0001;
+ my $SEM_NOGPFAULTERRORBOX = 0x0002;
+ $SetErrorMode->Call($SEM_FAILCRITICALERRORS | $SEM_NOGPFAULTERRORBOX);
+ }
}
# This is like request_cache, but it's used only by installation code
@@ -561,20 +594,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__
diff --git a/Bugzilla/Job/BugMail.pm b/Bugzilla/Job/BugMail.pm
index b4887c470..ebc884a32 100644
--- a/Bugzilla/Job/BugMail.pm
+++ b/Bugzilla/Job/BugMail.pm
@@ -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;
diff --git a/Bugzilla/Job/Mailer.pm b/Bugzilla/Job/Mailer.pm
index a376a9256..d73e4f009 100644
--- a/Bugzilla/Job/Mailer.pm
+++ b/Bugzilla/Job/Mailer.pm
@@ -17,40 +17,42 @@ BEGIN { eval "use base 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;
diff --git a/Bugzilla/JobQueue.pm b/Bugzilla/JobQueue.pm
index a78a4d0ae..cb8a2aa49 100644
--- a/Bugzilla/JobQueue.pm
+++ b/Bugzilla/JobQueue.pm
@@ -23,128 +23,127 @@ use base qw(TheSchwartz);
# 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 => 75;
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;
- }
+ 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};
+ return Bugzilla->request_cache->{job_map};
}
sub new {
- my $class = shift;
-
- if (!Bugzilla->feature('jobqueue')) {
- ThrowCodeError('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->dsn,
- user => $lc->{db_user},
- pass => $lc->{db_pass},
- prefix => 'ts_',
- }],
- driver_cache_expiration => DRIVER_CACHE_TIME,
- );
-
- return $self;
+ my $class = shift;
+
+ if (!Bugzilla->feature('jobqueue')) {
+ ThrowCodeError('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->dsn,
+ user => $lc->{db_user},
+ pass => $lc->{db_pass},
+ prefix => 'ts_',
+ }],
+ driver_cache_expiration => DRIVER_CACHE_TIME,
+ );
+
+ 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;
+ my $self = shift;
+ my $job = shift;
- my $mapped_job = Bugzilla::JobQueue->job_map()->{$job};
- ThrowCodeError('jobqueue_no_job_mapping', { job => $job })
- if !$mapped_job;
- unshift(@_, $mapped_job);
+ my $mapped_job = Bugzilla::JobQueue->job_map()->{$job};
+ ThrowCodeError('jobqueue_no_job_mapping', {job => $job}) if !$mapped_job;
+ unshift(@_, $mapped_job);
- my $retval = $self->SUPER::insert(@_);
- # 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;
+ my $retval = $self->SUPER::insert(@_);
- return $retval;
+ # 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;
}
sub debug {
- my ($self, @args) = @_;
- my $caller_pkg = caller;
- local $Log::Log4perl::caller_depth = $Log::Log4perl::caller_depth + 1;
- my $logger = Log::Log4perl->get_logger($caller_pkg);
- if ($args[0] && $args[0] eq "TheSchwartz::work_once found no jobs") {
- $logger->trace(@args);
- }
- else {
- $logger->info(@args);
- }
+ my ($self, @args) = @_;
+ my $caller_pkg = caller;
+ local $Log::Log4perl::caller_depth = $Log::Log4perl::caller_depth + 1;
+ my $logger = Log::Log4perl->get_logger($caller_pkg);
+ if ($args[0] && $args[0] eq "TheSchwartz::work_once found no jobs") {
+ $logger->trace(@args);
+ }
+ else {
+ $logger->info(@args);
+ }
}
sub work {
- my ($self, $delay) = @_;
- $delay ||= 1;
- my $loop = IO::Async::Loop->new;
- my $timer = IO::Async::Timer::Periodic->new(
- first_interval => 0,
- interval => $delay,
- reschedule => 'drift',
- on_tick => sub { $self->work_once }
- );
- DEBUG("working every $delay seconds");
- $loop->add($timer);
- $timer->start;
- Future->wait_any(map { catch_signal($_) } qw( INT TERM HUP ))->get;
- $timer->stop;
- $loop->remove($timer);
+ my ($self, $delay) = @_;
+ $delay ||= 1;
+ my $loop = IO::Async::Loop->new;
+ my $timer = IO::Async::Timer::Periodic->new(
+ first_interval => 0,
+ interval => $delay,
+ reschedule => 'drift',
+ on_tick => sub { $self->work_once }
+ );
+ DEBUG("working every $delay seconds");
+ $loop->add($timer);
+ $timer->start;
+ Future->wait_any(map { catch_signal($_) } qw( INT TERM HUP ))->get;
+ $timer->stop;
+ $loop->remove($timer);
}
# Clear the request cache at the start of each run.
sub work_once {
- my $self = shift;
- my $val = $self->SUPER::work_once(@_);
- Bugzilla::Hook::process('request_cleanup');
- Bugzilla::Bug->CLEANUP;
- Bugzilla->clear_request_cache();
- return $val;
+ my $self = shift;
+ my $val = $self->SUPER::work_once(@_);
+ Bugzilla::Hook::process('request_cleanup');
+ Bugzilla::Bug->CLEANUP;
+ Bugzilla->clear_request_cache();
+ return $val;
}
# Never process more than MAX_MESSAGES in one batch, to avoid memory
# leak/fragmentation issues.
sub work_until_done {
- my $self = shift;
- my $count = 0;
- while ($count++ < MAX_MESSAGES) {
- $self->work_once or last;
- }
+ my $self = shift;
+ my $count = 0;
+ while ($count++ < MAX_MESSAGES) {
+ $self->work_once or last;
+ }
}
1;
diff --git a/Bugzilla/JobQueue/Runner.pm b/Bugzilla/JobQueue/Runner.pm
index 0177de40a..8d840fc75 100644
--- a/Bugzilla/JobQueue/Runner.pm
+++ b/Bugzilla/JobQueue/Runner.pm
@@ -48,206 +48,205 @@ our $initscript = 'bugzilla-queue';
# only thing it uses from gd_preconfig is the "pidfile"
# config parameter.
sub gd_preconfig {
- my $self = shift;
+ my $self = shift;
- my $pidfile = $self->{gd_args}{pidfile};
- if ( !$pidfile ) {
- $pidfile = catfile(tmpdir(), $self->{gd_progname} . '.pid');
- }
- return ( pidfile => $pidfile );
+ my $pidfile = $self->{gd_args}{pidfile};
+ if (!$pidfile) {
+ $pidfile = catfile(tmpdir(), $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($PROGRAM_NAME);
- }
+ if ($self->{gd_args}{progname}) {
+ $self->{gd_progname} = $self->{gd_args}{progname};
+ }
+ else {
+ $self->{gd_progname} = basename($PROGRAM_NAME);
+ }
- # There are places that Daemon Generic's new() uses $PROGRAM_NAME instead of
- # gd_progname, which it really shouldn't, but this hack fixes it.
- $self->{_original_program_name} = $PROGRAM_NAME;
+ # There are places that Daemon Generic's new() uses $PROGRAM_NAME instead of
+ # gd_progname, which it really shouldn't, but this hack fixes it.
+ $self->{_original_program_name} = $PROGRAM_NAME;
- ## no critic (Variables::RequireLocalizedPunctuationVars)
- $PROGRAM_NAME = $self->{gd_progname};
- ## use critic
+ ## no critic (Variables::RequireLocalizedPunctuationVars)
+ $PROGRAM_NAME = $self->{gd_progname};
+ ## use critic
}
sub gd_postconfig {
- my $self = shift;
+ my $self = shift;
- # See the hack above in gd_getopt. This just reverses it
- # in case anything else needs the accurate $0.
- ## no critic (Variables::RequireLocalizedPunctuationVars)
- $PROGRAM_NAME = delete $self->{_original_program_name};
- ## use critic
+ # See the hack above in gd_getopt. This just reverses it
+ # in case anything else needs the accurate $0.
+ ## no critic (Variables::RequireLocalizedPunctuationVars)
+ $PROGRAM_NAME = delete $self->{_original_program_name};
+ ## use critic
}
sub gd_more_opt {
- my $self = shift;
- return (
- 'pidfile=s' => \$self->{gd_args}{pidfile},
- 'n=s' => \$self->{gd_args}{progname},
- 'jobs|j=i' => \$self->{gd_args}{jobs},
- );
+ my $self = shift;
+ return (
+ 'pidfile=s' => \$self->{gd_args}{pidfile},
+ 'n=s' => \$self->{gd_args}{progname},
+ 'jobs|j=i' => \$self->{gd_args}{jobs},
+ );
}
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 = "scripts/$initscript.rhel";
+ my $dest_file = "$initd/$initscript";
+ my $sysconfig = '/etc/sysconfig';
+ my $config_file = "$sysconfig/$initscript";
- my $source_file = "scripts/$initscript.rhel";
- my $dest_file = "$initd/$initscript";
- my $sysconfig = '/etc/sysconfig';
- my $config_file = "$sysconfig/$initscript";
+ if (!-x $chkconfig || !-d $initd) {
+ return $self->SUPER::gd_can_install(@_);
+ }
- if ( !-x $chkconfig || !-d $initd ) {
- return $self->SUPER::gd_can_install(@_);
+ 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: $!";
}
- 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;
- my $directory = abs_path( dirname( $self->{_original_program_name} ) );
- my $owner_id = ( stat $self->{_original_program_name} )[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;
+ my $directory = abs_path(dirname($self->{_original_program_name}));
+ my $owner_id = (stat $self->{_original_program_name})[4];
+ my $owner = getpwuid $owner_id;
+ print $config_fh <<"END";
#!/bin/sh
BUGZILLA="$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";
}
# override this to use IO::Async.
sub gd_setup_signals {
- my $self = shift;
- my @signals = qw( INT HUP TERM );
- $self->{_signal_future} = Future->wait_any( map { catch_signal( $_, $_ ) } @signals );
+ my $self = shift;
+ my @signals = qw( INT HUP TERM );
+ $self->{_signal_future}
+ = Future->wait_any(map { catch_signal($_, $_) } @signals);
}
sub gd_other_cmd {
- my ($self) = shift;
- if ( $ARGV[0] eq 'once' ) {
- Bugzilla::JobQueue::Worker->run('work_once');
- exit;
- }
+ my ($self) = shift;
+ if ($ARGV[0] eq 'once') {
+ Bugzilla::JobQueue::Worker->run('work_once');
+ exit;
+ }
- $self->SUPER::gd_other_cmd();
+ $self->SUPER::gd_other_cmd();
}
sub gd_quit_event { FATAL('gd_quit_event() should never be called') }
sub gd_reconfig_event { FATAL('gd_reconfig_event() should never be called') }
sub gd_run {
- my $self = shift;
- my $jobs = $self->{gd_args}{jobs} // 1;
- my $signal_f = $self->{_signal_future};
- my $workers_f = fmap_void { $self->run_worker() }
- concurrent => $jobs,
- generate => sub { !$signal_f->is_ready };
-
- # This is so the process shows up in (h)top in a useful way.
- local $PROGRAM_NAME = "$self->{gd_progname} [supervisor]";
- Future->wait_any($signal_f, $workers_f)->get;
- unlink $self->{gd_pidfile};
- exit 0;
+ my $self = shift;
+ my $jobs = $self->{gd_args}{jobs} // 1;
+ my $signal_f = $self->{_signal_future};
+ my $workers_f = fmap_void { $self->run_worker() }
+ concurrent => $jobs,
+ generate => sub { !$signal_f->is_ready };
+
+ # This is so the process shows up in (h)top in a useful way.
+ local $PROGRAM_NAME = "$self->{gd_progname} [supervisor]";
+ Future->wait_any($signal_f, $workers_f)->get;
+ unlink $self->{gd_pidfile};
+ exit 0;
}
# This executes the script "jobqueue-worker.pl"
# $EXECUTABLE_NAME is the name of the perl interpreter.
sub run_worker {
- my ( $self ) = @_;
-
- my $script = catfile( bz_locations->{cgi_path}, 'jobqueue-worker.pl' );
- my @command = ( $EXECUTABLE_NAME, $script);
- if ( $self->{gd_args}{progname} ) {
- push @command, '--name' => "$self->{gd_args}{progname} [worker]";
- }
-
- my $loop = IO::Async::Loop->new;
- my $exit_f = $loop->new_future;
- my $worker = IO::Async::Process->new(
- command => \@command,
- on_finish => on_finish($exit_f),
- on_exception => on_exception( 'jobqueue worker', $exit_f )
- );
- $exit_f->on_cancel(
- sub {
- DEBUG('terminate worker');
- $worker->kill('TERM');
- }
- );
- $loop->add($worker);
- return $exit_f;
+ my ($self) = @_;
+
+ my $script = catfile(bz_locations->{cgi_path}, 'jobqueue-worker.pl');
+ my @command = ($EXECUTABLE_NAME, $script);
+ if ($self->{gd_args}{progname}) {
+ push @command, '--name' => "$self->{gd_args}{progname} [worker]";
+ }
+
+ my $loop = IO::Async::Loop->new;
+ my $exit_f = $loop->new_future;
+ my $worker = IO::Async::Process->new(
+ command => \@command,
+ on_finish => on_finish($exit_f),
+ on_exception => on_exception('jobqueue worker', $exit_f)
+ );
+ $exit_f->on_cancel(sub {
+ DEBUG('terminate worker');
+ $worker->kill('TERM');
+ });
+ $loop->add($worker);
+ return $exit_f;
}
1;
diff --git a/Bugzilla/JobQueue/Worker.pm b/Bugzilla/JobQueue/Worker.pm
index db8ebe35e..b0c0826eb 100644
--- a/Bugzilla/JobQueue/Worker.pm
+++ b/Bugzilla/JobQueue/Worker.pm
@@ -14,17 +14,17 @@ use Bugzilla::Logging;
use Module::Runtime qw(require_module);
sub run {
- my ( $class, $fn ) = @_;
- DEBUG("Starting up for $fn");
- my $jq = Bugzilla->job_queue();
+ my ($class, $fn) = @_;
+ DEBUG("Starting up for $fn");
+ my $jq = Bugzilla->job_queue();
- DEBUG('Loading jobqueue modules');
- foreach my $module ( values %{ Bugzilla::JobQueue->job_map() } ) {
- DEBUG("JobQueue can do $module");
- require_module($module);
- $jq->can_do($module);
- }
- $jq->$fn;
+ DEBUG('Loading jobqueue modules');
+ foreach my $module (values %{Bugzilla::JobQueue->job_map()}) {
+ DEBUG("JobQueue can do $module");
+ require_module($module);
+ $jq->can_do($module);
+ }
+ $jq->$fn;
}
1;
diff --git a/Bugzilla/Keyword.pm b/Bugzilla/Keyword.pm
index 61038f602..35dc2f594 100644
--- a/Bugzilla/Keyword.pm
+++ b/Bugzilla/Keyword.pm
@@ -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) || $self->name ne $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) || $self->name ne $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 }
diff --git a/Bugzilla/Logging.pm b/Bugzilla/Logging.pm
index f334435fc..22c46b31c 100644
--- a/Bugzilla/Logging.pm
+++ b/Bugzilla/Logging.pm
@@ -18,105 +18,109 @@ use English qw(-no_match_vars $PROGRAM_NAME);
use Taint::Util qw(untaint);
sub logfile {
- my ($class, $name) = @_;
+ my ($class, $name) = @_;
- my $file = rel2abs(catfile(bz_locations->{logsdir}, $name));
- untaint($file);
- return $file;
+ my $file = rel2abs(catfile(bz_locations->{logsdir}, $name));
+ untaint($file);
+ return $file;
}
sub fields {
- return Log::Log4perl::MDC->get_context->{fields} //= {};
+ return Log::Log4perl::MDC->get_context->{fields} //= {};
}
BEGIN {
- my $file = $ENV{LOG4PERL_CONFIG_FILE} // 'log4perl-syslog.conf';
- Log::Log4perl::Logger::create_custom_level('NOTICE', 'WARN', 5, 2);
- Log::Log4perl->init(rel2abs($file, bz_locations->{confdir}));
- TRACE("logging enabled in $PROGRAM_NAME");
+ my $file = $ENV{LOG4PERL_CONFIG_FILE} // 'log4perl-syslog.conf';
+ Log::Log4perl::Logger::create_custom_level('NOTICE', 'WARN', 5, 2);
+ Log::Log4perl->init(rel2abs($file, bz_locations->{confdir}));
+ TRACE("logging enabled in $PROGRAM_NAME");
}
# this is copied from Log::Log4perl's :easy handling,
# except we also export NOTICE.
sub import {
- my $caller_pkg = caller;
-
- return 1 if $Log::Log4perl::IMPORT_CALLED{$caller_pkg}++;
-
- # Define default logger object in caller's package
- my $logger = Log::Log4perl->get_logger("$caller_pkg");
-
- # Define DEBUG, INFO, etc. routines in caller's package
- for (qw(TRACE DEBUG INFO NOTICE WARN ERROR FATAL ALWAYS)) {
- my $level = $_;
- $level = 'OFF' if $level eq 'ALWAYS';
- my $lclevel = lc $_;
- Log::Log4perl::easy_closure_create(
- $caller_pkg,
- $_,
- sub {
- Log::Log4perl::Logger::init_warn()
- unless $Log::Log4perl::Logger::INITIALIZED or $Log::Log4perl::Logger::NON_INIT_WARNED;
- $logger->{$level}->( $logger, @_, $level );
- },
- $logger
- );
- }
-
- # Define LOGCROAK, LOGCLUCK, etc. routines in caller's package
- for (qw(LOGCROAK LOGCLUCK LOGCARP LOGCONFESS)) {
- my $method = 'Log::Log4perl::Logger::' . lc $_;
-
- Log::Log4perl::easy_closure_create(
- $caller_pkg,
- $_,
- sub {
- unshift @_, $logger;
- goto &$method;
- },
- $logger
- );
- }
-
- # Define LOGDIE, LOGWARN
- Log::Log4perl::easy_closure_create(
- $caller_pkg,
- 'LOGDIE',
- sub {
- Log::Log4perl::Logger::init_warn()
- unless $Log::Log4perl::Logger::INITIALIZED or $Log::Log4perl::Logger::NON_INIT_WARNED;
- $logger->{FATAL}->( $logger, @_, 'FATAL' );
- $Log::Log4perl::LOGDIE_MESSAGE_ON_STDERR
- ? CORE::die( Log::Log4perl::Logger::callerline( join '', @_ ) )
- : exit $Log::Log4perl::LOGEXIT_CODE;
- },
- $logger
- );
+ my $caller_pkg = caller;
+
+ return 1 if $Log::Log4perl::IMPORT_CALLED{$caller_pkg}++;
+ # Define default logger object in caller's package
+ my $logger = Log::Log4perl->get_logger("$caller_pkg");
+
+ # Define DEBUG, INFO, etc. routines in caller's package
+ for (qw(TRACE DEBUG INFO NOTICE WARN ERROR FATAL ALWAYS)) {
+ my $level = $_;
+ $level = 'OFF' if $level eq 'ALWAYS';
+ my $lclevel = lc $_;
Log::Log4perl::easy_closure_create(
- $caller_pkg,
- 'LOGEXIT',
- sub {
- Log::Log4perl::Logger::init_warn()
- unless $Log::Log4perl::Logger::INITIALIZED or $Log::Log4perl::Logger::NON_INIT_WARNED;
- $logger->{FATAL}->( $logger, @_, 'FATAL' );
- exit $Log::Log4perl::LOGEXIT_CODE;
- },
- $logger
+ $caller_pkg,
+ $_,
+ sub {
+ Log::Log4perl::Logger::init_warn()
+ unless $Log::Log4perl::Logger::INITIALIZED
+ or $Log::Log4perl::Logger::NON_INIT_WARNED;
+ $logger->{$level}->($logger, @_, $level);
+ },
+ $logger
);
+ }
+
+ # Define LOGCROAK, LOGCLUCK, etc. routines in caller's package
+ for (qw(LOGCROAK LOGCLUCK LOGCARP LOGCONFESS)) {
+ my $method = 'Log::Log4perl::Logger::' . lc $_;
Log::Log4perl::easy_closure_create(
- $caller_pkg,
- 'LOGWARN',
- sub {
- Log::Log4perl::Logger::init_warn()
- unless $Log::Log4perl::Logger::INITIALIZED or $Log::Log4perl::Logger::NON_INIT_WARNED;
- $logger->{WARN}->( $logger, @_, 'WARN' );
- CORE::warn( Log::Log4perl::Logger::callerline( join '', @_ ) )
- if $Log::Log4perl::LOGDIE_MESSAGE_ON_STDERR;
- },
- $logger
+ $caller_pkg,
+ $_,
+ sub {
+ unshift @_, $logger;
+ goto &$method;
+ },
+ $logger
);
+ }
+
+ # Define LOGDIE, LOGWARN
+ Log::Log4perl::easy_closure_create(
+ $caller_pkg,
+ 'LOGDIE',
+ sub {
+ Log::Log4perl::Logger::init_warn()
+ unless $Log::Log4perl::Logger::INITIALIZED
+ or $Log::Log4perl::Logger::NON_INIT_WARNED;
+ $logger->{FATAL}->($logger, @_, 'FATAL');
+ $Log::Log4perl::LOGDIE_MESSAGE_ON_STDERR
+ ? CORE::die(Log::Log4perl::Logger::callerline(join '', @_))
+ : exit $Log::Log4perl::LOGEXIT_CODE;
+ },
+ $logger
+ );
+
+ Log::Log4perl::easy_closure_create(
+ $caller_pkg,
+ 'LOGEXIT',
+ sub {
+ Log::Log4perl::Logger::init_warn()
+ unless $Log::Log4perl::Logger::INITIALIZED
+ or $Log::Log4perl::Logger::NON_INIT_WARNED;
+ $logger->{FATAL}->($logger, @_, 'FATAL');
+ exit $Log::Log4perl::LOGEXIT_CODE;
+ },
+ $logger
+ );
+
+ Log::Log4perl::easy_closure_create(
+ $caller_pkg,
+ 'LOGWARN',
+ sub {
+ Log::Log4perl::Logger::init_warn()
+ unless $Log::Log4perl::Logger::INITIALIZED
+ or $Log::Log4perl::Logger::NON_INIT_WARNED;
+ $logger->{WARN}->($logger, @_, 'WARN');
+ CORE::warn(Log::Log4perl::Logger::callerline(join '', @_))
+ if $Log::Log4perl::LOGDIE_MESSAGE_ON_STDERR;
+ },
+ $logger
+ );
}
1;
diff --git a/Bugzilla/MFA.pm b/Bugzilla/MFA.pm
index 43c722364..4ce03817d 100644
--- a/Bugzilla/MFA.pm
+++ b/Bugzilla/MFA.pm
@@ -12,152 +12,156 @@ use strict;
use warnings;
use Bugzilla::RNG qw( irand );
-use Bugzilla::Token qw( issue_short_lived_session_token set_token_extra_data get_token_extra_data delete_token );
+use Bugzilla::Token
+ qw( issue_short_lived_session_token set_token_extra_data get_token_extra_data delete_token );
use Bugzilla::Util qw( trick_taint );
sub new {
- my ($class, $user) = @_;
- return bless({ user => $user }, $class);
+ my ($class, $user) = @_;
+ return bless({user => $user}, $class);
}
sub new_from {
- my ($class, $user, $mfa) = @_;
- $mfa //= '';
- if ($mfa eq 'TOTP') {
- require Bugzilla::MFA::TOTP;
- return Bugzilla::MFA::TOTP->new($user);
- }
- elsif ($mfa eq 'Duo' && Bugzilla->params->{duo_host}) {
- require Bugzilla::MFA::Duo;
- return Bugzilla::MFA::Duo->new($user);
- }
- else {
- require Bugzilla::MFA::Dummy;
- return Bugzilla::MFA::Dummy->new($user);
- }
+ my ($class, $user, $mfa) = @_;
+ $mfa //= '';
+ if ($mfa eq 'TOTP') {
+ require Bugzilla::MFA::TOTP;
+ return Bugzilla::MFA::TOTP->new($user);
+ }
+ elsif ($mfa eq 'Duo' && Bugzilla->params->{duo_host}) {
+ require Bugzilla::MFA::Duo;
+ return Bugzilla::MFA::Duo->new($user);
+ }
+ else {
+ require Bugzilla::MFA::Dummy;
+ return Bugzilla::MFA::Dummy->new($user);
+ }
}
# abstract methods
# called during enrollment
-sub enroll {}
+sub enroll { }
# api call, returns required data to user-prefs enrollment page
-sub enroll_api {}
+sub enroll_api { }
# called after the user has confirmed enrollment
-sub enrolled {}
+sub enrolled { }
# display page with verification prompt
-sub prompt {}
+sub prompt { }
# throws errors if code is invalid
-sub check {}
+sub check { }
# if true verifcation can happen inline (during enrollment/pref changes)
# if false then the mfa provider requires an intermediate verification page
-sub can_verify_inline { 0 }
+sub can_verify_inline {0}
# verification
sub verify_prompt {
- my ($self, $event) = @_;
- my $user = delete $event->{user} // Bugzilla->user;
-
- # generate token and attach mfa data
- my $token = issue_short_lived_session_token('mfa', $user);
- set_token_extra_data($token, $event);
-
- # trigger provider verification
- my $token_field = $event->{postback}->{token_field} // 'mfa_token';
- $event->{postback}->{fields}->{$token_field} = $token;
- $self->prompt($event);
- exit;
+ my ($self, $event) = @_;
+ my $user = delete $event->{user} // Bugzilla->user;
+
+ # generate token and attach mfa data
+ my $token = issue_short_lived_session_token('mfa', $user);
+ set_token_extra_data($token, $event);
+
+ # trigger provider verification
+ my $token_field = $event->{postback}->{token_field} // 'mfa_token';
+ $event->{postback}->{fields}->{$token_field} = $token;
+ $self->prompt($event);
+ exit;
}
sub verify_token {
- my ($self, $token) = @_;
+ my ($self, $token) = @_;
- # check token
- my ($user_id) = Bugzilla::Token::GetTokenData($token);
- my $user = Bugzilla::User->check({ id => $user_id, cache => 1 });
+ # check token
+ my ($user_id) = Bugzilla::Token::GetTokenData($token);
+ my $user = Bugzilla::User->check({id => $user_id, cache => 1});
- # verify mfa
- $self->verify_check(Bugzilla->input_params);
+ # verify mfa
+ $self->verify_check(Bugzilla->input_params);
- # return event data
- my $event = get_token_extra_data($token);
- delete_token($token);
- if (!$event) {
- print Bugzilla->cgi->redirect('index.cgi');
- exit;
- }
- return $event;
+ # return event data
+ my $event = get_token_extra_data($token);
+ delete_token($token);
+ if (!$event) {
+ print Bugzilla->cgi->redirect('index.cgi');
+ exit;
+ }
+ return $event;
}
sub verify_check {
- my ($self, $params) = @_;
- $params->{code} //= '';
-
- # recovery code verification
- if (length($params->{code}) == 9) {
- my $code = $params->{code};
- foreach my $i (1..10) {
- my $key = "recovery.$i";
- if (($self->property_get($key) // '') eq $code) {
- $self->property_delete($key);
- return;
- }
- }
+ my ($self, $params) = @_;
+ $params->{code} //= '';
+
+ # recovery code verification
+ if (length($params->{code}) == 9) {
+ my $code = $params->{code};
+ foreach my $i (1 .. 10) {
+ my $key = "recovery.$i";
+ if (($self->property_get($key) // '') eq $code) {
+ $self->property_delete($key);
+ return;
+ }
}
+ }
- # mfa verification
- $self->check($params);
+ # mfa verification
+ $self->check($params);
}
# methods
sub generate_recovery_codes {
- my ($self) = @_;
-
- my @codes;
- foreach my $i (1..10) {
- # generate 9 digit code
- my $code;
- $code .= irand(10) for 1..9;
- push @codes, $code;
- # store (replacing existing)
- $self->property_set("recovery.$i", $code);
- }
+ my ($self) = @_;
+
+ my @codes;
+ foreach my $i (1 .. 10) {
+
+ # generate 9 digit code
+ my $code;
+ $code .= irand(10) for 1 .. 9;
+ push @codes, $code;
+
+ # store (replacing existing)
+ $self->property_set("recovery.$i", $code);
+ }
- return \@codes;
+ return \@codes;
}
# helpers
sub property_get {
- my ($self, $name) = @_;
- trick_taint($name);
- return scalar Bugzilla->dbh->selectrow_array(
- "SELECT value FROM profile_mfa WHERE user_id = ? AND name = ?",
- undef, $self->{user}->id, $name);
+ my ($self, $name) = @_;
+ trick_taint($name);
+ return
+ scalar Bugzilla->dbh->selectrow_array(
+ "SELECT value FROM profile_mfa WHERE user_id = ? AND name = ?",
+ undef, $self->{user}->id, $name);
}
sub property_set {
- my ($self, $name, $value) = @_;
- trick_taint($name);
- trick_taint($value);
- Bugzilla->dbh->do(
- "INSERT INTO profile_mfa (user_id, name, value) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE value = ?",
- undef, $self->{user}->id, $name, $value, $value);
+ my ($self, $name, $value) = @_;
+ trick_taint($name);
+ trick_taint($value);
+ Bugzilla->dbh->do(
+ "INSERT INTO profile_mfa (user_id, name, value) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE value = ?",
+ undef, $self->{user}->id, $name, $value, $value
+ );
}
sub property_delete {
- my ($self, $name) = @_;
- trick_taint($name);
- Bugzilla->dbh->do(
- "DELETE FROM profile_mfa WHERE user_id = ? AND name = ?",
- undef, $self->{user}->id, $name);
+ my ($self, $name) = @_;
+ trick_taint($name);
+ Bugzilla->dbh->do("DELETE FROM profile_mfa WHERE user_id = ? AND name = ?",
+ undef, $self->{user}->id, $name);
}
1;
diff --git a/Bugzilla/MFA/Dummy.pm b/Bugzilla/MFA/Dummy.pm
index 03fbe76b5..0ba7a79a6 100644
--- a/Bugzilla/MFA/Dummy.pm
+++ b/Bugzilla/MFA/Dummy.pm
@@ -18,12 +18,12 @@ use base 'Bugzilla::MFA';
# it provides no 2fa protection at all, but prevents crashing.
sub prompt {
- my ($self, $vars) = @_;
- my $template = Bugzilla->template;
+ my ($self, $vars) = @_;
+ my $template = Bugzilla->template;
- print Bugzilla->cgi->header();
- $template->process('mfa/dummy/verify.html.tmpl', $vars)
- || ThrowTemplateError($template->error());
+ print Bugzilla->cgi->header();
+ $template->process('mfa/dummy/verify.html.tmpl', $vars)
+ || ThrowTemplateError($template->error());
}
1;
diff --git a/Bugzilla/MFA/Duo.pm b/Bugzilla/MFA/Duo.pm
index 19590944b..6b026f55b 100644
--- a/Bugzilla/MFA/Duo.pm
+++ b/Bugzilla/MFA/Duo.pm
@@ -18,58 +18,57 @@ use Bugzilla::DuoWeb;
use Bugzilla::Error;
sub can_verify_inline {
- return 0;
+ return 0;
}
sub enroll {
- my ($self, $params) = @_;
+ my ($self, $params) = @_;
- # verify that the user is enrolled with duo
- my $client = Bugzilla::DuoAPI->new(
- Bugzilla->params->{duo_ikey},
- Bugzilla->params->{duo_skey},
- Bugzilla->params->{duo_host}
- );
- my $response = $client->json_api_call('POST', '/auth/v2/preauth', { username => $params->{username} });
+ # verify that the user is enrolled with duo
+ my $client = Bugzilla::DuoAPI->new(
+ Bugzilla->params->{duo_ikey},
+ Bugzilla->params->{duo_skey},
+ Bugzilla->params->{duo_host}
+ );
+ my $response = $client->json_api_call('POST', '/auth/v2/preauth',
+ {username => $params->{username}});
- # not enrolled - show a nice error page instead of just throwing
- unless ($response->{result} eq 'auth' || $response->{result} eq 'allow') {
- print Bugzilla->cgi->header();
- my $template = Bugzilla->template;
- $template->process('mfa/duo/not_enrolled.html.tmpl', { email => $params->{username} })
- || ThrowTemplateError($template->error());
- exit;
- }
+ # not enrolled - show a nice error page instead of just throwing
+ unless ($response->{result} eq 'auth' || $response->{result} eq 'allow') {
+ print Bugzilla->cgi->header();
+ my $template = Bugzilla->template;
+ $template->process('mfa/duo/not_enrolled.html.tmpl',
+ {email => $params->{username}})
+ || ThrowTemplateError($template->error());
+ exit;
+ }
- $self->property_set('user', $params->{username});
+ $self->property_set('user', $params->{username});
}
sub prompt {
- my ($self, $vars) = @_;
- my $template = Bugzilla->template;
+ my ($self, $vars) = @_;
+ my $template = Bugzilla->template;
- $vars->{sig_request} = Bugzilla::DuoWeb::sign_request(
- Bugzilla->params->{duo_ikey},
- Bugzilla->params->{duo_skey},
- Bugzilla->params->{duo_akey},
- $self->property_get('user'),
- );
+ $vars->{sig_request} = Bugzilla::DuoWeb::sign_request(
+ Bugzilla->params->{duo_ikey}, Bugzilla->params->{duo_skey},
+ Bugzilla->params->{duo_akey}, $self->property_get('user'),
+ );
- print Bugzilla->cgi->header();
- $template->process('mfa/duo/verify.html.tmpl', $vars)
- || ThrowTemplateError($template->error());
+ print Bugzilla->cgi->header();
+ $template->process('mfa/duo/verify.html.tmpl', $vars)
+ || ThrowTemplateError($template->error());
}
sub check {
- my ($self, $params) = @_;
+ my ($self, $params) = @_;
- return if Bugzilla::DuoWeb::verify_response(
- Bugzilla->params->{duo_ikey},
- Bugzilla->params->{duo_skey},
- Bugzilla->params->{duo_akey},
- $params->{sig_response}
+ return
+ if Bugzilla::DuoWeb::verify_response(
+ Bugzilla->params->{duo_ikey}, Bugzilla->params->{duo_skey},
+ Bugzilla->params->{duo_akey}, $params->{sig_response}
);
- ThrowUserError('mfa_bad_code');
+ ThrowUserError('mfa_bad_code');
}
1;
diff --git a/Bugzilla/MFA/TOTP.pm b/Bugzilla/MFA/TOTP.pm
index 131dea676..2398fcbeb 100644
--- a/Bugzilla/MFA/TOTP.pm
+++ b/Bugzilla/MFA/TOTP.pm
@@ -21,60 +21,61 @@ use GD::Barcode::QRcode;
use MIME::Base64 qw( encode_base64 );
sub can_verify_inline {
- return 1;
+ return 1;
}
sub _auth {
- my ($self) = @_;
- return Auth::GoogleAuth->new({
- secret => $self->property_get('secret') // $self->property_get('secret.temp'),
- issuer => template_var('terms')->{BugzillaTitle},
- key_id => $self->{user}->login,
- });
+ my ($self) = @_;
+ return Auth::GoogleAuth->new({
+ secret => $self->property_get('secret') // $self->property_get('secret.temp'),
+ issuer => template_var('terms')->{BugzillaTitle},
+ key_id => $self->{user}->login,
+ });
}
sub enroll_api {
- my ($self) = @_;
-
- # create a new secret for the user
- # store it in secret.temp to avoid overwriting a valid secret
- $self->property_set('secret.temp', generate_random_password(16));
-
- # build the qr code
- my $auth = $self->_auth();
- my $otpauth = $auth->qr_code(undef, undef, undef, 1);
- my $png = GD::Barcode::QRcode->new($otpauth, { Version => 10, ModuleSize => 3 })->plot()->png();
- return { png => encode_base64($png), secret32 => $auth->secret32 };
+ my ($self) = @_;
+
+ # create a new secret for the user
+ # store it in secret.temp to avoid overwriting a valid secret
+ $self->property_set('secret.temp', generate_random_password(16));
+
+ # build the qr code
+ my $auth = $self->_auth();
+ my $otpauth = $auth->qr_code(undef, undef, undef, 1);
+ my $png = GD::Barcode::QRcode->new($otpauth, {Version => 10, ModuleSize => 3})
+ ->plot()->png();
+ return {png => encode_base64($png), secret32 => $auth->secret32};
}
sub enrolled {
- my ($self) = @_;
+ my ($self) = @_;
- # make the temporary secret permanent
- $self->property_set('secret', $self->property_get('secret.temp'));
- $self->property_delete('secret.temp');
+ # make the temporary secret permanent
+ $self->property_set('secret', $self->property_get('secret.temp'));
+ $self->property_delete('secret.temp');
}
sub prompt {
- my ($self, $vars) = @_;
- my $template = Bugzilla->template;
+ my ($self, $vars) = @_;
+ my $template = Bugzilla->template;
- print Bugzilla->cgi->header();
- $template->process('mfa/totp/verify.html.tmpl', $vars)
- || ThrowTemplateError($template->error());
+ print Bugzilla->cgi->header();
+ $template->process('mfa/totp/verify.html.tmpl', $vars)
+ || ThrowTemplateError($template->error());
}
sub check {
- my ($self, $params) = @_;
- my $code = $params->{code};
- return if $self->_auth()->verify($code, 1);
-
- if ($params->{mfa_action} && $params->{mfa_action} eq 'enable') {
- ThrowUserError('mfa_totp_bad_enrollment_code');
- }
- else {
- ThrowUserError('mfa_bad_code');
- }
+ my ($self, $params) = @_;
+ my $code = $params->{code};
+ return if $self->_auth()->verify($code, 1);
+
+ if ($params->{mfa_action} && $params->{mfa_action} eq 'enable') {
+ ThrowUserError('mfa_totp_bad_enrollment_code');
+ }
+ else {
+ ThrowUserError('mfa_bad_code');
+ }
}
1;
diff --git a/Bugzilla/Mailer.pm b/Bugzilla/Mailer.pm
index c9a458b47..9a2023a8c 100644
--- a/Bugzilla/Mailer.pm
+++ b/Bugzilla/Mailer.pm
@@ -32,259 +32,265 @@ use Try::Tiny;
# deprecated module. We have to set NO_CLUCK = 1 before loading Email::Send
# to disable these warnings.
BEGIN {
- $Return::Value::NO_CLUCK = 1;
+ $Return::Value::NO_CLUCK = 1;
}
use Email::Send;
use Sys::Hostname;
use Bugzilla::Version qw(vers_cmp);
sub MessageToMTA {
- my ($msg, $send_now) = (@_);
- my $method = Bugzilla->get_param_with_override('mail_delivery_method');
- return if $method eq 'None';
-
- if (Bugzilla->get_param_with_override('use_mailer_queue') and !$send_now) {
- Bugzilla->job_queue->insert('send_mail', { msg => $msg });
- return;
+ my ($msg, $send_now) = (@_);
+ my $method = Bugzilla->get_param_with_override('mail_delivery_method');
+ return if $method eq 'None';
+
+ if (Bugzilla->get_param_with_override('use_mailer_queue') and !$send_now) {
+ Bugzilla->job_queue->insert('send_mail', {msg => $msg});
+ return;
+ }
+
+ my $dbh = Bugzilla->dbh;
+
+ my $email;
+ if (ref $msg) {
+ $email = $msg;
+ }
+ else {
+ # RFC 2822 requires us to have CRLF for our line endings and
+ # Email::MIME doesn't do this for us until 1.911. 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
+ # We check for multiple CRs because of this Template-Toolkit bug:
+ # https://rt.cpan.org/Ticket/Display.html?id=43345
+ if (vers_cmp($Email::MIME::VERSION, 1.911) == -1) {
+ $msg =~ s/(?:\015+)?\012/\015\012/msg;
}
- my $dbh = Bugzilla->dbh;
+ $email = Email::MIME->new($msg);
+ }
- my $email;
- if (ref $msg) {
- $email = $msg;
- }
- else {
- # RFC 2822 requires us to have CRLF for our line endings and
- # Email::MIME doesn't do this for us until 1.911. 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
- # We check for multiple CRs because of this Template-Toolkit bug:
- # https://rt.cpan.org/Ticket/Display.html?id=43345
- if (vers_cmp($Email::MIME::VERSION, 1.911) == -1) {
- $msg =~ s/(?:\015+)?\012/\015\012/msg;
- }
-
- $email = Email::MIME->new($msg);
- }
+ # Ensure that we are not sending emails too quickly to recipients.
+ if (Bugzilla->get_param_with_override('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'));
- # Ensure that we are not sending emails too quickly to recipients.
- if (Bugzilla->get_param_with_override('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');
- my $recipient = $email->header('To');
-
- if (EMAIL_LIMIT_PER_MINUTE) {
- my $minute_rate = $dbh->selectrow_array(
- "SELECT COUNT(*)
+ 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;
- }
- }
+ . $dbh->sql_date_math('LOCALTIMESTAMP(0)', '-', '1', 'HOUR'), undef,
+ $recipient
+ );
+ if ($hour_rate >= EMAIL_LIMIT_PER_HOUR) {
+ die EMAIL_LIMIT_EXCEPTION;
+ }
}
+ }
- # We add this header to uniquely identify all email that we
- # send as coming from this Bugzilla installation.
- #
- $email->header_set('X-Bugzilla-URL', Bugzilla->localconfig->{urlbase});
-
- # We add this header to mark the mail as "auto-generated" and
- # thus to hopefully avoid auto replies.
- $email->header_set('Auto-Submitted', 'auto-generated');
-
- # MIME-Version must be set otherwise some mailsystems ignore the charset
- $email->header_set('MIME-Version', '1.0') if !$email->header('MIME-Version');
-
- # Encode the headers correctly in quoted-printable
- foreach my $header ($email->header_names) {
- my @values = $email->header($header);
- # We don't recode headers that happen multiple times.
- next if scalar(@values) > 1;
- if (my $value = $values[0]) {
- if (Bugzilla->params->{'utf8'} && !utf8::is_utf8($value)) {
- utf8::decode($value);
- }
-
- # avoid excessive line wrapping done by Encode.
- local $Encode::Encoding{'MIME-Q'}->{'bpl'} = 998;
-
- my $encoded = encode('MIME-Q', $value);
- $email->header_set($header, $encoded);
- }
- }
+ # We add this header to uniquely identify all email that we
+ # send as coming from this Bugzilla installation.
+ #
+ $email->header_set('X-Bugzilla-URL', Bugzilla->localconfig->{urlbase});
- my $from = $email->header('From');
+ # We add this header to mark the mail as "auto-generated" and
+ # thus to hopefully avoid auto replies.
+ $email->header_set('Auto-Submitted', 'auto-generated');
- my ($hostname, @args);
- my $mailer_class = $method;
- if ($method eq "Sendmail") {
- $mailer_class = 'Bugzilla::Send::Sendmail';
- if (ON_WINDOWS) {
- $Email::Send::Sendmail::SENDMAIL = SENDMAIL_EXE;
- }
- push @args, "-i";
- # We want to make sure that we pass *only* an email address.
- if ($from) {
- my ($email_obj) = Email::Address->parse($from);
- if ($email_obj) {
- my $from_email = $email_obj->address;
- push(@args, "-f$from_email") if $from_email;
- }
- }
- }
- else {
- # Sendmail will automatically append our hostname to the From
- # address, but other mailers won't.
- my $urlbase = Bugzilla->localconfig->{urlbase};
- $urlbase =~ m|//([^:/]+)[:/]?|;
- $hostname = $1;
- $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()));
- }
- }
+ # MIME-Version must be set otherwise some mailsystems ignore the charset
+ $email->header_set('MIME-Version', '1.0') if !$email->header('MIME-Version');
- # For tracking/diagnostic purposes, add our hostname
- my $generated_by = $email->header('X-Generated-By') || '';
- if ($generated_by =~ tr/\/// < 3) {
- $email->header_set('X-Generated-By' => $generated_by . '/' . hostname() . "($$)");
- }
+ # Encode the headers correctly in quoted-printable
+ foreach my $header ($email->header_names) {
+ my @values = $email->header($header);
- if ($method eq "SMTP") {
- push @args, Host => Bugzilla->params->{"smtpserver"},
- username => Bugzilla->params->{"smtp_username"},
- password => Bugzilla->params->{"smtp_password"},
- Hello => $hostname,
- Debug => Bugzilla->params->{'smtp_debug'};
- }
+ # We don't recode headers that happen multiple times.
+ next if scalar(@values) > 1;
+ if (my $value = $values[0]) {
+ if (Bugzilla->params->{'utf8'} && !utf8::is_utf8($value)) {
+ utf8::decode($value);
+ }
- Bugzilla::Hook::process('mailer_before_send',
- { email => $email, mailer_args => \@args });
-
- try {
- my $to = $email->header('to') or die qq{Unable to find "To:" address\n};
- my @recipients = Email::Address->parse($to);
- die qq{Unable to parse "To:" address - $to\n} unless @recipients;
- die qq{Did not expect more than one "To:" address in $to\n} if @recipients > 1;
- my $recipient = $recipients[0];
- my $badhosts = Bugzilla::Bloomfilter->lookup("badhosts");
- if ($badhosts && $badhosts->test($recipient->host)) {
- WARN("Attempted to send email to address in badhosts: $to");
- $email->header_set(to => '');
- }
- elsif ($recipient->host =~ /\.(?:bugs|tld)$/) {
- WARN("Attempted to send email to fake address: $to");
- $email->header_set(to => '');
- }
- } catch {
- ERROR($_);
- };
-
- # Allow for extensions to to drop the bugmail by clearing the 'to' header
- return if $email->header('to') eq '';
-
- $email->walk_parts(sub {
- my ($part) = @_;
- return if $part->parts > 1; # Top-level
- my $content_type = $part->content_type || '';
- $content_type =~ /charset=['"](.+)['"]/;
- # If no charset is defined or is the default us-ascii,
- # then we encode the email to UTF-8 if Bugzilla has utf8 enabled.
- # XXX - This is a hack to workaround bug 723944.
- if (!$1 || $1 eq 'us-ascii') {
- my $body = $part->body;
- if (Bugzilla->params->{'utf8'}) {
- $part->charset_set('UTF-8');
- # encoding_set works only with bytes, not with utf8 strings.
- my $raw = $part->body_raw;
- if (utf8::is_utf8($raw)) {
- utf8::encode($raw);
- $part->body_set($raw);
- }
- }
- $part->encoding_set('quoted-printable') if !is_7bit_clean($body);
- }
- });
-
- if ($method eq "Test") {
- my $filename = bz_locations()->{'datadir'} . '/mailer.testfile';
- open TESTFILE, '>>', $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 both Sendmail and Qmail, so we put it out here.
- local $ENV{PATH} = SENDMAIL_PATH;
- my $mailer = Email::Send->new({ mailer => $mailer_class,
- mailer_args => \@args });
- my $retval = $mailer->send($email);
- ThrowCodeError('mail_send_error', { msg => $retval, mail => $email })
- if !$retval;
- }
+ # avoid excessive line wrapping done by Encode.
+ local $Encode::Encoding{'MIME-Q'}->{'bpl'} = 998;
- # insert into email_rates
- if (Bugzilla->get_param_with_override('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')
- );
+ my $encoded = encode('MIME-Q', $value);
+ $email->header_set($header, $encoded);
}
-}
+ }
-# Builds header suitable for use as a threading marker in email notifications
-sub build_thread_marker {
- my ($bug_id, $user_id, $is_new) = @_;
+ my $from = $email->header('From');
- if (!defined $user_id) {
- $user_id = Bugzilla->user->id;
+ my ($hostname, @args);
+ my $mailer_class = $method;
+ if ($method eq "Sendmail") {
+ $mailer_class = 'Bugzilla::Send::Sendmail';
+ if (ON_WINDOWS) {
+ $Email::Send::Sendmail::SENDMAIL = SENDMAIL_EXE;
}
-
- my $sitespec = '@' . Bugzilla->localconfig->{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 '@'
+ push @args, "-i";
+
+ # We want to make sure that we pass *only* an email address.
+ if ($from) {
+ my ($email_obj) = Email::Address->parse($from);
+ if ($email_obj) {
+ my $from_email = $email_obj->address;
+ push(@args, "-f$from_email") if $from_email;
+ }
}
-
- my $threadingmarker = "References: <bug-$bug_id-$user_id$sitespec>";
- if ($is_new) {
- $threadingmarker .= "\nMessage-ID: <bug-$bug_id-$user_id$sitespec>";
+ }
+ else {
+ # Sendmail will automatically append our hostname to the From
+ # address, but other mailers won't.
+ my $urlbase = Bugzilla->localconfig->{urlbase};
+ $urlbase =~ m|//([^:/]+)[:/]?|;
+ $hostname = $1;
+ $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()));
+ }
+ }
+
+ # For tracking/diagnostic purposes, add our hostname
+ my $generated_by = $email->header('X-Generated-By') || '';
+ if ($generated_by =~ tr/\/// < 3) {
+ $email->header_set(
+ 'X-Generated-By' => $generated_by . '/' . hostname() . "($$)");
+ }
+
+ if ($method eq "SMTP") {
+ push @args,
+ Host => Bugzilla->params->{"smtpserver"},
+ username => Bugzilla->params->{"smtp_username"},
+ password => Bugzilla->params->{"smtp_password"},
+ Hello => $hostname,
+ Debug => Bugzilla->params->{'smtp_debug'};
+ }
+
+ Bugzilla::Hook::process('mailer_before_send',
+ {email => $email, mailer_args => \@args});
+
+ try {
+ my $to = $email->header('to') or die qq{Unable to find "To:" address\n};
+ my @recipients = Email::Address->parse($to);
+ die qq{Unable to parse "To:" address - $to\n} unless @recipients;
+ die qq{Did not expect more than one "To:" address in $to\n} if @recipients > 1;
+ my $recipient = $recipients[0];
+ my $badhosts = Bugzilla::Bloomfilter->lookup("badhosts");
+ if ($badhosts && $badhosts->test($recipient->host)) {
+ WARN("Attempted to send email to address in badhosts: $to");
+ $email->header_set(to => '');
+ }
+ elsif ($recipient->host =~ /\.(?:bugs|tld)$/) {
+ WARN("Attempted to send email to fake address: $to");
+ $email->header_set(to => '');
}
- else {
- my $rand_bits = generate_random_password(10);
- $threadingmarker .= "\nMessage-ID: <bug-$bug_id-$user_id-$rand_bits$sitespec>" .
- "\nIn-Reply-To: <bug-$bug_id-$user_id$sitespec>";
+ }
+ catch {
+ ERROR($_);
+ };
+
+ # Allow for extensions to to drop the bugmail by clearing the 'to' header
+ return if $email->header('to') eq '';
+
+ $email->walk_parts(sub {
+ my ($part) = @_;
+ return if $part->parts > 1; # Top-level
+ my $content_type = $part->content_type || '';
+ $content_type =~ /charset=['"](.+)['"]/;
+
+ # If no charset is defined or is the default us-ascii,
+ # then we encode the email to UTF-8 if Bugzilla has utf8 enabled.
+ # XXX - This is a hack to workaround bug 723944.
+ if (!$1 || $1 eq 'us-ascii') {
+ my $body = $part->body;
+ if (Bugzilla->params->{'utf8'}) {
+ $part->charset_set('UTF-8');
+
+ # encoding_set works only with bytes, not with utf8 strings.
+ my $raw = $part->body_raw;
+ if (utf8::is_utf8($raw)) {
+ utf8::encode($raw);
+ $part->body_set($raw);
+ }
+ }
+ $part->encoding_set('quoted-printable') if !is_7bit_clean($body);
}
+ });
+
+ if ($method eq "Test") {
+ my $filename = bz_locations()->{'datadir'} . '/mailer.testfile';
+ open TESTFILE, '>>', $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 both Sendmail and Qmail, so we put it out here.
+ local $ENV{PATH} = SENDMAIL_PATH;
+ my $mailer = Email::Send->new({mailer => $mailer_class, mailer_args => \@args});
+ my $retval = $mailer->send($email);
+ ThrowCodeError('mail_send_error', {msg => $retval, mail => $email}) if !$retval;
+ }
+
+ # insert into email_rates
+ if (Bugzilla->get_param_with_override('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')
+ );
+ }
+}
- return $threadingmarker;
+# 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->localconfig->{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 = "References: <bug-$bug_id-$user_id$sitespec>";
+ if ($is_new) {
+ $threadingmarker .= "\nMessage-ID: <bug-$bug_id-$user_id$sitespec>";
+ }
+ else {
+ my $rand_bits = generate_random_password(10);
+ $threadingmarker .= "\nMessage-ID: <bug-$bug_id-$user_id-$rand_bits$sitespec>"
+ . "\nIn-Reply-To: <bug-$bug_id-$user_id$sitespec>";
+ }
+
+ return $threadingmarker;
}
1;
diff --git a/Bugzilla/Markdown/GFM.pm b/Bugzilla/Markdown/GFM.pm
index 367dc7a53..437122093 100644
--- a/Bugzilla/Markdown/GFM.pm
+++ b/Bugzilla/Markdown/GFM.pm
@@ -17,55 +17,55 @@ use Bugzilla::Markdown::GFM::Node;
our @EXPORT_OK = qw(cmark_markdown_to_html);
my %OPTIONS = (
- default => 0,
- sourcepos => ( 1 << 1 ),
- hardbreaks => ( 1 << 2 ),
- safe => ( 1 << 3 ),
- nobreaks => ( 1 << 4 ),
- normalize => ( 1 << 8 ),
- validate_utf8 => ( 1 << 9 ),
- smart => ( 1 << 10 ),
- github_pre_lang => ( 1 << 11 ),
- liberal_html_tag => ( 1 << 12 ),
- footnotes => ( 1 << 13 ),
- strikethrough_double_tilde => ( 1 << 14 ),
- table_prefer_style_attributes => ( 1 << 15 ),
+ default => 0,
+ sourcepos => (1 << 1),
+ hardbreaks => (1 << 2),
+ safe => (1 << 3),
+ nobreaks => (1 << 4),
+ normalize => (1 << 8),
+ validate_utf8 => (1 << 9),
+ smart => (1 << 10),
+ github_pre_lang => (1 << 11),
+ liberal_html_tag => (1 << 12),
+ footnotes => (1 << 13),
+ strikethrough_double_tilde => (1 << 14),
+ table_prefer_style_attributes => (1 << 15),
);
my $FFI = FFI::Platypus->new(
- lib => [grep { not -l $_ } Alien::libcmark_gfm->dynamic_libs],
-);
+ lib => [grep { not -l $_ } Alien::libcmark_gfm->dynamic_libs],);
$FFI->custom_type(
- markdown_options_t => {
- native_type => 'int',
- native_to_perl => sub {
- my ($options) = @_;
- my $result = {};
- foreach my $key (keys %OPTIONS) {
- $result->{$key} = ($options & $OPTIONS{$key}) != 0;
- }
- return $result;
- },
- perl_to_native => sub {
- my ($options) = @_;
- my $result = 0;
- foreach my $key (keys %OPTIONS) {
- if ($options->{$key}) {
- $result |= $OPTIONS{$key};
- }
- }
- return $result;
+ markdown_options_t => {
+ native_type => 'int',
+ native_to_perl => sub {
+ my ($options) = @_;
+ my $result = {};
+ foreach my $key (keys %OPTIONS) {
+ $result->{$key} = ($options & $OPTIONS{$key}) != 0;
+ }
+ return $result;
+ },
+ perl_to_native => sub {
+ my ($options) = @_;
+ my $result = 0;
+ foreach my $key (keys %OPTIONS) {
+ if ($options->{$key}) {
+ $result |= $OPTIONS{$key};
}
+ }
+ return $result;
}
+ }
);
-$FFI->attach(cmark_markdown_to_html => ['opaque', 'int', 'markdown_options_t'] => 'string',
- sub {
- my $c_func = shift;
- my($markdown, $markdown_length) = scalar_to_buffer $_[0];
- return $c_func->($markdown, $markdown_length, $_[1]);
- }
+$FFI->attach(
+ cmark_markdown_to_html => ['opaque', 'int', 'markdown_options_t'] => 'string',
+ sub {
+ my $c_func = shift;
+ my ($markdown, $markdown_length) = scalar_to_buffer $_[0];
+ return $c_func->($markdown, $markdown_length, $_[1]);
+ }
);
# This has to happen after something from the main lib is loaded
diff --git a/Bugzilla/Markdown/GFM/Node.pm b/Bugzilla/Markdown/GFM/Node.pm
index da5af1a68..934cb4055 100644
--- a/Bugzilla/Markdown/GFM/Node.pm
+++ b/Bugzilla/Markdown/GFM/Node.pm
@@ -5,27 +5,25 @@ use strict;
use warnings;
sub SETUP {
- my ($class, $FFI) = @_;
+ my ($class, $FFI) = @_;
- $FFI->custom_type(
- markdown_node_t => {
- native_type => 'opaque',
- native_to_perl => sub {
- bless \$_[0], $class if $_[0];
- },
- perl_to_native => sub { ${ $_[0] } },
- }
- );
+ $FFI->custom_type(
+ markdown_node_t => {
+ native_type => 'opaque',
+ native_to_perl => sub {
+ bless \$_[0], $class if $_[0];
+ },
+ perl_to_native => sub { ${$_[0]} },
+ }
+ );
- $FFI->attach(
- [ cmark_node_free => 'DESTROY' ],
- [ 'markdown_node_t' ] => 'void'
- );
+ $FFI->attach([cmark_node_free => 'DESTROY'], ['markdown_node_t'] => 'void');
- $FFI->attach(
- [ cmark_render_html => 'render_html' ],
- [ 'markdown_node_t', 'markdown_options_t', 'markdown_syntax_extension_list_t'] => 'string',
- );
+ $FFI->attach(
+ [cmark_render_html => 'render_html'],
+ ['markdown_node_t', 'markdown_options_t',
+ 'markdown_syntax_extension_list_t'] => 'string',
+ );
}
1;
diff --git a/Bugzilla/Markdown/GFM/Parser.pm b/Bugzilla/Markdown/GFM/Parser.pm
index 5307b49c1..7e16b2b31 100644
--- a/Bugzilla/Markdown/GFM/Parser.pm
+++ b/Bugzilla/Markdown/GFM/Parser.pm
@@ -7,79 +7,74 @@ use warnings;
use FFI::Platypus::Buffer qw( scalar_to_buffer buffer_to_scalar );
sub new {
- my ($class, $options) = @_;
- my $extensions = delete $options->{extensions} // [];
- my $parser = $class->_new($options);
- $parser->{_options} = $options;
-
- eval {
- foreach my $name (@$extensions) {
- my $extension = Bugzilla::Markdown::GFM::SyntaxExtension->find($name)
- or die "unknown extension: $name";
- $parser->attach_syntax_extension($extension);
- }
- };
-
- return $parser;
+ my ($class, $options) = @_;
+ my $extensions = delete $options->{extensions} // [];
+ my $parser = $class->_new($options);
+ $parser->{_options} = $options;
+
+ eval {
+ foreach my $name (@$extensions) {
+ my $extension = Bugzilla::Markdown::GFM::SyntaxExtension->find($name)
+ or die "unknown extension: $name";
+ $parser->attach_syntax_extension($extension);
+ }
+ };
+
+ return $parser;
}
sub render_html {
- my ($self, $markdown) = @_;
- $self->feed($markdown);
- my $node = $self->finish;
- return $node->render_html($self->{_options}, $self->get_syntax_extensions);
+ my ($self, $markdown) = @_;
+ $self->feed($markdown);
+ my $node = $self->finish;
+ return $node->render_html($self->{_options}, $self->get_syntax_extensions);
}
sub SETUP {
- my ($class, $FFI) = @_;
-
- $FFI->custom_type(
- markdown_parser_t => {
- native_type => 'opaque',
- native_to_perl => sub {
- bless { _pointer => $_[0] }, $class;
- },
- perl_to_native => sub { $_[0]->{_pointer} },
- }
- );
-
- $FFI->attach(
- [ cmark_parser_new => '_new' ],
- [ 'markdown_options_t' ] => 'markdown_parser_t',
- sub {
- my $c_func = shift;
- return $c_func->($_[1]);
- }
- );
-
- $FFI->attach(
- [ cmark_parser_free => 'DESTROY' ],
- [ 'markdown_parser_t' ] => 'void'
- );
-
- $FFI->attach(
- [ cmark_parser_feed => 'feed'],
- ['markdown_parser_t', 'opaque', 'int'] => 'void',
- sub {
- my $c_func = shift;
- $c_func->($_[0], scalar_to_buffer $_[1]);
- }
- );
-
- $FFI->attach(
- [ cmark_parser_finish => 'finish' ],
- [ 'markdown_parser_t' ] => 'markdown_node_t',
- );
-
- $FFI->attach(
- [ cmark_parser_attach_syntax_extension => 'attach_syntax_extension' ],
- [ 'markdown_parser_t', 'markdown_syntax_extension_t' ] => 'void',
- );
-
- $FFI->attach(
- [ cmark_parser_get_syntax_extensions => 'get_syntax_extensions' ],
- [ 'markdown_parser_t' ] => 'markdown_syntax_extension_list_t',
- );
+ my ($class, $FFI) = @_;
+
+ $FFI->custom_type(
+ markdown_parser_t => {
+ native_type => 'opaque',
+ native_to_perl => sub {
+ bless {_pointer => $_[0]}, $class;
+ },
+ perl_to_native => sub { $_[0]->{_pointer} },
+ }
+ );
+
+ $FFI->attach(
+ [cmark_parser_new => '_new'],
+ ['markdown_options_t'] => 'markdown_parser_t',
+ sub {
+ my $c_func = shift;
+ return $c_func->($_[1]);
+ }
+ );
+
+ $FFI->attach([cmark_parser_free => 'DESTROY'], ['markdown_parser_t'] => 'void');
+
+ $FFI->attach(
+ [cmark_parser_feed => 'feed'],
+ ['markdown_parser_t', 'opaque', 'int'] => 'void',
+ sub {
+ my $c_func = shift;
+ $c_func->($_[0], scalar_to_buffer $_[1]);
+ }
+ );
+
+ $FFI->attach([cmark_parser_finish => 'finish'],
+ ['markdown_parser_t'] => 'markdown_node_t',);
+
+ $FFI->attach(
+ [cmark_parser_attach_syntax_extension => 'attach_syntax_extension'],
+ ['markdown_parser_t', 'markdown_syntax_extension_t'] => 'void',
+ );
+
+ $FFI->attach(
+ [cmark_parser_get_syntax_extensions => 'get_syntax_extensions'],
+ ['markdown_parser_t'] => 'markdown_syntax_extension_list_t',
+ );
}
1;
diff --git a/Bugzilla/Markdown/GFM/SyntaxExtension.pm b/Bugzilla/Markdown/GFM/SyntaxExtension.pm
index 56efa177a..213980485 100644
--- a/Bugzilla/Markdown/GFM/SyntaxExtension.pm
+++ b/Bugzilla/Markdown/GFM/SyntaxExtension.pm
@@ -5,25 +5,25 @@ use strict;
use warnings;
sub SETUP {
- my ($class, $FFI) = @_;
+ my ($class, $FFI) = @_;
- $FFI->custom_type(
- markdown_syntax_extension_t => {
- native_type => 'opaque',
- native_to_perl => sub {
- bless \$_[0], $class if $_[0];
- },
- perl_to_native => sub { $_[0] ? ${ $_[0] } : 0 },
- }
- );
- $FFI->attach(
- [ cmark_find_syntax_extension => 'find' ],
- [ 'string' ] => 'markdown_syntax_extension_t',
- sub {
- my $c_func = shift;
- return $c_func->($_[1]);
- }
- );
+ $FFI->custom_type(
+ markdown_syntax_extension_t => {
+ native_type => 'opaque',
+ native_to_perl => sub {
+ bless \$_[0], $class if $_[0];
+ },
+ perl_to_native => sub { $_[0] ? ${$_[0]} : 0 },
+ }
+ );
+ $FFI->attach(
+ [cmark_find_syntax_extension => 'find'],
+ ['string'] => 'markdown_syntax_extension_t',
+ sub {
+ my $c_func = shift;
+ return $c_func->($_[1]);
+ }
+ );
}
1;
diff --git a/Bugzilla/Markdown/GFM/SyntaxExtensionList.pm b/Bugzilla/Markdown/GFM/SyntaxExtensionList.pm
index 06a9798c2..963dec26d 100644
--- a/Bugzilla/Markdown/GFM/SyntaxExtensionList.pm
+++ b/Bugzilla/Markdown/GFM/SyntaxExtensionList.pm
@@ -5,17 +5,17 @@ use strict;
use warnings;
sub SETUP {
- my ($class, $FFI) = @_;
+ my ($class, $FFI) = @_;
- $FFI->custom_type(
- markdown_syntax_extension_list_t => {
- native_type => 'opaque',
- native_to_perl => sub {
- bless \$_[0], $class if $_[0];
- },
- perl_to_native => sub { $_[0] ? ${ $_[0] } : 0 },
- }
- );
+ $FFI->custom_type(
+ markdown_syntax_extension_list_t => {
+ native_type => 'opaque',
+ native_to_perl => sub {
+ bless \$_[0], $class if $_[0];
+ },
+ perl_to_native => sub { $_[0] ? ${$_[0]} : 0 },
+ }
+ );
}
1;
diff --git a/Bugzilla/Memcached.pm b/Bugzilla/Memcached.pm
index 6bbef080a..eda30de23 100644
--- a/Bugzilla/Memcached.pm
+++ b/Bugzilla/Memcached.pm
@@ -22,336 +22,335 @@ use Encode;
use Sys::Syslog qw(:DEFAULT);
# memcached keys have a maximum length of 250 bytes
-use constant MAX_KEY_LENGTH => 250;
+use constant MAX_KEY_LENGTH => 250;
use constant RATE_LIMIT_PREFIX => "rate:";
*new = \&_new;
sub _new {
- my $invocant = shift;
- my $class = ref($invocant) || $invocant;
- my $self = {};
-
- # always return an object to simplify calling code when memcached is
- # disabled.
- my $servers = Bugzilla->localconfig->{memcached_servers};
- if (Bugzilla->feature('memcached') && $servers) {
- $self->{namespace} = Bugzilla->localconfig->{memcached_namespace};
- TRACE("connecting servers: $servers, namespace: $self->{namespace}");
- $self->{memcached} = Cache::Memcached::Fast->new(
- {
- servers => [ _parse_memcached_server_list($servers) ],
- namespace => $self->{namespace},
- max_size => 1024 * 1024 * 4,
- max_failures => 1,
- failure_timeout => 60,
- io_timeout => 0.2,
- connect_timeout => 0.2,
- }
- );
- my $versions = $self->{memcached}->server_versions;
- if (keys %$versions) {
- # this is needed to ensure forked processes don't start out with a connected memcached socket.
- $self->{memcached}->disconnect_all;
- }
- else {
- WARN("No memcached servers");
- }
+ my $invocant = shift;
+ my $class = ref($invocant) || $invocant;
+ my $self = {};
+
+ # always return an object to simplify calling code when memcached is
+ # disabled.
+ my $servers = Bugzilla->localconfig->{memcached_servers};
+ if (Bugzilla->feature('memcached') && $servers) {
+ $self->{namespace} = Bugzilla->localconfig->{memcached_namespace};
+ TRACE("connecting servers: $servers, namespace: $self->{namespace}");
+ $self->{memcached} = Cache::Memcached::Fast->new({
+ servers => [_parse_memcached_server_list($servers)],
+ namespace => $self->{namespace},
+ max_size => 1024 * 1024 * 4,
+ max_failures => 1,
+ failure_timeout => 60,
+ io_timeout => 0.2,
+ connect_timeout => 0.2,
+ });
+ my $versions = $self->{memcached}->server_versions;
+ if (keys %$versions) {
+
+# this is needed to ensure forked processes don't start out with a connected memcached socket.
+ $self->{memcached}->disconnect_all;
}
else {
- TRACE("memcached feature is not enabled");
+ WARN("No memcached servers");
}
- return bless($self, $class);
+ }
+ else {
+ TRACE("memcached feature is not enabled");
+ }
+ return bless($self, $class);
}
sub _parse_memcached_server_list {
- my ($server_list) = @_;
- my @servers = split(/[, ]+/, trim($server_list));
+ my ($server_list) = @_;
+ my @servers = split(/[, ]+/, trim($server_list));
- return map { /:[0-9]+$/s ? $_ : "$_:11211" } @servers;
+ return map { /:[0-9]+$/s ? $_ : "$_:11211" } @servers;
}
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});
- }
-
- # { 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);
- }
+ 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);
}
+ }
- 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 set_bloomfilter {
- my ($self, $args) = @_;
- return unless $self->{memcached};
- if (exists $args->{name}) {
- return $self->_set($self->_bloomfilter_prefix . '.' . $args->{name}, $args->{filter});
- }
- else {
- ThrowCodeError('params_required', { function => "Bugzilla::Memcached::set_bloomfilter",
- params => [ 'name' ] });
- }
+ my ($self, $args) = @_;
+ return unless $self->{memcached};
+ if (exists $args->{name}) {
+ return $self->_set($self->_bloomfilter_prefix . '.' . $args->{name},
+ $args->{filter});
+ }
+ else {
+ ThrowCodeError('params_required',
+ {function => "Bugzilla::Memcached::set_bloomfilter", params => ['name']});
+ }
}
sub get_bloomfilter {
- my ($self, $args) = @_;
- return unless $self->{memcached};
- if (exists $args->{name}) {
- return $self->_get($self->_bloomfilter_prefix . '.' . $args->{name});
- }
- else {
- ThrowCodeError('params_required', { function => "Bugzilla::Memcached::set_bloomfilter",
- params => [ 'name' ] });
- }
+ my ($self, $args) = @_;
+ return unless $self->{memcached};
+ if (exists $args->{name}) {
+ return $self->_get($self->_bloomfilter_prefix . '.' . $args->{name});
+ }
+ else {
+ ThrowCodeError('params_required',
+ {function => "Bugzilla::Memcached::set_bloomfilter", params => ['name']});
+ }
}
sub clear_bloomfilter {
- my ($self, $args) = @_;
- return unless $self->{memcached};
- if ($args && exists $args->{name}) {
- $self->_delete($self->_config_prefix . '.' . $args->{name});
- }
- else {
- $self->_inc_prefix("bloomfilter");
- }
+ my ($self, $args) = @_;
+ return unless $self->{memcached};
+ if ($args && exists $args->{name}) {
+ $self->_delete($self->_config_prefix . '.' . $args->{name});
+ }
+ else {
+ $self->_inc_prefix("bloomfilter");
+ }
}
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 should_rate_limit {
- my ($self, $name, $rate_max, $rate_seconds, $tries) = @_;
- my $prefix = RATE_LIMIT_PREFIX . $name . ':';
- my $memcached = $self->{memcached};
-
- return 0 unless $name;
- return 0 unless $memcached;
-
- $tries //= 4;
-
- for my $try (1 .. $tries) {
- my $now = time;
- my ($key, @keys) = map { $prefix . ( $now - $_ ) } 0 .. $rate_seconds;
- $memcached->add($key, 0, $rate_seconds+1);
- my $tokens = $memcached->get_multi(@keys);
- my $cas = $memcached->gets($key);
- $tokens->{$key} = $cas->[1]++;
- return 1 if sum(values %$tokens) >= $rate_max;
- return 0 if $memcached->cas($key, @$cas, $rate_seconds+1);
- WARN("retry for $prefix (try $try of $tries)");
- }
- return 0;
+ my ($self, $name, $rate_max, $rate_seconds, $tries) = @_;
+ my $prefix = RATE_LIMIT_PREFIX . $name . ':';
+ my $memcached = $self->{memcached};
+
+ return 0 unless $name;
+ return 0 unless $memcached;
+
+ $tries //= 4;
+
+ for my $try (1 .. $tries) {
+ my $now = time;
+ my ($key, @keys) = map { $prefix . ($now - $_) } 0 .. $rate_seconds;
+ $memcached->add($key, 0, $rate_seconds + 1);
+ my $tokens = $memcached->get_multi(@keys);
+ my $cas = $memcached->gets($key);
+ $tokens->{$key} = $cas->[1]++;
+ return 1 if sum(values %$tokens) >= $rate_max;
+ return 0 if $memcached->cas($key, @$cas, $rate_seconds + 1);
+ WARN("retry for $prefix (try $try of $tries)");
+ }
+ return 0;
}
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;
- }
- return $request_cache->{$request_cache_key};
-}
+ my ($self, $name) = @_;
-sub _inc_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};
- if (!$memcached->incr($name, 1)) {
- $memcached->add($name, time());
+ 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;
+ }
}
- delete Bugzilla->request_cache->{"memcached_prefix_$name"};
+ $request_cache->{$request_cache_key} = $prefix;
+ }
+ return $request_cache->{$request_cache_key};
+}
- # BMO - log that we've wiped the cache
- TRACE("$name cache cleared");
+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"};
+
+ # BMO - log that we've wiped the cache
+ TRACE("$name cache cleared");
}
sub _global_prefix {
- return $_[0]->_prefix("global");
+ return $_[0]->_prefix("global");
}
sub _config_prefix {
- return $_[0]->_prefix("config");
+ return $_[0]->_prefix("config");
}
sub _bloomfilter_prefix {
- return $_[0]->_prefix("bloomfilter");
+ return $_[0]->_prefix("bloomfilter");
}
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"});
+ }
- my $enc_key = $self->_encode_key($key)
- or return;
- TRACE("set $enc_key");
- return $self->{memcached}->set($enc_key, $value);
+ my $enc_key = $self->_encode_key($key) or return;
+ TRACE("set $enc_key");
+ return $self->{memcached}->set($enc_key, $value);
}
sub _get {
- my ($self, $key) = @_;
+ my ($self, $key) = @_;
- my $enc_key = $self->_encode_key($key)
- or return;
- my $val = $self->{memcached}->get($enc_key);
- TRACE("get $enc_key: " . (defined $val ? "HIT" : "MISS"));
- return $val;
+ my $enc_key = $self->_encode_key($key) or return;
+ my $val = $self->{memcached}->get($enc_key);
+ TRACE("get $enc_key: " . (defined $val ? "HIT" : "MISS"));
+ return $val;
}
sub _delete {
- my ($self, $key) = @_;
- $key = $self->_encode_key($key)
- or return;
- return $self->{memcached}->delete($key);
+ my ($self, $key) = @_;
+ $key = $self->_encode_key($key) or return;
+ return $self->{memcached}->delete($key);
}
1;
diff --git a/Bugzilla/Migrate.pm b/Bugzilla/Migrate.pm
index 925a01355..74bce149f 100644
--- a/Bugzilla/Migrate.pm
+++ b/Bugzilla/Migrate.pm
@@ -20,7 +20,7 @@ use Bugzilla::Install::Requirements ();
use Bugzilla::Install::Util qw(indicate_progress);
use Bugzilla::Product;
use Bugzilla::Util qw(get_text trim generate_random_password);
-use Bugzilla::User ();
+use Bugzilla::User ();
use Bugzilla::Status ();
use Bugzilla::Version;
@@ -37,10 +37,10 @@ use constant REQUIRED_MODULES => [];
use constant NON_COMMENT_FIELDS => ();
use constant CONFIG_VARS => (
- {
- name => 'translate_fields',
- default => {},
- desc => <<'END',
+ {
+ name => 'translate_fields',
+ default => {},
+ desc => <<'END',
# This maps field names in your bug-tracker to Bugzilla field names. If a field
# has the same name in your bug-tracker and Bugzilla (case-insensitively), it
# doesn't need a mapping here. If a field isn't listed here and doesn't have
@@ -64,11 +64,11 @@ use constant CONFIG_VARS => (
# variable by default, then that field will be automatically created by
# the migrator and you don't have to worry about it.
END
- },
- {
- name => 'translate_values',
- default => {},
- desc => <<'END',
+ },
+ {
+ name => 'translate_values',
+ default => {},
+ desc => <<'END',
# This configuration variable allows you to say that a particular field
# value in your current bug-tracker should be translated to a different
# value when it's imported into Bugzilla.
@@ -108,22 +108,22 @@ END
#
# Values that don't get translated will be imported as-is.
END
- },
- {
- name => 'starting_bug_id',
- default => 0,
- desc => <<'END',
+ },
+ {
+ name => 'starting_bug_id',
+ default => 0,
+ desc => <<'END',
# What bug ID do you want the first imported bug to get? If you set this to
# 0, then the imported bug ids will just start right after the current
# bug ids. If you use this configuration variable, you must make sure that
# nobody else is using your Bugzilla while you run the migration, or a new
# bug filed by a user might take this ID instead.
END
- },
- {
- name => 'timezone',
- default => 'local',
- desc => <<'END',
+ },
+ {
+ name => 'timezone',
+ default => 'local',
+ desc => <<'END',
# If migrate.pl comes across any dates without timezones, while doing the
# migration, what timezone should we assume those dates are in?
# The best format for this variable is something like "America/Los Angeles".
@@ -133,7 +133,7 @@ END
# The special value "local" means "use the same timezone as the system I
# am running this script on now".
END
- },
+ },
);
use constant USER_FIELDS => qw(user assigned_to qa_contact reporter cc);
@@ -143,42 +143,44 @@ 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();
-
- # 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();
+
+ # 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();
+ }
}
################
@@ -186,24 +188,23 @@ sub do_migration {
################
sub new {
- my ($class) = @_;
- my $self = { };
- bless $self, $class;
- return $self;
+ my ($class) = @_;
+ my $self = {};
+ bless $self, $class;
+ return $self;
}
sub load {
- my ($class, $from) = @_;
- my $libdir = bz_locations()->{libpath};
- my @migration_modules = glob("$libdir/Bugzilla/Migrate/*");
- my ($module) = grep { basename($_) =~ /^\Q$from\E\.pm$/i }
- @migration_modules;
- if (!$module) {
- ThrowUserError('migrate_from_invalid', { from => $from });
- }
- require $module;
- my $canonical_name = _canonical_name($module);
- return "Bugzilla::Migrate::$canonical_name"->new;
+ my ($class, $from) = @_;
+ my $libdir = bz_locations()->{libpath};
+ my @migration_modules = glob("$libdir/Bugzilla/Migrate/*");
+ my ($module) = grep { basename($_) =~ /^\Q$from\E\.pm$/i } @migration_modules;
+ if (!$module) {
+ ThrowUserError('migrate_from_invalid', {from => $from});
+ }
+ require $module;
+ my $canonical_name = _canonical_name($module);
+ return "Bugzilla::Migrate::$canonical_name"->new;
}
#############
@@ -211,67 +212,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}) {
- print get_text('migrate_reading_users'), "\n";
- $self->{users} = $self->_read_users();
- }
- return $self->{users};
+ my $self = shift;
+ if (!exists $self->{users}) {
+ print get_text('migrate_reading_users'), "\n";
+ $self->{users} = $self->_read_users();
+ }
+ return $self->{users};
}
sub products {
- my $self = shift;
- if (!exists $self->{products}) {
- print get_text('migrate_reading_products'), "\n";
- $self->{products} = $self->_read_products();
- }
- return $self->{products};
+ my $self = shift;
+ if (!exists $self->{products}) {
+ print get_text('migrate_reading_products'), "\n";
+ $self->{products} = $self->_read_products();
+ }
+ return $self->{products};
}
sub bugs {
- my $self = shift;
- if (!exists $self->{bugs}) {
- print get_text('migrate_reading_bugs'), "\n";
- $self->{bugs} = $self->_read_bugs();
- }
- return $self->{bugs};
+ my $self = shift;
+ if (!exists $self->{bugs}) {
+ print get_text('migrate_reading_bugs'), "\n";
+ $self->{bugs} = $self->_read_bugs();
+ }
+ return $self->{bugs};
}
###########
@@ -279,49 +280,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;
}
###################
@@ -329,160 +330,167 @@ sub reset_serial_values {
###################
sub translate_all_bugs {
- my ($self, $bugs) = @_;
- print get_text('migrate_translating_bugs'), "\n";
- # 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) = @_;
+ print get_text('migrate_translating_bugs'), "\n";
+
+ # 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);
+ 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);
+ }
- return wantarray ? (\%bug, \%other_fields) : \%bug;
+ $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 = Bugzilla->local_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 = Bugzilla->local_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) = @_;
+ my ($self, $field, $value) = @_;
- if (!defined $value) {
- warn("Got undefined value for $field\n");
- $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 (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);
- }
- }
+ if (defined $self->map_value($field, $value)) {
+ return $self->map_value($field, $value);
+ }
- my $field_obj = $self->bug_fields->{$field};
- if ($field eq 'creation_ts'
- or $field eq 'delta_ts'
- or ($field_obj and
- ($field_obj->type == FIELD_TYPE_DATETIME
- or $field_obj->type == FIELD_TYPE_DATE)))
- {
- $value = trim($value);
- return undef if !$value;
- return $self->parse_date($value);
+ if (grep($_ eq $field, USER_FIELDS)) {
+ if (defined $self->map_value('user', $value)) {
+ return $self->map_value('user', $value);
}
-
- return $value;
+ }
+
+ my $field_obj = $self->bug_fields->{$field};
+ if (
+ $field eq 'creation_ts'
+ or $field eq 'delta_ts'
+ or (
+ $field_obj
+ and
+ ($field_obj->type == FIELD_TYPE_DATETIME or $field_obj->type == FIELD_TYPE_DATE)
+ )
+ )
+ {
+ $value = trim($value);
+ return undef if !$value;
+ return $self->parse_date($value);
+ }
+
+ return $value;
}
sub map_value {
- my ($self, $field, $value) = @_;
- return $self->_value_map->{$field}->{lc($value)};
+ my ($self, $field, $value) = @_;
+ return $self->_value_map->{$field}->{lc($value)};
}
sub _value_map {
- my $self = shift;
- if (!defined $self->{_value_map}) {
- # Lowercase all values to make them case-insensitive.
- my %map;
- my $translation = $self->config('translate_values');
- foreach my $field (keys %$translation) {
- my $value_mapping = $translation->{$field};
- foreach my $value (keys %$value_mapping) {
- $map{$field}->{lc($value)} = $value_mapping->{$value};
- }
- }
- $self->{_value_map} = \%map;
+ my $self = shift;
+ if (!defined $self->{_value_map}) {
+
+ # Lowercase all values to make them case-insensitive.
+ my %map;
+ my $translation = $self->config('translate_values');
+ foreach my $field (keys %$translation) {
+ my $value_mapping = $translation->{$field};
+ foreach my $value (keys %$value_mapping) {
+ $map{$field}->{lc($value)} = $value_mapping->{$value};
+ }
}
- return $self->{_value_map};
+ $self->{_value_map} = \%map;
+ }
+ return $self->{_value_map};
}
#################
@@ -490,386 +498,401 @@ 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 after_insert { }
+sub before_insert { }
+sub after_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";
- }
+ 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";
- }
+ 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);
- }
- print get_text('migrate_field_created', { field => $created }), "\n";
+ 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};
+ print get_text('migrate_field_created', {field => $created}), "\n";
+ }
+ 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 $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";
- 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;
- print get_text('migrate_creating_bugs'), "\n";
-
- 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 ($self, $bugs) = @_;
+ my $dbh = Bugzilla->dbh;
+ print get_text('migrate_creating_bugs'), "\n";
+
+ 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;
}
-
- 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} });
+ elsif ($field_name eq 'resolution') {
+ my $status = $statuses{lc($bug->{bug_status})};
+ if (!$status->is_open) {
+ $bug->{resolution} = $default_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;
+ else {
+ $bug->{$field_name} = $field->legal_values->[0]->name;
}
+ }
+ }
- # 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};
- }
- }
+ 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;
+ }
- # 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 his 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 = ?
+ # 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 his 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);
- }
+ $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);
+ }
- $self->_insert_comments($created, $comments);
- $self->_insert_history($created, $history);
- $self->_insert_attachments($created, $attachments);
+ # We don't need to send email for imported bugs.
+ $dbh->do('UPDATE bugs SET lastdiffed = delta_ts WHERE bug_id = ?',
+ undef, $created->id);
- # 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);
- }
+ # 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);
+ }
- if (!$self->verbose) {
- indicate_progress({ current => $count++, every => 5, total => $total });
- }
+ $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);
}
######################
@@ -877,11 +900,11 @@ sub _do_table_insert {
######################
sub _canonical_name {
- my ($module) = @_;
- $module =~ s{::}{/}g;
- $module = basename($module);
- $module =~ s/\.pm$//g;
- return $module;
+ my ($module) = @_;
+ $module =~ s{::}{/}g;
+ $module = basename($module);
+ $module =~ s/\.pm$//g;
+ return $module;
}
1;
diff --git a/Bugzilla/Migrate/Gnats.pm b/Bugzilla/Migrate/Gnats.pm
index 4ac9cd925..a562abf12 100644
--- a/Bugzilla/Migrate/Gnats.pm
+++ b/Bugzilla/Migrate/Gnats.pm
@@ -25,88 +25,87 @@ use List::MoreUtils qw(firstidx);
use List::Util qw(first);
use constant REQUIRED_MODULES => [
- {
- package => 'Email-Simple-FromHandle',
- module => 'Email::Simple::FromHandle',
- # This version added seekable handles.
- version => 0.050,
- },
+ {
+ package => 'Email-Simple-FromHandle',
+ module => 'Email::Simple::FromHandle',
+
+ # This version added seekable handles.
+ version => 0.050,
+ },
];
use constant FIELD_MAP => {
- 'Number' => 'bug_id',
- 'Category' => 'product',
- 'Synopsis' => 'short_desc',
- 'Responsible' => 'assigned_to',
- 'State' => 'bug_status',
- 'Class' => 'cf_type',
- 'Classification' => '',
- 'Originator' => 'reporter',
- 'Arrival-Date' => 'creation_ts',
- 'Last-Modified' => 'delta_ts',
- 'Release' => 'version',
- 'Severity' => 'bug_severity',
- 'Description' => 'comment',
+ 'Number' => 'bug_id',
+ 'Category' => 'product',
+ 'Synopsis' => 'short_desc',
+ 'Responsible' => 'assigned_to',
+ 'State' => 'bug_status',
+ 'Class' => 'cf_type',
+ 'Classification' => '',
+ 'Originator' => 'reporter',
+ 'Arrival-Date' => 'creation_ts',
+ 'Last-Modified' => 'delta_ts',
+ 'Release' => 'version',
+ 'Severity' => 'bug_severity',
+ 'Description' => 'comment',
};
use constant VALUE_MAP => {
- bug_severity => {
- 'serious' => 'major',
- 'cosmetic' => 'trivial',
- 'new-feature' => 'enhancement',
- 'non-critical' => 'normal',
- },
- bug_status => {
- 'open' => 'CONFIRMED',
- 'analyzed' => 'IN_PROGRESS',
- 'suspended' => 'RESOLVED',
- 'feedback' => 'RESOLVED',
- 'released' => 'VERIFIED',
- },
- bug_status_resolution => {
- 'feedback' => 'FIXED',
- 'released' => 'FIXED',
- 'closed' => 'FIXED',
- 'suspended' => 'LATER',
- },
- priority => {
- 'medium' => 'Normal',
- },
+ bug_severity => {
+ 'serious' => 'major',
+ 'cosmetic' => 'trivial',
+ 'new-feature' => 'enhancement',
+ 'non-critical' => 'normal',
+ },
+ bug_status => {
+ 'open' => 'CONFIRMED',
+ 'analyzed' => 'IN_PROGRESS',
+ 'suspended' => 'RESOLVED',
+ 'feedback' => 'RESOLVED',
+ 'released' => 'VERIFIED',
+ },
+ bug_status_resolution => {
+ 'feedback' => 'FIXED',
+ 'released' => 'FIXED',
+ 'closed' => 'FIXED',
+ 'suspended' => 'LATER',
+ },
+ priority => {'medium' => 'Normal',},
};
use constant GNATS_CONFIG_VARS => (
- {
- name => 'gnats_path',
- default => '/var/lib/gnats',
- desc => <<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 ($bug, $other_fields) = $self->SUPER::translate_bug($fields);
+ my ($self, $fields) = @_;
- $bug->{attachments} = delete $other_fields->{attachments};
+ my ($bug, $other_fields) = $self->SUPER::translate_bug($fields);
- if (defined $other_fields->{_add_to_comment}) {
- $bug->{comment} .= delete $other_fields->{_add_to_comment};
- }
+ $bug->{attachments} = delete $other_fields->{attachments};
- 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;
-
- if (trim($extra_comment)) {
- push(@comments, { thetext => $extra_comment, who => $bug->{reporter},
- bug_when => $bug->{delta_ts} || $bug->{creation_ts} });
- }
- $bug->{comments} = \@comments;
+ if (defined $other_fields->{_add_to_comment}) {
+ $bug->{comment} .= delete $other_fields->{_add_to_comment};
+ }
- $bug->{component} = $self->config('component_name');
- if (!$bug->{short_desc}) {
- $bug->{short_desc} = NO_SUBJECT;
- }
+ my ($changes, $extra_comment)
+ = $self->_parse_audit_trail($bug, $other_fields->{'Audit-Trail'});
- 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 ($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) {
- 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";
- }
+ # "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/>$//;
}
+ else {
+ # Sometimes names have extra stuff on the end like "(Somebody's Name)"
+ $value =~ s/\s+\(.+\)$//;
- if (scalar(@attachments) ne $num_attachments) {
- warn "WARNING: Expected $num_attachments attachments but got "
- . scalar(@attachments) . "\n" ;
- $self->debug($unformatted, 3);
+ # 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 = @_;
+ $args[1] = $value;
- my @args = @_;
+ $value = $self->SUPER::translate_value(@args);
+ return $value if ref $value;
+
+ if (grep($_ eq $field, $self->USER_FIELDS)) {
+ my $from_value = $value;
+ $value = $self->user_to_email($value);
$args[1] = $value;
+ # If we got something new from user_to_email, do any necessary
+ # translation of it.
$value = $self->SUPER::translate_value(@args);
- return $value if ref $value;
-
- if (grep($_ eq $field, $self->USER_FIELDS)) {
- my $from_value = $value;
- $value = $self->user_to_email($value);
- $args[1] = $value;
- # If we got something new from user_to_email, do any necessary
- # translation of it.
- $value = $self->SUPER::translate_value(@args);
- if (!$options->{check_only}) {
- $self->add_user($from_value, $value);
- }
+ if (!$options->{check_only}) {
+ $self->add_user($from_value, $value);
}
+ }
- return $value;
+ return $value;
}
1;
diff --git a/Bugzilla/Milestone.pm b/Bugzilla/Milestone.pm
index 2f10e1f00..277ae14e1 100644
--- a/Bugzilla/Milestone.pm
+++ b/Bugzilla/Milestone.pm
@@ -25,140 +25,140 @@ use Scalar::Util qw(blessed);
use constant DEFAULT_SORTKEY => 0;
-use constant DB_TABLE => 'milestones';
+use constant DB_TABLE => 'milestones';
use constant NAME_FIELD => 'value';
use constant LIST_ORDER => 'sortkey, value';
use constant DB_COLUMNS => qw(
- id
- value
- product_id
- sortkey
- isactive
+ id
+ value
+ product_id
+ sortkey
+ isactive
);
-use constant REQUIRED_FIELD_MAP => {
- product_id => 'product',
-};
+use constant REQUIRED_FIELD_MAP => {product_id => 'product',};
use constant UPDATE_COLUMNS => qw(
- value
- sortkey
- isactive
+ value
+ sortkey
+ isactive
);
use constant VALIDATORS => {
- product => \&_check_product,
- sortkey => \&_check_sortkey,
- value => \&_check_value,
- isactive => \&Bugzilla::Object::check_boolean,
+ product => \&_check_product,
+ sortkey => \&_check_sortkey,
+ value => \&_check_value,
+ isactive => \&Bugzilla::Object::check_boolean,
};
-use constant VALIDATOR_DEPENDENCIES => {
- value => ['product'],
-};
+use constant VALIDATOR_DEPENDENCIES => {value => ['product'],};
################################
sub new {
- my $class = shift;
- my $param = shift;
- my $dbh = Bugzilla->dbh;
-
- my $product;
- if (ref $param) {
- $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) {
+ $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;
diff --git a/Bugzilla/Object.pm b/Bugzilla/Object.pm
index eaafca219..11a6a5895 100644
--- a/Bugzilla/Object.pm
+++ b/Bugzilla/Object.pm
@@ -24,16 +24,17 @@ use constant NAME_FIELD => 'name';
use constant ID_FIELD => 'id';
use constant LIST_ORDER => NAME_FIELD;
-use constant UPDATE_VALIDATORS => {};
-use constant NUMERIC_COLUMNS => ();
-use constant DATE_COLUMNS => ();
+use constant UPDATE_VALIDATORS => {};
+use constant NUMERIC_COLUMNS => ();
+use constant DATE_COLUMNS => ();
use constant VALIDATOR_DEPENDENCIES => {};
+
# XXX At some point, this will be joined with FIELD_MAP.
-use constant REQUIRED_FIELD_MAP => {};
+use constant REQUIRED_FIELD_MAP => {};
use constant EXTRA_REQUIRED_FIELDS => ();
-use constant AUDIT_CREATES => 1;
-use constant AUDIT_UPDATES => 1;
-use constant AUDIT_REMOVES => 1;
+use constant AUDIT_CREATES => 1;
+use constant AUDIT_UPDATES => 1;
+use constant AUDIT_REMOVES => 1;
# When USE_MEMCACHED is true, the class is suitable for serialisation to
# Memcached. See documentation in Bugzilla::Memcached for more information.
@@ -50,54 +51,52 @@ use constant DYNAMIC_COLUMNS => 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;
}
@@ -106,349 +105,346 @@ 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' });
+ caller => caller,
+ function => $class . '::new',
+ argument => 'condition/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',
- { caller => caller,
- function => $class . '::new',
- argument => 'condition/values' });
- $condition = $param->{'condition'};
- push(@values, @{$param->{'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);
+ 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.
+ my $list = match($invocant, {$id_field => \@detainted_ids});
+
+ # BMO: Populate the object cache with bug objects, which helps
+ # inline-history when viewing bugs with dependencies.
+ if ($class eq 'Bugzilla::Bug') {
+ foreach my $object (@$list) {
+ $class->_object_cache_set({id => $object->id, cache => 1}, $object);
}
+ }
- # 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.
- my $list = match($invocant, { $id_field => \@detainted_ids });
-
- # BMO: Populate the object cache with bug objects, which helps
- # inline-history when viewing bugs with dependencies.
- if ($class eq 'Bugzilla::Bug') {
- foreach my $object (@$list) {
- $class->_object_cache_set(
- { id => $object->id, cache => 1 },
- $object
- );
- }
- }
-
- return $list;
+ return $list;
}
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_cahce
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, $object) = @_;
- $param->{cache} = 1;
- my $cache_key = $class->object_cache_key($param)
- || return;
- delete Bugzilla->request_cache->{$cache_key};
+ my $class = shift;
+ my ($param, $object) = @_;
+ $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;
+ }
}
sub object_cache_clearall {
- my $class = shift;
- my $cache = Bugzilla->request_cache;
- $class = blessed($class) if blessed($class);
- my $prefix = $class . ',';
- foreach my $key (grep { substr($_, 0, length($prefix)) eq $prefix } keys %$cache) {
- delete $cache->{$key};
- }
+ my $class = shift;
+ my $cache = Bugzilla->request_cache;
+ $class = blessed($class) if blessed($class);
+ my $prefix = $class . ',';
+ foreach
+ my $key (grep { substr($_, 0, length($prefix)) eq $prefix } keys %$cache)
+ {
+ delete $cache->{$key};
+ }
}
# 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 };
- }
-
- # 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 ($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});
}
-
- 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;
- }
-
- 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;
+ 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 && $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}; }
###############################
@@ -456,190 +452,200 @@ 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'
+ }
+ );
+
+ 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);
}
+ }
- $self->{$field} = $value;
+ $self->{$field} = $value;
- Bugzilla::Hook::process('object_end_of_set',
- { object => $self, field => $field });
+ 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);
-
- # BMO - allow altering values in a sane way
- Bugzilla::Hook::process('object_start_of_update',
- { object => $self, old_object => $old_self });
-
- 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 });
+ # BMO - allow altering values in a sane way
+ Bugzilla::Hook::process('object_start_of_update',
+ {object => $self, old_object => $old_self});
- $self->audit_log(\%changes) if $self->AUDIT_UPDATES;
+ 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);
- $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;
+ 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});
- if (wantarray) {
- return (\%changes, $old_self);
+ # 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;
}
- 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) = @{ $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) = @{$changes->{$field}};
+ $sth->execute($user_id, $class, $self->id, $field, $from, $to);
+ }
}
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;
}
###############################
@@ -647,127 +653,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 $validators = $class->_get_validators;
- my %field_values = %$params;
+ my ($class, $params, $options) = @_;
- # Make a hash skiplist for easier searching later
- my %skip_list = map { $_ => 1 } @{ $options->{skip} || [] };
+ my $validators = $class->_get_validators;
+ my %field_values = %$params;
- # Get the sorted field names
- my @sorted_names = $class->_sort_by_dep(keys %field_values);
+ # Make a hash skiplist for easier searching later
+ my %skip_list = map { $_ => 1 } @{$options->{skip} || []};
- # Remove the skipped names
- my @unskipped = grep { !$skip_list{$_} } @sorted_names;
+ # Get the sorted field names
+ my @sorted_names = $class->_sort_by_dep(keys %field_values);
- 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};
- }
+ # Remove the skipped names
+ my @unskipped = grep { !$skip_list{$_} } @sorted_names;
- # 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()};
}
###############################
@@ -777,20 +781,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;
}
@@ -799,26 +802,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
@@ -826,54 +828,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);
}
####################
@@ -886,67 +889,72 @@ 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};
- my @columns;
- if ($class->DYNAMIC_COLUMNS) {
- @columns = Bugzilla->dbh->bz_table_columns_real($class->DB_TABLE);
- }
- else {
- # 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 });
- @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};
+ my @columns;
+ if ($class->DYNAMIC_COLUMNS) {
+ @columns = Bugzilla->dbh->bz_table_columns_real($class->DB_TABLE);
+ }
+ else {
+ # 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});
+ @columns = ($invocant->DB_COLUMNS, @add_columns);
+ }
+ $cache->{$cache_key} = \@columns;
+ return @{$cache->{$cache_key}};
}
# This method is private and should only be called by Bugzilla::Object.
sub _get_validators {
- my $invocant = shift;
- my $class = ref($invocant) || $invocant;
- my $cache = Bugzilla->request_cache;
- my $cache_key = "object_${class}_validators";
- return $cache->{$cache_key} if $cache->{$cache_key};
- # We copy this into a hash so that the hook doesn't modify the constant.
- # (That could be bad in mod_perl.)
- my %validators = %{ $invocant->VALIDATORS };
- Bugzilla::Hook::process('object_validators',
- { class => $class, validators => \%validators });
- $cache->{$cache_key} = \%validators;
- return $cache->{$cache_key};
+ my $invocant = shift;
+ my $class = ref($invocant) || $invocant;
+ my $cache = Bugzilla->request_cache;
+ my $cache_key = "object_${class}_validators";
+ return $cache->{$cache_key} if $cache->{$cache_key};
+
+ # We copy this into a hash so that the hook doesn't modify the constant.
+ # (That could be bad in mod_perl.)
+ my %validators = %{$invocant->VALIDATORS};
+ Bugzilla::Hook::process('object_validators',
+ {class => $class, validators => \%validators});
+ $cache->{$cache_key} = \%validators;
+ return $cache->{$cache_key};
}
# These are all the fields that need to be checked, always, when
# calling create(), because they have no DEFAULT and they are marked
# NOT NULL.
sub _required_create_fields {
- my $class = shift;
- my $dbh = Bugzilla->dbh;
- my $table = $class->DB_TABLE;
-
- my @columns = $dbh->bz_table_columns($table);
- my @required;
- foreach my $column (@columns) {
- my $def = $dbh->bz_column_info($table, $column);
- if ($def->{NOTNULL} and !defined $def->{DEFAULT}
- # SERIAL fields effectively have a DEFAULT, but they're not
- # listed as having a DEFAULT in DB::Schema.
- and $def->{TYPE} !~ /serial/i)
- {
- my $field = $class->REQUIRED_FIELD_MAP->{$column} || $column;
- push(@required, $field);
- }
+ my $class = shift;
+ my $dbh = Bugzilla->dbh;
+ my $table = $class->DB_TABLE;
+
+ my @columns = $dbh->bz_table_columns($table);
+ my @required;
+ foreach my $column (@columns) {
+ my $def = $dbh->bz_column_info($table, $column);
+ if (
+ $def->{NOTNULL}
+ and !defined $def->{DEFAULT}
+
+ # SERIAL fields effectively have a DEFAULT, but they're not
+ # listed as having a DEFAULT in DB::Schema.
+ and $def->{TYPE} !~ /serial/i
+ )
+ {
+ my $field = $class->REQUIRED_FIELD_MAP->{$column} || $column;
+ push(@required, $field);
}
- push(@required, $class->EXTRA_REQUIRED_FIELDS);
- return @required;
+ }
+ push(@required, $class->EXTRA_REQUIRED_FIELDS);
+ return @required;
}
1;
diff --git a/Bugzilla/PSGI.pm b/Bugzilla/PSGI.pm
index 46352b319..e95b5291b 100644
--- a/Bugzilla/PSGI.pm
+++ b/Bugzilla/PSGI.pm
@@ -16,27 +16,25 @@ use Bugzilla::Logging;
our @EXPORT_OK = qw(compile_cgi);
sub compile_cgi {
- my ($script) = @_;
- require CGI::Compile;
- require CGI::Emulate::PSGI;
+ my ($script) = @_;
+ require CGI::Compile;
+ require CGI::Emulate::PSGI;
- my $cgi = CGI::Compile->compile($script);
- my $app = CGI::Emulate::PSGI->handler(
- sub {
- Bugzilla::init_page();
- $cgi->();
- }
- );
- return sub {
- my $env = shift;
- if ($env->{'psgix.cleanup'}) {
- push @{ $env->{'psgix.cleanup.handler'} }, \&Bugzilla::_cleanup;
- }
- my $res = $app->($env);
- Bugzilla::_cleanup() if not $env->{'psgix.cleanup'};
- return $res;
- };
+ my $cgi = CGI::Compile->compile($script);
+ my $app = CGI::Emulate::PSGI->handler(sub {
+ Bugzilla::init_page();
+ $cgi->();
+ });
+ return sub {
+ my $env = shift;
+ if ($env->{'psgix.cleanup'}) {
+ push @{$env->{'psgix.cleanup.handler'}}, \&Bugzilla::_cleanup;
+ }
+ my $res = $app->($env);
+ Bugzilla::_cleanup() if not $env->{'psgix.cleanup'};
+ return $res;
+ };
}
-1; \ No newline at end of file
+1;
diff --git a/Bugzilla/PatchReader/AddCVSContext.pm b/Bugzilla/PatchReader/AddCVSContext.pm
index 4e7da5661..094ef6ed8 100644
--- a/Bugzilla/PatchReader/AddCVSContext.pm
+++ b/Bugzilla/PatchReader/AddCVSContext.pm
@@ -9,7 +9,8 @@ use Bugzilla::PatchReader::CVSClient;
use Cwd;
use File::Temp;
-@Bugzilla::PatchReader::AddCVSContext::ISA = qw(Bugzilla::PatchReader::FilterPatch);
+@Bugzilla::PatchReader::AddCVSContext::ISA
+ = qw(Bugzilla::PatchReader::FilterPatch);
# XXX If you need to, get the entire patch worth of files and do a single
# cvs update of all files as soon as you find a file where you need to do a
@@ -31,7 +32,8 @@ sub my_rmtree {
foreach my $file (glob("$dir/*")) {
if (-d $file) {
$this->my_rmtree($file);
- } else {
+ }
+ else {
trick_taint($file);
unlink $file;
}
@@ -43,6 +45,7 @@ sub my_rmtree {
sub end_patch {
my $this = shift;
if (exists($this->{TMPDIR})) {
+
# Set as variable to get rid of taint
# One would like to use rmtree here, but that is not taint-safe.
$this->my_rmtree($this->{TMPDIR});
@@ -52,10 +55,10 @@ sub end_patch {
sub start_file {
my $this = shift;
my ($file) = @_;
- $this->{HAS_CVS_CONTEXT} = !$file->{is_add} && !$file->{is_remove} &&
- $file->{old_revision};
- $this->{REVISION} = $file->{old_revision};
- $this->{FILENAME} = $file->{filename};
+ $this->{HAS_CVS_CONTEXT}
+ = !$file->{is_add} && !$file->{is_remove} && $file->{old_revision};
+ $this->{REVISION} = $file->{old_revision};
+ $this->{FILENAME} = $file->{filename};
$this->{SECTION_END} = -1;
$this->{TARGET}->start_file(@_) if $this->{TARGET};
}
@@ -66,7 +69,7 @@ sub end_file {
if ($this->{FILE}) {
close $this->{FILE};
- unlink $this->{FILE}; # If it fails, it fails ...
+ unlink $this->{FILE}; # If it fails, it fails ...
delete $this->{FILE};
}
$this->{TARGET}->end_file(@_) if $this->{TARGET};
@@ -76,10 +79,12 @@ sub next_section {
my $this = shift;
my ($section) = @_;
$this->{NEXT_PATCH_LINE} = $section->{old_start};
- $this->{NEXT_NEW_LINE} = $section->{new_start};
+ $this->{NEXT_NEW_LINE} = $section->{new_start};
foreach my $line (@{$section->{lines}}) {
+
# If this is a line requiring context ...
if ($line =~ /^[-\+]/) {
+
# Determine how much context is needed for both the previous section line
# and this one:
# - If there is no old line, start new section
@@ -89,20 +94,23 @@ sub next_section {
# space and therefore we end the old section and start the new one
# - Else we add (old start context line through new line) context to
# existing section
- if (! exists($this->{SECTION})) {
+ if (!exists($this->{SECTION})) {
$this->_start_section();
- } elsif ($this->{CONTEXT} eq "file") {
+ }
+ elsif ($this->{CONTEXT} eq "file") {
$this->push_context_lines($this->{SECTION_END} + 1,
- $this->{NEXT_PATCH_LINE} - 1);
- } else {
+ $this->{NEXT_PATCH_LINE} - 1);
+ }
+ else {
my $start_context = $this->{NEXT_PATCH_LINE} - $this->{CONTEXT};
$start_context = $start_context > 0 ? $start_context : 0;
if (($this->{SECTION_END} + $this->{CONTEXT} + 1) < $start_context) {
$this->flush_section();
$this->_start_section();
- } else {
+ }
+ else {
$this->push_context_lines($this->{SECTION_END} + 1,
- $this->{NEXT_PATCH_LINE} - 1);
+ $this->{NEXT_PATCH_LINE} - 1);
}
}
push @{$this->{SECTION}{lines}}, $line;
@@ -110,16 +118,19 @@ sub next_section {
$this->{SECTION}{plus_lines}++;
$this->{SECTION}{new_lines}++;
$this->{NEXT_NEW_LINE}++;
- } else {
+ }
+ else {
$this->{SECTION_END}++;
$this->{SECTION}{minus_lines}++;
$this->{SECTION}{old_lines}++;
$this->{NEXT_PATCH_LINE}++;
}
- } else {
+ }
+ else {
$this->{NEXT_PATCH_LINE}++;
$this->{NEXT_NEW_LINE}++;
}
+
# If this is context, for now lose it (later we should try and determine if
# we can just use it instead of pulling the file all the time)
}
@@ -130,7 +141,8 @@ sub determine_start {
return 0 if $line < 0;
if ($this->{CONTEXT} eq "file") {
return 1;
- } else {
+ }
+ else {
my $start = $line - $this->{CONTEXT};
$start = $start > 0 ? $start : 1;
return $start;
@@ -146,23 +158,26 @@ sub _start_section {
$this->{SECTION}{old_lines} = 0;
$this->{SECTION}{new_lines} = 0;
$this->{SECTION}{minus_lines} = 0;
- $this->{SECTION}{plus_lines} = 0;
- $this->{SECTION_END} = $this->{SECTION}{old_start} - 1;
+ $this->{SECTION}{plus_lines} = 0;
+ $this->{SECTION_END} = $this->{SECTION}{old_start} - 1;
$this->push_context_lines($this->{SECTION}{old_start},
- $this->{NEXT_PATCH_LINE} - 1);
+ $this->{NEXT_PATCH_LINE} - 1);
}
sub flush_section {
my $this = shift;
if ($this->{SECTION}) {
+
# Add the necessary context to the end
if ($this->{CONTEXT} eq "file") {
$this->push_context_lines($this->{SECTION_END} + 1, "file");
- } else {
+ }
+ else {
$this->push_context_lines($this->{SECTION_END} + 1,
- $this->{SECTION_END} + $this->{CONTEXT});
+ $this->{SECTION_END} + $this->{CONTEXT});
}
+
# Send the section and line notifications
$this->{TARGET}->next_section($this->{SECTION}) if $this->{TARGET};
delete $this->{SECTION};
@@ -172,35 +187,41 @@ sub flush_section {
sub push_context_lines {
my $this = shift;
+
# Grab from start to end
my ($start, $end) = @_;
return if $end ne "file" && $start > $end;
# If it's an added / removed file, don't do anything
- return if ! $this->{HAS_CVS_CONTEXT};
+ return if !$this->{HAS_CVS_CONTEXT};
# Get and open the file if necessary
if (!$this->{FILE}) {
my $olddir = getcwd();
- if (! exists($this->{TMPDIR})) {
+ if (!exists($this->{TMPDIR})) {
$this->{TMPDIR} = File::Temp::tempdir();
- if (! -d $this->{TMPDIR}) {
+ if (!-d $this->{TMPDIR}) {
die "Could not get temporary directory";
}
}
chdir($this->{TMPDIR}) or die "Could not cd $this->{TMPDIR}";
- if (Bugzilla::PatchReader::CVSClient::cvs_co_rev($this->{CVSROOT}, $this->{REVISION}, $this->{FILENAME})) {
- die "Could not check out $this->{FILENAME} r$this->{REVISION} from $this->{CVSROOT}";
+ if (Bugzilla::PatchReader::CVSClient::cvs_co_rev(
+ $this->{CVSROOT}, $this->{REVISION}, $this->{FILENAME}
+ ))
+ {
+ die
+ "Could not check out $this->{FILENAME} r$this->{REVISION} from $this->{CVSROOT}";
}
open(my $fh, '<', $this->{FILENAME}) or die "Could not open $this->{FILENAME}";
- $this->{FILE} = $fh;
+ $this->{FILE} = $fh;
$this->{NEXT_FILE_LINE} = 1;
- trick_taint($olddir); # $olddir comes from getcwd()
+ trick_taint($olddir); # $olddir comes from getcwd()
chdir($olddir) or die "Could not cd back to $olddir";
}
# Read through the file to reach the line we need
- die "File read too far!" if $this->{NEXT_FILE_LINE} && $this->{NEXT_FILE_LINE} > $start;
+ die "File read too far!"
+ if $this->{NEXT_FILE_LINE} && $this->{NEXT_FILE_LINE} > $start;
my $fh = $this->{FILE};
while ($this->{NEXT_FILE_LINE} < $start) {
my $dummy = <$fh>;
diff --git a/Bugzilla/PatchReader/Base.pm b/Bugzilla/PatchReader/Base.pm
index 26cb9a9a0..58b08fd25 100644
--- a/Bugzilla/PatchReader/Base.pm
+++ b/Bugzilla/PatchReader/Base.pm
@@ -17,7 +17,8 @@ sub sends_data_to {
my $this = shift;
if (defined($_[0])) {
$this->{TARGET} = $_[0];
- } else {
+ }
+ else {
return $this->{TARGET};
}
}
diff --git a/Bugzilla/PatchReader/CVSClient.pm b/Bugzilla/PatchReader/CVSClient.pm
index 7a8875dc8..3f2a852f2 100644
--- a/Bugzilla/PatchReader/CVSClient.pm
+++ b/Bugzilla/PatchReader/CVSClient.pm
@@ -14,37 +14,36 @@ use strict;
use warnings;
sub parse_cvsroot {
- my $cvsroot = $_[0];
- # Format: :method:[user[:password]@]server[:[port]]/path
- if ($cvsroot =~ /^:([^:]*):(.*?)(\/.*)$/) {
- my %retval;
- $retval{protocol} = $1;
- $retval{rootdir} = $3;
- my $remote = $2;
- if ($remote =~ /^(([^\@:]*)(:([^\@]*))?\@)?([^:]*)(:(.*))?$/) {
- $retval{user} = $2;
- $retval{password} = $4;
- $retval{server} = $5;
- $retval{port} = $7;
- return %retval;
- }
+ my $cvsroot = $_[0];
+
+ # Format: :method:[user[:password]@]server[:[port]]/path
+ if ($cvsroot =~ /^:([^:]*):(.*?)(\/.*)$/) {
+ my %retval;
+ $retval{protocol} = $1;
+ $retval{rootdir} = $3;
+ my $remote = $2;
+ if ($remote =~ /^(([^\@:]*)(:([^\@]*))?\@)?([^:]*)(:(.*))?$/) {
+ $retval{user} = $2;
+ $retval{password} = $4;
+ $retval{server} = $5;
+ $retval{port} = $7;
+ return %retval;
}
+ }
- return (
- rootdir => $cvsroot
- );
+ return (rootdir => $cvsroot);
}
sub cvs_co {
- my ($cvsroot, @files) = @_;
- my $cvs = $::cvsbin || "cvs";
- return system($cvs, "-Q", "-d$cvsroot", "co", @files);
+ my ($cvsroot, @files) = @_;
+ my $cvs = $::cvsbin || "cvs";
+ return system($cvs, "-Q", "-d$cvsroot", "co", @files);
}
sub cvs_co_rev {
- my ($cvsroot, $rev, @files) = @_;
- my $cvs = $::cvsbin || "cvs";
- return system($cvs, "-Q", "-d$cvsroot", "co", "-r$rev", @files);
+ my ($cvsroot, $rev, @files) = @_;
+ my $cvs = $::cvsbin || "cvs";
+ return system($cvs, "-Q", "-d$cvsroot", "co", "-r$rev", @files);
}
1
diff --git a/Bugzilla/PatchReader/DiffPrinter/raw.pm b/Bugzilla/PatchReader/DiffPrinter/raw.pm
index b7a0d8db2..81e01c283 100644
--- a/Bugzilla/PatchReader/DiffPrinter/raw.pm
+++ b/Bugzilla/PatchReader/DiffPrinter/raw.pm
@@ -29,7 +29,8 @@ sub start_file {
my $fh = $this->{OUTFILE};
if ($file->{rcs_filename}) {
print $fh "Index: $file->{filename}\n";
- print $fh "===================================================================\n";
+ print $fh
+ "===================================================================\n";
print $fh "RCS file: $file->{rcs_filename}\n";
}
my $old_file = $file->{is_add} ? "/dev/null" : $file->{filename};
@@ -53,7 +54,8 @@ sub next_section {
return unless $section->{old_start} || $section->{new_start};
my $fh = $this->{OUTFILE};
- print $fh "@@ -$section->{old_start},$section->{old_lines} +$section->{new_start},$section->{new_lines} @@ $section->{func_info}\n";
+ print $fh
+ "@@ -$section->{old_start},$section->{old_lines} +$section->{new_start},$section->{new_lines} @@ $section->{func_info}\n";
foreach my $line (@{$section->{lines}}) {
$line =~ s/(\r?\n?)$/\n/;
print $fh $line;
diff --git a/Bugzilla/PatchReader/DiffPrinter/template.pm b/Bugzilla/PatchReader/DiffPrinter/template.pm
index 4120f1d8a..54f3b4419 100644
--- a/Bugzilla/PatchReader/DiffPrinter/template.pm
+++ b/Bugzilla/PatchReader/DiffPrinter/template.pm
@@ -11,10 +11,10 @@ sub new {
bless $this, $class;
$this->{TEMPLATE_PROCESSOR} = $_[0];
- $this->{HEADER_TEMPLATE} = $_[1];
- $this->{FILE_TEMPLATE} = $_[2];
- $this->{FOOTER_TEMPLATE} = $_[3];
- $this->{ARGS} = $_[4] || {};
+ $this->{HEADER_TEMPLATE} = $_[1];
+ $this->{FILE_TEMPLATE} = $_[2];
+ $this->{FOOTER_TEMPLATE} = $_[3];
+ $this->{ARGS} = $_[4] || {};
$this->{ARGS}{file_count} = 0;
return $this;
@@ -23,20 +23,20 @@ sub new {
sub start_patch {
my $this = shift;
$this->{TEMPLATE_PROCESSOR}->process($this->{HEADER_TEMPLATE}, $this->{ARGS})
- || ::ThrowTemplateError($this->{TEMPLATE_PROCESSOR}->error());
+ || ::ThrowTemplateError($this->{TEMPLATE_PROCESSOR}->error());
}
sub end_patch {
my $this = shift;
$this->{TEMPLATE_PROCESSOR}->process($this->{FOOTER_TEMPLATE}, $this->{ARGS})
- || ::ThrowTemplateError($this->{TEMPLATE_PROCESSOR}->error());
+ || ::ThrowTemplateError($this->{TEMPLATE_PROCESSOR}->error());
}
sub start_file {
my $this = shift;
$this->{ARGS}{file_count}++;
- $this->{ARGS}{file} = shift;
- $this->{ARGS}{file}{plus_lines} = 0;
+ $this->{ARGS}{file} = shift;
+ $this->{ARGS}{file}{plus_lines} = 0;
$this->{ARGS}{file}{minus_lines} = 0;
@{$this->{ARGS}{sections}} = ();
}
@@ -45,9 +45,11 @@ sub end_file {
my $this = shift;
my $file = $this->{ARGS}{file};
if ($file->{canonical} && $file->{old_revision} && $this->{ARGS}{bonsai_url}) {
- $this->{ARGS}{bonsai_prefix} = "$this->{ARGS}{bonsai_url}/cvsblame.cgi?file=$file->{filename}&amp;rev=$file->{old_revision}";
+ $this->{ARGS}{bonsai_prefix}
+ = "$this->{ARGS}{bonsai_url}/cvsblame.cgi?file=$file->{filename}&amp;rev=$file->{old_revision}";
}
if ($file->{canonical} && $this->{ARGS}{lxr_url}) {
+
# Cut off the lxr root, if any
my $filename = $file->{filename};
$filename = substr($filename, length($this->{ARGS}{lxr_root}));
@@ -55,7 +57,7 @@ sub end_file {
}
$this->{TEMPLATE_PROCESSOR}->process($this->{FILE_TEMPLATE}, $this->{ARGS})
- || ::ThrowTemplateError($this->{TEMPLATE_PROCESSOR}->error());
+ || ::ThrowTemplateError($this->{TEMPLATE_PROCESSOR}->error());
@{$this->{ARGS}{sections}} = ();
delete $this->{ARGS}{file};
}
@@ -64,47 +66,46 @@ sub next_section {
my $this = shift;
my ($section) = @_;
- $this->{ARGS}{file}{plus_lines} += $section->{plus_lines};
+ $this->{ARGS}{file}{plus_lines} += $section->{plus_lines};
$this->{ARGS}{file}{minus_lines} += $section->{minus_lines};
# Get groups of lines and print them
my $last_line_char = '';
- my $context_lines = [];
- my $plus_lines = [];
- my $minus_lines = [];
+ my $context_lines = [];
+ my $plus_lines = [];
+ my $minus_lines = [];
foreach my $line (@{$section->{lines}}) {
$line =~ s/\r?\n?$//;
if ($line =~ /^ /) {
if ($last_line_char ne ' ') {
- push @{$section->{groups}}, {context => $context_lines,
- plus => $plus_lines,
- minus => $minus_lines};
+ push @{$section->{groups}},
+ {context => $context_lines, plus => $plus_lines, minus => $minus_lines};
$context_lines = [];
- $plus_lines = [];
- $minus_lines = [];
+ $plus_lines = [];
+ $minus_lines = [];
}
$last_line_char = ' ';
push @{$context_lines}, substr($line, 1);
- } elsif ($line =~ /^\+/) {
+ }
+ elsif ($line =~ /^\+/) {
if ($last_line_char eq ' ' || $last_line_char eq '-' && @{$plus_lines}) {
- push @{$section->{groups}}, {context => $context_lines,
- plus => $plus_lines,
- minus => $minus_lines};
- $context_lines = [];
- $plus_lines = [];
- $minus_lines = [];
+ push @{$section->{groups}},
+ {context => $context_lines, plus => $plus_lines, minus => $minus_lines};
+ $context_lines = [];
+ $plus_lines = [];
+ $minus_lines = [];
$last_line_char = '';
}
$last_line_char = '+';
push @{$plus_lines}, substr($line, 1);
- } elsif ($line =~ /^-/) {
+ }
+ elsif ($line =~ /^-/) {
if ($last_line_char eq '+' && @{$minus_lines}) {
- push @{$section->{groups}}, {context => $context_lines,
- plus => $plus_lines,
- minus => $minus_lines};
- $context_lines = [];
- $plus_lines = [];
- $minus_lines = [];
+ push @{$section->{groups}},
+ {context => $context_lines, plus => $plus_lines, minus => $minus_lines};
+ $context_lines = [];
+ $plus_lines = [];
+ $minus_lines = [];
$last_line_char = '';
}
$last_line_char = '-';
@@ -112,9 +113,8 @@ sub next_section {
}
}
- push @{$section->{groups}}, {context => $context_lines,
- plus => $plus_lines,
- minus => $minus_lines};
+ push @{$section->{groups}},
+ {context => $context_lines, plus => $plus_lines, minus => $minus_lines};
push @{$this->{ARGS}{sections}}, $section;
}
diff --git a/Bugzilla/PatchReader/FilterPatch.pm b/Bugzilla/PatchReader/FilterPatch.pm
index 330f6329b..3891e4e57 100644
--- a/Bugzilla/PatchReader/FilterPatch.pm
+++ b/Bugzilla/PatchReader/FilterPatch.pm
@@ -11,7 +11,7 @@ use Bugzilla::PatchReader::Base;
sub new {
my $class = shift;
$class = ref($class) || $class;
- my $this = $class->SUPER::new();
+ my $this = $class->SUPER::new();
bless $this, $class;
return $this;
diff --git a/Bugzilla/PatchReader/FixPatchRoot.pm b/Bugzilla/PatchReader/FixPatchRoot.pm
index 1b0d250ad..a06682ba8 100644
--- a/Bugzilla/PatchReader/FixPatchRoot.pm
+++ b/Bugzilla/PatchReader/FixPatchRoot.pm
@@ -7,7 +7,8 @@ use warnings;
use Bugzilla::PatchReader::FilterPatch;
use Bugzilla::PatchReader::CVSClient;
-@Bugzilla::PatchReader::FixPatchRoot::ISA = qw(Bugzilla::PatchReader::FilterPatch);
+@Bugzilla::PatchReader::FixPatchRoot::ISA
+ = qw(Bugzilla::PatchReader::FilterPatch);
sub new {
my $class = shift;
@@ -26,26 +27,29 @@ sub diff_root {
my $this = shift;
if (@_) {
$this->{DIFF_ROOT} = $_[0];
- } else {
+ }
+ else {
return $this->{DIFF_ROOT};
}
}
sub flush_delayed_commands {
my $this = shift;
- return if ! $this->{DELAYED_COMMANDS};
+ return if !$this->{DELAYED_COMMANDS};
my $commands = $this->{DELAYED_COMMANDS};
delete $this->{DELAYED_COMMANDS};
$this->{FORCE_COMMANDS} = 1;
foreach my $command_arr (@{$commands}) {
my $command = $command_arr->[0];
- my $arg = $command_arr->[1];
+ my $arg = $command_arr->[1];
if ($command eq "start_file") {
$this->start_file($arg);
- } elsif ($command eq "end_file") {
+ }
+ elsif ($command eq "end_file") {
$this->end_file($arg);
- } elsif ($command eq "section") {
+ }
+ elsif ($command eq "section") {
$this->next_section($arg);
}
}
@@ -60,10 +64,12 @@ sub end_patch {
sub start_file {
my $this = shift;
my ($file) = @_;
+
# If the file is new, it will not have a filename that fits the repository
# root and therefore needs to be fixed up to have the same root as everyone
# else. At the same time we need to fix DIFF_ROOT too.
if (exists($this->{DIFF_ROOT})) {
+
# XXX Return error if there are multiple roots in the patch by verifying
# that the DIFF_ROOT is not different from the calculated diff root on this
# filename
@@ -71,31 +77,35 @@ sub start_file {
$file->{filename} = $this->{DIFF_ROOT} . $file->{filename};
$file->{canonical} = 1;
- } elsif ($file->{rcs_filename} &&
- substr($file->{rcs_filename}, 0, length($this->{REPOSITORY_ROOT})) eq
- $this->{REPOSITORY_ROOT}) {
+ }
+ elsif ($file->{rcs_filename}
+ && substr($file->{rcs_filename}, 0, length($this->{REPOSITORY_ROOT})) eq
+ $this->{REPOSITORY_ROOT})
+ {
# Since we know the repository we can determine where the user was in the
# repository when they did the diff by chopping off the repository root
# from the rcs filename
- $this->{DIFF_ROOT} = substr($file->{rcs_filename},
- length($this->{REPOSITORY_ROOT}));
+ $this->{DIFF_ROOT}
+ = substr($file->{rcs_filename}, length($this->{REPOSITORY_ROOT}));
$this->{DIFF_ROOT} =~ s/,v$//;
+
# If the RCS file exists in the Attic then we need to correct for
# this, stripping off the '/Attic' suffix in order to reduce the name
# to just the CVS root.
if ($this->{DIFF_ROOT} =~ m/Attic/) {
$this->{DIFF_ROOT} = substr($this->{DIFF_ROOT}, 0, -6);
}
+
# XXX More error checking--that filename exists and that it is in fact
# part of the rcs filename
- $this->{DIFF_ROOT} = substr($this->{DIFF_ROOT}, 0,
- -length($file->{filename}));
+ $this->{DIFF_ROOT} = substr($this->{DIFF_ROOT}, 0, -length($file->{filename}));
$this->flush_delayed_commands();
$file->{filename} = $this->{DIFF_ROOT} . $file->{filename};
$file->{canonical} = 1;
- } else {
+ }
+ else {
# DANGER Will Robinson. The first file in the patch is new. We will try
# "delayed command mode"
#
@@ -104,7 +114,7 @@ sub start_file {
# whatever the hell was in the patch)
if (!$this->{FORCE_COMMANDS}) {
- push @{$this->{DELAYED_COMMANDS}}, [ "start_file", { %{$file} } ];
+ push @{$this->{DELAYED_COMMANDS}}, ["start_file", {%{$file}}];
return;
}
}
@@ -114,8 +124,9 @@ sub start_file {
sub end_file {
my $this = shift;
if (exists($this->{DELAYED_COMMANDS})) {
- push @{$this->{DELAYED_COMMANDS}}, [ "end_file", { %{$_[0]} } ];
- } else {
+ push @{$this->{DELAYED_COMMANDS}}, ["end_file", {%{$_[0]}}];
+ }
+ else {
$this->{TARGET}->end_file(@_) if $this->{TARGET};
}
}
@@ -123,8 +134,9 @@ sub end_file {
sub next_section {
my $this = shift;
if (exists($this->{DELAYED_COMMANDS})) {
- push @{$this->{DELAYED_COMMANDS}}, [ "section", { %{$_[0]} } ];
- } else {
+ push @{$this->{DELAYED_COMMANDS}}, ["section", {%{$_[0]}}];
+ }
+ else {
$this->{TARGET}->next_section(@_) if $this->{TARGET};
}
}
diff --git a/Bugzilla/PatchReader/NarrowPatch.pm b/Bugzilla/PatchReader/NarrowPatch.pm
index 1441e8366..2dd1a647f 100644
--- a/Bugzilla/PatchReader/NarrowPatch.pm
+++ b/Bugzilla/PatchReader/NarrowPatch.pm
@@ -6,7 +6,8 @@ use 5.10.1;
use strict;
use warnings;
-@Bugzilla::PatchReader::NarrowPatch::ISA = qw(Bugzilla::PatchReader::FilterPatch);
+@Bugzilla::PatchReader::NarrowPatch::ISA
+ = qw(Bugzilla::PatchReader::FilterPatch);
sub new {
my $class = shift;
@@ -22,7 +23,9 @@ sub new {
sub start_file {
my $this = shift;
my ($file) = @_;
- if (grep { $_ eq substr($file->{filename}, 0, length($_)) } @{$this->{INCLUDE_FILES}}) {
+ if (grep { $_ eq substr($file->{filename}, 0, length($_)) }
+ @{$this->{INCLUDE_FILES}})
+ {
$this->{IS_INCLUDED} = 1;
$this->{TARGET}->start_file(@_) if $this->{TARGET};
}
diff --git a/Bugzilla/PatchReader/PatchInfoGrabber.pm b/Bugzilla/PatchReader/PatchInfoGrabber.pm
index 6fb35fd16..96d20d0ba 100644
--- a/Bugzilla/PatchReader/PatchInfoGrabber.pm
+++ b/Bugzilla/PatchReader/PatchInfoGrabber.pm
@@ -6,7 +6,8 @@ use 5.10.1;
use strict;
use warnings;
-@Bugzilla::PatchReader::PatchInfoGrabber::ISA = qw(Bugzilla::PatchReader::FilterPatch);
+@Bugzilla::PatchReader::PatchInfoGrabber::ISA
+ = qw(Bugzilla::PatchReader::FilterPatch);
sub new {
my $class = shift;
@@ -31,16 +32,18 @@ sub start_patch {
sub start_file {
my $this = shift;
my ($file) = @_;
- $this->{PATCH_INFO}{files}{$file->{filename}} = { %{$file} };
- $this->{FILE} = { %{$file} };
+ $this->{PATCH_INFO}{files}{$file->{filename}} = {%{$file}};
+ $this->{FILE} = {%{$file}};
$this->{TARGET}->start_file(@_) if $this->{TARGET};
}
sub next_section {
my $this = shift;
my ($section) = @_;
- $this->{PATCH_INFO}{files}{$this->{FILE}{filename}}{plus_lines} += $section->{plus_lines};
- $this->{PATCH_INFO}{files}{$this->{FILE}{filename}}{minus_lines} += $section->{minus_lines};
+ $this->{PATCH_INFO}{files}{$this->{FILE}{filename}}{plus_lines}
+ += $section->{plus_lines};
+ $this->{PATCH_INFO}{files}{$this->{FILE}{filename}}{minus_lines}
+ += $section->{minus_lines};
$this->{TARGET}->next_section(@_) if $this->{TARGET};
}
diff --git a/Bugzilla/PatchReader/Raw.pm b/Bugzilla/PatchReader/Raw.pm
index bb5a6cefd..06055c103 100644
--- a/Bugzilla/PatchReader/Raw.pm
+++ b/Bugzilla/PatchReader/Raw.pm
@@ -25,7 +25,7 @@ use Bugzilla::PatchReader::Base;
sub new {
my $class = shift;
$class = ref($class) || $class;
- my $this = $class->SUPER::new();
+ my $this = $class->SUPER::new();
bless $this, $class;
return $this;
@@ -49,7 +49,8 @@ sub next_line {
if ($1 eq "/dev/null") {
$this->{FILE_STATE}{is_add} = 1;
- } else {
+ }
+ else {
$this->{FILE_STATE}{filename} = $1;
}
$this->{FILE_STATE}{old_date_str} = $2;
@@ -57,7 +58,8 @@ sub next_line {
$this->{IN_HEADER} = 1;
- } elsif ($line =~ /^\+\+\+\s*([\S ]+)\s*\t([^\t\r\n]*)(\S*)/) {
+ }
+ elsif ($line =~ /^\+\+\+\s*([\S ]+)\s*\t([^\t\r\n]*)(\S*)/) {
if ($1 eq "/dev/null") {
$this->{FILE_STATE}{is_remove} = 1;
}
@@ -66,45 +68,57 @@ sub next_line {
$this->{IN_HEADER} = 1;
- } elsif ($line =~ /^RCS file: ([\S ]+)/) {
+ }
+ elsif ($line =~ /^RCS file: ([\S ]+)/) {
$this->{FILE_STATE}{rcs_filename} = $1;
$this->{IN_HEADER} = 1;
- } elsif ($line =~ /^retrieving revision (\S+)/) {
+ }
+ elsif ($line =~ /^retrieving revision (\S+)/) {
$this->{FILE_STATE}{old_revision} = $1;
$this->{IN_HEADER} = 1;
- } elsif ($line =~ /^Index:\s*([\S ]+)/) {
+ }
+ elsif ($line =~ /^Index:\s*([\S ]+)/) {
$this->_maybe_end_file();
$this->{FILE_STATE}{filename} = $1;
$this->{IN_HEADER} = 1;
- } elsif ($line =~ /^diff\s*(-\S+\s*)*(\S+)\s*(\S*)/ && $3) {
+ }
+ elsif ($line =~ /^diff\s*(-\S+\s*)*(\S+)\s*(\S*)/ && $3) {
+
# Simple diff <dir> <dir>
$this->_maybe_end_file();
$this->{FILE_STATE}{filename} = $2;
$this->{IN_HEADER} = 1;
- # section parsing
- } elsif ($line =~ /^@@\s*-(\d+),?(\d*)\s*\+(\d+),?(\d*)\s*(?:@@\s*(.*))?/) {
+ # section parsing
+ }
+ elsif ($line =~ /^@@\s*-(\d+),?(\d*)\s*\+(\d+),?(\d*)\s*(?:@@\s*(.*))?/) {
$this->{IN_HEADER} = 0;
$this->_maybe_start_file();
$this->_maybe_end_section();
$2 = 0 if !defined($2);
$4 = 0 if !defined($4);
- $this->{SECTION_STATE} = { old_start => $1, old_lines => $2,
- new_start => $3, new_lines => $4,
- func_info => $5,
- minus_lines => 0, plus_lines => 0,
- };
+ $this->{SECTION_STATE} = {
+ old_start => $1,
+ old_lines => $2,
+ new_start => $3,
+ new_lines => $4,
+ func_info => $5,
+ minus_lines => 0,
+ plus_lines => 0,
+ };
+
+ }
+ elsif ($line =~ /^(\d+),?(\d*)([acd])(\d+),?(\d*)/) {
- } elsif ($line =~ /^(\d+),?(\d*)([acd])(\d+),?(\d*)/) {
# Non-universal diff. Calculate as though it were universal.
$this->{IN_HEADER} = 0;
@@ -116,44 +130,56 @@ sub next_line {
my $new_start;
my $new_lines;
if ($3 eq 'a') {
+
# 'a' has the old number one off from diff -u ("insert after this line"
# vs. "insert at this line")
$old_start = $1 + 1;
$old_lines = 0;
- } else {
+ }
+ else {
$old_start = $1;
$old_lines = $2 ? ($2 - $1 + 1) : 1;
}
if ($3 eq 'd') {
+
# 'd' has the new number one off from diff -u ("delete after this line"
# vs. "delete at this line")
$new_start = $4 + 1;
$new_lines = 0;
- } else {
+ }
+ else {
$new_start = $4;
$new_lines = $5 ? ($5 - $4 + 1) : 1;
}
- $this->{SECTION_STATE} = { old_start => $old_start, old_lines => $old_lines,
- new_start => $new_start, new_lines => $new_lines,
- minus_lines => 0, plus_lines => 0
- };
+ $this->{SECTION_STATE} = {
+ old_start => $old_start,
+ old_lines => $old_lines,
+ new_start => $new_start,
+ new_lines => $new_lines,
+ minus_lines => 0,
+ plus_lines => 0
+ };
}
# line parsing (only when inside a section)
return if $this->{IN_HEADER};
if ($line =~ /^ /) {
push @{$this->{SECTION_STATE}{lines}}, $line;
- } elsif ($line =~ /^-/) {
+ }
+ elsif ($line =~ /^-/) {
$this->{SECTION_STATE}{minus_lines}++;
push @{$this->{SECTION_STATE}{lines}}, $line;
- } elsif ($line =~ /^\+/) {
+ }
+ elsif ($line =~ /^\+/) {
$this->{SECTION_STATE}{plus_lines}++;
push @{$this->{SECTION_STATE}{lines}}, $line;
- } elsif ($line =~ /^< /) {
+ }
+ elsif ($line =~ /^< /) {
$this->{SECTION_STATE}{minus_lines}++;
push @{$this->{SECTION_STATE}{lines}}, "-" . substr($line, 2);
- } elsif ($line =~ /^> /) {
+ }
+ elsif ($line =~ /^> /) {
$this->{SECTION_STATE}{plus_lines}++;
push @{$this->{SECTION_STATE}{lines}}, "+" . substr($line, 2);
}
@@ -179,14 +205,15 @@ sub end_lines {
sub _init_state {
my $this = shift;
$this->{SECTION_STATE}{minus_lines} ||= 0;
- $this->{SECTION_STATE}{plus_lines} ||= 0;
+ $this->{SECTION_STATE}{plus_lines} ||= 0;
}
sub _maybe_start_file {
my $this = shift;
$this->_init_state();
- if (exists($this->{FILE_STATE}) && !$this->{FILE_STARTED} ||
- $this->{FILE_NEVER_STARTED}) {
+ if (exists($this->{FILE_STATE}) && !$this->{FILE_STARTED}
+ || $this->{FILE_NEVER_STARTED})
+ {
$this->_start_file();
}
}
@@ -198,6 +225,7 @@ sub _maybe_end_file {
$this->_maybe_end_section();
if (exists($this->{FILE_STATE})) {
+
# Handle empty patch sections (if the file has not been started and we're
# already trying to end it, start it first!)
if (!$this->{FILE_STARTED}) {
@@ -216,7 +244,7 @@ sub _start_file {
# Send start notification and set state
if (!$this->{FILE_STATE}) {
- $this->{FILE_STATE} = { filename => "file_not_specified_in_diff" };
+ $this->{FILE_STATE} = {filename => "file_not_specified_in_diff"};
}
# Send start notification and set state
diff --git a/Bugzilla/Product.pm b/Bugzilla/Product.pm
index 3ac1692f0..a0ba5da27 100644
--- a/Bugzilla/Product.pm
+++ b/Bugzilla/Product.pm
@@ -40,32 +40,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
};
###############################
@@ -73,263 +73,288 @@ 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 can be NULLs
- foreach my $field (qw( default_op_sys_id default_platform_id )) {
- next unless exists $params->{$field} && defined $params->{$field};
- $params->{$field} = undef if $params->{$field} eq '';
- }
+ # 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};
+
+ # Some fields can be NULLs
+ foreach my $field (qw( default_op_sys_id default_platform_id )) {
+ next unless exists $params->{$field} && defined $params->{$field};
+ $params->{$field} = undef if $params->{$field} eq '';
+ }
- my $product = $class->insert_create_data($params);
- Bugzilla->user->clear_product_cache();
+ my $product = $class->insert_create_data($params);
+ Bugzilla->user->clear_product_cache();
- # Add the new version and milestone into the DB as valid values.
- Bugzilla::Version->create({ value => $version, product => $product });
- Bugzilla::Milestone->create({ value => $product->default_milestone,
- product => $product });
+ # Add the new version and milestone into the DB as valid values.
+ Bugzilla::Version->create({value => $version, product => $product});
+ Bugzilla::Milestone->create(
+ {value => $product->default_milestone, product => $product});
- # Create groups and series for the new product, if requested.
- $product->_create_bug_group() if Bugzilla->params->{'makeproductgroups'};
- $product->_create_series() if $create_series;
+ # Create groups and series for the new product, if requested.
+ $product->_create_bug_group() if Bugzilla->params->{'makeproductgroups'};
+ $product->_create_series() if $create_series;
- Bugzilla::Hook::process('product_end_of_create', { product => $product });
+ Bugzilla::Hook::process('product_end_of_create', {product => $product});
- $dbh->bz_commit_transaction();
- Bugzilla->memcached->clear_config();
- return $product;
+ $dbh->bz_commit_transaction();
+ Bugzilla->memcached->clear_config();
+ return $product;
}
# This is considerably faster than calling new_from_list three times
# for each product in the list, particularly with hundreds or thousands
# of products.
sub preload {
- my ($products, $preload_flagtypes, $flagtypes_params) = @_;
- my %prods = map { $_->id => $_ } @$products;
- my @prod_ids = keys %prods;
- return unless @prod_ids;
-
- my $dbh = Bugzilla->dbh;
- 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($flagtypes_params) foreach @$products;
+ my ($products, $preload_flagtypes, $flagtypes_params) = @_;
+ my %prods = map { $_->id => $_ } @$products;
+ my @prod_ids = keys %prods;
+ return unless @prod_ids;
+
+ my $dbh = Bugzilla->dbh;
+ 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($flagtypes_params) 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 he can't see,
- # which is okay, because he's 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 he can't see,
+ # which is okay, because he's 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);
}
+ }
- $dbh->do("DELETE FROM products WHERE id = ?", undef, $self->id);
+ $dbh->do("DELETE FROM products WHERE id = ?", undef, $self->id);
- $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};
}
@@ -338,91 +363,94 @@ sub remove_from_db {
###############################
sub _check_classification {
- my ($invocant, $classification_name) = @_;
-
- my $classification_id = 1;
- if (Bugzilla->params->{'useclassification'}) {
- my $classification = Bugzilla::Classification->check($classification_name);
- $classification_id = $classification->id;
- }
- return $classification_id;
+ my ($invocant, $classification_name) = @_;
+
+ my $classification_id = 1;
+ if (Bugzilla->params->{'useclassification'}) {
+ my $classification = Bugzilla::Classification->check($classification_name);
+ $classification_id = $classification->id;
+ }
+ return $classification_id;
}
sub _check_name {
- my ($invocant, $name) = @_;
+ my ($invocant, $name) = @_;
- $name = trim($name);
- $name || ThrowUserError('product_blank_name');
+ $name = trim($name);
+ $name || ThrowUserError('product_blank_name');
- if (length($name) > MAX_PRODUCT_SIZE) {
- ThrowUserError('product_name_too_long', {'name' => $name});
- }
+ if (length($name) > MAX_PRODUCT_SIZE) {
+ ThrowUserError('product_name_too_long', {'name' => $name});
+ }
- my $product = new Bugzilla::Product({name => $name});
- if ($product && (!ref $invocant || $product->id != $invocant->id)) {
- # Check for exact case sensitive match:
- if ($product->name eq $name) {
- ThrowUserError('product_name_already_in_use', {'product' => $product->name});
- }
- else {
- ThrowUserError('product_name_diff_in_case', {'product' => $name,
- 'existing_product' => $product->name});
- }
+ my $product = new Bugzilla::Product({name => $name});
+ if ($product && (!ref $invocant || $product->id != $invocant->id)) {
+
+ # Check for exact case sensitive match:
+ if ($product->name eq $name) {
+ ThrowUserError('product_name_already_in_use', {'product' => $product->name});
+ }
+ else {
+ ThrowUserError('product_name_diff_in_case',
+ {'product' => $name, 'existing_product' => $product->name});
}
- return $name;
+ }
+ return $name;
}
sub _check_description {
- my ($invocant, $description) = @_;
+ my ($invocant, $description) = @_;
- $description = trim($description);
- $description || ThrowUserError('product_must_have_description');
- return $description;
+ $description = trim($description);
+ $description || ThrowUserError('product_must_have_description');
+ return $description;
}
sub _check_version {
- my ($invocant, $version) = @_;
+ my ($invocant, $version) = @_;
- $version = trim($version);
- $version || ThrowUserError('product_must_have_version');
- # We will check the version length when Bugzilla::Version->create will do it.
- return $version;
+ $version = trim($version);
+ $version || ThrowUserError('product_must_have_version');
+
+ # We will check the version length when Bugzilla::Version->create will do it.
+ return $version;
}
sub _check_default_milestone {
- my ($invocant, $milestone) = @_;
+ my ($invocant, $milestone) = @_;
- # Do nothing if target milestones are not in use.
- unless (Bugzilla->params->{'usetargetmilestone'}) {
- return (ref $invocant) ? $invocant->default_milestone : '---';
- }
+ # Do nothing if target milestones are not in use.
+ unless (Bugzilla->params->{'usetargetmilestone'}) {
+ return (ref $invocant) ? $invocant->default_milestone : '---';
+ }
- $milestone = trim($milestone);
+ $milestone = trim($milestone);
- if (ref $invocant) {
- # The default milestone must be one of the existing milestones.
- my $mil_obj = new Bugzilla::Milestone({name => $milestone, product => $invocant});
+ if (ref $invocant) {
- $mil_obj || ThrowUserError('product_must_define_defaultmilestone',
- {product => $invocant->name,
- milestone => $milestone});
- }
- else {
- $milestone ||= '---';
- }
- return $milestone;
+ # The default milestone must be one of the existing milestones.
+ my $mil_obj
+ = new Bugzilla::Milestone({name => $milestone, product => $invocant});
+
+ $mil_obj || ThrowUserError('product_must_define_defaultmilestone',
+ {product => $invocant->name, milestone => $milestone});
+ }
+ else {
+ $milestone ||= '---';
+ }
+ return $milestone;
}
sub _check_milestone_url {
- my ($invocant, $url) = @_;
+ my ($invocant, $url) = @_;
- # Do nothing if target milestones are not in use.
- unless (Bugzilla->params->{'usetargetmilestone'}) {
- return (ref $invocant) ? $invocant->milestone_url : '';
- }
+ # Do nothing if target milestones are not in use.
+ unless (Bugzilla->params->{'usetargetmilestone'}) {
+ return (ref $invocant) ? $invocant->milestone_url : '';
+ }
- $url = trim($url || '');
- return $url;
+ $url = trim($url || '');
+ return $url;
}
#####################################
@@ -437,394 +465,430 @@ use constant is_default => 0;
###############################
sub _create_bug_group {
- my $self = shift;
- my $dbh = Bugzilla->dbh;
-
- my $group_name = $self->name;
- while (new Bugzilla::Group({name => $group_name})) {
- $group_name .= '_';
- }
- my $group_description = get_text('bug_group_description', {product => $self});
-
- my $group = Bugzilla::Group->create({name => $group_name,
- description => $group_description,
- isbuggroup => 1});
-
- # Associate the new group and new product.
- $dbh->do('INSERT INTO group_control_map
+ my $self = shift;
+ my $dbh = Bugzilla->dbh;
+
+ my $group_name = $self->name;
+ while (new Bugzilla::Group({name => $group_name})) {
+ $group_name .= '_';
+ }
+ my $group_description = get_text('bug_group_description', {product => $self});
+
+ my $group
+ = Bugzilla::Group->create({
+ name => $group_name, description => $group_description, isbuggroup => 1
+ });
+
+ # Associate the new group and new product.
+ $dbh->do(
+ 'INSERT INTO group_control_map
(group_id, product_id, membercontrol, othercontrol)
- VALUES (?, ?, ?, ?)',
- undef, ($group->id, $self->id, CONTROLMAPDEFAULT, CONTROLMAPNA));
+ VALUES (?, ?, ?, ?)', undef,
+ ($group->id, $self->id, CONTROLMAPDEFAULT, CONTROLMAPNA)
+ );
}
sub _create_series {
- my $self = shift;
-
- my @series;
- # We do every status, every resolution, and an "opened" one as well.
- foreach my $bug_status (@{get_legal_field_values('bug_status')}) {
- push(@series, [$bug_status, "bug_status=" . url_quote($bug_status)]);
- }
-
- foreach my $resolution (@{get_legal_field_values('resolution')}) {
- next if !$resolution;
- push(@series, [$resolution, "resolution=" . url_quote($resolution)]);
- }
-
- my @openedstatuses = BUG_STATE_OPEN;
- my $query = join("&", map { "bug_status=" . url_quote($_) } @openedstatuses);
- push(@series, [get_text('series_all_open'), $query]);
-
- foreach my $sdata (@series) {
- my $series = new Bugzilla::Series(undef, $self->name,
- get_text('series_subcategory'),
- $sdata->[0], Bugzilla->user->id, 1,
- $sdata->[1] . "&product=" . url_quote($self->name), 1);
- $series->writeToDatabase();
- }
+ my $self = shift;
+
+ my @series;
+
+ # We do every status, every resolution, and an "opened" one as well.
+ foreach my $bug_status (@{get_legal_field_values('bug_status')}) {
+ push(@series, [$bug_status, "bug_status=" . url_quote($bug_status)]);
+ }
+
+ foreach my $resolution (@{get_legal_field_values('resolution')}) {
+ next if !$resolution;
+ push(@series, [$resolution, "resolution=" . url_quote($resolution)]);
+ }
+
+ my @openedstatuses = BUG_STATE_OPEN;
+ my $query = join("&", map { "bug_status=" . url_quote($_) } @openedstatuses);
+ push(@series, [get_text('series_all_open'), $query]);
+
+ foreach my $sdata (@series) {
+ my $series
+ = new Bugzilla::Series(undef, $self->name, get_text('series_subcategory'),
+ $sdata->[0], Bugzilla->user->id, 1,
+ $sdata->[1] . "&product=" . url_quote($self->name), 1);
+ $series->writeToDatabase();
+ }
}
-sub set_name { $_[0]->set('name', $_[1]); }
-sub set_description { $_[0]->set('description', $_[1]); }
-sub set_default_milestone { $_[0]->set('defaultmilestone', $_[1]); }
-sub set_is_active { $_[0]->set('isactive', $_[1]); }
+sub set_name { $_[0]->set('name', $_[1]); }
+sub set_description { $_[0]->set('description', $_[1]); }
+sub set_default_milestone { $_[0]->set('defaultmilestone', $_[1]); }
+sub set_is_active { $_[0]->set('isactive', $_[1]); }
sub set_allows_unconfirmed { $_[0]->set('allows_unconfirmed', $_[1]); }
sub set_group_controls {
- my ($self, $group, $settings) = @_;
-
- $group->is_active_bug_group
- || ThrowUserError('product_illegal_group', {group => $group});
-
- scalar(keys %$settings)
- || ThrowCodeError('product_empty_group_controls', {group => $group});
-
- # We store current settings for this group.
- my $gs = $self->group_controls->{$group->id};
- # If there is no entry for this group yet, create a default hash.
- unless (defined $gs) {
- $gs = { entry => 0,
- membercontrol => CONTROLMAPNA,
- othercontrol => CONTROLMAPNA,
- canedit => 0,
- editcomponents => 0,
- editbugs => 0,
- canconfirm => 0,
- group => $group };
+ 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}});
}
-
- # 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};
- }
-
- 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};
- }
+ {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};
+ 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
+ # 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);
+ WHERE product_id = ?}, undef, $self->id
+ );
- $self->{milestones} = Bugzilla::Milestone->new_from_list($ids);
- }
- return $self->{milestones};
+ $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, $params) = @_;
- $params ||= {};
-
- 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, %$params });
-
- 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, $params) = @_;
+ $params ||= {};
+
+ 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, %$params});
+
+ 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'};
}
###############################
@@ -832,29 +896,29 @@ sub classification {
###############################
sub allows_unconfirmed { return $_[0]->{'allows_unconfirmed'}; }
-sub description { return $_[0]->{'description'}; }
-sub is_active { return $_[0]->{'isactive'}; }
-sub default_milestone { return $_[0]->{'defaultmilestone'}; }
-sub classification_id { return $_[0]->{'classification_id'}; }
+sub description { return $_[0]->{'description'}; }
+sub is_active { return $_[0]->{'isactive'}; }
+sub default_milestone { return $_[0]->{'defaultmilestone'}; }
+sub classification_id { return $_[0]->{'classification_id'}; }
###############################
#### Subroutines ######
###############################
sub check {
- my ($class, $params) = @_;
- $params = { name => $params } if !ref $params;
- if (!$params->{allow_inaccessible}) {
- $params->{_error} = 'product_access_denied';
- }
- my $product = $class->SUPER::check($params);
-
- if (!$params->{allow_inaccessible}
- && !Bugzilla->user->can_access_product($product))
- {
- ThrowUserError('product_access_denied', $params);
- }
- return $product;
+ my ($class, $params) = @_;
+ $params = {name => $params} if !ref $params;
+ if (!$params->{allow_inaccessible}) {
+ $params->{_error} = 'product_access_denied';
+ }
+ my $product = $class->SUPER::check($params);
+
+ if ( !$params->{allow_inaccessible}
+ && !Bugzilla->user->can_access_product($product))
+ {
+ ThrowUserError('product_access_denied', $params);
+ }
+ return $product;
}
1;
diff --git a/Bugzilla/Quantum.pm b/Bugzilla/Quantum.pm
index 4fddb8da9..1638f8c9a 100644
--- a/Bugzilla/Quantum.pm
+++ b/Bugzilla/Quantum.pm
@@ -96,8 +96,7 @@ sub setup_routes {
my $r = $self->routes;
Bugzilla::Quantum::CGI->load_all($r);
- Bugzilla::Quantum::CGI->load_one('bzapi_cgi',
- 'extensions/BzAPI/bin/rest.cgi');
+ Bugzilla::Quantum::CGI->load_one('bzapi_cgi', 'extensions/BzAPI/bin/rest.cgi');
$r->get('/home')->to('Home#index');
$r->any('/')->to('CGI#index_cgi');
@@ -118,8 +117,8 @@ sub setup_routes {
$r->any('/bzapi/*PATH_INFO')->to('CGI#bzapi_cgi');
$r->static_file('/__lbheartbeat__');
- $r->static_file('/__version__' =>
- {file => 'version.json', content_type => 'application/json'});
+ $r->static_file(
+ '/__version__' => {file => 'version.json', content_type => 'application/json'});
$r->static_file('/version.json', {content_type => 'application/json'});
$r->page('/review', 'splinter.html');
diff --git a/Bugzilla/Quantum/CGI.pm b/Bugzilla/Quantum/CGI.pm
index 79fbcfde6..5a654111e 100644
--- a/Bugzilla/Quantum/CGI.pm
+++ b/Bugzilla/Quantum/CGI.pm
@@ -54,8 +54,7 @@ sub load_one {
open STDIN, '<', $stdin->path
or die "STDIN @{[$stdin->path]}: $!"
if -s $stdin->path;
- tie *STDOUT, 'Bugzilla::Quantum::Stdout',
- controller => $c; ## no critic (tie)
+ tie *STDOUT, 'Bugzilla::Quantum::Stdout', controller => $c; ## no critic (tie)
# the finally block calls cleanup.
$c->stash->{cleanup_guard}->dismiss;
@@ -129,19 +128,17 @@ sub _ENV {
GATEWAY_INTERFACE => 'CGI/1.1',
HTTPS => $req->is_secure ? 'on' : 'off',
%env_headers,
- QUERY_STRING => $cgi_query->to_string,
- PATH_INFO => $path_info ? "/$path_info" : '',
- REMOTE_ADDR => $tx->original_remote_address,
- REMOTE_HOST => $tx->original_remote_address,
- REMOTE_PORT => $tx->remote_port,
- REMOTE_USER => $remote_user || '',
- REQUEST_METHOD => $req->method,
- SCRIPT_NAME => "$prefix$script_name",
- SERVER_NAME => hostname,
- SERVER_PORT => $tx->local_port,
- SERVER_PROTOCOL => $req->is_secure
- ? 'HTTPS'
- : 'HTTP', # TODO: Version is missing
+ QUERY_STRING => $cgi_query->to_string,
+ PATH_INFO => $path_info ? "/$path_info" : '',
+ REMOTE_ADDR => $tx->original_remote_address,
+ REMOTE_HOST => $tx->original_remote_address,
+ REMOTE_PORT => $tx->remote_port,
+ REMOTE_USER => $remote_user || '',
+ REQUEST_METHOD => $req->method,
+ SCRIPT_NAME => "$prefix$script_name",
+ SERVER_NAME => hostname,
+ SERVER_PORT => $tx->local_port,
+ SERVER_PROTOCOL => $req->is_secure ? 'HTTPS' : 'HTTP', # TODO: Version is missing
SERVER_SOFTWARE => __PACKAGE__,
);
}
diff --git a/Bugzilla/Quantum/Home.pm b/Bugzilla/Quantum/Home.pm
index 48d5e47bd..6a3021f64 100644
--- a/Bugzilla/Quantum/Home.pm
+++ b/Bugzilla/Quantum/Home.pm
@@ -16,8 +16,7 @@ sub index {
my ($c) = @_;
$c->bugzilla->login(LOGIN_REQUIRED) or return;
try {
- ThrowUserError('invalid_username', {login => 'batman'})
- if $c->param('error');
+ ThrowUserError('invalid_username', {login => 'batman'}) if $c->param('error');
$c->render(handler => 'bugzilla', template => 'index');
}
catch {
diff --git a/Bugzilla/Quantum/Plugin/BasicAuth.pm b/Bugzilla/Quantum/Plugin/BasicAuth.pm
index e17273404..e0d4e8ecc 100644
--- a/Bugzilla/Quantum/Plugin/BasicAuth.pm
+++ b/Bugzilla/Quantum/Plugin/BasicAuth.pm
@@ -12,29 +12,29 @@ use Bugzilla::Logging;
use Carp;
sub register {
- my ( $self, $app, $conf ) = @_;
+ my ($self, $app, $conf) = @_;
- $app->renderer->add_helper(
- basic_auth => sub {
- my ( $c, $realm, $auth_user, $auth_pass ) = @_;
- my $req = $c->req;
- my ( $user, $password ) = $req->url->to_abs->userinfo =~ /^([^:]+):(.*)/;
+ $app->renderer->add_helper(
+ basic_auth => sub {
+ my ($c, $realm, $auth_user, $auth_pass) = @_;
+ my $req = $c->req;
+ my ($user, $password) = $req->url->to_abs->userinfo =~ /^([^:]+):(.*)/;
- unless ( $realm && $auth_user && $auth_pass ) {
- croak 'basic_auth() called with missing parameters.';
- }
+ unless ($realm && $auth_user && $auth_pass) {
+ croak 'basic_auth() called with missing parameters.';
+ }
- unless ( $user eq $auth_user && $password eq $auth_pass ) {
- WARN('username and password do not match');
- $c->res->headers->www_authenticate("Basic realm=\"$realm\"");
- $c->res->code(401);
- $c->rendered;
- return 0;
- }
+ unless ($user eq $auth_user && $password eq $auth_pass) {
+ WARN('username and password do not match');
+ $c->res->headers->www_authenticate("Basic realm=\"$realm\"");
+ $c->res->code(401);
+ $c->rendered;
+ return 0;
+ }
- return 1;
- }
- );
+ return 1;
+ }
+ );
}
-1; \ No newline at end of file
+1;
diff --git a/Bugzilla/Quantum/Plugin/Glue.pm b/Bugzilla/Quantum/Plugin/Glue.pm
index f04b9c025..de016356c 100644
--- a/Bugzilla/Quantum/Plugin/Glue.pm
+++ b/Bugzilla/Quantum/Plugin/Glue.pm
@@ -157,8 +157,7 @@ sub register {
);
$app->log(MojoX::Log::Log4perl::Tiny->new(
- logger => Log::Log4perl->get_logger(ref $app)
- ));
+ logger => Log::Log4perl->get_logger(ref $app)));
}
1;
diff --git a/Bugzilla/Quantum/Plugin/Hostage.pm b/Bugzilla/Quantum/Plugin/Hostage.pm
index 418b09a0c..df3e40ec1 100644
--- a/Bugzilla/Quantum/Plugin/Hostage.pm
+++ b/Bugzilla/Quantum/Plugin/Hostage.pm
@@ -3,78 +3,77 @@ use 5.10.1;
use Mojo::Base 'Mojolicious::Plugin';
sub _attachment_root {
- my ($base) = @_;
- return undef unless $base;
- return $base =~ m{^https?://(?:bug)?\%bugid\%\.([a-zA-Z\.-]+)}
- ? $1
- : undef;
+ my ($base) = @_;
+ return undef unless $base;
+ return $base =~ m{^https?://(?:bug)?\%bugid\%\.([a-zA-Z\.-]+)} ? $1 : undef;
}
sub _attachment_host_regex {
- my ($base) = @_;
- return undef unless $base;
- my $val = $base;
- $val =~ s{^https?://}{}s;
- $val =~ s{/$}{}s;
- my $regex = quotemeta $val;
- $regex =~ s/\\\%bugid\\\%/\\d+/g;
- return qr/^$regex$/s;
+ my ($base) = @_;
+ return undef unless $base;
+ my $val = $base;
+ $val =~ s{^https?://}{}s;
+ $val =~ s{/$}{}s;
+ my $regex = quotemeta $val;
+ $regex =~ s/\\\%bugid\\\%/\\d+/g;
+ return qr/^$regex$/s;
}
sub register {
- my ( $self, $app, $conf ) = @_;
+ my ($self, $app, $conf) = @_;
- $app->hook(before_routes => \&_before_routes);
+ $app->hook(before_routes => \&_before_routes);
}
sub _before_routes {
- my ( $c ) = @_;
- state $urlbase = Bugzilla->localconfig->{urlbase};
- state $urlbase_uri = URI->new($urlbase);
- state $urlbase_host = $urlbase_uri->host;
- state $urlbase_host_regex = qr/^bug(\d+)\.\Q$urlbase_host\E$/;
- state $attachment_base = Bugzilla->localconfig->{attachment_base};
- state $attachment_root = _attachment_root($attachment_base);
- state $attachment_host_regex = _attachment_host_regex($attachment_base);
+ my ($c) = @_;
+ state $urlbase = Bugzilla->localconfig->{urlbase};
+ state $urlbase_uri = URI->new($urlbase);
+ state $urlbase_host = $urlbase_uri->host;
+ state $urlbase_host_regex = qr/^bug(\d+)\.\Q$urlbase_host\E$/;
+ state $attachment_base = Bugzilla->localconfig->{attachment_base};
+ state $attachment_root = _attachment_root($attachment_base);
+ state $attachment_host_regex = _attachment_host_regex($attachment_base);
- my $stash = $c->stash;
- my $req = $c->req;
- my $url = $req->url->to_abs;
+ my $stash = $c->stash;
+ my $req = $c->req;
+ my $url = $req->url->to_abs;
- return if $stash->{'mojo.static'};
+ return if $stash->{'mojo.static'};
- my $hostname = $url->host;
- return if $hostname eq $urlbase_host;
+ my $hostname = $url->host;
+ return if $hostname eq $urlbase_host;
- my $path = $url->path;
- return if $path eq '/__lbheartbeat__';
+ my $path = $url->path;
+ return if $path eq '/__lbheartbeat__';
- if ($attachment_base && $hostname eq $attachment_root) {
- $c->redirect_to($urlbase);
- return;
- }
- elsif ($attachment_base && $hostname =~ $attachment_host_regex) {
- if ($path =~ m{^/attachment\.cgi}s) {
- return;
- } else {
- my $new_uri = $url->clone;
- $new_uri->scheme($urlbase_uri->scheme);
- $new_uri->host($urlbase_host);
- $c->redirect_to($new_uri);
- return;
- }
- }
- elsif (my ($id) = $hostname =~ $urlbase_host_regex) {
- my $new_uri = $urlbase_uri->clone;
- $new_uri->path('/show_bug.cgi');
- $new_uri->query_form(id => $id);
- $c->redirect_to($new_uri);
- return;
+ if ($attachment_base && $hostname eq $attachment_root) {
+ $c->redirect_to($urlbase);
+ return;
+ }
+ elsif ($attachment_base && $hostname =~ $attachment_host_regex) {
+ if ($path =~ m{^/attachment\.cgi}s) {
+ return;
}
else {
- $c->redirect_to($urlbase);
- return;
+ my $new_uri = $url->clone;
+ $new_uri->scheme($urlbase_uri->scheme);
+ $new_uri->host($urlbase_host);
+ $c->redirect_to($new_uri);
+ return;
}
+ }
+ elsif (my ($id) = $hostname =~ $urlbase_host_regex) {
+ my $new_uri = $urlbase_uri->clone;
+ $new_uri->path('/show_bug.cgi');
+ $new_uri->query_form(id => $id);
+ $c->redirect_to($new_uri);
+ return;
+ }
+ else {
+ $c->redirect_to($urlbase);
+ return;
+ }
}
1;
diff --git a/Bugzilla/Quantum/SES.pm b/Bugzilla/Quantum/SES.pm
index 03916075d..9d2149978 100644
--- a/Bugzilla/Quantum/SES.pm
+++ b/Bugzilla/Quantum/SES.pm
@@ -1,4 +1,5 @@
package Bugzilla::Quantum::SES;
+
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
@@ -22,233 +23,228 @@ use Types::Standard qw( :all );
use Type::Utils;
use Type::Params qw( compile );
-my $Invocant = class_type { class => __PACKAGE__ };
+my $Invocant = class_type {class => __PACKAGE__};
sub main {
- my ($self) = @_;
- try {
- $self->_main;
- }
- catch {
- FATAL("Error in SES Handler: ", $_);
- $self->_respond( 400 => 'Bad Request' );
- };
+ my ($self) = @_;
+ try {
+ $self->_main;
+ }
+ catch {
+ FATAL("Error in SES Handler: ", $_);
+ $self->_respond(400 => 'Bad Request');
+ };
}
sub _main {
- my ($self) = @_;
- Bugzilla->error_mode(ERROR_MODE_DIE);
- my $message = $self->_decode_json_wrapper( $self->req->body ) // return;
- my $message_type = $self->req->headers->header('X-Amz-SNS-Message-Type') // '(missing)';
-
- if ( $message_type eq 'SubscriptionConfirmation' ) {
- $self->_confirm_subscription($message);
+ my ($self) = @_;
+ Bugzilla->error_mode(ERROR_MODE_DIE);
+ my $message = $self->_decode_json_wrapper($self->req->body) // return;
+ my $message_type = $self->req->headers->header('X-Amz-SNS-Message-Type')
+ // '(missing)';
+
+ if ($message_type eq 'SubscriptionConfirmation') {
+ $self->_confirm_subscription($message);
+ }
+
+ elsif ($message_type eq 'Notification') {
+ my $notification = $self->_decode_json_wrapper($message->{Message}) // return;
+ unless (
+# https://docs.aws.amazon.com/ses/latest/DeveloperGuide/event-publishing-retrieving-sns-contents.html
+ $self->_handle_notification($notification, 'eventType')
+
+ # https://docs.aws.amazon.com/ses/latest/DeveloperGuide/notification-contents.html
+ || $self->_handle_notification($notification, 'notificationType')
+ )
+ {
+ WARN('Failed to find notification type');
+ $self->_respond(400 => 'Bad Request');
}
+ }
- elsif ( $message_type eq 'Notification' ) {
- my $notification = $self->_decode_json_wrapper( $message->{Message} ) // return;
- unless (
- # https://docs.aws.amazon.com/ses/latest/DeveloperGuide/event-publishing-retrieving-sns-contents.html
- $self->_handle_notification( $notification, 'eventType' )
-
- # https://docs.aws.amazon.com/ses/latest/DeveloperGuide/notification-contents.html
- || $self->_handle_notification( $notification, 'notificationType' )
- )
- {
- WARN('Failed to find notification type');
- $self->_respond( 400 => 'Bad Request' );
- }
- }
-
- else {
- WARN("Unsupported message-type: $message_type");
- $self->_respond( 200 => 'OK' );
- }
+ else {
+ WARN("Unsupported message-type: $message_type");
+ $self->_respond(200 => 'OK');
+ }
}
sub _confirm_subscription {
- state $check = compile($Invocant, Dict[SubscribeURL => Str, slurpy Any]);
- my ($self, $message) = $check->(@_);
-
- my $subscribe_url = $message->{SubscribeURL};
- if ( !$subscribe_url ) {
- WARN('Bad SubscriptionConfirmation request: missing SubscribeURL');
- $self->_respond( 400 => 'Bad Request' );
- return;
- }
-
- my $ua = ua();
- my $res = $ua->get( $message->{SubscribeURL} );
- if ( !$res->is_success ) {
- WARN( 'Bad response from SubscribeURL: ' . $res->status_line );
- $self->_respond( 400 => 'Bad Request' );
- return;
- }
-
- $self->_respond( 200 => 'OK' );
+ state $check = compile($Invocant, Dict [SubscribeURL => Str, slurpy Any]);
+ my ($self, $message) = $check->(@_);
+
+ my $subscribe_url = $message->{SubscribeURL};
+ if (!$subscribe_url) {
+ WARN('Bad SubscriptionConfirmation request: missing SubscribeURL');
+ $self->_respond(400 => 'Bad Request');
+ return;
+ }
+
+ my $ua = ua();
+ my $res = $ua->get($message->{SubscribeURL});
+ if (!$res->is_success) {
+ WARN('Bad response from SubscribeURL: ' . $res->status_line);
+ $self->_respond(400 => 'Bad Request');
+ return;
+ }
+
+ $self->_respond(200 => 'OK');
}
my $NotificationType = Enum [qw( Bounce Complaint )];
my $TypeField = Enum [qw(eventType notificationType)];
-my $Notification = Dict [
- eventType => Optional [$NotificationType],
- notificationType => Optional [$NotificationType],
- slurpy Any,
+my $Notification = Dict [
+ eventType => Optional [$NotificationType],
+ notificationType => Optional [$NotificationType],
+ slurpy Any,
];
sub _handle_notification {
- state $check = compile($Invocant, $Notification, $TypeField );
- my ( $self, $notification, $type_field ) = $check->(@_);
-
- if ( !exists $notification->{$type_field} ) {
- return 0;
- }
- my $type = $notification->{$type_field};
-
- if ( $type eq 'Bounce' ) {
- $self->_process_bounce($notification);
- }
- elsif ( $type eq 'Complaint' ) {
- $self->_process_complaint($notification);
- }
- else {
- WARN("Unsupported notification-type: $type");
- $self->_respond( 200 => 'OK' );
- }
- return 1;
+ state $check = compile($Invocant, $Notification, $TypeField);
+ my ($self, $notification, $type_field) = $check->(@_);
+
+ if (!exists $notification->{$type_field}) {
+ return 0;
+ }
+ my $type = $notification->{$type_field};
+
+ if ($type eq 'Bounce') {
+ $self->_process_bounce($notification);
+ }
+ elsif ($type eq 'Complaint') {
+ $self->_process_complaint($notification);
+ }
+ else {
+ WARN("Unsupported notification-type: $type");
+ $self->_respond(200 => 'OK');
+ }
+ return 1;
}
-my $BouncedRecipients = ArrayRef[
- Dict[
- emailAddress => Str,
- action => Str,
- diagnosticCode => Str,
- slurpy Any,
- ],
+my $BouncedRecipients = ArrayRef [
+ Dict [emailAddress => Str, action => Str, diagnosticCode => Str, slurpy Any,],
];
my $BounceNotification = Dict [
- bounce => Dict [
- bouncedRecipients => $BouncedRecipients,
- reportingMTA => Str,
- bounceSubType => Str,
- bounceType => Str,
- slurpy Any,
- ],
+ bounce => Dict [
+ bouncedRecipients => $BouncedRecipients,
+ reportingMTA => Str,
+ bounceSubType => Str,
+ bounceType => Str,
slurpy Any,
+ ],
+ slurpy Any,
];
sub _process_bounce {
- state $check = compile($Invocant, $BounceNotification);
- my ($self, $notification) = $check->(@_);
-
- # disable each account that is bouncing
- foreach my $recipient ( @{ $notification->{bounce}->{bouncedRecipients} } ) {
- my $address = $recipient->{emailAddress};
- my $reason = sprintf '(%s) %s', $recipient->{action} // 'error', $recipient->{diagnosticCode} // 'unknown';
-
- my $user = Bugzilla::User->new( { name => $address, cache => 1 } );
- if ($user) {
-
- # never auto-disable admin accounts
- if ( $user->in_group('admin') ) {
- Bugzilla->audit("ignoring bounce for admin <$address>: $reason");
- }
-
- else {
- my $template = Bugzilla->template_inner();
- my $vars = {
- mta => $notification->{bounce}->{reportingMTA} // 'unknown',
- reason => $reason,
- };
- my $disable_text;
- $template->process( 'admin/users/bounce-disabled.txt.tmpl', $vars, \$disable_text )
- || die $template->error();
-
- $user->set_disabledtext($disable_text);
- $user->set_disable_mail(1);
- $user->update();
- Bugzilla->audit( "bounce for <$address> disabled userid-" . $user->id . ": $reason" );
- }
- }
-
- else {
- Bugzilla->audit("bounce for <$address> has no user: $reason");
- }
+ state $check = compile($Invocant, $BounceNotification);
+ my ($self, $notification) = $check->(@_);
+
+ # disable each account that is bouncing
+ foreach my $recipient (@{$notification->{bounce}->{bouncedRecipients}}) {
+ my $address = $recipient->{emailAddress};
+ my $reason = sprintf '(%s) %s', $recipient->{action} // 'error',
+ $recipient->{diagnosticCode} // 'unknown';
+
+ my $user = Bugzilla::User->new({name => $address, cache => 1});
+ if ($user) {
+
+ # never auto-disable admin accounts
+ if ($user->in_group('admin')) {
+ Bugzilla->audit("ignoring bounce for admin <$address>: $reason");
+ }
+
+ else {
+ my $template = Bugzilla->template_inner();
+ my $vars = {
+ mta => $notification->{bounce}->{reportingMTA} // 'unknown',
+ reason => $reason,
+ };
+ my $disable_text;
+ $template->process('admin/users/bounce-disabled.txt.tmpl',
+ $vars, \$disable_text)
+ || die $template->error();
+
+ $user->set_disabledtext($disable_text);
+ $user->set_disable_mail(1);
+ $user->update();
+ Bugzilla->audit(
+ "bounce for <$address> disabled userid-" . $user->id . ": $reason");
+ }
+ }
+
+ else {
+ Bugzilla->audit("bounce for <$address> has no user: $reason");
}
+ }
- $self->_respond( 200 => 'OK' );
+ $self->_respond(200 => 'OK');
}
-my $ComplainedRecipients = ArrayRef[Dict[ emailAddress => Str, slurpy Any ]];
-my $ComplaintNotification = Dict[
- complaint => Dict [
- complainedRecipients => $ComplainedRecipients,
- complaintFeedbackType => Str,
- slurpy Any,
- ],
+my $ComplainedRecipients = ArrayRef [Dict [emailAddress => Str, slurpy Any]];
+my $ComplaintNotification = Dict [
+ complaint => Dict [
+ complainedRecipients => $ComplainedRecipients,
+ complaintFeedbackType => Str,
slurpy Any,
+ ],
+ slurpy Any,
];
sub _process_complaint {
- state $check = compile($Invocant, $ComplaintNotification);
- my ($self, $notification) = $check->(@_);
- my $template = Bugzilla->template_inner();
- my $json = JSON::MaybeXS->new(
- pretty => 1,
- utf8 => 1,
- canonical => 1,
- );
-
- foreach my $recipient ( @{ $notification->{complaint}->{complainedRecipients} } ) {
- my $reason = $notification->{complaint}->{complaintFeedbackType} // 'unknown';
- my $address = $recipient->{emailAddress};
- Bugzilla->audit("complaint for <$address> for '$reason'");
- my $vars = {
- email => $address,
- user => Bugzilla::User->new( { name => $address, cache => 1 } ),
- reason => $reason,
- notification => $json->encode($notification),
- };
- my $message;
- $template->process( 'email/ses-complaint.txt.tmpl', $vars, \$message )
- || die $template->error();
- MessageToMTA($message);
- }
+ state $check = compile($Invocant, $ComplaintNotification);
+ my ($self, $notification) = $check->(@_);
+ my $template = Bugzilla->template_inner();
+ my $json = JSON::MaybeXS->new(pretty => 1, utf8 => 1, canonical => 1,);
+
+ foreach my $recipient (@{$notification->{complaint}->{complainedRecipients}}) {
+ my $reason = $notification->{complaint}->{complaintFeedbackType} // 'unknown';
+ my $address = $recipient->{emailAddress};
+ Bugzilla->audit("complaint for <$address> for '$reason'");
+ my $vars = {
+ email => $address,
+ user => Bugzilla::User->new({name => $address, cache => 1}),
+ reason => $reason,
+ notification => $json->encode($notification),
+ };
+ my $message;
+ $template->process('email/ses-complaint.txt.tmpl', $vars, \$message)
+ || die $template->error();
+ MessageToMTA($message);
+ }
- $self->_respond( 200 => 'OK' );
+ $self->_respond(200 => 'OK');
}
sub _respond {
- my ( $self, $code, $message ) = @_;
- $self->render(text => "$message\n", status => $code);
+ my ($self, $code, $message) = @_;
+ $self->render(text => "$message\n", status => $code);
}
sub _decode_json_wrapper {
- state $check = compile($Invocant, Str);
- my ($self, $json) = $check->(@_);
- my $result;
- my $ok = try {
- $result = decode_json($json);
- }
- catch {
- WARN( 'Malformed JSON from ' . $self->tx->remote_address );
- $self->_respond( 400 => 'Bad Request' );
- return undef;
- };
- return $ok ? $result : undef;
+ state $check = compile($Invocant, Str);
+ my ($self, $json) = $check->(@_);
+ my $result;
+ my $ok = try {
+ $result = decode_json($json);
+ }
+ catch {
+ WARN('Malformed JSON from ' . $self->tx->remote_address);
+ $self->_respond(400 => 'Bad Request');
+ return undef;
+ };
+ return $ok ? $result : undef;
}
sub ua {
- my $ua = LWP::UserAgent->new();
- $ua->timeout(10);
- $ua->protocols_allowed( [ 'http', 'https' ] );
- if ( my $proxy_url = Bugzilla->params->{'proxy_url'} ) {
- $ua->proxy( [ 'http', 'https' ], $proxy_url );
- }
- else {
- $ua->env_proxy;
- }
- return $ua;
+ my $ua = LWP::UserAgent->new();
+ $ua->timeout(10);
+ $ua->protocols_allowed(['http', 'https']);
+ if (my $proxy_url = Bugzilla->params->{'proxy_url'}) {
+ $ua->proxy(['http', 'https'], $proxy_url);
+ }
+ else {
+ $ua->env_proxy;
+ }
+ return $ua;
}
1;
diff --git a/Bugzilla/RNG.pm b/Bugzilla/RNG.pm
index cc2dfc58c..b3b89a0ca 100644
--- a/Bugzilla/RNG.pm
+++ b/Bugzilla/RNG.pm
@@ -28,7 +28,7 @@ our @EXPORT_OK = qw(rand srand irand);
use constant DIVIDE_BY => 2**32;
# How many bytes of seed to read.
-use constant SEED_SIZE => 16; # 128 bits.
+use constant SEED_SIZE => 16; # 128 bits.
#################
# Windows Stuff #
@@ -43,6 +43,7 @@ use constant PROV_RSA_FULL => 1;
# Flags for CryptGenRandom:
# Don't ever display a UI to the user, just fail if one would be needed.
use constant CRYPT_SILENT => 64;
+
# Don't require existing public/private keypairs.
use constant CRYPT_VERIFYCONTEXT => 0xF0000000;
@@ -60,40 +61,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;
}
##########################
@@ -101,123 +104,123 @@ sub _to_float {
##########################
sub _create_rng {
- my ($params) = @_;
+ my ($params) = @_;
- if (!defined $params->{seed}) {
- $params->{seed} = _get_seed();
- }
+ if (!defined $params->{seed}) {
+ $params->{seed} = _get_seed();
+ }
- _check_seed($params->{seed});
+ _check_seed($params->{seed});
- my @seed_ints = unpack('L*', $params->{seed});
+ my @seed_ints = unpack('L*', $params->{seed});
- my $rng = Math::Random::ISAAC->new(@seed_ints);
+ my $rng = Math::Random::ISAAC->new(@seed_ints);
- # It's faster to skip the frontend interface of Math::Random::ISAAC
- # and just use the backend directly. However, in case the internal
- # code of Math::Random::ISAAC changes at some point, we do make sure
- # that the {backend} element actually exists first.
- return $rng->{backend} ? $rng->{backend} : $rng;
+ # It's faster to skip the frontend interface of Math::Random::ISAAC
+ # and just use the backend directly. However, in case the internal
+ # code of Math::Random::ISAAC changes at some point, we do make sure
+ # that the {backend} element actually exists first.
+ return $rng->{backend} ? $rng->{backend} : $rng;
}
sub _check_seed {
- my ($seed) = @_;
- if (length($seed) < 8) {
- warn "Your seed is less than 8 bytes (64 bits). It could be"
- . " easy to crack";
- }
- # If it looks like we were seeded with a 32-bit integer, warn the
- # user that they are making a dangerous, easily-crackable mistake.
- elsif (length($seed) <= 10 and $seed =~ /^\d+$/) {
- warn "RNG seeded with a 32-bit integer, this is easy to crack";
- }
+ my ($seed) = @_;
+ if (length($seed) < 8) {
+ warn "Your seed is less than 8 bytes (64 bits). It could be" . " easy to crack";
+ }
+
+ # If it looks like we were seeded with a 32-bit integer, warn the
+ # user that they are making a dangerous, easily-crackable mistake.
+ elsif (length($seed) <= 10 and $seed =~ /^\d+$/) {
+ warn "RNG seeded with a 32-bit integer, this is easy to crack";
+ }
}
sub _get_seed {
- return _windows_seed() if ON_WINDOWS;
+ return _windows_seed() if ON_WINDOWS;
- if (-r '/dev/urandom') {
- return _read_seed_from('/dev/urandom');
- }
+ if (-r '/dev/urandom') {
+ return _read_seed_from('/dev/urandom');
+ }
- return _read_seed_from('/dev/random');
+ return _read_seed_from('/dev/random');
}
sub _read_seed_from {
- my ($from) = @_;
-
- my $fh = IO::File->new($from, "r") or die "$from: $!";
- my $buffer;
- $fh->read($buffer, SEED_SIZE);
- if (length($buffer) < SEED_SIZE) {
- die "Could not read enough seed bytes from $from, got only "
- . length($buffer);
- }
- $fh->close;
- return $buffer;
+ my ($from) = @_;
+
+ my $fh = IO::File->new($from, "r") or die "$from: $!";
+ my $buffer;
+ $fh->read($buffer, SEED_SIZE);
+ if (length($buffer) < SEED_SIZE) {
+ die "Could not read enough seed bytes from $from, got only " . length($buffer);
+ }
+ $fh->close;
+ return $buffer;
}
sub _windows_seed {
- my ($major, $minor) = (Win32::GetOSVersion())[1,2];
- if ($major < 5) {
- die "Bugzilla does not support versions of Windows before"
- . " Windows 2000";
- }
- # This means Windows 2000.
- if ($major == 5 and $minor == 0) {
- return _win2k_seed();
- }
-
- my $rtlgenrand = Win32::API->new('advapi32', RTLGENRANDOM_PROTO);
- if (!defined $rtlgenrand) {
- die "Could not import RtlGenRand: $^E";
- }
- my $buffer = chr(0) x SEED_SIZE;
- my $result = $rtlgenrand->Call($buffer, SEED_SIZE);
- if (!$result) {
- die "RtlGenRand failed: $^E";
- }
- return $buffer;
+ my ($major, $minor) = (Win32::GetOSVersion())[1, 2];
+ if ($major < 5) {
+ die "Bugzilla does not support versions of Windows before" . " Windows 2000";
+ }
+
+ # This means Windows 2000.
+ if ($major == 5 and $minor == 0) {
+ return _win2k_seed();
+ }
+
+ my $rtlgenrand = Win32::API->new('advapi32', RTLGENRANDOM_PROTO);
+ if (!defined $rtlgenrand) {
+ die "Could not import RtlGenRand: $^E";
+ }
+ my $buffer = chr(0) x SEED_SIZE;
+ my $result = $rtlgenrand->Call($buffer, SEED_SIZE);
+ if (!$result) {
+ die "RtlGenRand failed: $^E";
+ }
+ return $buffer;
}
sub _win2k_seed {
- my $crypt_acquire = Win32::API->new(
- "advapi32", 'CryptAcquireContext', 'PPPNN', 'I');
- if (!defined $crypt_acquire) {
- die "Could not import CryptAcquireContext: $^E";
- }
-
- my $crypt_release = Win32::API->new(
- "advapi32", 'CryptReleaseContext', 'NN', 'I');
- if (!defined $crypt_release) {
- die "Could not import CryptReleaseContext: $^E";
- }
-
- my $crypt_gen_random = Win32::API->new(
- "advapi32", 'CryptGenRandom', 'NNP', 'I');
- if (!defined $crypt_gen_random) {
- die "Could not import CryptGenRandom: $^E";
- }
-
- my $context = chr(0) x Win32::API::Type->sizeof('PULONG');
- my $acquire_result = $crypt_acquire->Call(
- $context, 0, 0, PROV_RSA_FULL, CRYPT_SILENT | CRYPT_VERIFYCONTEXT);
- if (!defined $acquire_result) {
- die "CryptAcquireContext failed: $^E";
- }
-
- my $pack_type = Win32::API::Type::packing('PULONG');
- $context = unpack($pack_type, $context);
-
- my $buffer = chr(0) x SEED_SIZE;
- my $rand_result = $crypt_gen_random->Call($context, SEED_SIZE, $buffer);
- my $rand_error = $^E;
- # We don't check this if it fails, we don't care.
- $crypt_release->Call($context, 0);
- if (!defined $rand_result) {
- die "CryptGenRandom failed: $rand_error";
- }
- return $buffer;
+ my $crypt_acquire
+ = Win32::API->new("advapi32", 'CryptAcquireContext', 'PPPNN', 'I');
+ if (!defined $crypt_acquire) {
+ die "Could not import CryptAcquireContext: $^E";
+ }
+
+ my $crypt_release
+ = Win32::API->new("advapi32", 'CryptReleaseContext', 'NN', 'I');
+ if (!defined $crypt_release) {
+ die "Could not import CryptReleaseContext: $^E";
+ }
+
+ my $crypt_gen_random
+ = Win32::API->new("advapi32", 'CryptGenRandom', 'NNP', 'I');
+ if (!defined $crypt_gen_random) {
+ die "Could not import CryptGenRandom: $^E";
+ }
+
+ my $context = chr(0) x Win32::API::Type->sizeof('PULONG');
+ my $acquire_result = $crypt_acquire->Call($context, 0, 0, PROV_RSA_FULL,
+ CRYPT_SILENT | CRYPT_VERIFYCONTEXT);
+ if (!defined $acquire_result) {
+ die "CryptAcquireContext failed: $^E";
+ }
+
+ my $pack_type = Win32::API::Type::packing('PULONG');
+ $context = unpack($pack_type, $context);
+
+ my $buffer = chr(0) x SEED_SIZE;
+ my $rand_result = $crypt_gen_random->Call($context, SEED_SIZE, $buffer);
+ my $rand_error = $^E;
+
+ # We don't check this if it fails, we don't care.
+ $crypt_release->Call($context, 0);
+ if (!defined $rand_result) {
+ die "CryptGenRandom failed: $rand_error";
+ }
+ return $buffer;
}
1;
diff --git a/Bugzilla/Report/SecurityRisk.pm b/Bugzilla/Report/SecurityRisk.pm
index 5eb98fd7f..53a8e3224 100644
--- a/Bugzilla/Report/SecurityRisk.pm
+++ b/Bugzilla/Report/SecurityRisk.pm
@@ -21,101 +21,72 @@ use POSIX qw(ceil);
use Type::Utils;
use Types::Standard qw(Num Int Bool Str HashRef ArrayRef CodeRef Map Dict Enum);
-my $DateTime = class_type { class => 'DateTime' };
+my $DateTime = class_type {class => 'DateTime'};
-has 'start_date' => (
- is => 'ro',
- required => 1,
- isa => $DateTime,
-);
+has 'start_date' => (is => 'ro', required => 1, isa => $DateTime,);
-has 'end_date' => (
- is => 'ro',
- required => 1,
- isa => $DateTime,
-);
+has 'end_date' => (is => 'ro', required => 1, isa => $DateTime,);
-has 'products' => (
- is => 'ro',
- required => 1,
- isa => ArrayRef [Str],
-);
+has 'products' => (is => 'ro', required => 1, isa => ArrayRef [Str],);
-has 'sec_keywords' => (
- is => 'ro',
- required => 1,
- isa => ArrayRef [Str],
-);
+has 'sec_keywords' => (is => 'ro', required => 1, isa => ArrayRef [Str],);
-has 'initial_bug_ids' => (
- is => 'lazy',
- isa => ArrayRef [Int],
-);
+has 'initial_bug_ids' => (is => 'lazy', isa => ArrayRef [Int],);
has 'initial_bugs' => (
- is => 'lazy',
- isa => HashRef [
- Dict [
- id => Int,
- product => Str,
- sec_level => Str,
- is_open => Bool,
- created_at => $DateTime,
- ],
+ is => 'lazy',
+ isa => HashRef [
+ Dict [
+ id => Int,
+ product => Str,
+ sec_level => Str,
+ is_open => Bool,
+ created_at => $DateTime,
],
+ ],
);
-has 'check_open_state' => (
- is => 'ro',
- isa => CodeRef,
- default => sub { return \&is_open_state; },
-);
+has 'check_open_state' =>
+ (is => 'ro', isa => CodeRef, default => sub { return \&is_open_state; },);
has 'events' => (
- is => 'lazy',
- isa => ArrayRef [
- Dict [
- bug_id => Int,
- bug_when => $DateTime,
- field_name => Enum [qw(bug_status keywords)],
- removed => Str,
- added => Str,
- ],
+ is => 'lazy',
+ isa => ArrayRef [
+ Dict [
+ bug_id => Int,
+ bug_when => $DateTime,
+ field_name => Enum [qw(bug_status keywords)],
+ removed => Str,
+ added => Str,
],
+ ],
);
has 'results' => (
- is => 'lazy',
- isa => ArrayRef [
- Dict [
- date => $DateTime,
- bugs_by_product => HashRef [
- Dict [
- open => ArrayRef [Int],
- closed => ArrayRef [Int],
- median_age_open => Num
- ]
- ],
- bugs_by_sec_keyword => HashRef [
- Dict [
- open => ArrayRef [Int],
- closed => ArrayRef [Int],
- median_age_open => Num
- ]
- ],
- ],
+ is => 'lazy',
+ isa => ArrayRef [
+ Dict [
+ date => $DateTime,
+ bugs_by_product => HashRef [
+ Dict [open => ArrayRef [Int], closed => ArrayRef [Int], median_age_open => Num]
+ ],
+ bugs_by_sec_keyword => HashRef [
+ Dict [open => ArrayRef [Int], closed => ArrayRef [Int], median_age_open => Num]
+ ],
],
+ ],
);
sub _build_initial_bug_ids {
- # TODO: Handle changes in product (e.g. gravyarding) by searching the events table
- # for changes to the 'product' field where one of $self->products is found in
- # the 'removed' field, add the related bug id to the list of initial bugs.
- my ($self) = @_;
- my $dbh = Bugzilla->dbh;
- my $products = join ', ', map { $dbh->quote($_) } @{ $self->products };
- my $sec_keywords = join ', ', map { $dbh->quote($_) } @{ $self->sec_keywords };
- my $query = qq{
+
+# TODO: Handle changes in product (e.g. gravyarding) by searching the events table
+# for changes to the 'product' field where one of $self->products is found in
+# the 'removed' field, add the related bug id to the list of initial bugs.
+ my ($self) = @_;
+ my $dbh = Bugzilla->dbh;
+ my $products = join ', ', map { $dbh->quote($_) } @{$self->products};
+ my $sec_keywords = join ', ', map { $dbh->quote($_) } @{$self->sec_keywords};
+ my $query = qq{
SELECT
bug_id
FROM
@@ -128,39 +99,40 @@ sub _build_initial_bug_ids {
keyword.name IN ($sec_keywords)
AND product.name IN ($products)
};
- return Bugzilla->dbh->selectcol_arrayref($query);
+ return Bugzilla->dbh->selectcol_arrayref($query);
}
sub _build_initial_bugs {
- my ($self) = @_;
- my $bugs = {};
- my $bugs_list = Bugzilla::Bug->new_from_list( $self->initial_bug_ids );
- for my $bug (@$bugs_list) {
- $bugs->{ $bug->id } = {
- id => $bug->id,
- product => $bug->product,
- sec_level => (
- # Select the first keyword matching one of the target keywords
- # (of which there _should_ only be one found anyway).
- first {
- my $x = $_;
- grep { lc($_) eq lc( $x->name ) } @{ $self->sec_keywords }
- }
- @{ $bug->keyword_objects }
- )->name,
- is_open => $self->check_open_state->( $bug->status->name ),
- created_at => datetime_from( $bug->creation_ts ),
- };
- }
- return $bugs;
+ my ($self) = @_;
+ my $bugs = {};
+ my $bugs_list = Bugzilla::Bug->new_from_list($self->initial_bug_ids);
+ for my $bug (@$bugs_list) {
+ $bugs->{$bug->id} = {
+ id => $bug->id,
+ product => $bug->product,
+ sec_level => (
+
+ # Select the first keyword matching one of the target keywords
+ # (of which there _should_ only be one found anyway).
+ first {
+ my $x = $_;
+ grep { lc($_) eq lc($x->name) } @{$self->sec_keywords}
+ }
+ @{$bug->keyword_objects}
+ )->name,
+ is_open => $self->check_open_state->($bug->status->name),
+ created_at => datetime_from($bug->creation_ts),
+ };
+ }
+ return $bugs;
}
sub _build_events {
- my ($self) = @_;
- return [] if !(@{$self->initial_bug_ids});
- my $bug_ids = join ', ', @{ $self->initial_bug_ids };
- my $start_date = $self->start_date->ymd('-');
- my $query = qq{
+ my ($self) = @_;
+ return [] if !(@{$self->initial_bug_ids});
+ my $bug_ids = join ', ', @{$self->initial_bug_ids};
+ my $start_date = $self->start_date->ymd('-');
+ my $query = qq{
SELECT
bug_id,
bug_when,
@@ -177,138 +149,153 @@ sub _build_events {
AND bug_when >= '$start_date 00:00:00'
GROUP BY bug_id , bug_when , field.name
};
- my $result = Bugzilla->dbh->selectall_hashref( $query, 'bug_id' );
- my @events = values %$result;
- foreach my $event (@events) {
- $event->{bug_when} = datetime_from( $event->{bug_when} );
- }
+ my $result = Bugzilla->dbh->selectall_hashref($query, 'bug_id');
+ my @events = values %$result;
+ foreach my $event (@events) {
+ $event->{bug_when} = datetime_from($event->{bug_when});
+ }
- # We sort by reverse chronological order instead of ORDER BY
- # since values %hash doesn't guareentee any order.
- @events = sort { $b->{bug_when} cmp $a->{bug_when} } @events;
- return \@events;
+ # We sort by reverse chronological order instead of ORDER BY
+ # since values %hash doesn't guareentee any order.
+ @events = sort { $b->{bug_when} cmp $a->{bug_when} } @events;
+ return \@events;
}
sub _build_results {
- my ($self) = @_;
- my $e = 0;
- my $bugs = $self->initial_bugs;
- my @results = ();
-
- # We must generate a report for each week in the target time interval, regardless of
- # whether anything changed. The for loop here ensures that we do so.
- for ( my $report_date = $self->end_date; $report_date >= $self->start_date; $report_date->subtract( weeks => 1 ) ) {
- # We rewind events while there are still events existing which occured after the start
- # of the report week. The bugs will reflect a snapshot of how they were at the start of the week.
- # $self->events is ordered reverse chronologically, so the end of the array is the earliest event.
- while ( $e < @{ $self->events }
- && ( @{ $self->events }[$e] )->{bug_when} > $report_date )
- {
- my $event = @{ $self->events }[$e];
- my $bug = $bugs->{ $event->{bug_id} };
-
- # Undo bug status changes
- if ( $event->{field_name} eq 'bug_status' ) {
- $bug->{is_open} = $self->check_open_state->( $event->{removed} );
- }
-
- # Undo keyword changes
- if ( $event->{field_name} eq 'keywords' ) {
- my $bug_sec_level = $bug->{sec_level};
- if ( $event->{added} =~ /\b\Q$bug_sec_level\E\b/ ) {
- # If the currently set sec level was added in this event, remove it.
- $bug->{sec_level} = undef;
- }
- if ( $event->{removed} ) {
- # If a target sec keyword was removed, add the first one back.
- my $removed_sec = first { $event->{removed} =~ /\b\Q$_\E\b/ } @{ $self->sec_keywords };
- $bug->{sec_level} = $removed_sec if ($removed_sec);
- }
- }
-
- $e++;
+ my ($self) = @_;
+ my $e = 0;
+ my $bugs = $self->initial_bugs;
+ my @results = ();
+
+# We must generate a report for each week in the target time interval, regardless of
+# whether anything changed. The for loop here ensures that we do so.
+ for (
+ my $report_date = $self->end_date;
+ $report_date >= $self->start_date;
+ $report_date->subtract(weeks => 1)
+ )
+ {
+# We rewind events while there are still events existing which occured after the start
+# of the report week. The bugs will reflect a snapshot of how they were at the start of the week.
+# $self->events is ordered reverse chronologically, so the end of the array is the earliest event.
+ while ($e < @{$self->events}
+ && (@{$self->events}[$e])->{bug_when} > $report_date)
+ {
+ my $event = @{$self->events}[$e];
+ my $bug = $bugs->{$event->{bug_id}};
+
+ # Undo bug status changes
+ if ($event->{field_name} eq 'bug_status') {
+ $bug->{is_open} = $self->check_open_state->($event->{removed});
+ }
+
+ # Undo keyword changes
+ if ($event->{field_name} eq 'keywords') {
+ my $bug_sec_level = $bug->{sec_level};
+ if ($event->{added} =~ /\b\Q$bug_sec_level\E\b/) {
+
+ # If the currently set sec level was added in this event, remove it.
+ $bug->{sec_level} = undef;
}
+ if ($event->{removed}) {
- # Remove uncreated bugs
- foreach my $bug_key ( keys %$bugs ) {
- if ( $bugs->{$bug_key}->{created_at} > $report_date ) {
- delete $bugs->{$bug_key};
- }
+ # If a target sec keyword was removed, add the first one back.
+ my $removed_sec
+ = first { $event->{removed} =~ /\b\Q$_\E\b/ } @{$self->sec_keywords};
+ $bug->{sec_level} = $removed_sec if ($removed_sec);
}
+ }
- # Report!
- my $date_snapshot = $report_date->clone();
- my @bugs_snapshot = values %$bugs;
- my $result = {
- date => $date_snapshot,
- bugs_by_product => $self->_bugs_by_product( $date_snapshot, @bugs_snapshot ),
- bugs_by_sec_keyword => $self->_bugs_by_sec_keyword( $date_snapshot, @bugs_snapshot ),
- };
- push @results, $result;
+ $e++;
}
- return [reverse @results];
+ # Remove uncreated bugs
+ foreach my $bug_key (keys %$bugs) {
+ if ($bugs->{$bug_key}->{created_at} > $report_date) {
+ delete $bugs->{$bug_key};
+ }
+ }
+
+ # Report!
+ my $date_snapshot = $report_date->clone();
+ my @bugs_snapshot = values %$bugs;
+ my $result = {
+ date => $date_snapshot,
+ bugs_by_product => $self->_bugs_by_product($date_snapshot, @bugs_snapshot),
+ bugs_by_sec_keyword =>
+ $self->_bugs_by_sec_keyword($date_snapshot, @bugs_snapshot),
+ };
+ push @results, $result;
+ }
+
+ return [reverse @results];
}
sub _bugs_by_product {
- my ( $self, $report_date, @bugs ) = @_;
- my $result = {};
- my $groups = {};
- foreach my $product ( @{ $self->products } ) {
- $groups->{$product} = [];
- }
- foreach my $bug (@bugs) {
- # We skip over bugs with no sec level which can happen during event rewinding.
- if ( $bug->{sec_level} ) {
- push @{ $groups->{ $bug->{product} } }, $bug;
- }
- }
- foreach my $product ( @{ $self->products } ) {
- my @open = map { $_->{id} } grep { ( $_->{is_open} ) } @{ $groups->{$product} };
- my @closed = map { $_->{id} } grep { !( $_->{is_open} ) } @{ $groups->{$product} };
- my @ages = map { $_->{created_at}->subtract_datetime_absolute($report_date)->seconds / 86_400; }
- grep { ( $_->{is_open} ) } @{ $groups->{$product} };
- $result->{$product} = {
- open => \@open,
- closed => \@closed,
- median_age_open => @ages ? _median(@ages) : 0,
- };
+ my ($self, $report_date, @bugs) = @_;
+ my $result = {};
+ my $groups = {};
+ foreach my $product (@{$self->products}) {
+ $groups->{$product} = [];
+ }
+ foreach my $bug (@bugs) {
+
+ # We skip over bugs with no sec level which can happen during event rewinding.
+ if ($bug->{sec_level}) {
+ push @{$groups->{$bug->{product}}}, $bug;
}
+ }
+ foreach my $product (@{$self->products}) {
+ my @open = map { $_->{id} } grep { ($_->{is_open}) } @{$groups->{$product}};
+ my @closed = map { $_->{id} } grep { !($_->{is_open}) } @{$groups->{$product}};
+ my @ages = map {
+ $_->{created_at}->subtract_datetime_absolute($report_date)->seconds / 86_400;
+ } grep { ($_->{is_open}) } @{$groups->{$product}};
+ $result->{$product} = {
+ open => \@open,
+ closed => \@closed,
+ median_age_open => @ages ? _median(@ages) : 0,
+ };
+ }
- return $result;
+ return $result;
}
sub _bugs_by_sec_keyword {
- my ( $self, $report_date, @bugs ) = @_;
- my $result = {};
- my $groups = {};
- foreach my $sec_keyword ( @{ $self->sec_keywords } ) {
- $groups->{$sec_keyword} = [];
- }
- foreach my $bug (@bugs) {
- # We skip over bugs with no sec level which can happen during event rewinding.
- if ( $bug->{sec_level} ) {
- push @{ $groups->{ $bug->{sec_level} } }, $bug;
- }
- }
- foreach my $sec_keyword ( @{ $self->sec_keywords } ) {
- my @open = map { $_->{id} } grep { ( $_->{is_open} ) } @{ $groups->{$sec_keyword} };
- my @closed = map { $_->{id} } grep { !( $_->{is_open} ) } @{ $groups->{$sec_keyword} };
- my @ages = map { $_->{created_at}->subtract_datetime_absolute($report_date)->seconds / 86_400 }
- grep { ( $_->{is_open} ) } @{ $groups->{$sec_keyword} };
- $result->{$sec_keyword} = {
- open => \@open,
- closed => \@closed,
- median_age_open => @ages ? _median(@ages) : 0,
- };
+ my ($self, $report_date, @bugs) = @_;
+ my $result = {};
+ my $groups = {};
+ foreach my $sec_keyword (@{$self->sec_keywords}) {
+ $groups->{$sec_keyword} = [];
+ }
+ foreach my $bug (@bugs) {
+
+ # We skip over bugs with no sec level which can happen during event rewinding.
+ if ($bug->{sec_level}) {
+ push @{$groups->{$bug->{sec_level}}}, $bug;
}
+ }
+ foreach my $sec_keyword (@{$self->sec_keywords}) {
+ my @open = map { $_->{id} } grep { ($_->{is_open}) } @{$groups->{$sec_keyword}};
+ my @closed
+ = map { $_->{id} } grep { !($_->{is_open}) } @{$groups->{$sec_keyword}};
+ my @ages = map {
+ $_->{created_at}->subtract_datetime_absolute($report_date)->seconds / 86_400
+ } grep { ($_->{is_open}) } @{$groups->{$sec_keyword}};
+ $result->{$sec_keyword} = {
+ open => \@open,
+ closed => \@closed,
+ median_age_open => @ages ? _median(@ages) : 0,
+ };
+ }
- return $result;
+ return $result;
}
sub _median {
- # From tlm @ https://www.perlmonks.org/?node_id=474564. Jul 14, 2005
- return sum( ( sort { $a <=> $b } @_ )[ int( $#_ / 2 ), ceil( $#_ / 2 ) ] ) / 2;
+
+ # From tlm @ https://www.perlmonks.org/?node_id=474564. Jul 14, 2005
+ return sum((sort { $a <=> $b } @_)[int($#_ / 2), ceil($#_ / 2)]) / 2;
}
1;
diff --git a/Bugzilla/S3.pm b/Bugzilla/S3.pm
index 26d77562f..ceb1451fa 100644
--- a/Bugzilla/S3.pm
+++ b/Bugzilla/S3.pm
@@ -28,7 +28,7 @@ use XML::Simple;
use base qw(Class::Accessor::Fast);
__PACKAGE__->mk_accessors(
- qw(aws_access_key_id aws_secret_access_key secure ua err errstr timeout retry host)
+ qw(aws_access_key_id aws_secret_access_key secure ua err errstr timeout retry host)
);
our $VERSION = '0.45bmo';
@@ -37,148 +37,149 @@ my $METADATA_PREFIX = 'x-amz-meta-';
my $KEEP_ALIVE_CACHESIZE = 10;
sub new {
- my $class = shift;
- my $self = $class->SUPER::new(@_);
-
- die "No aws_access_key_id" unless $self->aws_access_key_id;
- die "No aws_secret_access_key" unless $self->aws_secret_access_key;
-
- $self->secure(1) if not defined $self->secure;
- $self->timeout(30) if not defined $self->timeout;
- $self->host('s3.amazonaws.com') if not defined $self->host;
-
- my $ua;
- if ($self->retry) {
- require LWP::UserAgent::Determined;
- $ua = LWP::UserAgent::Determined->new(
- keep_alive => $KEEP_ALIVE_CACHESIZE,
- requests_redirectable => [qw(GET HEAD DELETE PUT)],
- );
- $ua->timing('1,2,4,8,16,32');
- }
- else {
- $ua = LWP::UserAgent->new(
- keep_alive => $KEEP_ALIVE_CACHESIZE,
- requests_redirectable => [qw(GET HEAD DELETE PUT)],
- );
- }
-
- $ua->timeout($self->timeout);
- if (my $proxy = Bugzilla->params->{proxy_url}) {
- $ua->proxy([ 'https', 'http' ], $proxy);
- }
- $self->ua($ua);
- return $self;
+ my $class = shift;
+ my $self = $class->SUPER::new(@_);
+
+ die "No aws_access_key_id" unless $self->aws_access_key_id;
+ die "No aws_secret_access_key" unless $self->aws_secret_access_key;
+
+ $self->secure(1) if not defined $self->secure;
+ $self->timeout(30) if not defined $self->timeout;
+ $self->host('s3.amazonaws.com') if not defined $self->host;
+
+ my $ua;
+ if ($self->retry) {
+ require LWP::UserAgent::Determined;
+ $ua = LWP::UserAgent::Determined->new(
+ keep_alive => $KEEP_ALIVE_CACHESIZE,
+ requests_redirectable => [qw(GET HEAD DELETE PUT)],
+ );
+ $ua->timing('1,2,4,8,16,32');
+ }
+ else {
+ $ua = LWP::UserAgent->new(
+ keep_alive => $KEEP_ALIVE_CACHESIZE,
+ requests_redirectable => [qw(GET HEAD DELETE PUT)],
+ );
+ }
+
+ $ua->timeout($self->timeout);
+ if (my $proxy = Bugzilla->params->{proxy_url}) {
+ $ua->proxy(['https', 'http'], $proxy);
+ }
+ $self->ua($ua);
+ return $self;
}
sub bucket {
- my ($self, $bucketname) = @_;
- return Bugzilla::S3::Bucket->new({bucket => $bucketname, account => $self});
+ my ($self, $bucketname) = @_;
+ return Bugzilla::S3::Bucket->new({bucket => $bucketname, account => $self});
}
sub _validate_acl_short {
- my ($self, $policy_name) = @_;
-
- if (!grep({$policy_name eq $_}
- qw(private public-read public-read-write authenticated-read)))
- {
- croak "$policy_name is not a supported canned access policy";
- }
+ my ($self, $policy_name) = @_;
+
+ if (
+ !grep({ $policy_name eq $_ }
+ qw(private public-read public-read-write authenticated-read)))
+ {
+ croak "$policy_name is not a supported canned access policy";
+ }
}
# EU buckets must be accessed via their DNS name. This routine figures out if
# a given bucket name can be safely used as a DNS name.
sub _is_dns_bucket {
- my $bucketname = $_[0];
+ my $bucketname = $_[0];
- if (length $bucketname > 63) {
- return 0;
- }
- if (length $bucketname < 3) {
- return;
- }
- return 0 unless $bucketname =~ m{^[a-z0-9][a-z0-9.-]+$};
- my @components = split /\./, $bucketname;
- for my $c (@components) {
- return 0 if $c =~ m{^-};
- return 0 if $c =~ m{-$};
- return 0 if $c eq '';
- }
- return 1;
+ if (length $bucketname > 63) {
+ return 0;
+ }
+ if (length $bucketname < 3) {
+ return;
+ }
+ return 0 unless $bucketname =~ m{^[a-z0-9][a-z0-9.-]+$};
+ my @components = split /\./, $bucketname;
+ for my $c (@components) {
+ return 0 if $c =~ m{^-};
+ return 0 if $c =~ m{-$};
+ return 0 if $c eq '';
+ }
+ return 1;
}
# make the HTTP::Request object
sub _make_request {
- my ($self, $method, $path, $headers, $data, $metadata) = @_;
- croak 'must specify method' unless $method;
- croak 'must specify path' unless defined $path;
- $headers ||= {};
- $data = '' if not defined $data;
- $metadata ||= {};
- my $http_headers = $self->_merge_meta($headers, $metadata);
-
- $self->_add_auth_header($http_headers, $method, $path)
- unless exists $headers->{Authorization};
- my $protocol = $self->secure ? 'https' : 'http';
- my $host = $self->host;
- my $url = "$protocol://$host/$path";
- if ($path =~ m{^([^/?]+)(.*)} && _is_dns_bucket($1)) {
- $url = "$protocol://$1.$host$2";
- }
-
- my $request = HTTP::Request->new($method, $url, $http_headers);
- $request->content($data);
-
- # my $req_as = $request->as_string;
- # $req_as =~ s/[^\n\r\x20-\x7f]/?/g;
- # $req_as = substr( $req_as, 0, 1024 ) . "\n\n";
- # warn $req_as;
-
- return $request;
+ my ($self, $method, $path, $headers, $data, $metadata) = @_;
+ croak 'must specify method' unless $method;
+ croak 'must specify path' unless defined $path;
+ $headers ||= {};
+ $data = '' if not defined $data;
+ $metadata ||= {};
+ my $http_headers = $self->_merge_meta($headers, $metadata);
+
+ $self->_add_auth_header($http_headers, $method, $path)
+ unless exists $headers->{Authorization};
+ my $protocol = $self->secure ? 'https' : 'http';
+ my $host = $self->host;
+ my $url = "$protocol://$host/$path";
+ if ($path =~ m{^([^/?]+)(.*)} && _is_dns_bucket($1)) {
+ $url = "$protocol://$1.$host$2";
+ }
+
+ my $request = HTTP::Request->new($method, $url, $http_headers);
+ $request->content($data);
+
+ # my $req_as = $request->as_string;
+ # $req_as =~ s/[^\n\r\x20-\x7f]/?/g;
+ # $req_as = substr( $req_as, 0, 1024 ) . "\n\n";
+ # warn $req_as;
+
+ return $request;
}
# $self->_send_request($HTTP::Request)
# $self->_send_request(@params_to_make_request)
sub _send_request {
- my $self = shift;
- my $request;
- if (@_ == 1) {
- $request = shift;
- }
- else {
- $request = $self->_make_request(@_);
- }
+ my $self = shift;
+ my $request;
+ if (@_ == 1) {
+ $request = shift;
+ }
+ else {
+ $request = $self->_make_request(@_);
+ }
- my $response = $self->_do_http($request);
- my $content = $response->content;
+ my $response = $self->_do_http($request);
+ my $content = $response->content;
- return $content unless $response->content_type eq 'application/xml';
- return unless $content;
- return $self->_xpc_of_content($content);
+ return $content unless $response->content_type eq 'application/xml';
+ return unless $content;
+ return $self->_xpc_of_content($content);
}
# centralize all HTTP work, for debugging
sub _do_http {
- my ($self, $request, $filename) = @_;
+ my ($self, $request, $filename) = @_;
- # convenient time to reset any error conditions
- $self->err(undef);
- $self->errstr(undef);
- return $self->ua->request($request, $filename);
+ # convenient time to reset any error conditions
+ $self->err(undef);
+ $self->errstr(undef);
+ return $self->ua->request($request, $filename);
}
sub _send_request_expect_nothing {
- my $self = shift;
- my $request = $self->_make_request(@_);
+ my $self = shift;
+ my $request = $self->_make_request(@_);
- my $response = $self->_do_http($request);
- my $content = $response->content;
+ my $response = $self->_do_http($request);
+ my $content = $response->content;
- return 1 if $response->code =~ /^2\d\d$/;
+ return 1 if $response->code =~ /^2\d\d$/;
- # anything else is a failure, and we save the parsed result
- $self->_remember_errors($response->content);
- return 0;
+ # anything else is a failure, and we save the parsed result
+ $self->_remember_errors($response->content);
+ return 0;
}
# Send a HEAD request first, to find out if we'll be hit with a 307 redirect.
@@ -189,185 +190,187 @@ sub _send_request_expect_nothing {
# first time we used it. Thus, we need to probe first to find out what's going on,
# before we start sending any actual data.
sub _send_request_expect_nothing_probed {
- my $self = shift;
- my ($method, $path, $conf, $value) = @_;
- my $request = $self->_make_request('HEAD', $path);
- my $override_uri = undef;
+ my $self = shift;
+ my ($method, $path, $conf, $value) = @_;
+ my $request = $self->_make_request('HEAD', $path);
+ my $override_uri = undef;
- my $old_redirectable = $self->ua->requests_redirectable;
- $self->ua->requests_redirectable([]);
+ my $old_redirectable = $self->ua->requests_redirectable;
+ $self->ua->requests_redirectable([]);
- my $response = $self->_do_http($request);
+ my $response = $self->_do_http($request);
- if ($response->code =~ /^3/ && defined $response->header('Location')) {
- $override_uri = $response->header('Location');
- }
- $request = $self->_make_request(@_);
- $request->uri($override_uri) if defined $override_uri;
+ if ($response->code =~ /^3/ && defined $response->header('Location')) {
+ $override_uri = $response->header('Location');
+ }
+ $request = $self->_make_request(@_);
+ $request->uri($override_uri) if defined $override_uri;
- $response = $self->_do_http($request);
- $self->ua->requests_redirectable($old_redirectable);
+ $response = $self->_do_http($request);
+ $self->ua->requests_redirectable($old_redirectable);
- my $content = $response->content;
+ my $content = $response->content;
- return 1 if $response->code =~ /^2\d\d$/;
+ return 1 if $response->code =~ /^2\d\d$/;
- # anything else is a failure, and we save the parsed result
- $self->_remember_errors($response->content);
- return 0;
+ # anything else is a failure, and we save the parsed result
+ $self->_remember_errors($response->content);
+ return 0;
}
sub _check_response {
- my ($self, $response) = @_;
- return 1 if $response->code =~ /^2\d\d$/;
- $self->err("network_error");
- $self->errstr($response->status_line);
- $self->_remember_errors($response->content);
- return undef;
+ my ($self, $response) = @_;
+ return 1 if $response->code =~ /^2\d\d$/;
+ $self->err("network_error");
+ $self->errstr($response->status_line);
+ $self->_remember_errors($response->content);
+ return undef;
}
sub _croak_if_response_error {
- my ($self, $response) = @_;
- unless ($response->code =~ /^2\d\d$/) {
- $self->err("network_error");
- $self->errstr($response->status_line);
- croak "Bugzilla::S3: Amazon responded with "
- . $response->status_line . "\n";
- }
+ my ($self, $response) = @_;
+ unless ($response->code =~ /^2\d\d$/) {
+ $self->err("network_error");
+ $self->errstr($response->status_line);
+ croak "Bugzilla::S3: Amazon responded with " . $response->status_line . "\n";
+ }
}
sub _xpc_of_content {
- return XMLin($_[1], 'KeepRoot' => 1, 'SuppressEmpty' => '', 'ForceArray' => ['Contents']);
+ return XMLin(
+ $_[1],
+ 'KeepRoot' => 1,
+ 'SuppressEmpty' => '',
+ 'ForceArray' => ['Contents']
+ );
}
# returns 1 if errors were found
sub _remember_errors {
- my ($self, $src) = @_;
+ my ($self, $src) = @_;
- unless (ref $src || $src =~ m/^[[:space:]]*</) { # if not xml
- (my $code = $src) =~ s/^[[:space:]]*\([0-9]*\).*$/$1/;
- $self->err($code);
- $self->errstr($src);
- return 1;
- }
+ unless (ref $src || $src =~ m/^[[:space:]]*</) { # if not xml
+ (my $code = $src) =~ s/^[[:space:]]*\([0-9]*\).*$/$1/;
+ $self->err($code);
+ $self->errstr($src);
+ return 1;
+ }
- my $r = ref $src ? $src : $self->_xpc_of_content($src);
+ my $r = ref $src ? $src : $self->_xpc_of_content($src);
- if ($r->{Error}) {
- $self->err($r->{Error}{Code});
- $self->errstr($r->{Error}{Message});
- return 1;
- }
- return 0;
+ if ($r->{Error}) {
+ $self->err($r->{Error}{Code});
+ $self->errstr($r->{Error}{Message});
+ return 1;
+ }
+ return 0;
}
sub _add_auth_header {
- my ($self, $headers, $method, $path) = @_;
- my $aws_access_key_id = $self->aws_access_key_id;
- my $aws_secret_access_key = $self->aws_secret_access_key;
-
- if (not $headers->header('Date')) {
- $headers->header(Date => time2str(time));
- }
- my $canonical_string = $self->_canonical_string($method, $path, $headers);
- my $encoded_canonical =
- $self->_encode($aws_secret_access_key, $canonical_string);
- $headers->header(
- Authorization => "AWS $aws_access_key_id:$encoded_canonical");
+ my ($self, $headers, $method, $path) = @_;
+ my $aws_access_key_id = $self->aws_access_key_id;
+ my $aws_secret_access_key = $self->aws_secret_access_key;
+
+ if (not $headers->header('Date')) {
+ $headers->header(Date => time2str(time));
+ }
+ my $canonical_string = $self->_canonical_string($method, $path, $headers);
+ my $encoded_canonical
+ = $self->_encode($aws_secret_access_key, $canonical_string);
+ $headers->header(Authorization => "AWS $aws_access_key_id:$encoded_canonical");
}
# generates an HTTP::Headers objects given one hash that represents http
# headers to set and another hash that represents an object's metadata.
sub _merge_meta {
- my ($self, $headers, $metadata) = @_;
- $headers ||= {};
- $metadata ||= {};
-
- my $http_header = HTTP::Headers->new;
- while (my ($k, $v) = each %$headers) {
- $http_header->header($k => $v);
- }
- while (my ($k, $v) = each %$metadata) {
- $http_header->header("$METADATA_PREFIX$k" => $v);
- }
-
- return $http_header;
+ my ($self, $headers, $metadata) = @_;
+ $headers ||= {};
+ $metadata ||= {};
+
+ my $http_header = HTTP::Headers->new;
+ while (my ($k, $v) = each %$headers) {
+ $http_header->header($k => $v);
+ }
+ while (my ($k, $v) = each %$metadata) {
+ $http_header->header("$METADATA_PREFIX$k" => $v);
+ }
+
+ return $http_header;
}
# generate a canonical string for the given parameters. expires is optional and is
# only used by query string authentication.
sub _canonical_string {
- my ($self, $method, $path, $headers, $expires) = @_;
- my %interesting_headers = ();
- while (my ($key, $value) = each %$headers) {
- my $lk = lc $key;
- if ( $lk eq 'content-md5'
- or $lk eq 'content-type'
- or $lk eq 'date'
- or $lk =~ /^$AMAZON_HEADER_PREFIX/)
- {
- $interesting_headers{$lk} = trim($value);
- }
+ my ($self, $method, $path, $headers, $expires) = @_;
+ my %interesting_headers = ();
+ while (my ($key, $value) = each %$headers) {
+ my $lk = lc $key;
+ if ( $lk eq 'content-md5'
+ or $lk eq 'content-type'
+ or $lk eq 'date'
+ or $lk =~ /^$AMAZON_HEADER_PREFIX/)
+ {
+ $interesting_headers{$lk} = trim($value);
}
+ }
- # these keys get empty strings if they don't exist
- $interesting_headers{'content-type'} ||= '';
- $interesting_headers{'content-md5'} ||= '';
-
- # just in case someone used this. it's not necessary in this lib.
- $interesting_headers{'date'} = ''
- if $interesting_headers{'x-amz-date'};
-
- # if you're using expires for query string auth, then it trumps date
- # (and x-amz-date)
- $interesting_headers{'date'} = $expires if $expires;
-
- my $buf = "$method\n";
- foreach my $key (sort keys %interesting_headers) {
- if ($key =~ /^$AMAZON_HEADER_PREFIX/) {
- $buf .= "$key:$interesting_headers{$key}\n";
- }
- else {
- $buf .= "$interesting_headers{$key}\n";
- }
- }
+ # these keys get empty strings if they don't exist
+ $interesting_headers{'content-type'} ||= '';
+ $interesting_headers{'content-md5'} ||= '';
- # don't include anything after the first ? in the resource...
- $path =~ /^([^?]*)/;
- $buf .= "/$1";
+ # just in case someone used this. it's not necessary in this lib.
+ $interesting_headers{'date'} = '' if $interesting_headers{'x-amz-date'};
- # ...unless there is an acl or torrent parameter
- if ($path =~ /[&?]acl($|=|&)/) {
- $buf .= '?acl';
- }
- elsif ($path =~ /[&?]torrent($|=|&)/) {
- $buf .= '?torrent';
+ # if you're using expires for query string auth, then it trumps date
+ # (and x-amz-date)
+ $interesting_headers{'date'} = $expires if $expires;
+
+ my $buf = "$method\n";
+ foreach my $key (sort keys %interesting_headers) {
+ if ($key =~ /^$AMAZON_HEADER_PREFIX/) {
+ $buf .= "$key:$interesting_headers{$key}\n";
}
- elsif ($path =~ /[&?]location($|=|&)/) {
- $buf .= '?location';
+ else {
+ $buf .= "$interesting_headers{$key}\n";
}
-
- return $buf;
+ }
+
+ # don't include anything after the first ? in the resource...
+ $path =~ /^([^?]*)/;
+ $buf .= "/$1";
+
+ # ...unless there is an acl or torrent parameter
+ if ($path =~ /[&?]acl($|=|&)/) {
+ $buf .= '?acl';
+ }
+ elsif ($path =~ /[&?]torrent($|=|&)/) {
+ $buf .= '?torrent';
+ }
+ elsif ($path =~ /[&?]location($|=|&)/) {
+ $buf .= '?location';
+ }
+
+ return $buf;
}
# finds the hmac-sha1 hash of the canonical string and the aws secret access key and then
# base64 encodes the result (optionally urlencoding after that).
sub _encode {
- my ($self, $aws_secret_access_key, $str, $urlencode) = @_;
- my $hmac = Digest::HMAC_SHA1->new($aws_secret_access_key);
- $hmac->add($str);
- my $b64 = encode_base64($hmac->digest, '');
- if ($urlencode) {
- return $self->_urlencode($b64);
- }
- else {
- return $b64;
- }
+ my ($self, $aws_secret_access_key, $str, $urlencode) = @_;
+ my $hmac = Digest::HMAC_SHA1->new($aws_secret_access_key);
+ $hmac->add($str);
+ my $b64 = encode_base64($hmac->digest, '');
+ if ($urlencode) {
+ return $self->_urlencode($b64);
+ }
+ else {
+ return $b64;
+ }
}
sub _urlencode {
- my ($self, $unencoded) = @_;
- return uri_escape_utf8($unencoded, '^A-Za-z0-9_-');
+ my ($self, $unencoded) = @_;
+ return uri_escape_utf8($unencoded, '^A-Za-z0-9_-');
}
1;
diff --git a/Bugzilla/S3/Bucket.pm b/Bugzilla/S3/Bucket.pm
index a53ab5c51..8e6731ce5 100644
--- a/Bugzilla/S3/Bucket.pm
+++ b/Bugzilla/S3/Bucket.pm
@@ -14,170 +14,166 @@ use base qw(Class::Accessor::Fast);
__PACKAGE__->mk_accessors(qw(bucket creation_date account));
sub new {
- my $class = shift;
- my $self = $class->SUPER::new(@_);
- croak "no bucket" unless $self->bucket;
- croak "no account" unless $self->account;
- return $self;
+ my $class = shift;
+ my $self = $class->SUPER::new(@_);
+ croak "no bucket" unless $self->bucket;
+ croak "no account" unless $self->account;
+ return $self;
}
sub _uri {
- my ($self, $key) = @_;
- return ($key)
- ? $self->bucket . "/" . $self->account->_urlencode($key)
- : $self->bucket . "/";
+ my ($self, $key) = @_;
+ return ($key)
+ ? $self->bucket . "/" . $self->account->_urlencode($key)
+ : $self->bucket . "/";
}
# returns bool
sub add_key {
- my ($self, $key, $value, $conf) = @_;
- croak 'must specify key' unless $key && length $key;
-
- if ($conf->{acl_short}) {
- $self->account->_validate_acl_short($conf->{acl_short});
- $conf->{'x-amz-acl'} = $conf->{acl_short};
- delete $conf->{acl_short};
- }
-
- if (ref($value) eq 'SCALAR') {
- $conf->{'Content-Length'} ||= -s $$value;
- $value = _content_sub($$value);
- }
- else {
- $conf->{'Content-Length'} ||= length $value;
- }
-
- # If we're pushing to a bucket that's under DNS flux, we might get a 307
- # Since LWP doesn't support actually waiting for a 100 Continue response,
- # we'll just send a HEAD first to see what's going on
-
- if (ref($value)) {
- return $self->account->_send_request_expect_nothing_probed('PUT',
- $self->_uri($key), $conf, $value);
- }
- else {
- return $self->account->_send_request_expect_nothing('PUT',
- $self->_uri($key), $conf, $value);
- }
+ my ($self, $key, $value, $conf) = @_;
+ croak 'must specify key' unless $key && length $key;
+
+ if ($conf->{acl_short}) {
+ $self->account->_validate_acl_short($conf->{acl_short});
+ $conf->{'x-amz-acl'} = $conf->{acl_short};
+ delete $conf->{acl_short};
+ }
+
+ if (ref($value) eq 'SCALAR') {
+ $conf->{'Content-Length'} ||= -s $$value;
+ $value = _content_sub($$value);
+ }
+ else {
+ $conf->{'Content-Length'} ||= length $value;
+ }
+
+ # If we're pushing to a bucket that's under DNS flux, we might get a 307
+ # Since LWP doesn't support actually waiting for a 100 Continue response,
+ # we'll just send a HEAD first to see what's going on
+
+ if (ref($value)) {
+ return $self->account->_send_request_expect_nothing_probed('PUT',
+ $self->_uri($key), $conf, $value);
+ }
+ else {
+ return $self->account->_send_request_expect_nothing('PUT', $self->_uri($key),
+ $conf, $value);
+ }
}
sub add_key_filename {
- my ($self, $key, $value, $conf) = @_;
- return $self->add_key($key, \$value, $conf);
+ my ($self, $key, $value, $conf) = @_;
+ return $self->add_key($key, \$value, $conf);
}
sub head_key {
- my ($self, $key) = @_;
- return $self->get_key($key, "HEAD");
+ my ($self, $key) = @_;
+ return $self->get_key($key, "HEAD");
}
sub get_key {
- my ($self, $key, $method, $filename) = @_;
- $method ||= "GET";
- $filename = $$filename if ref $filename;
- my $acct = $self->account;
-
- my $request = $acct->_make_request($method, $self->_uri($key), {});
- my $response = $acct->_do_http($request, $filename);
-
- if ($response->code == 404) {
- $acct->err(404);
- $acct->errstr('The requested key was not found');
- return undef;
- }
-
- return undef unless $acct->_check_response($response);
-
- my $etag = $response->header('ETag');
- if ($etag) {
- $etag =~ s/^"//;
- $etag =~ s/"$//;
- }
-
- my $return = {
- content_length => $response->content_length || 0,
- content_type => $response->content_type,
- etag => $etag,
- value => $response->content,
- };
-
- foreach my $header ($response->headers->header_field_names) {
- next unless $header =~ /x-amz-meta-/i;
- $return->{lc $header} = $response->header($header);
- }
-
- return $return;
+ my ($self, $key, $method, $filename) = @_;
+ $method ||= "GET";
+ $filename = $$filename if ref $filename;
+ my $acct = $self->account;
+
+ my $request = $acct->_make_request($method, $self->_uri($key), {});
+ my $response = $acct->_do_http($request, $filename);
+
+ if ($response->code == 404) {
+ $acct->err(404);
+ $acct->errstr('The requested key was not found');
+ return undef;
+ }
+
+ return undef unless $acct->_check_response($response);
+
+ my $etag = $response->header('ETag');
+ if ($etag) {
+ $etag =~ s/^"//;
+ $etag =~ s/"$//;
+ }
+
+ my $return = {
+ content_length => $response->content_length || 0,
+ content_type => $response->content_type,
+ etag => $etag,
+ value => $response->content,
+ };
+
+ foreach my $header ($response->headers->header_field_names) {
+ next unless $header =~ /x-amz-meta-/i;
+ $return->{lc $header} = $response->header($header);
+ }
+
+ return $return;
}
sub get_key_filename {
- my ($self, $key, $method, $filename) = @_;
- $filename = $key unless defined $filename;
- return $self->get_key($key, $method, \$filename);
+ my ($self, $key, $method, $filename) = @_;
+ $filename = $key unless defined $filename;
+ return $self->get_key($key, $method, \$filename);
}
# returns bool
sub delete_key {
- my ($self, $key) = @_;
- croak 'must specify key' unless $key && length $key;
- return $self->account->_send_request_expect_nothing('DELETE',
- $self->_uri($key), {});
+ my ($self, $key) = @_;
+ croak 'must specify key' unless $key && length $key;
+ return $self->account->_send_request_expect_nothing('DELETE',
+ $self->_uri($key), {});
}
sub get_acl {
- my ($self, $key) = @_;
- my $acct = $self->account;
+ my ($self, $key) = @_;
+ my $acct = $self->account;
- my $request = $acct->_make_request('GET', $self->_uri($key) . '?acl', {});
- my $response = $acct->_do_http($request);
+ my $request = $acct->_make_request('GET', $self->_uri($key) . '?acl', {});
+ my $response = $acct->_do_http($request);
- if ($response->code == 404) {
- return undef;
- }
+ if ($response->code == 404) {
+ return undef;
+ }
- return undef unless $acct->_check_response($response);
+ return undef unless $acct->_check_response($response);
- return $response->content;
+ return $response->content;
}
sub set_acl {
- my ($self, $conf) = @_;
- $conf ||= {};
+ my ($self, $conf) = @_;
+ $conf ||= {};
- unless ($conf->{acl_xml} || $conf->{acl_short}) {
- croak "need either acl_xml or acl_short";
- }
+ unless ($conf->{acl_xml} || $conf->{acl_short}) {
+ croak "need either acl_xml or acl_short";
+ }
- if ($conf->{acl_xml} && $conf->{acl_short}) {
- croak "cannot provide both acl_xml and acl_short";
- }
+ if ($conf->{acl_xml} && $conf->{acl_short}) {
+ croak "cannot provide both acl_xml and acl_short";
+ }
- my $path = $self->_uri($conf->{key}) . '?acl';
+ my $path = $self->_uri($conf->{key}) . '?acl';
- my $hash_ref =
- ($conf->{acl_short})
- ? {'x-amz-acl' => $conf->{acl_short}}
- : {};
+ my $hash_ref = ($conf->{acl_short}) ? {'x-amz-acl' => $conf->{acl_short}} : {};
- my $xml = $conf->{acl_xml} || '';
+ my $xml = $conf->{acl_xml} || '';
- return $self->account->_send_request_expect_nothing('PUT', $path,
- $hash_ref, $xml);
+ return $self->account->_send_request_expect_nothing('PUT', $path, $hash_ref,
+ $xml);
}
sub get_location_constraint {
- my ($self) = @_;
+ my ($self) = @_;
- my $xpc =
- $self->account->_send_request('GET', $self->bucket . '/?location');
- return undef unless $xpc && !$self->account->_remember_errors($xpc);
+ my $xpc = $self->account->_send_request('GET', $self->bucket . '/?location');
+ return undef unless $xpc && !$self->account->_remember_errors($xpc);
- my $lc = $xpc->{content};
- if (defined $lc && $lc eq '') {
- $lc = undef;
- }
- return $lc;
+ my $lc = $xpc->{content};
+ if (defined $lc && $lc eq '') {
+ $lc = undef;
+ }
+ return $lc;
}
# proxy up the err requests
@@ -187,42 +183,37 @@ sub err { $_[0]->account->err }
sub errstr { $_[0]->account->errstr }
sub _content_sub {
- my $filename = shift;
- my $stat = stat($filename);
- my $remaining = $stat->size;
- my $blksize = $stat->blksize || 4096;
-
- croak "$filename not a readable file with fixed size"
- unless -r $filename
- and $remaining;
-
- my $fh = IO::File->new($filename, 'r')
- or croak "Could not open $filename: $!";
- $fh->binmode;
-
- return sub {
- my $buffer;
-
- # upon retries the file is closed and we must reopen it
- unless ($fh->opened) {
- $fh = IO::File->new($filename, 'r')
- or croak "Could not open $filename: $!";
- $fh->binmode;
- $remaining = $stat->size;
- }
-
- unless (my $read = $fh->read($buffer, $blksize)) {
- croak
- "Error while reading upload content $filename ($remaining remaining) $!"
- if $! and $remaining;
- $fh->close # otherwise, we found EOF
- or croak "close of upload content $filename failed: $!";
- $buffer
- ||= ''; # LWP expects an empty string on finish, read returns 0
- }
- $remaining -= length($buffer);
- return $buffer;
- };
+ my $filename = shift;
+ my $stat = stat($filename);
+ my $remaining = $stat->size;
+ my $blksize = $stat->blksize || 4096;
+
+ croak "$filename not a readable file with fixed size"
+ unless -r $filename and $remaining;
+
+ my $fh = IO::File->new($filename, 'r') or croak "Could not open $filename: $!";
+ $fh->binmode;
+
+ return sub {
+ my $buffer;
+
+ # upon retries the file is closed and we must reopen it
+ unless ($fh->opened) {
+ $fh = IO::File->new($filename, 'r') or croak "Could not open $filename: $!";
+ $fh->binmode;
+ $remaining = $stat->size;
+ }
+
+ unless (my $read = $fh->read($buffer, $blksize)) {
+ croak "Error while reading upload content $filename ($remaining remaining) $!"
+ if $! and $remaining;
+ $fh->close # otherwise, we found EOF
+ or croak "close of upload content $filename failed: $!";
+ $buffer ||= ''; # LWP expects an empty string on finish, read returns 0
+ }
+ $remaining -= length($buffer);
+ return $buffer;
+ };
}
1;
diff --git a/Bugzilla/Search.pm b/Bugzilla/Search.pm
index e15c60f7f..34e103a33 100644
--- a/Bugzilla/Search.pm
+++ b/Bugzilla/Search.pm
@@ -13,8 +13,8 @@ use warnings;
use base qw(Exporter);
@Bugzilla::Search::EXPORT = qw(
- IsValidQueryType
- split_order_term
+ IsValidQueryType
+ split_order_term
);
use Bugzilla::Error;
@@ -109,9 +109,7 @@ use Time::HiRes qw(gettimeofday tv_interval);
#############
# BMO - product aliases for searching
-use constant PRODUCT_ALIASES => {
- 'Boot2Gecko' => 'Firefox OS',
-};
+use constant PRODUCT_ALIASES => {'Boot2Gecko' => 'Firefox OS',};
# When doing searches, NULL datetimes are treated as this date.
use constant EMPTY_DATETIME => '1970-01-01 00:00:00';
@@ -140,320 +138,281 @@ use constant NUMBER_REGEX => qr/
# If you specify a search type in the boolean charts, this describes
# which operator maps to which internal function here.
use constant OPERATORS => {
- equals => \&_simple_operator,
- notequals => \&_simple_operator,
- casesubstring => \&_casesubstring,
- substring => \&_substring,
- substr => \&_substring,
- notsubstring => \&_notsubstring,
- regexp => \&_regexp,
- notregexp => \&_notregexp,
- lessthan => \&_simple_operator,
- lessthaneq => \&_simple_operator,
- matches => sub { ThrowUserError("search_content_without_matches"); },
- notmatches => sub { ThrowUserError("search_content_without_matches"); },
- greaterthan => \&_simple_operator,
- greaterthaneq => \&_simple_operator,
- anyexact => \&_anyexact,
- anywordssubstr => \&_anywordsubstr,
- allwordssubstr => \&_allwordssubstr,
- nowordssubstr => \&_nowordssubstr,
- anywords => \&_anywords,
- allwords => \&_allwords,
- nowords => \&_nowords,
- changedbefore => \&_changedbefore_changedafter,
- changedafter => \&_changedbefore_changedafter,
- changedfrom => \&_changedfrom_changedto,
- changedto => \&_changedfrom_changedto,
- changedby => \&_changedby,
- isempty => \&_isempty,
- isnotempty => \&_isnotempty,
+ equals => \&_simple_operator,
+ notequals => \&_simple_operator,
+ casesubstring => \&_casesubstring,
+ substring => \&_substring,
+ substr => \&_substring,
+ notsubstring => \&_notsubstring,
+ regexp => \&_regexp,
+ notregexp => \&_notregexp,
+ lessthan => \&_simple_operator,
+ lessthaneq => \&_simple_operator,
+ matches => sub { ThrowUserError("search_content_without_matches"); },
+ notmatches => sub { ThrowUserError("search_content_without_matches"); },
+ greaterthan => \&_simple_operator,
+ greaterthaneq => \&_simple_operator,
+ anyexact => \&_anyexact,
+ anywordssubstr => \&_anywordsubstr,
+ allwordssubstr => \&_allwordssubstr,
+ nowordssubstr => \&_nowordssubstr,
+ anywords => \&_anywords,
+ allwords => \&_allwords,
+ nowords => \&_nowords,
+ changedbefore => \&_changedbefore_changedafter,
+ changedafter => \&_changedbefore_changedafter,
+ changedfrom => \&_changedfrom_changedto,
+ changedto => \&_changedfrom_changedto,
+ changedby => \&_changedby,
+ isempty => \&_isempty,
+ isnotempty => \&_isnotempty,
};
# Some operators are really just standard SQL operators, and are
# all implemented by the _simple_operator function, which uses this
# constant.
use constant SIMPLE_OPERATORS => {
- equals => '=',
- notequals => '!=',
- greaterthan => '>',
- greaterthaneq => '>=',
- lessthan => '<',
- lessthaneq => "<=",
+ equals => '=',
+ notequals => '!=',
+ greaterthan => '>',
+ greaterthaneq => '>=',
+ lessthan => '<',
+ lessthaneq => "<=",
};
# Most operators just reverse by removing or adding "not" from/to them.
# However, some operators reverse in a different way, so those are listed
# here.
use constant OPERATOR_REVERSE => {
- nowords => 'anywords',
- nowordssubstr => 'anywordssubstr',
- anywords => 'nowords',
- anywordssubstr => 'nowordssubstr',
- lessthan => 'greaterthaneq',
- lessthaneq => 'greaterthan',
- greaterthan => 'lessthaneq',
- greaterthaneq => 'lessthan',
- isempty => 'isnotempty',
- isnotempty => 'isempty',
- # The following don't currently have reversals:
- # casesubstring, anyexact, allwords, allwordssubstr
+ nowords => 'anywords',
+ nowordssubstr => 'anywordssubstr',
+ anywords => 'nowords',
+ anywordssubstr => 'nowordssubstr',
+ lessthan => 'greaterthaneq',
+ lessthaneq => 'greaterthan',
+ greaterthan => 'lessthaneq',
+ greaterthaneq => 'lessthan',
+ isempty => 'isnotempty',
+ isnotempty => 'isempty',
+
+ # The following don't currently have reversals:
+ # casesubstring, anyexact, allwords, allwordssubstr
};
# For these operators, even if a field is numeric (is_numeric returns true),
# we won't treat the input like a number.
use constant NON_NUMERIC_OPERATORS => qw(
- changedafter
- changedbefore
- changedfrom
- changedto
- regexp
- notregexp
+ changedafter
+ changedbefore
+ changedfrom
+ changedto
+ regexp
+ notregexp
);
# These operators ignore the entered value
use constant NO_VALUE_OPERATORS => qw(
- isempty
- isnotempty
+ isempty
+ isnotempty
);
use constant MULTI_SELECT_OVERRIDE => {
- notequals => \&_multiselect_negative,
- notregexp => \&_multiselect_negative,
- notsubstring => \&_multiselect_negative,
- nowords => \&_multiselect_negative,
- nowordssubstr => \&_multiselect_negative,
-
- allwords => \&_multiselect_multiple,
- allwordssubstr => \&_multiselect_multiple,
- anyexact => \&_multiselect_multiple,
- anywords => \&_multiselect_multiple,
- anywordssubstr => \&_multiselect_multiple,
-
- _non_changed => \&_multiselect_nonchanged,
+ notequals => \&_multiselect_negative,
+ notregexp => \&_multiselect_negative,
+ notsubstring => \&_multiselect_negative,
+ nowords => \&_multiselect_negative,
+ nowordssubstr => \&_multiselect_negative,
+
+ allwords => \&_multiselect_multiple,
+ allwordssubstr => \&_multiselect_multiple,
+ anyexact => \&_multiselect_multiple,
+ anywords => \&_multiselect_multiple,
+ anywordssubstr => \&_multiselect_multiple,
+
+ _non_changed => \&_multiselect_nonchanged,
};
use constant OPERATOR_FIELD_OVERRIDE => {
- # User fields
- 'attachments.submitter' => {
- _non_changed => \&_user_nonchanged,
- },
- assigned_to => {
- _non_changed => \&_user_nonchanged,
- },
- cc => {
- _non_changed => \&_user_nonchanged,
- },
- commenter => {
- _non_changed => \&_user_nonchanged,
- },
- reporter => {
- _non_changed => \&_user_nonchanged,
- },
- 'requestees.login_name' => {
- _non_changed => \&_user_nonchanged,
- },
- 'setters.login_name' => {
- _non_changed => \&_user_nonchanged,
- },
- qa_contact => {
- _non_changed => \&_user_nonchanged,
- },
-
- # General Bug Fields
- alias => { _non_changed => \&_nullable },
- # We check all attachment fields against this.
- attachments => MULTI_SELECT_OVERRIDE,
- assignee_last_login => {
- _default => \&_assignee_last_login,
- },
- blocked => MULTI_SELECT_OVERRIDE,
- bug_file_loc => { _non_changed => \&_nullable },
- bug_group => MULTI_SELECT_OVERRIDE,
- classification => {
- _non_changed => \&_classification_nonchanged,
- },
- component => {
- _non_changed => \&_component_nonchanged,
- },
- content => {
- matches => \&_content_matches,
- notmatches => \&_content_matches,
- _default => sub { ThrowUserError("search_content_without_matches"); },
- },
- days_elapsed => {
- _default => \&_days_elapsed,
- },
- dependson => MULTI_SELECT_OVERRIDE,
- keywords => MULTI_SELECT_OVERRIDE,
- 'flagtypes.name' => {
- _non_changed => \&_flagtypes_nonchanged,
- },
- longdesc => {
- changedby => \&_long_desc_changedby,
- changedbefore => \&_long_desc_changedbefore_after,
- changedafter => \&_long_desc_changedbefore_after,
- _non_changed => \&_long_desc_nonchanged,
- },
- 'longdescs.count' => {
- changedby => \&_long_desc_changedby,
- changedbefore => \&_long_desc_changedbefore_after,
- changedafter => \&_long_desc_changedbefore_after,
- changedfrom => \&_invalid_combination,
- changedto => \&_invalid_combination,
- _default => \&_long_descs_count,
- },
- 'longdescs.isprivate' => MULTI_SELECT_OVERRIDE,
- owner_idle_time => {
- greaterthan => \&_owner_idle_time_greater_less,
- greaterthaneq => \&_owner_idle_time_greater_less,
- lessthan => \&_owner_idle_time_greater_less,
- lessthaneq => \&_owner_idle_time_greater_less,
- _default => \&_invalid_combination,
- },
- product => {
- _non_changed => \&_product_nonchanged,
- },
- tag => MULTI_SELECT_OVERRIDE,
- comment_tag => MULTI_SELECT_OVERRIDE,
- # Timetracking Fields
- deadline => { _non_changed => \&_deadline },
- percentage_complete => {
- _non_changed => \&_percentage_complete,
- },
- work_time => {
- changedby => \&_work_time_changedby,
- changedbefore => \&_work_time_changedbefore_after,
- changedafter => \&_work_time_changedbefore_after,
- _default => \&_work_time,
- },
- last_visit_ts => {
- _non_changed => \&_last_visit_ts,
- _default => \&_invalid_operator,
- },
- bug_interest_ts => {
- _non_changed => \&_bug_interest_ts,
- _default => \&_invalid_operator,
- },
- triage_owner => {
- _non_changed => \&_triage_owner_nonchanged,
- },
- # 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,},
+ cc => {_non_changed => \&_user_nonchanged,},
+ commenter => {_non_changed => \&_user_nonchanged,},
+ reporter => {_non_changed => \&_user_nonchanged,},
+ 'requestees.login_name' => {_non_changed => \&_user_nonchanged,},
+ 'setters.login_name' => {_non_changed => \&_user_nonchanged,},
+ qa_contact => {_non_changed => \&_user_nonchanged,},
+
+ # General Bug Fields
+ alias => {_non_changed => \&_nullable},
+
+ # We check all attachment fields against this.
+ attachments => MULTI_SELECT_OVERRIDE,
+ assignee_last_login => {_default => \&_assignee_last_login,},
+ blocked => MULTI_SELECT_OVERRIDE,
+ bug_file_loc => {_non_changed => \&_nullable},
+ bug_group => MULTI_SELECT_OVERRIDE,
+ classification => {_non_changed => \&_classification_nonchanged,},
+ component => {_non_changed => \&_component_nonchanged,},
+ content => {
+ matches => \&_content_matches,
+ notmatches => \&_content_matches,
+ _default => sub { ThrowUserError("search_content_without_matches"); },
+ },
+ days_elapsed => {_default => \&_days_elapsed,},
+ dependson => MULTI_SELECT_OVERRIDE,
+ keywords => MULTI_SELECT_OVERRIDE,
+ 'flagtypes.name' => {_non_changed => \&_flagtypes_nonchanged,},
+ longdesc => {
+ changedby => \&_long_desc_changedby,
+ changedbefore => \&_long_desc_changedbefore_after,
+ changedafter => \&_long_desc_changedbefore_after,
+ _non_changed => \&_long_desc_nonchanged,
+ },
+ 'longdescs.count' => {
+ changedby => \&_long_desc_changedby,
+ changedbefore => \&_long_desc_changedbefore_after,
+ changedafter => \&_long_desc_changedbefore_after,
+ changedfrom => \&_invalid_combination,
+ changedto => \&_invalid_combination,
+ _default => \&_long_descs_count,
+ },
+ 'longdescs.isprivate' => MULTI_SELECT_OVERRIDE,
+ owner_idle_time => {
+ greaterthan => \&_owner_idle_time_greater_less,
+ greaterthaneq => \&_owner_idle_time_greater_less,
+ lessthan => \&_owner_idle_time_greater_less,
+ lessthaneq => \&_owner_idle_time_greater_less,
+ _default => \&_invalid_combination,
+ },
+ product => {_non_changed => \&_product_nonchanged,},
+ tag => MULTI_SELECT_OVERRIDE,
+ comment_tag => MULTI_SELECT_OVERRIDE,
+
+ # Timetracking Fields
+ deadline => {_non_changed => \&_deadline},
+ percentage_complete => {_non_changed => \&_percentage_complete,},
+ work_time => {
+ changedby => \&_work_time_changedby,
+ changedbefore => \&_work_time_changedbefore_after,
+ changedafter => \&_work_time_changedbefore_after,
+ _default => \&_work_time,
+ },
+ last_visit_ts =>
+ {_non_changed => \&_last_visit_ts, _default => \&_invalid_operator,},
+ bug_interest_ts =>
+ {_non_changed => \&_bug_interest_ts, _default => \&_invalid_operator,},
+ triage_owner => {_non_changed => \&_triage_owner_nonchanged,},
+
+ # 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 => \&_cc_pronoun,
- commenter => \&_commenter_pronoun,
- qa_contact => \&_contact_pronoun,
- reporter => \&_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,
- bug_interest_ts => \&_last_visit_datetime,
-
- # BMO - Add ability to use pronoun for bug mentors field
- bug_mentor => \&_commenter_pronoun,
-
- # BMO - add ability to use pronoun for triage owners
- triage_owner => \&_triage_owner_pronoun,
- };
- foreach my $field (Bugzilla->active_custom_fields) {
- if ($field->type == FIELD_TYPE_DATETIME) {
- $map->{$field->name} = \&_datetime_translate;
- } elsif ($field->type == FIELD_TYPE_DATE) {
- $map->{$field->name} = \&_date_translate;
- }
+ my $map = {
+
+ # Pronoun Fields (Ones that can accept %user%, etc.)
+ assigned_to => \&_contact_pronoun,
+ 'attachments.submitter' => \&_contact_pronoun,
+ cc => \&_cc_pronoun,
+ commenter => \&_commenter_pronoun,
+ qa_contact => \&_contact_pronoun,
+ reporter => \&_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,
+ bug_interest_ts => \&_last_visit_datetime,
+
+ # BMO - Add ability to use pronoun for bug mentors field
+ bug_mentor => \&_commenter_pronoun,
+
+ # BMO - add ability to use pronoun for triage owners
+ triage_owner => \&_triage_owner_pronoun,
+ };
+ foreach my $field (Bugzilla->active_custom_fields) {
+ if ($field->type == FIELD_TYPE_DATETIME) {
+ $map->{$field->name} = \&_datetime_translate;
}
- return $map;
-};
+ elsif ($field->type == FIELD_TYPE_DATE) {
+ $map->{$field->name} = \&_date_translate;
+ }
+ }
+ return $map;
+}
# Information about fields that represent "users", used by _user_nonchanged.
# There are other user fields than the ones listed here, but those use
# defaults in _user_nonchanged.
use constant USER_FIELDS => {
- 'attachments.submitter' => {
- field => 'submitter_id',
- join => { table => 'attachments' },
- isprivate => 1,
- },
- cc => {
- field => 'who',
- join => { table => 'cc' },
- },
- commenter => {
- field => 'who',
- join => { table => 'longdescs', join => 'INNER' },
- isprivate => 1,
- },
- qa_contact => {
- nullable => 1,
- },
- 'requestees.login_name' => {
- nullable => 1,
- field => 'requestee_id',
- join => { table => 'flags' },
- },
- 'setters.login_name' => {
- field => 'setter_id',
- join => { table => 'flags' },
- },
- # BMO - Ability to search for bugs with specific mentors
- 'bug_mentor' => {
- field => 'user_id',
- join => { table => 'bug_mentors' },
- }
+ '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'},},
+
+ # BMO - Ability to search for bugs with specific mentors
+ 'bug_mentor' => {field => 'user_id', join => {table => 'bug_mentors'},}
};
# Backwards compatibility for times that we changed the names of fields
# or URL parameters.
use constant FIELD_MAP => {
- bugidtype => 'bug_id_type',
- changedin => 'days_elapsed',
- long_desc => 'longdesc',
+ bugidtype => 'bug_id_type',
+ changedin => 'days_elapsed',
+ long_desc => 'longdesc',
};
# 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'],
- triage_owner => ['component'],
+ classification => ['product'],
+ percentage_complete => ['actual_time', 'remaining_time'],
+ triage_owner => ['component'],
};
# This describes tables that must be joined when you want to display
@@ -461,112 +420,87 @@ 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 $user = Bugzilla->user;
-
- my $joins = {
- actual_time => {
- table => '(SELECT bug_id, SUM(work_time) AS total'
- . ' FROM longdescs GROUP BY bug_id)',
- join => 'INNER',
- },
- assigned_to => {
- from => 'assigned_to',
- to => 'userid',
- table => 'profiles',
- join => 'INNER',
- },
- assignee_last_login => {
- as => 'assignee',
- 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',
- },
- },
- 'triage_owner' => {
- table => 'profiles',
- as => 'map_triage_owner',
- from => 'map_component.triage_owner_id',
- to => 'userid',
- join => 'LEFT',
- },
- 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',
- },
- 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',
- },
- bug_interest_ts => {
- as => 'bug_interest',
- table => 'bug_interest',
- extra => ['bug_interest.user_id = ' . $user->id],
- from => 'bug_id',
- to => 'bug_id',
- },
- };
- return $joins;
-};
+ my $user = Bugzilla->user;
+
+ my $joins = {
+ actual_time => {
+ table => '(SELECT bug_id, SUM(work_time) AS total'
+ . ' FROM longdescs GROUP BY bug_id)',
+ join => 'INNER',
+ },
+ assigned_to => {
+ from => 'assigned_to',
+ to => 'userid',
+ table => 'profiles',
+ join => 'INNER',
+ },
+ assignee_last_login => {
+ as => 'assignee',
+ 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',
+ },
+ },
+ 'triage_owner' => {
+ table => 'profiles',
+ as => 'map_triage_owner',
+ from => 'map_component.triage_owner_id',
+ to => 'userid',
+ join => 'LEFT',
+ },
+ 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',},
+ 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',
+ },
+ bug_interest_ts => {
+ as => 'bug_interest',
+ table => 'bug_interest',
+ extra => ['bug_interest.user_id = ' . $user->id],
+ from => 'bug_id',
+ to => 'bug_id',
+ },
+ };
+ return $joins;
+}
# This constant defines the columns that can be selected in a query
# and/or displayed in a bug list. Column records include the following
@@ -589,161 +523,160 @@ 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};
- }
+ my $invocant = shift;
+ my $user = blessed($invocant) ? $invocant->_user : Bugzilla->user;
+ my $dbh = Bugzilla->dbh;
+ my $cache = Bugzilla->request_cache;
- # 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' },
- assigned_to_realname => { title => 'Assignee' },
- reporter_realname => { title => 'Reporter' },
- qa_contact_realname => { title => 'QA Contact' },
- );
-
- # Next we define columns that have special SQL instead of just something
- # like "bugs.bug_id".
- my $total_time = "(map_actual_time.total + bugs.remaining_time)";
- my %special_sql = (
- 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')),
-
- '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)',
- last_visit_ts => 'bug_user_last_visit.last_visit_ts',
- bug_interest_ts => 'bug_interest.modification_time',
- assignee_last_login => 'assignee.last_seen_date',
- );
-
- if ($user->id) {
- $special_sql{triage_owner} = 'map_triage_owner.login_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'},
+ assigned_to_realname => {title => 'Assignee'},
+ reporter_realname => {title => 'Reporter'},
+ qa_contact_realname => {title => 'QA Contact'},
+ );
+
+ # Next we define columns that have special SQL instead of just something
+ # like "bugs.bug_id".
+ my $total_time = "(map_actual_time.total + bugs.remaining_time)";
+ my %special_sql = (
+ 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')
+ ),
+
+ '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)',
+ last_visit_ts => 'bug_user_last_visit.last_visit_ts',
+ bug_interest_ts => 'bug_interest.modification_time',
+ assignee_last_login => 'assignee.last_seen_date',
+ );
+
+ if ($user->id) {
+ $special_sql{triage_owner} = 'map_triage_owner.login_name';
+ }
+ else {
+ $special_sql{triage_owner} = 'map_triage_owner.realname';
+ }
+
+ # 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;
+ $columns{"${col}_realname"}->{name} = "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');
}
else {
- $special_sql{triage_owner} = 'map_triage_owner.realname';
+ $sql = 'bugs.' . $field->name;
}
+ $columns{$id} = {name => $sql, title => $field->description};
+ }
- # 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;
- $columns{"${col}_realname"}->{name} = "map_${col}.realname";
- }
+ # The short_short_desc column is identical to short_desc
+ $columns{'short_short_desc'} = $columns{'short_desc'};
- foreach my $col (@id_fields) {
- $special_sql{$col} = "map_${col}.name";
- }
+ Bugzilla::Hook::process('buglist_columns', {columns => \%columns});
- # 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 };
- }
+ $cache->{search_columns}->{$user->id} = \%columns;
+ return $cache->{search_columns}->{$user->id};
+}
- # The short_short_desc column is identical to short_desc
- $columns{'short_short_desc'} = $columns{'short_desc'};
+sub REPORT_COLUMNS {
+ my $invocant = shift;
+ my $user = blessed($invocant) ? $invocant->_user : Bugzilla->user;
- Bugzilla::Hook::process('buglist_columns', { columns => \%columns });
+ my $columns = dclone(blessed($invocant) ? $invocant->COLUMNS : COLUMNS);
- $cache->{search_columns}->{$user->id} = \%columns;
- return $cache->{search_columns}->{$user->id};
-}
+ # 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 keywords relevance);
-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 keywords relevance);
-
- # Multi-select fields are not currently supported.
- my @multi_selects = @{Bugzilla->fields(
- { obsolete => 0, type => FIELD_TYPE_MULTI_SELECT })};
- push(@no_report_columns, map { $_->name } @multi_selects);
-
- # If you're not a time-tracker, you can't use time-tracking
- # columns.
- if (!$user->is_timetracker) {
- push(@no_report_columns, TIMETRACKING_FIELDS);
- }
+ # Multi-select fields are not currently supported.
+ my @multi_selects
+ = @{Bugzilla->fields({obsolete => 0, type => FIELD_TYPE_MULTI_SELECT})};
+ push(@no_report_columns, map { $_->name } @multi_selects);
- foreach my $name (@no_report_columns) {
- delete $columns->{$name};
- }
- return $columns;
+ # 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(
- blocked
- bug_id
- dependson
- flagtypes.name
- keywords
- longdescs.count
- percentage_complete
+ blocked
+ bug_id
+ dependson
+ flagtypes.name
+ keywords
+ longdescs.count
+ percentage_complete
);
###############
@@ -752,27 +685,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 $invocant = shift;
+ my $class = ref($invocant) || $invocant;
- my $self = { @_ };
- bless($self, $class);
- $self->{'user'} ||= Bugzilla->user;
+ 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;
+ # 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;
+ return $self;
}
@@ -781,234 +714,246 @@ sub new {
####################
sub data {
- my $self = shift;
- return $self->{data} if $self->{data};
- my $dbh = Bugzilla->dbh;
-
- Bugzilla->log_user_request(undef, undef, "search") if Bugzilla->user->id;
- # 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} =~ /^bugs\.\w+$/;
- $self->{fields} = ['bug_id'];
- $all_in_bugs_table = 0;
- last;
+ my $self = shift;
+ return $self->{data} if $self->{data};
+ my $dbh = Bugzilla->dbh;
+
+ Bugzilla->log_user_request(undef, undef, "search") if Bugzilla->user->id;
+
+ # 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} =~ /^bugs\.\w+$/;
+ $self->{fields} = ['bug_id'];
+ $all_in_bugs_table = 0;
+ last;
+ }
+
+ # BMO - to avoid massive amounts of joins, if we're selecting a lot of
+ # tracking flags, replace them with placeholders. the values will be
+ # retrieved later and injected into the result.
+ if (Bugzilla->has_extension('TrackingFlags')) {
+ my %tf_map
+ = map { $_ => 1 } Bugzilla::Extension::TrackingFlags::Flag->get_all_names();
+ my @tf_selected = grep { exists $tf_map{$_} } @orig_fields;
+
+ # mysql has a limit of 61 joins, and we want to avoid massive amounts of joins
+ # 30 ensures we won't hit the limit, nor generate too many joins
+ if (scalar @tf_selected > 30) {
+ foreach my $column (@tf_selected) {
+ $self->COLUMNS->{$column}->{name} = "'---'";
+ }
+ $self->{tracking_flags} = \@tf_selected;
}
-
- # BMO - to avoid massive amounts of joins, if we're selecting a lot of
- # tracking flags, replace them with placeholders. the values will be
- # retrieved later and injected into the result.
- if (Bugzilla->has_extension('TrackingFlags')) {
- my %tf_map = map { $_ => 1 } Bugzilla::Extension::TrackingFlags::Flag->get_all_names();
- my @tf_selected = grep { exists $tf_map{$_} } @orig_fields;
- # mysql has a limit of 61 joins, and we want to avoid massive amounts of joins
- # 30 ensures we won't hit the limit, nor generate too many joins
- if (scalar @tf_selected > 30) {
- foreach my $column (@tf_selected) {
- $self->COLUMNS->{$column}->{name} = "'---'";
- }
- $self->{tracking_flags} = \@tf_selected;
- }
- else {
- $self->{tracking_flags} = [];
- }
+ else {
+ $self->{tracking_flags} = [];
}
+ }
- 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;
-
- # BMO if the caller only wants the count, that's all we need to return
- return $bug_ids->[0]->[0] if $self->_params->{count_only};
-
- # 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};
- }
+ my $start_time = [gettimeofday()];
+ my $sql = $self->_sql;
- # 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;
- }
+ # 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)});
- # 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);
+ # Restore the original 'fields' argument, just in case.
+ $self->{fields} = \@orig_fields unless $all_in_bugs_table;
- $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];
-
- # BMO - get tracking flags values, and insert into result
- if (Bugzilla->has_extension('TrackingFlags') && @{ $self->{tracking_flags} }) {
- # read values
- my $values;
- $sql = "
+ # BMO if the caller only wants the count, that's all we need to return
+ return $bug_ids->[0]->[0] if $self->_params->{count_only};
+
+ # 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];
+
+ # BMO - get tracking flags values, and insert into result
+ if (Bugzilla->has_extension('TrackingFlags') && @{$self->{tracking_flags}}) {
+
+ # read values
+ my $values;
+ $sql = "
SELECT bugs.bug_id, tracking_flags.name, tracking_flags_bugs.value
FROM bugs
LEFT JOIN tracking_flags_bugs ON tracking_flags_bugs.bug_id = bugs.bug_id
LEFT JOIN tracking_flags ON tracking_flags.id = tracking_flags_bugs.tracking_flag_id
WHERE " . $dbh->sql_in('bugs.bug_id', $bug_ids);
- $start_time = [gettimeofday()];
- my $rows = $dbh->selectall_arrayref($sql);
- push(@extra_data, {sql => $sql, time => tv_interval($start_time)});
- foreach my $row (@$rows) {
- $values->{$row->[0]}{$row->[1]} = $row->[2] if defined($row->[2]);
- }
+ $start_time = [gettimeofday()];
+ my $rows = $dbh->selectall_arrayref($sql);
+ push(@extra_data, {sql => $sql, time => tv_interval($start_time)});
+ foreach my $row (@$rows) {
+ $values->{$row->[0]}{$row->[1]} = $row->[2] if defined($row->[2]);
+ }
- # find the columns of the tracking flags
- my %tf_pos;
- for (my $i = 0; $i <= $#orig_fields; $i++) {
- if (grep { $_ eq $orig_fields[$i] } @{ $self->{tracking_flags} }) {
- $tf_pos{$orig_fields[$i]} = $i;
- }
- }
+ # find the columns of the tracking flags
+ my %tf_pos;
+ for (my $i = 0; $i <= $#orig_fields; $i++) {
+ if (grep { $_ eq $orig_fields[$i] } @{$self->{tracking_flags}}) {
+ $tf_pos{$orig_fields[$i]} = $i;
+ }
+ }
- # replace the placeholder value with the field's value
- foreach my $row (@{ $self->{data} }) {
- my $bug_id = $row->[$pos];
- next unless exists $values->{$bug_id};
- foreach my $field (keys %{ $values->{$bug_id} }) {
- if (exists $tf_pos{$field}) {
- $row->[$tf_pos{$field}] = $values->{$bug_id}{$field};
- }
- }
+ # replace the placeholder value with the field's value
+ foreach my $row (@{$self->{data}}) {
+ my $bug_id = $row->[$pos];
+ next unless exists $values->{$bug_id};
+ foreach my $field (keys %{$values->{$bug_id}}) {
+ if (exists $tf_pos{$field}) {
+ $row->[$tf_pos{$field}] = $values->{$bug_id}{$field};
}
+ }
}
+ }
- return wantarray ? ($self->{data}, \@extra_data) : $self->{data};
+ 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;
-
- # BMO allow fetching just the number of matching bugs
- if ($self->_params->{count_only}) {
- $select = 'COUNT(*) AS count';
- $group_by = '';
- $order_by = '';
- $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;
+
+ # BMO allow fetching just the number of matching bugs
+ if ($self->_params->{count_only}) {
+ $select = 'COUNT(*) AS count';
+ $group_by = '';
+ $order_by = '';
+ $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) {
-
- # BMO - product aliases
- # display the new product name on the search results name to avoid a
- # disparity between the search summary and the results.
- if ($params->{field} eq 'product') {
- my $aliased;
- my @values = split(/,/, $params->{value});
- foreach my $value (@values) {
- if (exists PRODUCT_ALIASES->{lc($value)}) {
- $value = PRODUCT_ALIASES->{lc($value)};
- $aliased = 1;
- }
- }
- if ($aliased) {
- $params->{value} = join(',', @values);
- }
- }
+ 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;
+ # BMO - product aliases
+ # display the new product name on the search results name to avoid a
+ # disparity between the search summary and the results.
+ if ($params->{field} eq 'product') {
+ my $aliased;
+ my @values = split(/,/, $params->{value});
+ foreach my $value (@values) {
+ if (exists PRODUCT_ALIASES->{lc($value)}) {
+ $value = PRODUCT_ALIASES->{lc($value)};
+ $aliased = 1;
+ }
+ }
+ if ($aliased) {
+ $params->{value} = join(',', @values);
+ }
}
- return $self->{'search_description'};
+
+ 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 @as_params = $self->_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 @as_params = $self->_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;
}
######################
@@ -1017,38 +962,39 @@ sub order {
# Fields that are legal for boolean charts of any kind.
sub _chart_fields {
- my ($self) = @_;
- return $self->{chart_fields} //= search_fields({ user => $self->_user });
+ my ($self) = @_;
+ return $self->{chart_fields} //= search_fields({user => $self->_user});
}
# There are various places in Search.pm that we need to know the list of
# valid multi-select fields--or really, fields that are stored like
# multi-selects, which includes BUG_URLS fields.
sub _multi_select_fields {
- my ($self) = @_;
- $self->{multi_select_fields} ||= Bugzilla->fields({
- by_name => 1,
- type => [FIELD_TYPE_MULTI_SELECT, FIELD_TYPE_BUG_URLS]});
- return $self->{multi_select_fields};
+ my ($self) = @_;
+ $self->{multi_select_fields}
+ ||= Bugzilla->fields({
+ by_name => 1, type => [FIELD_TYPE_MULTI_SELECT, FIELD_TYPE_BUG_URLS]
+ });
+ return $self->{multi_select_fields};
}
# $self->{params} contains values that could be undef, could be a string,
# or could be an arrayref. Sometimes we want that value as an array,
# always.
sub _param_array {
- my ($self, $name) = @_;
- my $value = $self->_params->{$name};
- if (!defined $value) {
- return ();
- }
- if (ref($value) eq 'ARRAY') {
- return @$value;
- }
- return ($value);
-}
-
-sub _params { $_[0]->{params} }
-sub _user { return $_[0]->{user} }
+ my ($self, $name) = @_;
+ my $value = $self->_params->{$name};
+ if (!defined $value) {
+ return ();
+ }
+ if (ref($value) eq 'ARRAY') {
+ return @$value;
+ }
+ return ($value);
+}
+
+sub _params { $_[0]->{params} }
+sub _user { return $_[0]->{user} }
sub _sharer_id { $_[0]->{sharer} }
##############################
@@ -1057,75 +1003,78 @@ 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 ($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 @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 $alias = $column;
- # Aliases cannot contain dots in them. We convert them to underscores.
- $alias =~ s/\./_/g;
- my $sql = $self->COLUMNS->{$column}->{name} . " AS $alias";
- push(@sql_fields, $sql);
- }
- return @sql_fields;
+ my ($self) = @_;
+ my @sql_fields;
+ foreach my $column ($self->_display_columns) {
+ my $alias = $column;
+
+ # Aliases cannot contain dots in them. We convert them to underscores.
+ $alias =~ s/\./_/g;
+ my $sql = $self->COLUMNS->{$column}->{name} . " AS $alias";
+ push(@sql_fields, $sql);
+ }
+ return @sql_fields;
}
################################
@@ -1134,85 +1083,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;
}
#############################
@@ -1220,32 +1167,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 '';
}
############################
@@ -1253,173 +1198,172 @@ 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',
+ 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);
+
+ if ($user->id) {
+ $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_join);
+ push(@joins, $security_cc_join);
+ }
- if ($user->id) {
- $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);
- }
-
- return @joins;
+ 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;
}
#############################
@@ -1432,47 +1376,50 @@ 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 $security_term = 'security_map.group_id IS NULL';
-
- my $user = $self->_user;
- 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 $security_term = 'security_map.group_id IS NULL';
+
+ my $user = $self->_user;
+ 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;
}
################################
@@ -1482,40 +1429,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};
- 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);
- }
+ # 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};
+ 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;
+ @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));
+ # 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};
}
##############################################
@@ -1524,244 +1471,254 @@ sub _skip_group_by {
# Backwards compatibility for old field names.
sub _convert_old_params {
- my ($self) = @_;
- my $params = $self->_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';
- }
+ # 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};
- }
+ foreach my $old_name (keys %{FIELD_MAP()}) {
+ if (defined $params->{$old_name}) {
+ my $new_name = FIELD_MAP->{$old_name};
+ $params->{$new_name} = delete $params->{$old_name};
}
+ }
}
# This parses all the standard search parameters except for the boolean
# charts.
sub _special_charts {
- my ($self) = @_;
- $self->_convert_old_params();
- $self->_special_parse_bug_status();
- $self->_special_parse_resolution();
- my $clause = new Bugzilla::Search::Clause();
- $clause->add( $self->_parse_basic_fields() );
- $clause->add( $self->_special_parse_email() );
- $clause->add( $self->_special_parse_chfield() );
- $clause->add( $self->_special_parse_deadline() );
- return $clause;
+ my ($self) = @_;
+ $self->_convert_old_params();
+ $self->_special_parse_bug_status();
+ $self->_special_parse_resolution();
+ my $clause = new Bugzilla::Search::Clause();
+ $clause->add($self->_parse_basic_fields());
+ $clause->add($self->_special_parse_email());
+ $clause->add($self->_special_parse_chfield());
+ $clause->add($self->_special_parse_deadline());
+ return $clause;
}
sub _parse_basic_fields {
- my ($self) = @_;
- my $params = $self->_params;
- my $chart_fields = $self->_chart_fields;
-
- my $clause = new Bugzilla::Search::Clause();
- foreach my $field_name (keys %$chart_fields) {
- # CGI params shouldn't have periods in them, so we only accept
- # period-separated fields with underscores where the periods go.
- my $param_name = $field_name;
- $param_name =~ s/\./_/g;
- my @values = $self->_param_array($param_name);
- next if !@values;
- my $default_op = $param_name eq 'content' ? 'matches' : 'anyexact';
- my $operator = $params->{"${param_name}_type"} || $default_op;
- # Fields that are displayed as multi-selects are passed as arrays,
- # so that they can properly search values that contain commas.
- # However, other fields are sent as strings, so that they are properly
- # split on commas if required.
- my $field = $chart_fields->{$field_name};
- my $pass_value;
- if ($field->is_select or $field->name eq 'version'
- or $field->name eq 'target_milestone')
- {
- $pass_value = \@values;
- }
- else {
- $pass_value = join(',', @values);
- }
- $clause->add($field_name, $operator, $pass_value);
+ my ($self) = @_;
+ my $params = $self->_params;
+ my $chart_fields = $self->_chart_fields;
+
+ my $clause = new Bugzilla::Search::Clause();
+ foreach my $field_name (keys %$chart_fields) {
+
+ # CGI params shouldn't have periods in them, so we only accept
+ # period-separated fields with underscores where the periods go.
+ my $param_name = $field_name;
+ $param_name =~ s/\./_/g;
+ my @values = $self->_param_array($param_name);
+ next if !@values;
+ my $default_op = $param_name eq 'content' ? 'matches' : 'anyexact';
+ my $operator = $params->{"${param_name}_type"} || $default_op;
+
+ # Fields that are displayed as multi-selects are passed as arrays,
+ # so that they can properly search values that contain commas.
+ # However, other fields are sent as strings, so that they are properly
+ # split on commas if required.
+ my $field = $chart_fields->{$field_name};
+ my $pass_value;
+ if ( $field->is_select
+ or $field->name eq 'version'
+ or $field->name eq 'target_milestone')
+ {
+ $pass_value = \@values;
+ }
+ else {
+ $pass_value = join(',', @values);
}
- return $clause;
+ $clause->add($field_name, $operator, $pass_value);
+ }
+ return $clause;
}
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;
+ 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;
}
sub _special_parse_deadline {
- my ($self) = @_;
- return if !$self->_user->is_timetracker;
- my $params = $self->_params;
+ my ($self) = @_;
+ return if !$self->_user->is_timetracker;
+ 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;
+ return $clause;
}
sub _special_parse_email {
- my ($self) = @_;
- my $params = $self->_params;
-
- my @email_params = grep { $_ =~ /^email\d+$/ } keys %$params;
-
- my $clause = new Bugzilla::Search::Clause();
- foreach my $param (@email_params) {
- $param =~ /(\d+)$/;
- my $id = $1;
- my $email = trim($params->{"email$id"});
- next if !$email;
- my $type = $params->{"emailtype$id"} || 'anyexact';
- $type = "anyexact" if $type eq "exact";
-
- my $or_clause = new Bugzilla::Search::Clause('OR');
- foreach my $field (qw(assigned_to reporter cc qa_contact bug_mentor)) {
- if ($params->{"email$field$id"}) {
- $or_clause->add($field, $type, $email);
- }
- }
- if ($params->{"emaillongdesc$id"}) {
- $or_clause->add("commenter", $type, $email);
- }
+ my ($self) = @_;
+ my $params = $self->_params;
+
+ my @email_params = grep { $_ =~ /^email\d+$/ } keys %$params;
- $clause->add($or_clause);
+ my $clause = new Bugzilla::Search::Clause();
+ foreach my $param (@email_params) {
+ $param =~ /(\d+)$/;
+ my $id = $1;
+ my $email = trim($params->{"email$id"});
+ next if !$email;
+ my $type = $params->{"emailtype$id"} || 'anyexact';
+ $type = "anyexact" if $type eq "exact";
+
+ my $or_clause = new Bugzilla::Search::Clause('OR');
+ foreach my $field (qw(assigned_to reporter cc qa_contact bug_mentor)) {
+ if ($params->{"email$field$id"}) {
+ $or_clause->add($field, $type, $email);
+ }
+ }
+ if ($params->{"emaillongdesc$id"}) {
+ $or_clause->add("commenter", $type, $email);
}
- return $clause;
+ $clause->add($or_clause);
+ }
+
+ return $clause;
}
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);
+ }
+ elsif (grep { $_->name eq $item } @$valid) {
+ push(@result, $item);
}
- return @result;
+ }
+ return @result;
}
######################################
@@ -1769,213 +1726,220 @@ 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 ($self) = @_;
- my $clause = $self->_params_to_data_structure();
- my $chart_id = 0;
- $clause->walk_conditions(sub { $self->_handle_chart($chart_id++, @_) });
- return $clause;
+ 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) = @_;
+ 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;
+ # 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 );
+ # 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 );
+ # And then process the modern "custom search" format.
+ $clause->add($self->_custom_search);
- return $clause;
+ return $clause;
}
sub _boolean_charts {
- my ($self) = @_;
-
- my $params = $self->_params;
- my @param_list = keys %$params;
-
- my @all_field_params = grep { /^field-?\d+/ } @param_list;
- my @chart_ids = map { /^field(-?\d+)/; $1 } @all_field_params;
- @chart_ids = sort { $a <=> $b } uniq @chart_ids;
-
- my $clause = new Bugzilla::Search::Clause();
- foreach my $chart_id (@chart_ids) {
- my @all_and = grep { /^field$chart_id-\d+/ } @param_list;
- my @and_ids = map { /^field$chart_id-(\d+)/; $1 } @all_and;
- @and_ids = sort { $a <=> $b } uniq @and_ids;
-
- my $and_clause = new Bugzilla::Search::Clause();
- foreach my $and_id (@and_ids) {
- my @all_or = grep { /^field$chart_id-$and_id-\d+/ } @param_list;
- my @or_ids = map { /^field$chart_id-$and_id-(\d+)/; $1 } @all_or;
- @or_ids = sort { $a <=> $b } uniq @or_ids;
-
- my $or_clause = new Bugzilla::Search::Clause('OR');
- foreach my $or_id (@or_ids) {
- my $identifier = "$chart_id-$and_id-$or_id";
- my $field = $params->{"field$identifier"};
- my $operator = $params->{"type$identifier"};
- my $value = $params->{"value$identifier"};
- # no-value operators ignore the value, however a value needs to be set
- $value = ' ' if $operator && grep { $_ eq $operator } NO_VALUE_OPERATORS;
- $or_clause->add($field, $operator, $value);
- }
- $and_clause->add($or_clause);
- $and_clause->negate(1) if $params->{"negate$chart_id"};
- }
- $clause->add($and_clause);
+ my ($self) = @_;
+
+ my $params = $self->_params;
+ my @param_list = keys %$params;
+
+ my @all_field_params = grep {/^field-?\d+/} @param_list;
+ my @chart_ids = map { /^field(-?\d+)/; $1 } @all_field_params;
+ @chart_ids = sort { $a <=> $b } uniq @chart_ids;
+
+ my $clause = new Bugzilla::Search::Clause();
+ foreach my $chart_id (@chart_ids) {
+ my @all_and = grep {/^field$chart_id-\d+/} @param_list;
+ my @and_ids = map { /^field$chart_id-(\d+)/; $1 } @all_and;
+ @and_ids = sort { $a <=> $b } uniq @and_ids;
+
+ my $and_clause = new Bugzilla::Search::Clause();
+ foreach my $and_id (@and_ids) {
+ my @all_or = grep {/^field$chart_id-$and_id-\d+/} @param_list;
+ my @or_ids = map { /^field$chart_id-$and_id-(\d+)/; $1 } @all_or;
+ @or_ids = sort { $a <=> $b } uniq @or_ids;
+
+ my $or_clause = new Bugzilla::Search::Clause('OR');
+ foreach my $or_id (@or_ids) {
+ my $identifier = "$chart_id-$and_id-$or_id";
+ my $field = $params->{"field$identifier"};
+ my $operator = $params->{"type$identifier"};
+ my $value = $params->{"value$identifier"};
+
+ # no-value operators ignore the value, however a value needs to be set
+ $value = ' ' if $operator && grep { $_ eq $operator } NO_VALUE_OPERATORS;
+ $or_clause->add($field, $operator, $value);
+ }
+ $and_clause->add($or_clause);
+ $and_clause->negate(1) if $params->{"negate$chart_id"};
}
+ $clause->add($and_clause);
+ }
- return $clause;
+ return $clause;
}
sub _custom_search {
- my ($self) = @_;
- my $params = $self->_params;
-
- my $joiner = $params->{j_top} || '';
- my $current_clause = $joiner eq 'AND_G'
+ my ($self) = @_;
+ my $params = $self->_params;
+
+ 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 ($self->_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);
- my @clause_stack;
- foreach my $id ($self->_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);
+ $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"};
- # 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;
+ # 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 ($self) = @_;
+ my $params = $self->_params;
+ my @param_list = keys %$params;
- my @field_params = grep { /^f\d+$/ } @param_list;
- my @field_ids = map { /(\d+)/; $1 } @field_params;
- @field_ids = sort { $a <=> $b } @field_ids;
- return @field_ids;
+ my @field_params = grep {/^f\d+$/} @param_list;
+ my @field_ids = map { /(\d+)/; $1 } @field_params;
+ @field_ids = sort { $a <=> $b } @field_ids;
+ return @field_ids;
}
sub _handle_chart {
- my ($self, $chart_id, $clause, $condition) = @_;
- my $dbh = Bugzilla->dbh;
- my $params = $self->_params;
- my ($field, $operator, $value) = $condition->fov;
- return if (!defined $field or !defined $operator or !defined $value);
- $field = FIELD_MAP->{$field} || $field;
-
- my $string_value;
- if (ref $value eq 'ARRAY') {
- # Trim input and ignore blank values.
- @$value = map { trim($_) } @$value;
- @$value = grep { defined $_ and $_ ne '' } @$value;
- return if !@$value;
- $string_value = join(',', @$value);
- }
- else {
- return if $value eq '';
- $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 => $string_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);
+ 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;
+ if (ref $value eq 'ARRAY') {
+
+ # Trim input and ignore blank values.
+ @$value = map { trim($_) } @$value;
+ @$value = grep { defined $_ and $_ ne '' } @$value;
+ return if !@$value;
+ $string_value = join(',', @$value);
+ }
+ else {
+ return if $value eq '';
+ $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 => $string_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);
}
##################################
@@ -1985,122 +1949,128 @@ sub _handle_chart {
# 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 ($self, $args) = @_;
+ my ($field, $operator) = @$args{qw(field operator)};
- 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 (my $parse_func = SPECIAL_PARSING->{$field}) {
+ $self->$parse_func($args);
- if ($override) {
- my $search_func = $self->_pick_override_function($override, $operator);
- $self->$search_func($args) if $search_func;
- }
+ # 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};
+ }
- # 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);
- }
+ my $operator_field_override = $self->_get_operator_field_override();
+ my $override = $operator_field_override->{$field};
- 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 });
- }
+ # 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 = %{ COLUMN_JOINS() };
- # BMO - add search object to hook
- Bugzilla::Hook::process('buglist_column_joins',
- { column_joins => \%column_joins, search => $self });
+ my %column_joins = %{COLUMN_JOINS()};
- $cache->{column_joins} = \%column_joins;
- return $cache->{column_joins};
+ # BMO - add search object to hook
+ Bugzilla::Hook::process('buglist_column_joins',
+ {column_joins => \%column_joins, search => $self});
+
+ $cache->{column_joins} = \%column_joins;
+ return $cache->{column_joins};
}
###########################
@@ -2112,33 +2082,34 @@ 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;
- if ($is_numeric) {
- my $quoted = $value;
- trick_taint($quoted);
- return $quoted;
- }
- 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;
+ if ($is_numeric) {
+ my $quoted = $value;
+ trick_taint($quoted);
+ return $quoted;
+ }
+ return Bugzilla->dbh->quote($value);
}
sub build_subselect {
- my ($outer, $inner, $table, $cond, $negate) = @_;
- # 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) = @_;
+
+ # 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
@@ -2146,70 +2117,71 @@ 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;
- }
+ my ($self, $args, $split_on) = @_;
+ $split_on ||= qr/[\s,]+/;
+ my $dbh = Bugzilla->dbh;
+ my $all_values = $args->{all_values};
- if ($args->{field} eq 'resolution') {
- @array = map { $_ eq '---' ? '' : $_ } @array;
- }
+ 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;
+ 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;
- @words = map { $dbh->quote($_) } @words;
- my @terms = map { $dbh->sql_iposition($_, $args->{full_field}) . " > 0" }
- @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;
+ @words = map { $dbh->quote($_) } @words;
+ my @terms
+ = map { $dbh->sql_iposition($_, $args->{full_field}) . " > 0" } @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");
- }
+ 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);
- return @terms;
+ # 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;
}
#####################################
@@ -2217,106 +2189,115 @@ sub _word_terms {
#####################################
sub _timestamp_translate {
- my ($self, $ignore_time, $args) = @_;
- my $value = $args->{value};
- my $dbh = Bugzilla->dbh;
+ my ($self, $ignore_time, $args) = @_;
+ my $value = $args->{value};
+ my $dbh = Bugzilla->dbh;
- return if $value !~ /^(?:[\+\-]?\d+[hdwmy]s?|now)$/i;
+ return if $value !~ /^(?:[\+\-]?\d+[hdwmy]s?|now)$/i;
- $value = SqlifyDate($value);
- # By default, the time is appended to the date, which we don't always want.
- if ($ignore_time) {
- ($value) = split(/\s/, $value);
- }
- $args->{value} = $value;
- $args->{quoted} = $dbh->quote($value);
+ $value = SqlifyDate($value);
+
+ # By default, the time is appended to the date, which we don't always want.
+ if ($ignore_time) {
+ ($value) = split(/\s/, $value);
+ }
+ $args->{value} = $value;
+ $args->{quoted} = $dbh->quote($value);
}
sub _datetime_translate {
- return shift->_timestamp_translate(0, @_);
+ return shift->_timestamp_translate(0, @_);
}
sub _last_visit_datetime {
- my ($self, $args) = @_;
- my $value = $args->{value};
-
- $self->_datetime_translate($args);
- if ($value eq $args->{value}) {
- # Failed to translate a datetime. let's try the pronoun expando.
- if ($value eq '%last_changed%') {
- $self->_add_extra_column('changeddate');
- $args->{value} = $args->{quoted} = 'bugs.delta_ts';
- }
+ my ($self, $args) = @_;
+ my $value = $args->{value};
+
+ $self->_datetime_translate($args);
+ if ($value eq $args->{value}) {
+
+ # Failed to translate a datetime. let's try the pronoun expando.
+ if ($value eq '%last_changed%') {
+ $self->_add_extra_column('changeddate');
+ $args->{value} = $args->{quoted} = 'bugs.delta_ts';
}
+ }
}
sub _date_translate {
- return shift->_timestamp_translate(1, @_);
+ return shift->_timestamp_translate(1, @_);
}
sub SqlifyDate {
- my ($str) = @_;
- my $fmt = "%Y-%m-%d %H:%M:%S";
- $str = "" if (!defined $str || lc($str) eq 'now');
- if ($str eq "") {
- my ($sec, $min, $hour, $mday, $month, $year, $wday) = localtime(time());
- return sprintf("%4d-%02d-%02d 00:00:00", $year+1900, $month+1, $mday);
- }
-
- if ($str =~ /^(-|\+)?(\d+)([hdwmy])(s?)$/i) { # relative date
- my ($sign, $amount, $unit, $startof, $date) = ($1, $2, lc $3, lc $4, time);
- my ($sec, $min, $hour, $mday, $month, $year, $wday) = localtime($date);
- if ($sign && $sign eq '+') { $amount = -$amount; }
- $startof = 1 if $amount == 0;
- if ($unit eq 'w') { # convert weeks to days
- $amount = 7*$amount;
- $amount += $wday if $startof;
- $unit = 'd';
- }
- if ($unit eq 'd') {
- if ($startof) {
- $fmt = "%Y-%m-%d 00:00:00";
- $date -= $sec + 60*$min + 3600*$hour;
- }
- $date -= 24*3600*$amount;
- return time2str($fmt, $date);
- }
- elsif ($unit eq 'y') {
- if ($startof) {
- return sprintf("%4d-01-01 00:00:00", $year+1900-$amount);
- }
- else {
- return sprintf("%4d-%02d-%02d %02d:%02d:%02d",
- $year+1900-$amount, $month+1, $mday, $hour, $min, $sec);
- }
- }
- elsif ($unit eq 'm') {
- $month -= $amount;
- while ($month<0) { $year--; $month += 12; }
- if ($startof) {
- return sprintf("%4d-%02d-01 00:00:00", $year+1900, $month+1);
- }
- else {
- return sprintf("%4d-%02d-%02d %02d:%02d:%02d",
- $year+1900, $month+1, $mday, $hour, $min, $sec);
- }
- }
- elsif ($unit eq 'h') {
- # Special case for 'beginning of an hour'
- if ($startof) {
- $fmt = "%Y-%m-%d %H:00:00";
- }
- $date -= 3600*$amount;
- return time2str($fmt, $date);
- }
- return undef; # should not happen due to regexp at top
- }
- my $date = str2time($str);
- if (!defined($date)) {
- ThrowUserError("illegal_date", { date => $str });
- }
- return time2str($fmt, $date);
+ my ($str) = @_;
+ my $fmt = "%Y-%m-%d %H:%M:%S";
+ $str = "" if (!defined $str || lc($str) eq 'now');
+ if ($str eq "") {
+ my ($sec, $min, $hour, $mday, $month, $year, $wday) = localtime(time());
+ return sprintf("%4d-%02d-%02d 00:00:00", $year + 1900, $month + 1, $mday);
+ }
+
+ if ($str =~ /^(-|\+)?(\d+)([hdwmy])(s?)$/i) { # relative date
+ my ($sign, $amount, $unit, $startof, $date) = ($1, $2, lc $3, lc $4, time);
+ my ($sec, $min, $hour, $mday, $month, $year, $wday) = localtime($date);
+ if ($sign && $sign eq '+') { $amount = -$amount; }
+ $startof = 1 if $amount == 0;
+ if ($unit eq 'w') { # convert weeks to days
+ $amount = 7 * $amount;
+ $amount += $wday if $startof;
+ $unit = 'd';
+ }
+ if ($unit eq 'd') {
+ if ($startof) {
+ $fmt = "%Y-%m-%d 00:00:00";
+ $date -= $sec + 60 * $min + 3600 * $hour;
+ }
+ $date -= 24 * 3600 * $amount;
+ return time2str($fmt, $date);
+ }
+ elsif ($unit eq 'y') {
+ if ($startof) {
+ return sprintf("%4d-01-01 00:00:00", $year + 1900 - $amount);
+ }
+ else {
+ return sprintf(
+ "%4d-%02d-%02d %02d:%02d:%02d",
+ $year + 1900 - $amount,
+ $month + 1, $mday, $hour, $min, $sec
+ );
+ }
+ }
+ elsif ($unit eq 'm') {
+ $month -= $amount;
+ while ($month < 0) { $year--; $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);
}
######################################
@@ -2324,163 +2305,170 @@ 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');
}
- return 0;
+ }
+ 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)";
+ }
+ return 0;
}
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) =
- @$args{qw(value operator field chart_id joins)};
- 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);
- my $table = "user_group_map_$chart_id";
- my $join = {
- table => 'user_group_map',
- as => $table,
- from => $field,
- 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)
+ = @$args{qw(value operator field chart_id joins)};
+ 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);
+ my $table = "user_group_map_$chart_id";
+ my $join = {
+ table => 'user_group_map',
+ as => $table,
+ from => $field,
+ 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 _cc_pronoun {
- my ($self, $args) = @_;
- my ($full_field, $value) = @$args{qw(full_field value)};
- my $user = $self->_user;
+ my ($self, $args) = @_;
+ my ($full_field, $value) = @$args{qw(full_field value)};
+ my $user = $self->_user;
- if ($value =~ /\%group/) {
- return $self->_cc_exact_group($args);
- }
- elsif ($value =~ /^(%\w+%)$/) {
- $args->{value} = pronoun($1, $user);
- $args->{quoted} = $args->{value};
- $args->{value_is_id} = 1;
- }
+ if ($value =~ /\%group/) {
+ return $self->_cc_exact_group($args);
+ }
+ elsif ($value =~ /^(%\w+%)$/) {
+ $args->{value} = pronoun($1, $user);
+ $args->{quoted} = $args->{value};
+ $args->{value_is_id} = 1;
+ }
}
sub _cc_exact_group {
- my ($self, $args) = @_;
- my ($chart_id, $sequence, $joins, $operator, $value) =
- @$args{qw(chart_id sequence joins operator value)};
- my $user = $self->_user;
- my $dbh = Bugzilla->dbh;
-
- $value =~ m/%group\.([^%]+)%/;
- my $group = Bugzilla::Group->check({ name => $1, _error => 'invalid_group_name' });
- $group->check_members_are_visible();
- $user->in_group($group)
- || ThrowUserError('invalid_group_name', {name => $group->name});
-
- my $all_groups = Bugzilla::Group->flatten_group_membership($group->id);
-
- # This is for the email1, email2, email3 fields from query.cgi.
- if ($chart_id eq "") {
- $chart_id = "CC$$sequence";
- $args->{sequence}++;
- }
-
- my $cc_table = "cc_$chart_id";
- push(@$joins, { table => 'cc', as => $cc_table });
- my $group_table = "user_group_map_$chart_id";
- my $group_join = {
- table => 'user_group_map',
- as => $group_table,
- from => "$cc_table.who",
- to => 'user_id',
- extra => [$dbh->sql_in("$group_table.group_id", $all_groups),
- "$group_table.isbless = 0"],
- };
- push(@$joins, $group_join);
-
- if ($operator =~ /^not/) {
- $args->{term} = "$group_table.group_id IS NULL";
- }
- else {
- $args->{term} = "$group_table.group_id IS NOT NULL";
- }
+ my ($self, $args) = @_;
+ my ($chart_id, $sequence, $joins, $operator, $value)
+ = @$args{qw(chart_id sequence joins operator value)};
+ my $user = $self->_user;
+ my $dbh = Bugzilla->dbh;
+
+ $value =~ m/%group\.([^%]+)%/;
+ my $group
+ = Bugzilla::Group->check({name => $1, _error => 'invalid_group_name'});
+ $group->check_members_are_visible();
+ $user->in_group($group)
+ || ThrowUserError('invalid_group_name', {name => $group->name});
+
+ my $all_groups = Bugzilla::Group->flatten_group_membership($group->id);
+
+ # This is for the email1, email2, email3 fields from query.cgi.
+ if ($chart_id eq "") {
+ $chart_id = "CC$$sequence";
+ $args->{sequence}++;
+ }
+
+ my $cc_table = "cc_$chart_id";
+ push(@$joins, {table => 'cc', as => $cc_table});
+ my $group_table = "user_group_map_$chart_id";
+ my $group_join = {
+ table => 'user_group_map',
+ as => $group_table,
+ from => "$cc_table.who",
+ to => 'user_id',
+ extra => [
+ $dbh->sql_in("$group_table.group_id", $all_groups),
+ "$group_table.isbless = 0"
+ ],
+ };
+ push(@$joins, $group_join);
+
+ if ($operator =~ /^not/) {
+ $args->{term} = "$group_table.group_id IS NULL";
+ }
+ else {
+ $args->{term} = "$group_table.group_id IS NOT NULL";
+ }
}
# XXX This should probably be merged with cc_pronoun.
sub _commenter_pronoun {
- my ($self, $args) = @_;
- my $value = $args->{value};
- my $user = $self->_user;
-
- if ($value =~ /^(%\w+%)$/) {
- $args->{value} = pronoun($1, $user);
- $args->{quoted} = $args->{value};
- $args->{value_is_id} = 1;
- }
+ my ($self, $args) = @_;
+ my $value = $args->{value};
+ my $user = $self->_user;
+
+ if ($value =~ /^(%\w+%)$/) {
+ $args->{value} = pronoun($1, $user);
+ $args->{quoted} = $args->{value};
+ $args->{value_is_id} = 1;
+ }
}
# XXX only works with %user% currently
sub _triage_owner_pronoun {
- my ($self, $args) = @_;
- my $value = $args->{value};
- my $user = $self->_user;
- if ($value eq "%user%") {
- if ($user->id) {
- $args->{value} = $user->id;
- $args->{quoted} = $args->{value};
- $args->{value_is_id} = 1;
- } else {
- ThrowUserError('login_required_for_pronoun');
- }
+ my ($self, $args) = @_;
+ my $value = $args->{value};
+ my $user = $self->_user;
+ if ($value eq "%user%") {
+ if ($user->id) {
+ $args->{value} = $user->id;
+ $args->{quoted} = $args->{value};
+ $args->{value_is_id} = 1;
+ }
+ else {
+ ThrowUserError('login_required_for_pronoun');
}
+ }
}
#####################################################################
@@ -2488,571 +2476,584 @@ sub _triage_owner_pronoun {
#####################################################################
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;
+ }
+ else {
+ my $as = "name_${field}_$chart_id";
+
+ # For fields with periods in their name.
+ $as =~ s/\./_/;
+ my $join = {
+ table => 'profiles',
+ as => $as,
+ from => $args->{full_field},
+ to => 'userid',
+ join => (!$is_in_other_table and !$is_nullable) ? 'INNER' : undef,
+ };
+ push(@$joins, $join);
+ $args->{full_field} = "$as.login_name";
+ }
+
+ # We COALESCE fields that can be NULL, to make "not"-style operators
+ # continue to work properly. For example, "qa_contact is not equal to bob"
+ # should also show bugs where the qa_contact is NULL. With COALESCE,
+ # it does.
+ if ($is_nullable) {
+ $args->{full_field} = "COALESCE($args->{full_field}, $null_alternate)";
+ }
+
+ # For fields whose values are stored in other tables, negation (NOT)
+ # only works properly if we put the condition into the JOIN instead
+ # of the WHERE.
+ if ($is_in_other_table) {
+
+ # Using the last join works properly whether we're searching based
+ # on userid or login_name.
+ my $last_join = $joins->[-1];
+
+ # For negative operators, the system we're using here
+ # only works properly if we reverse the operator and check IS NULL
+ # in the WHERE.
+ my $is_negative = $operator =~ /^(?:no|isempty)/ ? 1 : 0;
+ if ($is_negative) {
+ $args->{operator} = $self->_reverse_operator($operator);
}
-
- 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;
+ $self->_do_operator_function($args);
+ push(@{$last_join->{extra}}, $args->{term});
+
+ # For login_name searches, we only want a single join.
+ # So we create a subselect table out of our two joins. This makes
+ # negation (NOT) work properly for values that are in other
+ # tables.
+ if ($last_join->{table} eq 'profiles') {
+ pop @$joins;
+ $last_join->{join} = 'INNER';
+ my ($join_sql) = $self->_translate_join($last_join);
+ my $first_join = $joins->[-1];
+ my $as = $first_join->{as};
+ my $table = $first_join->{table};
+ my $columns = "bug_id";
+ $columns .= ",isprivate" if @{$first_join->{extra}};
+ my $new_table = "SELECT DISTINCT $columns FROM $table AS $as $join_sql";
+ $first_join->{table} = "($new_table)";
+
+ # We always want to LEFT JOIN the generated table.
+ delete $first_join->{join};
+
+ # To support OR charts, we need multiple tables.
+ my $new_as = $first_join->{as} . "_$sequence";
+ $_ =~ s/\Q$as\E/$new_as/ foreach @{$first_join->{extra}};
+ $first_join->{as} = $new_as;
+ $last_join = $first_join;
+ }
+
+ # If we're joining the first table (we're using a pronoun and
+ # searching by user id) then we need to check $other_table->{field}.
+ my $check_field = $last_join->{as} . '.bug_id';
+ if ($is_negative) {
+ $args->{term} = "$check_field IS NULL";
}
else {
- my $as = "name_${field}_$chart_id";
- # For fields with periods in their name.
- $as =~ s/\./_/;
- my $join = {
- table => 'profiles',
- as => $as,
- from => $args->{full_field},
- to => 'userid',
- join => (!$is_in_other_table and !$is_nullable) ? 'INNER' : undef,
- };
- push(@$joins, $join);
- $args->{full_field} = "$as.login_name";
- }
-
- # We COALESCE fields that can be NULL, to make "not"-style operators
- # continue to work properly. For example, "qa_contact is not equal to bob"
- # should also show bugs where the qa_contact is NULL. With COALESCE,
- # it does.
- if ($is_nullable) {
- $args->{full_field} = "COALESCE($args->{full_field}, $null_alternate)";
- }
-
- # For fields whose values are stored in other tables, negation (NOT)
- # only works properly if we put the condition into the JOIN instead
- # of the WHERE.
- if ($is_in_other_table) {
- # Using the last join works properly whether we're searching based
- # on userid or login_name.
- my $last_join = $joins->[-1];
-
- # For negative operators, the system we're using here
- # only works properly if we reverse the operator and check IS NULL
- # in the WHERE.
- my $is_negative = $operator =~ /^(?:no|isempty)/ ? 1 : 0;
- if ($is_negative) {
- $args->{operator} = $self->_reverse_operator($operator);
- }
- $self->_do_operator_function($args);
- push(@{ $last_join->{extra} }, $args->{term});
-
- # For login_name searches, we only want a single join.
- # So we create a subselect table out of our two joins. This makes
- # negation (NOT) work properly for values that are in other
- # tables.
- if ($last_join->{table} eq 'profiles') {
- pop @$joins;
- $last_join->{join} = 'INNER';
- my ($join_sql) = $self->_translate_join($last_join);
- my $first_join = $joins->[-1];
- my $as = $first_join->{as};
- my $table = $first_join->{table};
- my $columns = "bug_id";
- $columns .= ",isprivate" if @{ $first_join->{extra} };
- my $new_table = "SELECT DISTINCT $columns FROM $table AS $as $join_sql";
- $first_join->{table} = "($new_table)";
- # We always want to LEFT JOIN the generated table.
- delete $first_join->{join};
- # To support OR charts, we need multiple tables.
- my $new_as = $first_join->{as} . "_$sequence";
- $_ =~ s/\Q$as\E/$new_as/ foreach @{ $first_join->{extra} };
- $first_join->{as} = $new_as;
- $last_join = $first_join;
- }
-
- # If we're joining the first table (we're using a pronoun and
- # searching by user id) then we need to check $other_table->{field}.
- my $check_field = $last_join->{as} . '.bug_id';
- if ($is_negative) {
- $args->{term} = "$check_field IS NULL";
- }
- else {
- $args->{term} = "$check_field IS NOT NULL";
- }
+ $args->{term} = "$check_field IS NOT NULL";
}
+ }
}
# XXX This duplicates having Commenter as a search field.
sub _long_desc_changedby {
- my ($self, $args) = @_;
- my ($chart_id, $joins, $value) = @$args{qw(chart_id joins value)};
+ my ($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 = login_to_id($value, THROW_ERROR);
- $args->{term} = "$table.who = $user_id";
+ my $table = "longdescs_$chart_id";
+ push(@$joins, {table => 'longdescs', as => $table});
+ my $user_id = login_to_id($value, THROW_ERROR);
+ $args->{term} = "$table.who = $user_id";
}
sub _long_desc_changedbefore_after {
- my ($self, $args) = @_;
- my ($chart_id, $operator, $value, $joins) =
- @$args{qw(chart_id operator value joins)};
- my $dbh = Bugzilla->dbh;
-
- my $sql_operator = ($operator =~ /before/) ? '<=' : '>=';
- my $table = "longdescs_$chart_id";
- my $sql_date = $dbh->quote(SqlifyDate($value));
- my $join = {
- table => 'longdescs',
- as => $table,
- extra => ["$table.bug_when $sql_operator $sql_date"],
- };
- push(@$joins, $join);
- $args->{term} = "$table.bug_when IS NOT NULL";
-
- # If the user is not part of the insiders group, they cannot see
- # private comments
- if (!$self->_user->is_insider) {
- $args->{term} .= " AND $table.isprivate = 0";
- }
+ my ($self, $args) = @_;
+ my ($chart_id, $operator, $value, $joins)
+ = @$args{qw(chart_id operator value joins)};
+ my $dbh = Bugzilla->dbh;
+
+ my $sql_operator = ($operator =~ /before/) ? '<=' : '>=';
+ my $table = "longdescs_$chart_id";
+ my $sql_date = $dbh->quote(SqlifyDate($value));
+ my $join = {
+ table => 'longdescs',
+ as => $table,
+ extra => ["$table.bug_when $sql_operator $sql_date"],
+ };
+ push(@$joins, $join);
+ $args->{term} = "$table.bug_when IS NOT NULL";
+
+ # If the user is not part of the insiders group, they cannot see
+ # private comments
+ if (!$self->_user->is_insider) {
+ $args->{term} .= " AND $table.isprivate = 0";
+ }
}
sub _long_desc_nonchanged {
- my ($self, $args) = @_;
- my ($chart_id, $operator, $value, $joins, $bugs_table) =
- @$args{qw(chart_id operator value joins bugs_table)};
-
- if ($operator =~ /^is(not)?empty$/) {
- $args->{term} = $self->_multiselect_isempty($args, $operator eq 'isnotempty');
- return;
- }
- my $dbh = Bugzilla->dbh;
-
- my $table = "longdescs_$chart_id";
- my $join_args = {
- chart_id => $chart_id,
- sequence => $chart_id,
- field => 'longdesc',
- full_field => "$table.thetext",
- operator => $operator,
- value => $value,
- all_values => $value,
- quoted => $dbh->quote($value),
- joins => [],
- bugs_table => $bugs_table,
- };
- $self->_do_operator_function($join_args);
-
- # If the user is not part of the insiders group, they cannot see
- # private comments
- if (!$self->_user->is_insider) {
- $join_args->{term} .= ($join_args->{term} ? " AND " : "")
- . "$table.isprivate = 0";
- }
-
- my $join = {
- table => 'longdescs',
- as => $table,
- extra => [ $join_args->{term} ],
- };
- push(@$joins, $join);
-
- $args->{term} = "$table.comment_id IS NOT NULL";
+ my ($self, $args) = @_;
+ my ($chart_id, $operator, $value, $joins, $bugs_table)
+ = @$args{qw(chart_id operator value joins bugs_table)};
+
+ if ($operator =~ /^is(not)?empty$/) {
+ $args->{term} = $self->_multiselect_isempty($args, $operator eq 'isnotempty');
+ return;
+ }
+ my $dbh = Bugzilla->dbh;
+
+ my $table = "longdescs_$chart_id";
+ my $join_args = {
+ chart_id => $chart_id,
+ sequence => $chart_id,
+ field => 'longdesc',
+ full_field => "$table.thetext",
+ operator => $operator,
+ value => $value,
+ all_values => $value,
+ quoted => $dbh->quote($value),
+ joins => [],
+ bugs_table => $bugs_table,
+ };
+ $self->_do_operator_function($join_args);
+
+ # If the user is not part of the insiders group, they cannot see
+ # private comments
+ if (!$self->_user->is_insider) {
+ $join_args->{term}
+ .= ($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 ($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 = login_to_id($value, THROW_ERROR);
- $args->{term} = "$table.who = $user_id AND $table.work_time != 0";
+ my $table = "longdescs_$chart_id";
+ push(@$joins, {table => 'longdescs', as => $table});
+ my $user_id = login_to_id($value, THROW_ERROR);
+ $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);
+ my ($self, $args) = @_;
+ my ($chart_id, $operator, $value, $joins)
+ = @$args{qw(chart_id operator value joins)};
+ my $dbh = Bugzilla->dbh;
- $args->{term} = "$table.bug_when IS NOT NULL";
+ 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) = @_;
+ my ($self, $args) = @_;
- $args->{full_field} = $self->COLUMNS->{percentage_complete}->{name};
+ $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 _bug_interest_ts {
- my ($self, $args) = @_;
+ my ($self, $args) = @_;
- $args->{full_field} = $self->COLUMNS->{bug_interest_ts}->{name};
- $self->_add_extra_column('bug_interest_ts');
+ $args->{full_field} = $self->COLUMNS->{bug_interest_ts}->{name};
+ $self->_add_extra_column('bug_interest_ts');
}
sub _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;
+ my ($self, $args) = @_;
+ my $dbh = Bugzilla->dbh;
- $args->{full_field} = "(" . $dbh->sql_to_days('NOW()') . " - " .
- $dbh->sql_to_days('bugs.delta_ts') . ")";
+ $args->{full_field}
+ = "("
+ . $dbh->sql_to_days('NOW()') . " - "
+ . $dbh->sql_to_days('bugs.delta_ts') . ")";
}
sub _assignee_last_login {
- my ($self, $args) = @_;
-
- push @{ $args->{joins} }, {
- as => 'assignee',
- table => 'profiles',
- from => 'assigned_to',
- to => 'userid',
- join => 'INNER',
+ my ($self, $args) = @_;
+
+ push @{$args->{joins}},
+ {
+ as => 'assignee',
+ table => 'profiles',
+ from => 'assigned_to',
+ to => 'userid',
+ join => 'INNER',
};
- # coalesce to 1998 to make it easy to search for users who haven't logged
- # in since we added last_seen_date
- $args->{full_field} = "COALESCE(assignee.last_seen_date, '1998-01-01')";
+
+ # coalesce to 1998 to make it easy to search for users who haven't logged
+ # in since we added last_seen_date
+ $args->{full_field} = "COALESCE(assignee.last_seen_date, '1998-01-01')";
}
sub _component_nonchanged {
- my ($self, $args) = @_;
+ 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});
+ $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) = @_;
-
- # BMO - product aliases
- # swap out old product names for new ones
- if (ref($args->{all_values})) {
- my $aliased;
- foreach my $value (@{ $args->{all_values} }) {
- if (exists PRODUCT_ALIASES->{lc($value)}) {
- $value = PRODUCT_ALIASES->{lc($value)};
- $aliased = 1;
- }
- }
- if ($aliased) {
- $args->{value} = join(',', @{ $args->{all_values} });
- $args->{quoted} = Bugzilla->dbh->quote($args->{value});
- }
- }
- elsif (exists PRODUCT_ALIASES->{lc($args->{value})}) {
- $args->{value} = PRODUCT_ALIASES->{lc($args->{value})};
- $args->{all_values} = $args->{value};
- $args->{quoted} = Bugzilla->dbh->quote($args->{value});
- }
-
- # 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) = @_;
+
+ # BMO - product aliases
+ # swap out old product names for new ones
+ if (ref($args->{all_values})) {
+ my $aliased;
+ foreach my $value (@{$args->{all_values}}) {
+ if (exists PRODUCT_ALIASES->{lc($value)}) {
+ $value = PRODUCT_ALIASES->{lc($value)};
+ $aliased = 1;
+ }
+ }
+ if ($aliased) {
+ $args->{value} = join(',', @{$args->{all_values}});
+ $args->{quoted} = Bugzilla->dbh->quote($args->{value});
+ }
+ }
+ elsif (exists PRODUCT_ALIASES->{lc($args->{value})}) {
+ $args->{value} = PRODUCT_ALIASES->{lc($args->{value})};
+ $args->{all_values} = $args->{value};
+ $args->{quoted} = Bugzilla->dbh->quote($args->{value});
+ }
+
+ # 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 _classification_nonchanged {
- my ($self, $args) = @_;
- my $joins = $args->{joins};
+ my ($self, $args) = @_;
+ my $joins = $args->{joins};
- # This joins the right tables for us.
- $self->_add_extra_column('product');
+ # 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);
+ # 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 _triage_owner_nonchanged {
- my ($self, $args) = @_;
- $self->_add_extra_column('triage_owner');
- $args->{full_field} = $args->{value_is_id} ? 'profiles.userid' : 'profiles.login_name';
- $self->_do_operator_function($args);
- my $term = $args->{term};
- $args->{term} = build_subselect('bugs.component_id', 'components.id',
- 'profiles JOIN components ON components.triage_owner_id = profiles.userid', $term);
+ my ($self, $args) = @_;
+ $self->_add_extra_column('triage_owner');
+ $args->{full_field}
+ = $args->{value_is_id} ? 'profiles.userid' : 'profiles.login_name';
+ $self->_do_operator_function($args);
+ my $term = $args->{term};
+ $args->{term}
+ = build_subselect('bugs.component_id', 'components.id',
+ 'profiles JOIN components ON components.triage_owner_id = profiles.userid',
+ $term);
}
sub _nullable {
- my ($self, $args) = @_;
- my $field = $args->{full_field};
- $args->{full_field} = "COALESCE($field, '')";
+ my ($self, $args) = @_;
+ my $field = $args->{full_field};
+ $args->{full_field} = "COALESCE($field, '')";
}
sub _nullable_int {
- my ($self, $args) = @_;
- my $field = $args->{full_field};
- $args->{full_field} = "COALESCE($field, 0)";
+ my ($self, $args) = @_;
+ my $field = $args->{full_field};
+ $args->{full_field} = "COALESCE($field, 0)";
}
sub _nullable_datetime {
- my ($self, $args) = @_;
- my $field = $args->{full_field};
- my $empty = Bugzilla->dbh->quote(EMPTY_DATETIME);
- $args->{full_field} = "COALESCE($field, $empty)";
+ my ($self, $args) = @_;
+ my $field = $args->{full_field};
+ my $empty = Bugzilla->dbh->quote(EMPTY_DATETIME);
+ $args->{full_field} = "COALESCE($field, $empty)";
}
sub _nullable_date {
- my ($self, $args) = @_;
- my $field = $args->{full_field};
- my $empty = Bugzilla->dbh->quote(EMPTY_DATE);
- $args->{full_field} = "COALESCE($field, $empty)";
+ my ($self, $args) = @_;
+ my $field = $args->{full_field};
+ my $empty = Bugzilla->dbh->quote(EMPTY_DATE);
+ $args->{full_field} = "COALESCE($field, $empty)";
}
sub _deadline {
- my ($self, $args) = @_;
- my $field = $args->{full_field};
- # This makes "equals" searches work on all DBs (even on MySQL, which
- # has a bug: http://bugs.mysql.com/bug.php?id=60324).
- $args->{full_field} = Bugzilla->dbh->sql_date_format($field, '%Y-%m-%d');
- $self->_nullable_datetime($args);
+ my ($self, $args) = @_;
+ my $field = $args->{full_field};
+
+ # This makes "equals" searches work on all DBs (even on MySQL, which
+ # has a bug: http://bugs.mysql.com/bug.php?id=60324).
+ $args->{full_field} = Bugzilla->dbh->sql_date_format($field, '%Y-%m-%d');
+ $self->_nullable_datetime($args);
}
sub _owner_idle_time_greater_less {
- my ($self, $args) = @_;
- my ($chart_id, $joins, $value, $operator) =
- @$args{qw(chart_id joins value operator)};
- my $dbh = Bugzilla->dbh;
-
- my $table = "idle_$chart_id";
- my $quoted = $dbh->quote(SqlifyDate($value));
-
- my $ld_table = "comment_$table";
- my $act_table = "activity_$table";
- my $comments_join = {
- table => 'longdescs',
- as => $ld_table,
- from => 'assigned_to',
- to => 'who',
- extra => ["$ld_table.bug_when > $quoted"],
- };
- my $activity_join = {
- table => 'bugs_activity',
- as => $act_table,
- from => 'assigned_to',
- to => 'who',
- extra => ["$act_table.bug_when > $quoted"]
- };
-
- push(@$joins, $comments_join, $activity_join);
-
- if ($operator =~ /greater/) {
- $args->{term} =
- "$ld_table.who IS NULL AND $act_table.who IS NULL";
- } else {
- $args->{term} =
- "($ld_table.who IS NOT NULL OR $act_table.who IS NOT NULL)";
- }
+ my ($self, $args) = @_;
+ my ($chart_id, $joins, $value, $operator)
+ = @$args{qw(chart_id joins value operator)};
+ my $dbh = Bugzilla->dbh;
+
+ my $table = "idle_$chart_id";
+ my $quoted = $dbh->quote(SqlifyDate($value));
+
+ my $ld_table = "comment_$table";
+ my $act_table = "activity_$table";
+ my $comments_join = {
+ table => 'longdescs',
+ as => $ld_table,
+ from => 'assigned_to',
+ to => 'who',
+ extra => ["$ld_table.bug_when > $quoted"],
+ };
+ my $activity_join = {
+ table => 'bugs_activity',
+ as => $act_table,
+ from => 'assigned_to',
+ to => 'who',
+ extra => ["$act_table.bug_when > $quoted"]
+ };
+
+ push(@$joins, $comments_join, $activity_join);
+
+ if ($operator =~ /greater/) {
+ $args->{term} = "$ld_table.who IS NULL AND $act_table.who IS NULL";
+ }
+ else {
+ $args->{term} = "($ld_table.who IS NOT NULL OR $act_table.who IS NOT NULL)";
+ }
}
sub _multiselect_negative {
- my ($self, $args) = @_;
- my ($field, $operator) = @$args{qw(field operator)};
+ my ($self, $args) = @_;
+ my ($field, $operator) = @$args{qw(field operator)};
- $args->{operator} = $self->_reverse_operator($operator);
- $args->{term} = $self->_multiselect_term($args, 1);
+ $args->{operator} = $self->_reverse_operator($operator);
+ $args->{term} = $self->_multiselect_term($args, 1);
}
sub _multiselect_multiple {
- my ($self, $args) = @_;
- my ($chart_id, $field, $operator, $value)
- = @$args{qw(chart_id field operator value)};
- my $dbh = Bugzilla->dbh;
-
- # We want things like "cf_multi_select=two+words" to still be
- # considered a search for two separate words, unless we're using
- # anyexact. (_all_values would consider that to be one "word" with a
- # space in it, because it's not in the Boolean Charts).
- my @words = $operator eq 'anyexact' ? $self->_all_values($args)
- : split(/[\s,]+/, $value);
-
- my @terms;
- foreach my $word (@words) {
- next if $word eq '';
- $args->{value} = $word;
- $args->{quoted} = $dbh->quote($word);
- push(@terms, $self->_multiselect_term($args));
- }
-
- # The spacing in the joins helps make the resulting SQL more readable.
- if ($operator =~ /^any/) {
- $args->{term} = join("\n OR ", @terms);
- }
- else {
- $args->{term} = join("\n AND ", @terms);
- }
+ my ($self, $args) = @_;
+ my ($chart_id, $field, $operator, $value)
+ = @$args{qw(chart_id field operator value)};
+ my $dbh = Bugzilla->dbh;
+
+ # We want things like "cf_multi_select=two+words" to still be
+ # considered a search for two separate words, unless we're using
+ # anyexact. (_all_values would consider that to be one "word" with a
+ # space in it, because it's not in the Boolean Charts).
+ my @words
+ = $operator eq 'anyexact'
+ ? $self->_all_values($args)
+ : split(/[\s,]+/, $value);
+
+ my @terms;
+ foreach my $word (@words) {
+ next if $word eq '';
+ $args->{value} = $word;
+ $args->{quoted} = $dbh->quote($word);
+ push(@terms, $self->_multiselect_term($args));
+ }
+
+ # The spacing in the joins helps make the resulting SQL more readable.
+ if ($operator =~ /^any/) {
+ $args->{term} = join("\n OR ", @terms);
+ }
+ else {
+ $args->{term} = join("\n AND ", @terms);
+ }
}
sub _flagtypes_nonchanged {
- my ($self, $args) = @_;
- my ($chart_id, $operator, $value, $joins, $bugs_table, $condition) =
- @$args{qw(chart_id operator value joins bugs_table condition)};
-
- if ($operator =~ /^is(not)?empty$/) {
- $args->{term} = $self->_multiselect_isempty($args, $operator eq 'isnotempty');
- return;
- }
-
- my $dbh = Bugzilla->dbh;
-
- # For 'not' operators, we need to negate the whole term.
- # If you search for "Flags" (does not contain) "approval+" we actually want
- # to return *bugs* that don't contain an approval+ flag. Without rewriting
- # the negation we'll search for *flags* which don't contain approval+.
- if ($operator =~ s/^not//) {
- $args->{operator} = $operator;
- $condition->operator($operator);
- $condition->negate(1);
- }
-
- my $subselect_args = {
- chart_id => $chart_id,
- sequence => $chart_id,
- field => 'flagtypes.name',
- full_field => $dbh->sql_string_concat("flagtypes_$chart_id.name", "flags_$chart_id.status"),
- operator => $operator,
- value => $value,
- all_values => $value,
- quoted => $dbh->quote($value),
- joins => [],
- bugs_table => "bugs_$chart_id",
- };
- $self->_do_operator_function($subselect_args);
- my $subselect_term = $subselect_args->{term};
-
- # don't call build_subselect as this must run as a true sub-select
- $args->{term} = "EXISTS (
+ my ($self, $args) = @_;
+ my ($chart_id, $operator, $value, $joins, $bugs_table, $condition)
+ = @$args{qw(chart_id operator value joins bugs_table condition)};
+
+ if ($operator =~ /^is(not)?empty$/) {
+ $args->{term} = $self->_multiselect_isempty($args, $operator eq 'isnotempty');
+ return;
+ }
+
+ my $dbh = Bugzilla->dbh;
+
+ # For 'not' operators, we need to negate the whole term.
+ # If you search for "Flags" (does not contain) "approval+" we actually want
+ # to return *bugs* that don't contain an approval+ flag. Without rewriting
+ # the negation we'll search for *flags* which don't contain approval+.
+ if ($operator =~ s/^not//) {
+ $args->{operator} = $operator;
+ $condition->operator($operator);
+ $condition->negate(1);
+ }
+
+ my $subselect_args = {
+ chart_id => $chart_id,
+ sequence => $chart_id,
+ field => 'flagtypes.name',
+ full_field =>
+ $dbh->sql_string_concat("flagtypes_$chart_id.name", "flags_$chart_id.status"),
+ operator => $operator,
+ value => $value,
+ all_values => $value,
+ quoted => $dbh->quote($value),
+ joins => [],
+ bugs_table => "bugs_$chart_id",
+ };
+ $self->_do_operator_function($subselect_args);
+ my $subselect_term = $subselect_args->{term};
+
+ # don't call build_subselect as this must run as a true sub-select
+ $args->{term} = "EXISTS (
SELECT 1
FROM $bugs_table bugs_$chart_id
LEFT JOIN attachments AS attachments_$chart_id
@@ -3069,182 +3070,192 @@ sub _flagtypes_nonchanged {
}
sub _multiselect_nonchanged {
- my ($self, $args) = @_;
- my ($chart_id, $joins, $field, $operator) =
- @$args{qw(chart_id joins field operator)};
- $args->{term} = $self->_multiselect_term($args)
+ my ($self, $args) = @_;
+ my ($chart_id, $joins, $field, $operator)
+ = @$args{qw(chart_id joins field operator)};
+ $args->{term} = $self->_multiselect_term($args);
}
sub _multiselect_table {
- my ($self, $args) = @_;
- my ($field, $chart_id) = @$args{qw(field chart_id)};
- my $dbh = Bugzilla->dbh;
-
- if ($field eq 'keywords') {
- $args->{full_field} = 'keyworddefs.name';
- return "keywords INNER JOIN keyworddefs".
- " ON keywords.keywordid = keyworddefs.id";
- }
- elsif ($field eq 'tag') {
- $args->{full_field} = 'tag.name';
- return "bug_tag INNER JOIN tag ON bug_tag.tag_id = tag.id AND user_id = "
- . ($self->_sharer_id || $self->_user->id);
- }
- elsif ($field eq 'bug_group') {
- $args->{full_field} = 'groups.name';
- return "bug_group_map INNER JOIN groups
+ my ($self, $args) = @_;
+ my ($field, $chart_id) = @$args{qw(field chart_id)};
+ my $dbh = Bugzilla->dbh;
+
+ if ($field eq 'keywords') {
+ $args->{full_field} = 'keyworddefs.name';
+ return "keywords INNER JOIN keyworddefs"
+ . " ON keywords.keywordid = keyworddefs.id";
+ }
+ elsif ($field eq 'tag') {
+ $args->{full_field} = 'tag.name';
+ return "bug_tag INNER JOIN tag ON bug_tag.tag_id = tag.id AND user_id = "
+ . ($self->_sharer_id || $self->_user->id);
+ }
+ elsif ($field eq 'bug_group') {
+ $args->{full_field} = 'groups.name';
+ return "bug_group_map INNER JOIN groups
ON bug_group_map.group_id = groups.id";
- }
- elsif ($field eq 'blocked' or $field eq 'dependson') {
- my $select = $field eq 'blocked' ? 'dependson' : 'blocked';
- $args->{_select_field} = $select;
- $args->{full_field} = $field;
- return "dependencies";
- }
- elsif ($field eq 'longdesc') {
- $args->{_extra_where} = " AND isprivate = 0"
- if !$self->_user->is_insider;
- $args->{full_field} = 'thetext';
- return "longdescs";
- }
- elsif ($field eq 'longdescs.isprivate') {
- ThrowUserError('auth_failure', { action => 'search',
- object => 'bug_fields',
- field => 'longdescs.isprivate' })
- if !$self->_user->is_insider;
- $args->{full_field} = 'isprivate';
- return "longdescs";
- }
- elsif ($field =~ /^attachments/) {
- $args->{_extra_where} = " AND isprivate = 0"
- if !$self->_user->is_insider;
- $field =~ /^attachments\.(.+)$/;
- $args->{full_field} = $1;
- return "attachments";
- }
- elsif ($field eq 'flagtypes.name') {
- $args->{full_field} = $dbh->sql_string_concat("flagtypes.name",
- "flags.status");
- return "flags INNER JOIN flagtypes ON flags.type_id = flagtypes.id";
- }
- elsif ($field eq 'comment_tag') {
- $args->{_extra_where} = " AND longdescs.isprivate = 0"
- if !$self->_user->is_insider;
- $args->{full_field} = 'longdescs_tags.tag';
- return "longdescs INNER JOIN longdescs_tags".
- " ON longdescs.comment_id = longdescs_tags.comment_id";
- }
- my $table = "bug_$field";
- $args->{full_field} = "bug_$field.value";
- return $table;
+ }
+ elsif ($field eq 'blocked' or $field eq 'dependson') {
+ my $select = $field eq 'blocked' ? 'dependson' : 'blocked';
+ $args->{_select_field} = $select;
+ $args->{full_field} = $field;
+ return "dependencies";
+ }
+ elsif ($field eq 'longdesc') {
+ $args->{_extra_where} = " AND isprivate = 0" if !$self->_user->is_insider;
+ $args->{full_field} = 'thetext';
+ return "longdescs";
+ }
+ elsif ($field eq 'longdescs.isprivate') {
+ ThrowUserError('auth_failure',
+ {action => 'search', object => 'bug_fields', field => 'longdescs.isprivate'})
+ if !$self->_user->is_insider;
+ $args->{full_field} = 'isprivate';
+ return "longdescs";
+ }
+ elsif ($field =~ /^attachments/) {
+ $args->{_extra_where} = " AND isprivate = 0" if !$self->_user->is_insider;
+ $field =~ /^attachments\.(.+)$/;
+ $args->{full_field} = $1;
+ return "attachments";
+ }
+ elsif ($field eq 'flagtypes.name') {
+ $args->{full_field} = $dbh->sql_string_concat("flagtypes.name", "flags.status");
+ return "flags INNER JOIN flagtypes ON flags.type_id = flagtypes.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};
- # 'empty' operators require special handling
- return $self->_multiselect_isempty($args, $not)
- if $operator =~ /^is(not)?empty$/;
- 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};
+
+ # 'empty' operators require special handling
+ return $self->_multiselect_isempty($args, $not)
+ if $operator =~ /^is(not)?empty$/;
+ my $table = $self->_multiselect_table($args);
+ $self->_do_operator_function($args);
+ my $term = $args->{term};
+ $term .= $args->{_extra_where} || '';
+ my $select = $args->{_select_field} || 'bug_id';
+ return build_subselect("$args->{bugs_table}.bug_id", $select, $table, $term,
+ $not);
}
# We can't use the normal operator_functions to build isempty queries which
# join to different tables.
sub _multiselect_isempty {
- my ($self, $args, $not) = @_;
- my ($field, $operator, $joins, $chart_id) = @$args{qw(field operator joins chart_id)};
- my $dbh = Bugzilla->dbh;
- $operator = $self->_reverse_operator($operator) if $not;
- $not = $operator eq 'isnotempty' ? 'NOT' : '';
-
- if ($field eq 'keywords') {
- push @$joins, {
- table => 'keywords',
- as => "keywords_$chart_id",
- from => 'bug_id',
- to => 'bug_id',
- };
- return "keywords_$chart_id.bug_id IS $not NULL";
- }
- elsif ($field eq 'bug_group') {
- push @$joins, {
- table => 'bug_group_map',
- as => "bug_group_map_$chart_id",
- from => 'bug_id',
- to => 'bug_id',
- };
- return "bug_group_map_$chart_id.bug_id IS $not NULL";
- }
- elsif ($field eq 'flagtypes.name') {
- push @$joins, {
- table => 'flags',
- as => "flags_$chart_id",
- from => 'bug_id',
- to => 'bug_id',
- };
- return "flags_$chart_id.bug_id IS $not NULL";
- }
- elsif ($field eq 'blocked' or $field eq 'dependson') {
- my $to = $field eq 'blocked' ? 'dependson' : 'blocked';
- push @$joins, {
- table => 'dependencies',
- as => "dependencies_$chart_id",
- from => 'bug_id',
- to => $to,
- };
- return "dependencies_$chart_id.$to IS $not NULL";
- }
- elsif ($field eq 'longdesc') {
- my @extra = ( "longdescs_$chart_id.type != " . CMT_HAS_DUPE );
- push @extra, "longdescs_$chart_id.isprivate = 0"
- unless $self->_user->is_insider;
- push @$joins, {
- table => 'longdescs',
- as => "longdescs_$chart_id",
- from => 'bug_id',
- to => 'bug_id',
- extra => \@extra,
- };
- return $not
- ? "longdescs_$chart_id.thetext != ''"
- : "longdescs_$chart_id.thetext = ''";
- }
- elsif ($field eq 'longdescs.isprivate') {
- ThrowUserError('search_field_operator_invalid', { field => $field,
- operator => $operator });
- }
- elsif ($field =~ /^attachments\.(.+)/) {
- my $sub_field = $1;
- if ($sub_field eq 'description' || $sub_field eq 'filename' || $sub_field eq 'mimetype') {
- # can't be null/empty
- return $not ? '1=1' : '1=2';
- } else {
- # all other fields which get here are boolean
- ThrowUserError('search_field_operator_invalid', { field => $field,
- operator => $operator });
- }
- }
- elsif ($field eq '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";
+ my ($self, $args, $not) = @_;
+ my ($field, $operator, $joins, $chart_id)
+ = @$args{qw(field operator joins chart_id)};
+ my $dbh = Bugzilla->dbh;
+ $operator = $self->_reverse_operator($operator) if $not;
+ $not = $operator eq 'isnotempty' ? 'NOT' : '';
+
+ if ($field eq 'keywords') {
+ push @$joins,
+ {
+ table => 'keywords',
+ as => "keywords_$chart_id",
+ from => 'bug_id',
+ to => 'bug_id',
+ };
+ return "keywords_$chart_id.bug_id IS $not NULL";
+ }
+ elsif ($field eq 'bug_group') {
+ push @$joins,
+ {
+ table => 'bug_group_map',
+ as => "bug_group_map_$chart_id",
+ from => 'bug_id',
+ to => 'bug_id',
+ };
+ return "bug_group_map_$chart_id.bug_id IS $not NULL";
+ }
+ elsif ($field eq 'flagtypes.name') {
+ push @$joins,
+ {
+ table => 'flags',
+ as => "flags_$chart_id",
+ from => 'bug_id',
+ to => 'bug_id',
+ };
+ return "flags_$chart_id.bug_id IS $not NULL";
+ }
+ elsif ($field eq 'blocked' or $field eq 'dependson') {
+ my $to = $field eq 'blocked' ? 'dependson' : 'blocked';
+ push @$joins,
+ {
+ table => 'dependencies',
+ as => "dependencies_$chart_id",
+ from => 'bug_id',
+ to => $to,
+ };
+ return "dependencies_$chart_id.$to IS $not NULL";
+ }
+ elsif ($field eq 'longdesc') {
+ my @extra = ("longdescs_$chart_id.type != " . CMT_HAS_DUPE);
+ push @extra, "longdescs_$chart_id.isprivate = 0"
+ unless $self->_user->is_insider;
+ push @$joins,
+ {
+ table => 'longdescs',
+ as => "longdescs_$chart_id",
+ from => 'bug_id',
+ to => 'bug_id',
+ extra => \@extra,
+ };
+ return $not
+ ? "longdescs_$chart_id.thetext != ''"
+ : "longdescs_$chart_id.thetext = ''";
+ }
+ elsif ($field eq 'longdescs.isprivate') {
+ ThrowUserError('search_field_operator_invalid',
+ {field => $field, operator => $operator});
+ }
+ elsif ($field =~ /^attachments\.(.+)/) {
+ my $sub_field = $1;
+ if ( $sub_field eq 'description'
+ || $sub_field eq 'filename'
+ || $sub_field eq 'mimetype')
+ {
+ # can't be null/empty
+ return $not ? '1=1' : '1=2';
}
+ else {
+ # all other fields which get here are boolean
+ ThrowUserError('search_field_operator_invalid',
+ {field => $field, operator => $operator});
+ }
+ }
+ elsif ($field eq '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";
+ }
}
###############################
@@ -3252,236 +3263,238 @@ 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, $quoted) = @$args{qw(full_field quoted)};
- my $dbh = Bugzilla->dbh;
+ my ($self, $args) = @_;
+ my ($full_field, $quoted) = @$args{qw(full_field quoted)};
+ my $dbh = Bugzilla->dbh;
- $args->{term} = $dbh->sql_position($quoted, $full_field) . " > 0";
+ $args->{term} = $dbh->sql_position($quoted, $full_field) . " > 0";
}
sub _substring {
- my ($self, $args) = @_;
- my ($full_field, $quoted) = @$args{qw(full_field quoted)};
- my $dbh = Bugzilla->dbh;
+ my ($self, $args) = @_;
+ my ($full_field, $quoted) = @$args{qw(full_field quoted)};
+ my $dbh = Bugzilla->dbh;
- # XXX This should probably be changed to just use LIKE
- $args->{term} = $dbh->sql_iposition($quoted, $full_field) . " > 0";
+ # XXX This should probably be changed to just use LIKE
+ $args->{term} = $dbh->sql_iposition($quoted, $full_field) . " > 0";
}
sub _notsubstring {
- my ($self, $args) = @_;
- my ($full_field, $quoted) = @$args{qw(full_field quoted)};
- my $dbh = Bugzilla->dbh;
+ my ($self, $args) = @_;
+ my ($full_field, $quoted) = @$args{qw(full_field quoted)};
+ my $dbh = Bugzilla->dbh;
- # XXX This should probably be changed to just use NOT LIKE
- $args->{term} = $dbh->sql_iposition($quoted, $full_field) . " = 0";
+ # XXX This should probably be changed to just use NOT LIKE
+ $args->{term} = $dbh->sql_iposition($quoted, $full_field) . " = 0";
}
sub _regexp {
- my ($self, $args) = @_;
- my ($full_field, $quoted) = @$args{qw(full_field quoted)};
- my $dbh = Bugzilla->dbh;
+ my ($self, $args) = @_;
+ my ($full_field, $quoted) = @$args{qw(full_field quoted)};
+ my $dbh = Bugzilla->dbh;
- $args->{term} = $dbh->sql_regexp($full_field, $quoted);
+ $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;
+ 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);
+ $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 ($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;
+ 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} = '';
- }
+ if (@list) {
+ $args->{term} = $dbh->sql_in($full_field, \@list);
+ }
+ else {
+ $args->{term} = '';
+ }
}
sub _anywordsubstr {
- my ($self, $args) = @_;
+ my ($self, $args) = @_;
- my @terms = $self->_substring_terms($args);
- $args->{term} = @terms ? '(' . join("\n\tOR ", @terms) . ')' : '';
+ my @terms = $self->_substring_terms($args);
+ $args->{term} = @terms ? '(' . join("\n\tOR ", @terms) . ')' : '';
}
sub _allwordssubstr {
- my ($self, $args) = @_;
+ my ($self, $args) = @_;
- my @terms = $self->_substring_terms($args);
- $args->{term} = @terms ? '(' . join("\n\tAND ", @terms) . ')' : '';
+ my @terms = $self->_substring_terms($args);
+ $args->{term} = @terms ? '(' . join("\n\tAND ", @terms) . ')' : '';
}
sub _nowordssubstr {
- my ($self, $args) = @_;
- $self->_anywordsubstr($args);
- my $term = $args->{term};
- $args->{term} = "NOT($term)";
+ my ($self, $args) = @_;
+ $self->_anywordsubstr($args);
+ my $term = $args->{term};
+ $args->{term} = "NOT($term)";
}
sub _anywords {
- my ($self, $args) = @_;
+ my ($self, $args) = @_;
+
+ my @terms = $self->_word_terms($args);
- my @terms = $self->_word_terms($args);
- # Because _word_terms uses AND, we need to parenthesize its terms
- # if there are more than one.
- @terms = map("($_)", @terms) if scalar(@terms) > 1;
- $args->{term} = @terms ? '(' . join("\n\tOR ", @terms) . ')' : '';
+ # Because _word_terms uses AND, we need to parenthesize its terms
+ # if there are more than one.
+ @terms = map("($_)", @terms) if scalar(@terms) > 1;
+ $args->{term} = @terms ? '(' . join("\n\tOR ", @terms) . ')' : '';
}
sub _allwords {
- my ($self, $args) = @_;
+ my ($self, $args) = @_;
- my @terms = $self->_word_terms($args);
- $args->{term} = @terms ? '(' . join("\n\tAND ", @terms) . ')' : '';
+ my @terms = $self->_word_terms($args);
+ $args->{term} = @terms ? '(' . join("\n\tAND ", @terms) . ')' : '';
}
sub _nowords {
- my ($self, $args) = @_;
- $self->_anywords($args);
- my $term = $args->{term};
- $args->{term} = "NOT($term)";
+ my ($self, $args) = @_;
+ $self->_anywords($args);
+ my $term = $args->{term};
+ $args->{term} = "NOT($term)";
}
sub _changedbefore_changedafter {
- my ($self, $args) = @_;
- my ($chart_id, $joins, $field, $operator, $value) =
- @$args{qw(chart_id joins field operator value)};
- my $dbh = Bugzilla->dbh;
-
- my $field_object = $self->_chart_fields->{$field}
- || ThrowCodeError("invalid_field_name", { field => $field });
-
- # Asking when creation_ts changed is just asking when the bug was created.
- if ($field_object->name eq 'creation_ts') {
- $args->{operator} =
- $operator eq 'changedbefore' ? 'lessthaneq' : 'greaterthaneq';
- return $self->_do_operator_function($args);
- }
+ my ($self, $args) = @_;
+ my ($chart_id, $joins, $field, $operator, $value)
+ = @$args{qw(chart_id joins field operator value)};
+ my $dbh = Bugzilla->dbh;
- 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 $field_object = $self->_chart_fields->{$field}
+ || ThrowCodeError("invalid_field_name", {field => $field});
- 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"],
- };
+ # 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);
+ }
- $args->{term} = "$table.bug_when IS NOT NULL";
- $self->_changed_security_check($args, $join);
- push(@$joins, $join);
+ 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 = login_to_id($value, THROW_ERROR);
- 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 = login_to_id($value, THROW_ERROR);
+ my $join = {
+ table => 'bugs_activity',
+ as => $table,
+ extra => ["$table.fieldid = $field_id", "$table.who = $user_id"],
+ };
+
+ $args->{term} = "$table.bug_when IS NOT NULL";
+ $self->_changed_security_check($args, $join);
+ push(@$joins, $join);
}
sub _changed_security_check {
- my ($self, $args, $join) = @_;
- my ($chart_id, $field) = @$args{qw(chart_id field)};
-
- my $field_object = $self->_chart_fields->{$field}
- || ThrowCodeError("invalid_field_name", { field => $field });
- my $field_id = $field_object->id;
-
- # If the user is not part of the insiders group, they cannot see
- # changes to attachments (including attachment flags) that are private
- if ($field =~ /^(?:flagtypes\.name$|attach)/ and !$self->_user->is_insider) {
- $join->{then_to} = {
- as => "attach_${field_id}_$chart_id",
- table => 'attachments',
- from => "act_${field_id}_$chart_id.attach_id",
- to => 'attach_id',
- };
-
- $args->{term} .= " AND COALESCE(attach_${field_id}_$chart_id.isprivate, 0) = 0";
- }
+ my ($self, $args, $join) = @_;
+ my ($chart_id, $field) = @$args{qw(chart_id field)};
+
+ my $field_object = $self->_chart_fields->{$field}
+ || ThrowCodeError("invalid_field_name", {field => $field});
+ my $field_id = $field_object->id;
+
+ # If the user is not part of the insiders group, they cannot see
+ # changes to attachments (including attachment flags) that are private
+ if ($field =~ /^(?:flagtypes\.name$|attach)/ and !$self->_user->is_insider) {
+ $join->{then_to} = {
+ as => "attach_${field_id}_$chart_id",
+ table => 'attachments',
+ from => "act_${field_id}_$chart_id.attach_id",
+ to => 'attach_id',
+ };
+
+ $args->{term} .= " AND COALESCE(attach_${field_id}_$chart_id.isprivate, 0) = 0";
+ }
}
sub _isempty {
- my ($self, $args) = @_;
- my $full_field = $args->{full_field};
- $args->{term} = "$full_field IS NULL OR $full_field = " . $self->_empty_value($args->{field});
+ my ($self, $args) = @_;
+ my $full_field = $args->{full_field};
+ $args->{term} = "$full_field IS NULL OR $full_field = "
+ . $self->_empty_value($args->{field});
}
sub _isnotempty {
- my ($self, $args) = @_;
- my $full_field = $args->{full_field};
- $args->{term} = "$full_field IS NOT NULL AND $full_field != " . $self->_empty_value($args->{field});
+ my ($self, $args) = @_;
+ my $full_field = $args->{full_field};
+ $args->{term} = "$full_field IS NOT NULL AND $full_field != "
+ . $self->_empty_value($args->{field});
}
sub _empty_value {
- my ($self, $field) = @_;
- my $field_obj = $self->_chart_fields->{$field};
- return "0" if $field_obj->type == FIELD_TYPE_BUG_ID;
- return Bugzilla->dbh->quote(EMPTY_DATETIME) if $field_obj->type == FIELD_TYPE_DATETIME;
- return Bugzilla->dbh->quote(EMPTY_DATE) if $field_obj->type == FIELD_TYPE_DATE;
- return "''";
+ my ($self, $field) = @_;
+ my $field_obj = $self->_chart_fields->{$field};
+ return "0" if $field_obj->type == FIELD_TYPE_BUG_ID;
+ return Bugzilla->dbh->quote(EMPTY_DATETIME)
+ if $field_obj->type == FIELD_TYPE_DATETIME;
+ return Bugzilla->dbh->quote(EMPTY_DATE) if $field_obj->type == FIELD_TYPE_DATE;
+ return "''";
}
######################
@@ -3489,75 +3502,77 @@ sub _empty_value {
######################
# Validate that the query type is one we can deal with
-sub IsValidQueryType
-{
- my ($queryType) = @_;
- # BMO: Added google and instant
- if (grep { $_ eq $queryType } qw(specific advanced google instant)) {
- return 1;
- }
- return 0;
+sub IsValidQueryType {
+ my ($queryType) = @_;
+
+ # BMO: Added google and instant
+ if (grep { $_ eq $queryType } qw(specific advanced google instant)) {
+ 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;
- }
+ # All old SQL fragments have a period in them somewhere.
+ return $column if $column !~ /\./;
- # 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;
- }
+ if ($column =~ /\bAS\s+(\w+)$/i) {
+ return $1;
+ }
- return $column;
+ # 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;
}
# Returns an hashref of Bugzilla::Field objects the current user can search
sub search_fields {
- my ($params) = @_;
+ my ($params) = @_;
- $params //= {};
- $params->{by_name} = 1;
- my $user = delete $params->{user} // Bugzilla->user;
- my $fields = Bugzilla->fields($params);
+ $params //= {};
+ $params->{by_name} = 1;
+ my $user = delete $params->{user} // Bugzilla->user;
+ my $fields = Bugzilla->fields($params);
- # if we're not in the time-tracking group, exclude time-tracking fields
- if (!$user->is_timetracker) {
- foreach my $field (TIMETRACKING_FIELDS) {
- delete $fields->{$field};
- }
+ # if we're not in the time-tracking group, exclude time-tracking fields
+ if (!$user->is_timetracker) {
+ foreach my $field (TIMETRACKING_FIELDS) {
+ delete $fields->{$field};
}
+ }
- # always exclude attachment data searching
- delete $fields->{'attach_data.thedata'};
+ # always exclude attachment data searching
+ delete $fields->{'attach_data.thedata'};
- return $fields;
+ return $fields;
}
# BMO - make product aliases lowercase
-foreach my $name (keys %{ PRODUCT_ALIASES() }) {
- PRODUCT_ALIASES->{lc($name)} = PRODUCT_ALIASES->{$name};
- delete PRODUCT_ALIASES->{$name};
+foreach my $name (keys %{PRODUCT_ALIASES()}) {
+ PRODUCT_ALIASES->{lc($name)} = PRODUCT_ALIASES->{$name};
+ delete PRODUCT_ALIASES->{$name};
}
1;
diff --git a/Bugzilla/Search/Clause.pm b/Bugzilla/Search/Clause.pm
index 4426ea576..b0eaddeb0 100644
--- a/Bugzilla/Search/Clause.pm
+++ b/Bugzilla/Search/Clause.pm
@@ -16,121 +16,123 @@ use Bugzilla::Search::Condition qw(condition);
use Bugzilla::Util qw(trick_taint);
sub new {
- my ($class, $joiner) = @_;
- if ($joiner and $joiner ne 'OR' and $joiner ne 'AND') {
- ThrowCodeError('search_invalid_joiner', { joiner => $joiner });
- }
- # This will go into SQL directly so needs to be untainted.
- trick_taint($joiner) if $joiner;
- bless { joiner => $joiner || 'AND' }, $class;
+ my ($class, $joiner) = @_;
+ if ($joiner and $joiner ne 'OR' and $joiner ne 'AND') {
+ ThrowCodeError('search_invalid_joiner', {joiner => $joiner});
+ }
+
+ # This will go into SQL directly so needs to be untainted.
+ trick_taint($joiner) if $joiner;
+ bless {joiner => $joiner || 'AND'}, $class;
}
sub children {
- my ($self) = @_;
- $self->{children} ||= [];
- return $self->{children};
+ my ($self) = @_;
+ $self->{children} ||= [];
+ return $self->{children};
}
sub update_search_args {
- my ($self, $search_args) = @_;
- # abstract
+ my ($self, $search_args) = @_;
+
+ # abstract
}
sub joiner { return $_[0]->{joiner} }
sub has_translated_conditions {
- my ($self) = @_;
- my $children = $self->children;
- return 1 if grep { $_->isa('Bugzilla::Search::Condition')
- && $_->translated } @$children;
- foreach my $child (@$children) {
- next if $child->isa('Bugzilla::Search::Condition');
- return 1 if $child->has_translated_conditions;
- }
- return 0;
+ my ($self) = @_;
+ my $children = $self->children;
+ return 1
+ if grep { $_->isa('Bugzilla::Search::Condition') && $_->translated }
+ @$children;
+ foreach my $child (@$children) {
+ next if $child->isa('Bugzilla::Search::Condition');
+ return 1 if $child->has_translated_conditions;
+ }
+ return 0;
}
sub add {
- my $self = shift;
- my $children = $self->children;
- if (@_ == 3) {
- push(@$children, condition(@_));
- return;
- }
-
- my ($child) = @_;
- return if !defined $child;
- $child->isa(__PACKAGE__) || $child->isa('Bugzilla::Search::Condition')
- || die 'child not the right type: ' . $child;
- push(@{ $self->children }, $child);
+ my $self = shift;
+ my $children = $self->children;
+ if (@_ == 3) {
+ push(@$children, condition(@_));
+ return;
+ }
+
+ my ($child) = @_;
+ return if !defined $child;
+ $child->isa(__PACKAGE__)
+ || $child->isa('Bugzilla::Search::Condition')
+ || die 'child not the right type: ' . $child;
+ push(@{$self->children}, $child);
}
sub negate {
- my ($self, $value) = @_;
- if (@_ == 2) {
- $self->{negate} = $value ? 1 : 0;
- }
- return $self->{negate};
+ my ($self, $value) = @_;
+ if (@_ == 2) {
+ $self->{negate} = $value ? 1 : 0;
+ }
+ return $self->{negate};
}
sub walk_conditions {
- my ($self, $callback) = @_;
- foreach my $child (@{ $self->children }) {
- if ($child->isa('Bugzilla::Search::Condition')) {
- $callback->($self, $child);
- }
- else {
- $child->walk_conditions($callback);
- }
+ my ($self, $callback) = @_;
+ foreach my $child (@{$self->children}) {
+ if ($child->isa('Bugzilla::Search::Condition')) {
+ $callback->($self, $child);
+ }
+ else {
+ $child->walk_conditions($callback);
}
+ }
}
sub as_string {
- my ($self) = @_;
- if (!$self->{sql}) {
- my @strings;
- foreach my $child (@{ $self->children }) {
- next if $child->isa(__PACKAGE__) && !$child->has_translated_conditions;
- next if $child->isa('Bugzilla::Search::Condition')
- && !$child->translated;
-
- my $string = $child->as_string;
- next unless $string;
- if ($self->joiner eq 'AND') {
- $string = "( $string )" if $string =~ /OR/;
- }
- else {
- $string = "( $string )" if $string =~ /AND/;
- }
- push(@strings, $string);
- }
-
- my $sql = join(' ' . $self->joiner . ' ', @strings);
- $sql = "NOT( $sql )" if $sql && $self->negate;
- $self->{sql} = $sql;
+ my ($self) = @_;
+ if (!$self->{sql}) {
+ my @strings;
+ foreach my $child (@{$self->children}) {
+ next if $child->isa(__PACKAGE__) && !$child->has_translated_conditions;
+ next if $child->isa('Bugzilla::Search::Condition') && !$child->translated;
+
+ my $string = $child->as_string;
+ next unless $string;
+ if ($self->joiner eq 'AND') {
+ $string = "( $string )" if $string =~ /OR/;
+ }
+ else {
+ $string = "( $string )" if $string =~ /AND/;
+ }
+ push(@strings, $string);
}
- return $self->{sql};
+
+ my $sql = join(' ' . $self->joiner . ' ', @strings);
+ $sql = "NOT( $sql )" if $sql && $self->negate;
+ $self->{sql} = $sql;
+ }
+ return $self->{sql};
}
# Search.pm converts URL parameters to Clause objects. This helps do the
# reverse.
sub as_params {
- my ($self) = @_;
- my @params;
- foreach my $child (@{ $self->children }) {
- if ($child->isa(__PACKAGE__)) {
- my %open_paren = (f => 'OP', n => scalar $child->negate,
- j => $child->joiner);
- push(@params, \%open_paren);
- push(@params, $child->as_params);
- my %close_paren = (f => 'CP');
- push(@params, \%close_paren);
- }
- else {
- push(@params, $child->as_params);
- }
+ my ($self) = @_;
+ my @params;
+ foreach my $child (@{$self->children}) {
+ if ($child->isa(__PACKAGE__)) {
+ my %open_paren = (f => 'OP', n => scalar $child->negate, j => $child->joiner);
+ push(@params, \%open_paren);
+ push(@params, $child->as_params);
+ my %close_paren = (f => 'CP');
+ push(@params, \%close_paren);
+ }
+ else {
+ push(@params, $child->as_params);
}
- return @params;
+ }
+ return @params;
}
1;
diff --git a/Bugzilla/Search/ClauseGroup.pm b/Bugzilla/Search/ClauseGroup.pm
index c7d3e6ae7..5c063a803 100644
--- a/Bugzilla/Search/ClauseGroup.pm
+++ b/Bugzilla/Search/ClauseGroup.pm
@@ -19,79 +19,82 @@ use Bugzilla::Util qw(trick_taint);
use List::MoreUtils qw(uniq);
use constant UNSUPPORTED_FIELDS => qw(
- classification
- commenter
- component
- longdescs.count
- product
- owner_idle_time
+ 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 ) {
- ThrowUserError('search_grouped_field_invalid', { field => $field });
- }
-
- $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) {
+ ThrowUserError('search_grouped_field_invalid', {field => $field});
+ }
+
+ $self->SUPER::add(@args);
}
sub update_search_args {
- my ($self, $search_args) = @_;
+ my ($self, $search_args) = @_;
- # No need to change things if there's only one child condition
- return unless scalar(@{ $self->children }) > 1;
+ # No need to change things if there's only one child condition
+ return unless scalar(@{$self->children}) > 1;
- # we want all the terms to use the same join table
- if (!exists $self->{_first_chart_id}) {
- $self->{_first_chart_id} = $search_args->{chart_id};
- } else {
- $search_args->{chart_id} = $self->{_first_chart_id};
- }
+ # we want all the terms to use the same join table
+ if (!exists $self->{_first_chart_id}) {
+ $self->{_first_chart_id} = $search_args->{chart_id};
+ }
+ else {
+ $search_args->{chart_id} = $self->{_first_chart_id};
+ }
- my $suffix = '_g' . $self->{_first_chart_id};
- $self->{group_condition}->{translated}->{joins}->[0]->{as} = "bugs$suffix";
+ my $suffix = '_g' . $self->{_first_chart_id};
+ $self->{group_condition}->{translated}->{joins}->[0]->{as} = "bugs$suffix";
- $search_args->{full_field} =~ s/^bugs\./bugs$suffix\./;
+ $search_args->{full_field} =~ s/^bugs\./bugs$suffix\./;
- $search_args->{table_suffix} = $suffix;
- $search_args->{bugs_table} = "bugs$suffix";
+ $search_args->{table_suffix} = $suffix;
+ $search_args->{bugs_table} = "bugs$suffix";
}
1;
diff --git a/Bugzilla/Search/Condition.pm b/Bugzilla/Search/Condition.pm
index 1c38c1f3e..33b0bfd8b 100644
--- a/Bugzilla/Search/Condition.pm
+++ b/Bugzilla/Search/Condition.pm
@@ -15,55 +15,59 @@ use base qw(Exporter);
our @EXPORT_OK = qw(condition);
sub new {
- my ($class, $params) = @_;
- my %self = %$params;
- bless \%self, $class;
- return \%self;
+ my ($class, $params) = @_;
+ my %self = %$params;
+ bless \%self, $class;
+ return \%self;
}
-sub field { return $_[0]->{field} }
-sub value { return $_[0]->{value} }
+sub field { return $_[0]->{field} }
+sub value { return $_[0]->{value} }
sub operator {
- my ($self, $value) = @_;
- if (@_ == 2) {
- $self->{operator} = $value;
- }
- return $self->{operator};
+ my ($self, $value) = @_;
+ if (@_ == 2) {
+ $self->{operator} = $value;
+ }
+ return $self->{operator};
}
sub fov {
- my ($self) = @_;
- return ($self->field, $self->operator, $self->value);
+ my ($self) = @_;
+ return ($self->field, $self->operator, $self->value);
}
sub translated {
- my ($self, $params) = @_;
- if (@_ == 2) {
- $self->{translated} = $params;
- }
- return $self->{translated};
+ my ($self, $params) = @_;
+ if (@_ == 2) {
+ $self->{translated} = $params;
+ }
+ return $self->{translated};
}
sub as_string {
- my ($self) = @_;
- my $term = $self->translated->{term};
- $term = "NOT( $term )" if $term && $self->negate;
- return $term;
+ my ($self) = @_;
+ my $term = $self->translated->{term};
+ $term = "NOT( $term )" if $term && $self->negate;
+ return $term;
}
sub as_params {
- my ($self) = @_;
- return { f => $self->field, o => $self->operator, v => $self->value,
- n => scalar $self->negate };
+ my ($self) = @_;
+ return {
+ f => $self->field,
+ o => $self->operator,
+ v => $self->value,
+ n => scalar $self->negate
+ };
}
sub negate {
- my ($self, $value) = @_;
- if (@_ == 2) {
- $self->{negate} = $value ? 1 : 0;
- }
- return $self->{negate};
+ my ($self, $value) = @_;
+ if (@_ == 2) {
+ $self->{negate} = $value ? 1 : 0;
+ }
+ return $self->{negate};
}
###########################
@@ -71,9 +75,9 @@ sub negate {
###########################
sub condition {
- my ($field, $operator, $value) = @_;
- return __PACKAGE__->new({ field => $field, operator => $operator,
- value => $value });
+ my ($field, $operator, $value) = @_;
+ return __PACKAGE__->new(
+ {field => $field, operator => $operator, value => $value});
}
1;
diff --git a/Bugzilla/Search/Quicksearch.pm b/Bugzilla/Search/Quicksearch.pm
index 6c0253e27..f2bb83e2e 100644
--- a/Bugzilla/Search/Quicksearch.pm
+++ b/Bugzilla/Search/Quicksearch.pm
@@ -27,256 +27,265 @@ use base 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",
- "mentor" => "bug_mentor",
-
- # 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",
- "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",
+ "mentor" => "bug_mentor",
+
+ # 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",
+ "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, $ELASTIC);
sub quicksearch {
- my ($searchstring) = (@_);
- my $cgi = Bugzilla->cgi;
+ my ($searchstring) = (@_);
+ my $cgi = Bugzilla->cgi;
- $chart = 0;
- $and = 0;
- $or = 0;
+ $chart = 0;
+ $and = 0;
+ $or = 0;
- # Remove leading and trailing commas and whitespace.
- $searchstring =~ s/(^[\s,]+|[\s,]+$)//g;
- ThrowUserError('buglist_parameters_required') unless ($searchstring);
+ # 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, quicksearch => $searchstring });
-
- # A query cannot start with AND or OR, nor can it end with AND, OR or NOT.
+ 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, quicksearch => $searchstring});
+
+ # A query cannot start with AND or OR, nor can it end with AND, OR or NOT.
+ ThrowUserError('quicksearch_invalid_query', {quicksearch => $searchstring})
+ if ($words[0] =~ /^(?:AND|OR)$/ || $words[$#words] =~ /^(?:AND|OR|NOT)$/);
+
+ $fulltext = Bugzilla->user->setting('quicksearch_fulltext') eq 'on' ? 1 : 0;
+
+ 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',
- { quicksearch => $searchstring })
- if ($words[0] =~ /^(?:AND|OR)$/ || $words[$#words] =~ /^(?:AND|OR|NOT)$/);
-
- $fulltext = Bugzilla->user->setting('quicksearch_fulltext') eq 'on' ? 1 : 0;
-
- 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'], quicksearch => $searchstring })
- 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'], quicksearch => $searchstring })
- 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], quicksearch => $searchstring });
- }
- unshift(@words, "-$word");
- }
- # --comment and ++comment disable or enable fulltext searching
- elsif ($word =~ /^(--|\+\+)comments?$/i) {
- $fulltext = $1 eq '--' ? 0 : 1;
- }
- 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 = ();
- }
- }
- }
+ {operators => ['AND', 'OR'], quicksearch => $searchstring})
+ if $words[0] eq 'OR';
+ }
- _handle_status_and_resolution($qswords[0]);
- shift(@qswords) if $bug_status_set;
-
- my (@unknownFields, %ambiguous_fields);
-
- # 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;
+ # |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'], quicksearch => $searchstring})
+ 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], quicksearch => $searchstring});
}
-
- # 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);
+ unshift(@words, "-$word");
+ }
+
+ # --comment and ++comment disable or enable fulltext searching
+ elsif ($word =~ /^(--|\+\+)comments?$/i) {
+ $fulltext = $1 eq '--' ? 0 : 1;
+ }
+ 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 = ();
}
+ }
+ }
+
+ _handle_status_and_resolution($qswords[0]);
+ shift(@qswords) if $bug_status_set;
+
+ my (@unknownFields, %ambiguous_fields);
- # Inform user about any unknown fields
- if (scalar(@unknownFields) || scalar(keys %ambiguous_fields)) {
- ThrowUserError("quicksearch_unknown_field",
- { unknown => \@unknownFields,
- ambiguous => \%ambiguous_fields,
- quicksearch => $searchstring });
+ # 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);
}
- # Make sure we have some query terms left
- scalar($cgi->param())>0 || ThrowUserError("buglist_parameters_required");
+ 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;
+ }
+
+ # 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);
+ }
+
+ # Inform user about any unknown fields
+ if (scalar(@unknownFields) || scalar(keys %ambiguous_fields)) {
+ ThrowUserError(
+ "quicksearch_unknown_field",
+ {
+ unknown => \@unknownFields,
+ ambiguous => \%ambiguous_fields,
+ quicksearch => $searchstring
+ }
+ );
}
- # List of quicksearch-specific CGI parameters to get rid of.
- my @params_to_strip = ('quicksearch', 'load', 'run');
- my $modified_query_string = $cgi->canonicalise_query(@params_to_strip);
+ # Make sure we have some query terms left
+ scalar($cgi->param()) > 0 || ThrowUserError("buglist_parameters_required");
+ }
- if ($cgi->param('load')) {
- my $urlbase = Bugzilla->localconfig->{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 = Bugzilla->localconfig->{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;
}
##########################
@@ -284,336 +293,353 @@ 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 => Bugzilla->localconfig->{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 => Bugzilla->localconfig->{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 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 => Bugzilla->localconfig->{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 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 => Bugzilla->localconfig->{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 =~ s/^(ALL|OPEN)\+$/$1/) {
- Bugzilla->cgi->param('limit' => 0);
- }
-
- 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 =~ s/^(ALL|OPEN)\+$/$1/) {
+ Bugzilla->cgi->param('limit' => 0);
+ }
+
+ 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) = @_;
-
- # Flag and requestee shortcut
- if ($or_operand =~ /^(?:flag:)?([^\?]+\?)([^\?]*)$/) {
- # BMO: Do not treat custom fields as flags if value is ?
- if ($1 !~ /^cf_/) {
- my ($flagtype, $requestee) = ($1, $2);
- addChart('flagtypes.name', 'substring', $flagtype, $negate);
- if ($requestee) {
- # AND
- $chart++;
- $and = $or = 0;
- addChart('requestees.login_name', 'substring', $requestee, $negate);
- }
- return 1;
+ my ($or_operand, $negate, $unknownFields, $ambiguous_fields) = @_;
+
+ # Flag and requestee shortcut
+ if ($or_operand =~ /^(?:flag:)?([^\?]+\?)([^\?]*)$/) {
+
+ # BMO: Do not treat custom fields as flags if value is ?
+ if ($1 !~ /^cf_/) {
+ my ($flagtype, $requestee) = ($1, $2);
+ addChart('flagtypes.name', 'substring', $flagtype, $negate);
+ if ($requestee) {
+
+ # AND
+ $chart++;
+ $and = $or = 0;
+ addChart('requestees.login_name', 'substring', $requestee, $negate);
+ }
+ return 1;
+ }
+ }
+
+ # Generic field1,field2,field3:value1,value2 notation.
+ # We have to correctly ignore commas and colons in quotes.
+ # 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;
}
- }
-
- # 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) {
- next unless defined $value;
- 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 the value is a pair of matching quotes, the person wanted the empty string
- elsif ($value =~ /^(["'])\1$/ || $translated eq 'resolution' && $value eq '---') {
- $value = "";
- $operator = "isempty";
- }
- addChart($translated, $operator, $value, $negate);
- }
- }
+ foreach my $value (@values) {
+ next unless defined $value;
+ 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 the value is a pair of matching quotes, the person wanted the empty string
+ elsif ($value =~ /^(["'])\1$/ || $translated eq 'resolution' && $value eq '---')
+ {
+ $value = "";
+ $operator = "isempty";
+ }
+ addChart($translated, $operator, $value, $negate);
}
- return 1;
+ }
}
- return 0;
+ return 1;
+ }
+ return 0;
}
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;
- }
-
- # 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;
+ 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;
}
sub _special_field_syntax {
- my ($word, $negate) = @_;
- return unless defined($word);
-
- # P1-5 Syntax
- if ($word =~ m/^P(\d+)(?:-(\d+))?$/i) {
- my ($p_start, $p_end) = ($1, $2);
- my $legal_priorities = get_legal_field_values('priority');
-
- # If Pn exists explicitly, use it.
- my $start = firstidx { $_ eq "P$p_start" } @$legal_priorities;
- my $end;
- $end = firstidx { $_ eq "P$p_end" } @$legal_priorities if defined $p_end;
-
- # If Pn doesn't exist explicitly, then we mean the nth priority.
- if ($start == -1) {
- $start = max(0, $p_start - 1);
- }
- my $prios = $legal_priorities->[$start];
-
- if (defined $end) {
- # If Pn doesn't exist explicitly, then we mean the nth priority.
- if ($end == -1) {
- $end = min(scalar(@$legal_priorities), $p_end) - 1;
- $end = max(0, $end); # Just in case the user typed P0.
- }
- ($start, $end) = ($end, $start) if $end < $start;
- $prios = join(',', @$legal_priorities[$start..$end])
- }
+ my ($word, $negate) = @_;
+ return unless defined($word);
- addChart('priority', 'anyexact', $prios, $negate);
- return 1;
- }
- return 0;
-}
+ # P1-5 Syntax
+ if ($word =~ m/^P(\d+)(?:-(\d+))?$/i) {
+ my ($p_start, $p_end) = ($1, $2);
+ my $legal_priorities = get_legal_field_values('priority');
-sub _default_quicksearch_word {
- my ($word, $negate) = @_;
- return unless defined($word);
+ # 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 (!grep { lc($word) eq $_ } PRODUCT_EXCEPTIONS and length($word) > 2) {
- addChart('product', 'substring', $word, $negate);
+ # 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 (!grep { lc($word) eq $_ } COMPONENT_EXCEPTIONS and length($word) > 2) {
- addChart('component', 'substring', $word, $negate);
- }
+ if (defined $end) {
- my @legal_keywords = map($_->name, Bugzilla::Keyword->get_all);
- if (grep { lc($word) eq lc($_) } @legal_keywords) {
- addChart('keywords', 'substring', $word, $negate);
+ # 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]);
}
- addChart('alias', 'substring', $word, $negate);
- addChart('short_desc', 'substring', $word, $negate);
- addChart('status_whiteboard', 'substring', $word, $negate);
- addChart('longdesc', 'substring', $word, $negate) if $ELASTIC;
- addChart('content', 'matches', _matches_phrase($word), $negate) if $fulltext && !$ELASTIC;
+ addChart('priority', 'anyexact', $prios, $negate);
+ return 1;
+ }
+ return 0;
+}
- # BMO Bug 664124 - Include the crash signature (sig:) field in default quicksearches
- addChart('cf_crash_signature', 'substring', $word, $negate);
+sub _default_quicksearch_word {
+ my ($word, $negate) = @_;
+ return unless defined($word);
+
+ 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('longdesc', 'substring', $word, $negate) if $ELASTIC;
+ addChart('content', 'matches', _matches_phrase($word), $negate)
+ if $fulltext && !$ELASTIC;
+
+# BMO Bug 664124 - Include the crash signature (sig:) field in default quicksearches
+ addChart('cf_crash_signature', 'substring', $word, $negate);
}
sub _handle_urls {
- my ($word, $negate) = @_;
- return unless defined($word);
-
- # 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) = @_;
+ return unless defined($word);
+
+ # 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);
+ }
}
###########################################################################
@@ -622,77 +648,77 @@ sub _handle_urls {
# Quote and escape a phrase appropriately for a "content matches" search.
sub _matches_phrase {
- my ($phrase) = @_;
- return $phrase if $ELASTIC;
- $phrase =~ s/"/\\"/g;
- return "\"$phrase\"";
+ my ($phrase) = @_;
+ return $phrase if $ELASTIC;
+ $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);
- if ($ar_prefixes[-1] =~ s/\+$//) {
- Bugzilla->cgi->param(limit => 0);
- }
- 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);
+ if ($ar_prefixes[-1] =~ s/\+$//) {
+ Bugzilla->cgi->param(limit => 0);
+ }
+ 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;
-
- if ($comparisonType eq 'anywords') {
- return 'nowords';
- }
- elsif ($comparisonType eq 'isempty') {
- return 'isnotempty';
- }
- return "not$comparisonType";
+ my $comparisonType = shift;
+
+ if ($comparisonType eq 'anywords') {
+ return 'nowords';
+ }
+ elsif ($comparisonType eq 'isempty') {
+ return 'isnotempty';
+ }
+ return "not$comparisonType";
}
# Add a boolean chart
sub addChart {
- my ($field, $comparisonType, $value, $negate) = @_;
-
- $negate && ($comparisonType = negateComparisonType($comparisonType));
- makeChart("$chart-$and-$or", $field, $comparisonType, $value);
- if ($negate) {
- $and++;
- $or = 0;
- }
- else {
- $or++;
- }
+ my ($field, $comparisonType, $value, $negate) = @_;
+
+ $negate && ($comparisonType = negateComparisonType($comparisonType));
+ makeChart("$chart-$and-$or", $field, $comparisonType, $value);
+ if ($negate) {
+ $and++;
+ $or = 0;
+ }
+ else {
+ $or++;
+ }
}
# Create the CGI parameters for a boolean chart
sub makeChart {
- my ($expr, $field, $type, $value) = @_;
+ my ($expr, $field, $type, $value) = @_;
- my $cgi = Bugzilla->cgi;
- $cgi->param("field$expr", $field);
- $cgi->param("type$expr", $type);
- $cgi->param("value$expr", $value);
+ my $cgi = Bugzilla->cgi;
+ $cgi->param("field$expr", $field);
+ $cgi->param("type$expr", $type);
+ $cgi->param("value$expr", $value);
}
1;
diff --git a/Bugzilla/Search/Recent.pm b/Bugzilla/Search/Recent.pm
index a5d9e2417..5738dc93f 100644
--- a/Bugzilla/Search/Recent.pm
+++ b/Bugzilla/Search/Recent.pm
@@ -21,24 +21,25 @@ use Bugzilla::Util;
# Constants #
#############
-use constant DB_TABLE => 'profile_search';
+use constant DB_TABLE => 'profile_search';
use constant LIST_ORDER => 'id DESC';
+
# Do not track buglists viewed by users.
use constant AUDIT_CREATES => 0;
use constant AUDIT_UPDATES => 0;
use constant AUDIT_REMOVES => 0;
use constant DB_COLUMNS => qw(
- id
- user_id
- bug_list
- list_order
+ id
+ user_id
+ bug_list
+ list_order
);
use constant VALIDATORS => {
- user_id => \&_check_user_id,
- bug_list => \&_check_bug_list,
- list_order => \&_check_list_order,
+ user_id => \&_check_user_id,
+ bug_list => \&_check_bug_list,
+ list_order => \&_check_list_order,
};
use constant UPDATE_COLUMNS => qw(bug_list list_order);
@@ -51,29 +52,30 @@ use constant USE_MEMCACHED => 0;
###################
sub create {
- my $class = shift;
- my $dbh = Bugzilla->dbh;
- $dbh->bz_start_transaction();
- my $search = $class->SUPER::create(@_);
- my $user_id = $search->user_id;
-
- # Enforce there only being SAVE_NUM_SEARCHES per user.
- my @ids = @{ $dbh->selectcol_arrayref(
- "SELECT id FROM profile_search WHERE user_id = ? ORDER BY id",
- undef, $user_id) };
- if (scalar(@ids) > SAVE_NUM_SEARCHES) {
- splice(@ids, - SAVE_NUM_SEARCHES);
- $dbh->do(
- "DELETE FROM profile_search WHERE id IN (" . join(',', @ids) . ")");
- }
- $dbh->bz_commit_transaction();
- return $search;
+ my $class = shift;
+ my $dbh = Bugzilla->dbh;
+ $dbh->bz_start_transaction();
+ my $search = $class->SUPER::create(@_);
+ my $user_id = $search->user_id;
+
+ # Enforce there only being SAVE_NUM_SEARCHES per user.
+ my @ids = @{
+ $dbh->selectcol_arrayref(
+ "SELECT id FROM profile_search WHERE user_id = ? ORDER BY id", undef,
+ $user_id
+ )
+ };
+ if (scalar(@ids) > SAVE_NUM_SEARCHES) {
+ splice(@ids, - SAVE_NUM_SEARCHES);
+ $dbh->do("DELETE FROM profile_search WHERE id IN (" . join(',', @ids) . ")");
+ }
+ $dbh->bz_commit_transaction();
+ return $search;
}
sub create_placeholder {
- my $class = shift;
- return $class->create({ user_id => Bugzilla->user->id,
- bug_list => '' });
+ my $class = shift;
+ return $class->create({user_id => Bugzilla->user->id, bug_list => ''});
}
###############
@@ -81,41 +83,43 @@ sub create_placeholder {
###############
sub check {
- my $class = shift;
- my $search = $class->SUPER::check(@_);
- my $user = Bugzilla->user;
- if ($search->user_id != $user->id) {
- ThrowUserError('object_does_not_exist', { id => $search->id });
- }
- return $search;
+ my $class = shift;
+ my $search = $class->SUPER::check(@_);
+ my $user = Bugzilla->user;
+ if ($search->user_id != $user->id) {
+ ThrowUserError('object_does_not_exist', {id => $search->id});
+ }
+ return $search;
}
sub check_quietly {
- my $class = shift;
- my $error_mode = Bugzilla->error_mode;
- Bugzilla->error_mode(ERROR_MODE_DIE);
- my $search = eval { $class->check(@_) };
- Bugzilla->error_mode($error_mode);
- return $search;
+ my $class = shift;
+ my $error_mode = Bugzilla->error_mode;
+ Bugzilla->error_mode(ERROR_MODE_DIE);
+ my $search = eval { $class->check(@_) };
+ Bugzilla->error_mode($error_mode);
+ return $search;
}
sub new_from_cookie {
- my ($invocant, $bug_ids) = @_;
- my $class = ref($invocant) || $invocant;
+ my ($invocant, $bug_ids) = @_;
+ my $class = ref($invocant) || $invocant;
- my $search = { id => 'cookie',
- user_id => Bugzilla->user->id,
- bug_list => join(',', @$bug_ids) };
+ my $search = {
+ id => 'cookie',
+ user_id => Bugzilla->user->id,
+ bug_list => join(',', @$bug_ids)
+ };
- bless $search, $class;
- return $search;
+ bless $search, $class;
+ return $search;
}
####################
# Simple Accessors #
####################
-sub bug_list { return [split(',', $_[0]->{'bug_list'})]; }
+sub bug_list { return [split(',', $_[0]->{'bug_list'})]; }
sub list_order { return $_[0]->{'list_order'}; }
sub user_id { return $_[0]->{'user_id'}; }
@@ -131,17 +135,17 @@ sub set_list_order { $_[0]->set('list_order', $_[1]); }
##############
sub _check_user_id {
- my ($invocant, $id) = @_;
- require Bugzilla::User;
- return Bugzilla::User->check({ id => $id })->id;
+ my ($invocant, $id) = @_;
+ require Bugzilla::User;
+ return Bugzilla::User->check({id => $id})->id;
}
sub _check_bug_list {
- my ($invocant, $list) = @_;
+ my ($invocant, $list) = @_;
- my @bug_ids = ref($list) ? @$list : split(',', $list || '');
- detaint_natural($_) foreach @bug_ids;
- return join(',', @bug_ids);
+ my @bug_ids = ref($list) ? @$list : split(',', $list || '');
+ detaint_natural($_) foreach @bug_ids;
+ return join(',', @bug_ids);
}
sub _check_list_order { defined $_[1] ? trim($_[1]) : '' }
diff --git a/Bugzilla/Search/Saved.pm b/Bugzilla/Search/Saved.pm
index 1511cd87b..c24d333a8 100644
--- a/Bugzilla/Search/Saved.pm
+++ b/Bugzilla/Search/Saved.pm
@@ -28,22 +28,23 @@ use Scalar::Util qw(blessed);
#############
use constant DB_TABLE => 'namedqueries';
+
# Do not track buglists saved by users.
use constant AUDIT_CREATES => 0;
use constant AUDIT_UPDATES => 0;
use constant AUDIT_REMOVES => 0;
use constant DB_COLUMNS => qw(
- id
- userid
- name
- query
+ id
+ userid
+ name
+ query
);
use constant VALIDATORS => {
- name => \&_check_name,
- query => \&_check_query,
- link_in_footer => \&_check_link_in_footer,
+ name => \&_check_name,
+ query => \&_check_query,
+ link_in_footer => \&_check_link_in_footer,
};
use constant UPDATE_COLUMNS => qw(name query);
@@ -53,56 +54,53 @@ use constant UPDATE_COLUMNS => qw(name query);
###############
sub new {
- my $class = shift;
- my $param = shift;
- my $dbh = Bugzilla->dbh;
-
- my $user;
- if (ref $param) {
- $user = $param->{user} || Bugzilla->user;
- my $name = $param->{name};
- if (!defined $name) {
- ThrowCodeError('bad_arg',
- {argument => 'name',
- function => "${class}::new"});
- }
- my $condition = 'userid = ? AND name = ?';
- my $user_id = blessed $user ? $user->id : $user;
- detaint_natural($user_id)
- || ThrowCodeError('param_must_be_numeric',
- {function => $class . '::_init', param => 'user'});
- my @values = ($user_id, $name);
- $param = { condition => $condition, values => \@values };
- }
-
- unshift @_, $param;
- my $self = $class->SUPER::new(@_);
- if ($self) {
- $self->{user} = $user if blessed $user;
-
- # Some DBs (read: Oracle) incorrectly mark the query string as UTF-8
- # when it's coming out of the database, even though it has no UTF-8
- # characters in it, which prevents Bugzilla::CGI from later reading
- # it correctly.
- utf8::downgrade($self->{query}) if utf8::is_utf8($self->{query});
+ my $class = shift;
+ my $param = shift;
+ my $dbh = Bugzilla->dbh;
+
+ my $user;
+ if (ref $param) {
+ $user = $param->{user} || Bugzilla->user;
+ my $name = $param->{name};
+ if (!defined $name) {
+ ThrowCodeError('bad_arg', {argument => 'name', function => "${class}::new"});
}
- return $self;
+ my $condition = 'userid = ? AND name = ?';
+ my $user_id = blessed $user ? $user->id : $user;
+ detaint_natural($user_id)
+ || ThrowCodeError('param_must_be_numeric',
+ {function => $class . '::_init', param => 'user'});
+ my @values = ($user_id, $name);
+ $param = {condition => $condition, values => \@values};
+ }
+
+ unshift @_, $param;
+ my $self = $class->SUPER::new(@_);
+ if ($self) {
+ $self->{user} = $user if blessed $user;
+
+ # Some DBs (read: Oracle) incorrectly mark the query string as UTF-8
+ # when it's coming out of the database, even though it has no UTF-8
+ # characters in it, which prevents Bugzilla::CGI from later reading
+ # it correctly.
+ utf8::downgrade($self->{query}) if utf8::is_utf8($self->{query});
+ }
+ return $self;
}
sub check {
- my $class = shift;
- my $search = $class->SUPER::check(@_);
- my $user = Bugzilla->user;
- return $search if $search->user->id == $user->id;
-
- if (!$search->shared_with_group
- or !$user->in_group($search->shared_with_group))
- {
- ThrowUserError('missing_query', { name => $search->name,
- sharer_id => $search->user->id });
- }
-
- return $search;
+ my $class = shift;
+ my $search = $class->SUPER::check(@_);
+ my $user = Bugzilla->user;
+ return $search if $search->user->id == $user->id;
+
+ if (!$search->shared_with_group or !$user->in_group($search->shared_with_group))
+ {
+ ThrowUserError('missing_query',
+ {name => $search->name, sharer_id => $search->user->id});
+ }
+
+ return $search;
}
##############
@@ -112,24 +110,25 @@ sub check {
sub _check_link_in_footer { return $_[1] ? 1 : 0; }
sub _check_name {
- my ($invocant, $name) = @_;
- $name = trim($name);
- $name || ThrowUserError("query_name_missing");
- $name !~ /[<>&]/ || ThrowUserError("illegal_query_name");
- if (length($name) > MAX_LEN_QUERY_NAME) {
- ThrowUserError("query_name_too_long");
- }
- return $name;
+ my ($invocant, $name) = @_;
+ $name = trim($name);
+ $name || ThrowUserError("query_name_missing");
+ $name !~ /[<>&]/ || ThrowUserError("illegal_query_name");
+ if (length($name) > MAX_LEN_QUERY_NAME) {
+ ThrowUserError("query_name_too_long");
+ }
+ return $name;
}
sub _check_query {
- my ($invocant, $query) = @_;
- $query || ThrowUserError("buglist_parameters_required");
- my $cgi = new Bugzilla::CGI($query);
- $cgi->clean_search_url;
- # Don't store the query name as a parameter.
- $cgi->delete('known_name');
- return $cgi->query_string;
+ my ($invocant, $query) = @_;
+ $query || ThrowUserError("buglist_parameters_required");
+ my $cgi = new Bugzilla::CGI($query);
+ $cgi->clean_search_url;
+
+ # Don't store the query name as a parameter.
+ $cgi->delete('known_name');
+ return $cgi->query_string;
}
#########################
@@ -137,170 +136,180 @@ sub _check_query {
#########################
sub create {
- my $class = shift;
- Bugzilla->login(LOGIN_REQUIRED);
- my $dbh = Bugzilla->dbh;
- $class->check_required_create_fields(@_);
- $dbh->bz_start_transaction();
- my $params = $class->run_create_validators(@_);
-
- # Right now you can only create a Saved Search for the current user.
- $params->{userid} = Bugzilla->user->id;
-
- my $lif = delete $params->{link_in_footer};
- my $obj = $class->insert_create_data($params);
- if ($lif) {
- $dbh->do('INSERT INTO namedqueries_link_in_footer
- (user_id, namedquery_id) VALUES (?,?)',
- undef, $params->{userid}, $obj->id);
- }
- $dbh->bz_commit_transaction();
-
- return $obj;
+ my $class = shift;
+ Bugzilla->login(LOGIN_REQUIRED);
+ my $dbh = Bugzilla->dbh;
+ $class->check_required_create_fields(@_);
+ $dbh->bz_start_transaction();
+ my $params = $class->run_create_validators(@_);
+
+ # Right now you can only create a Saved Search for the current user.
+ $params->{userid} = Bugzilla->user->id;
+
+ my $lif = delete $params->{link_in_footer};
+ my $obj = $class->insert_create_data($params);
+ if ($lif) {
+ $dbh->do(
+ 'INSERT INTO namedqueries_link_in_footer
+ (user_id, namedquery_id) VALUES (?,?)', undef, $params->{userid},
+ $obj->id
+ );
+ }
+ $dbh->bz_commit_transaction();
+
+ return $obj;
}
sub rename_field_value {
- my ($class, $field, $old_value, $new_value) = @_;
-
- my $old = url_quote($old_value);
- my $new = url_quote($new_value);
- my $old_sql = $old;
- $old_sql =~ s/([_\%])/\\$1/g;
-
- my $table = $class->DB_TABLE;
- my $id_field = $class->ID_FIELD;
-
- my $dbh = Bugzilla->dbh;
- $dbh->bz_start_transaction();
-
- my %queries = @{ $dbh->selectcol_arrayref(
- "SELECT $id_field, query FROM $table WHERE query LIKE ?",
- {Columns=>[1,2]}, "\%$old_sql\%") };
- foreach my $id (keys %queries) {
- my $query = $queries{$id};
- $query =~ s/\b$field=\Q$old\E\b/$field=$new/gi;
- # Fix boolean charts.
- while ($query =~ /\bfield(\d+-\d+-\d+)=\Q$field\E\b/gi) {
- my $chart_id = $1;
- # Note that this won't handle lists or substrings inside of
- # boolean charts. Users will have to fix those themselves.
- $query =~ s/\bvalue\Q$chart_id\E=\Q$old\E\b/value$chart_id=$new/i;
- }
- $dbh->do("UPDATE $table SET query = ? WHERE $id_field = ?",
- undef, $query, $id);
- Bugzilla->memcached->clear({ table => $table, id => $id });
+ my ($class, $field, $old_value, $new_value) = @_;
+
+ my $old = url_quote($old_value);
+ my $new = url_quote($new_value);
+ my $old_sql = $old;
+ $old_sql =~ s/([_\%])/\\$1/g;
+
+ my $table = $class->DB_TABLE;
+ my $id_field = $class->ID_FIELD;
+
+ my $dbh = Bugzilla->dbh;
+ $dbh->bz_start_transaction();
+
+ my %queries = @{
+ $dbh->selectcol_arrayref(
+ "SELECT $id_field, query FROM $table WHERE query LIKE ?",
+ {Columns => [1, 2]},
+ "\%$old_sql\%"
+ )
+ };
+ foreach my $id (keys %queries) {
+ my $query = $queries{$id};
+ $query =~ s/\b$field=\Q$old\E\b/$field=$new/gi;
+
+ # Fix boolean charts.
+ while ($query =~ /\bfield(\d+-\d+-\d+)=\Q$field\E\b/gi) {
+ my $chart_id = $1;
+
+ # Note that this won't handle lists or substrings inside of
+ # boolean charts. Users will have to fix those themselves.
+ $query =~ s/\bvalue\Q$chart_id\E=\Q$old\E\b/value$chart_id=$new/i;
}
+ $dbh->do("UPDATE $table SET query = ? WHERE $id_field = ?", undef, $query, $id);
+ Bugzilla->memcached->clear({table => $table, id => $id});
+ }
- $dbh->bz_commit_transaction();
+ $dbh->bz_commit_transaction();
}
sub preload {
- my ($searches) = @_;
- my $dbh = Bugzilla->dbh;
+ my ($searches) = @_;
+ my $dbh = Bugzilla->dbh;
- return unless scalar @$searches;
+ return unless scalar @$searches;
- my @query_ids = map { $_->id } @$searches;
- my $queries_in_footer = $dbh->selectcol_arrayref(
- 'SELECT namedquery_id
+ my @query_ids = map { $_->id } @$searches;
+ my $queries_in_footer = $dbh->selectcol_arrayref(
+ 'SELECT namedquery_id
FROM namedqueries_link_in_footer
WHERE ' . $dbh->sql_in('namedquery_id', \@query_ids) . ' AND user_id = ?',
- undef, Bugzilla->user->id);
+ undef, Bugzilla->user->id
+ );
- my %links_in_footer = map { $_ => 1 } @$queries_in_footer;
- foreach my $query (@$searches) {
- $query->{link_in_footer} = ($links_in_footer{$query->id}) ? 1 : 0;
- }
+ my %links_in_footer = map { $_ => 1 } @$queries_in_footer;
+ foreach my $query (@$searches) {
+ $query->{link_in_footer} = ($links_in_footer{$query->id}) ? 1 : 0;
+ }
}
#####################
# Complex Accessors #
#####################
sub edit_link {
- my ($self) = @_;
- return $self->{edit_link} if defined $self->{edit_link};
- my $cgi = new Bugzilla::CGI($self->url);
- if (!$cgi->param('query_type')
- || !IsValidQueryType($cgi->param('query_type')))
- {
- $cgi->param('query_type', 'advanced');
- }
- $self->{edit_link} = $cgi->canonicalise_query;
- return $self->{edit_link};
+ my ($self) = @_;
+ return $self->{edit_link} if defined $self->{edit_link};
+ my $cgi = new Bugzilla::CGI($self->url);
+ if (!$cgi->param('query_type') || !IsValidQueryType($cgi->param('query_type')))
+ {
+ $cgi->param('query_type', 'advanced');
+ }
+ $self->{edit_link} = $cgi->canonicalise_query;
+ return $self->{edit_link};
}
sub used_in_whine {
- my ($self) = @_;
- return $self->{used_in_whine} if exists $self->{used_in_whine};
- ($self->{used_in_whine}) = Bugzilla->dbh->selectrow_array(
- 'SELECT 1 FROM whine_events INNER JOIN whine_queries
+ my ($self) = @_;
+ return $self->{used_in_whine} if exists $self->{used_in_whine};
+ ($self->{used_in_whine}) = Bugzilla->dbh->selectrow_array(
+ 'SELECT 1 FROM whine_events INNER JOIN whine_queries
ON whine_events.id = whine_queries.eventid
WHERE whine_events.owner_userid = ? AND query_name = ?', undef,
- $self->{userid}, $self->name) || 0;
- return $self->{used_in_whine};
+ $self->{userid}, $self->name
+ ) || 0;
+ return $self->{used_in_whine};
}
sub link_in_footer {
- my ($self, $user) = @_;
- # We only cache link_in_footer for the current Bugzilla->user.
- return $self->{link_in_footer} if exists $self->{link_in_footer} && !$user;
- my $user_id = $user ? $user->id : Bugzilla->user->id;
- my $link_in_footer = Bugzilla->dbh->selectrow_array(
- 'SELECT 1 FROM namedqueries_link_in_footer
- WHERE namedquery_id = ? AND user_id = ?',
- undef, $self->id, $user_id) || 0;
- $self->{link_in_footer} = $link_in_footer if !$user;
- return $link_in_footer;
+ my ($self, $user) = @_;
+
+ # We only cache link_in_footer for the current Bugzilla->user.
+ return $self->{link_in_footer} if exists $self->{link_in_footer} && !$user;
+ my $user_id = $user ? $user->id : Bugzilla->user->id;
+ my $link_in_footer = Bugzilla->dbh->selectrow_array(
+ 'SELECT 1 FROM namedqueries_link_in_footer
+ WHERE namedquery_id = ? AND user_id = ?', undef, $self->id, $user_id
+ ) || 0;
+ $self->{link_in_footer} = $link_in_footer if !$user;
+ return $link_in_footer;
}
sub shared_with_group {
- my ($self) = @_;
- return $self->{shared_with_group} if exists $self->{shared_with_group};
- # Bugzilla only currently supports sharing with one group, even
- # though the database backend allows for an infinite number.
- my ($group_id) = Bugzilla->dbh->selectrow_array(
- 'SELECT group_id FROM namedquery_group_map WHERE namedquery_id = ?',
- undef, $self->id);
- $self->{shared_with_group} = $group_id ? new Bugzilla::Group($group_id)
- : undef;
- return $self->{shared_with_group};
+ my ($self) = @_;
+ return $self->{shared_with_group} if exists $self->{shared_with_group};
+
+ # Bugzilla only currently supports sharing with one group, even
+ # though the database backend allows for an infinite number.
+ my ($group_id)
+ = Bugzilla->dbh->selectrow_array(
+ 'SELECT group_id FROM namedquery_group_map WHERE namedquery_id = ?',
+ undef, $self->id);
+ $self->{shared_with_group} = $group_id ? new Bugzilla::Group($group_id) : undef;
+ return $self->{shared_with_group};
}
sub shared_with_users {
- my $self = shift;
- my $dbh = Bugzilla->dbh;
+ my $self = shift;
+ my $dbh = Bugzilla->dbh;
- if (!exists $self->{shared_with_users}) {
- $self->{shared_with_users} =
- $dbh->selectrow_array('SELECT COUNT(*)
+ if (!exists $self->{shared_with_users}) {
+ $self->{shared_with_users} = $dbh->selectrow_array(
+ 'SELECT COUNT(*)
FROM namedqueries_link_in_footer
INNER JOIN namedqueries
ON namedquery_id = id
WHERE namedquery_id = ?
- AND user_id != userid',
- undef, $self->id);
- }
- return $self->{shared_with_users};
+ AND user_id != userid', undef, $self->id
+ );
+ }
+ return $self->{shared_with_users};
}
####################
# Simple Accessors #
####################
-sub url { return $_[0]->{'query'}; }
+sub url { return $_[0]->{'query'}; }
sub user {
- my ($self) = @_;
- return $self->{user} ||=
- Bugzilla::User->new({ id => $self->{userid}, cache => 1 });
+ my ($self) = @_;
+ return $self->{user}
+ ||= Bugzilla::User->new({id => $self->{userid}, cache => 1});
}
############
# Mutators #
############
-sub set_name { $_[0]->set('name', $_[1]); }
-sub set_url { $_[0]->set('query', $_[1]); }
+sub set_name { $_[0]->set('name', $_[1]); }
+sub set_url { $_[0]->set('query', $_[1]); }
1;
diff --git a/Bugzilla/Send/Sendmail.pm b/Bugzilla/Send/Sendmail.pm
index 81c2190e5..fe27a0c70 100644
--- a/Bugzilla/Send/Sendmail.pm
+++ b/Bugzilla/Send/Sendmail.pm
@@ -16,80 +16,98 @@ use Return::Value;
use Symbol qw(gensym);
sub send {
- my ($class, $message, @args) = @_;
- my $mailer = $class->_find_sendmail;
+ my ($class, $message, @args) = @_;
+ my $mailer = $class->_find_sendmail;
- return failure "Couldn't find 'sendmail' executable in your PATH"
- ." and Email::Send::Sendmail::SENDMAIL is not set"
- unless $mailer;
+ return failure "Couldn't find 'sendmail' executable in your PATH"
+ . " and Email::Send::Sendmail::SENDMAIL is not set"
+ unless $mailer;
- return failure "Found $mailer but cannot execute it"
- unless -x $mailer;
+ return failure "Found $mailer but cannot execute it" unless -x $mailer;
- local $SIG{'CHLD'} = 'DEFAULT';
+ local $SIG{'CHLD'} = 'DEFAULT';
- my $pipe = gensym;
+ my $pipe = gensym;
- open($pipe, '|-', "$mailer -t -oi @args")
- || return failure "Error executing $mailer: $!";
- print($pipe $message->as_string)
- || return failure "Error printing via pipe to $mailer: $!";
- unless (close $pipe) {
- return failure "error when closing pipe to $mailer: $!" if $!;
- my ($error_message, $is_transient) = _map_exitcode($? >> 8);
- if (Bugzilla->get_param_with_override('use_mailer_queue')) {
- # Return success for errors which are fatal so Bugzilla knows to
- # remove them from the queue
- if ($is_transient) {
- return failure "error when closing pipe to $mailer: $error_message";
- } else {
- warn "error when closing pipe to $mailer: $error_message\n";
- return success;
- }
- } else {
- return failure "error when closing pipe to $mailer: $error_message";
- }
+ open($pipe, '|-', "$mailer -t -oi @args")
+ || return failure "Error executing $mailer: $!";
+ print($pipe $message->as_string)
+ || return failure "Error printing via pipe to $mailer: $!";
+ unless (close $pipe) {
+ return failure "error when closing pipe to $mailer: $!" if $!;
+ my ($error_message, $is_transient) = _map_exitcode($? >> 8);
+ if (Bugzilla->get_param_with_override('use_mailer_queue')) {
+
+ # Return success for errors which are fatal so Bugzilla knows to
+ # remove them from the queue
+ if ($is_transient) {
+ return failure "error when closing pipe to $mailer: $error_message";
+ }
+ else {
+ warn "error when closing pipe to $mailer: $error_message\n";
+ return success;
+ }
+ }
+ else {
+ return failure "error when closing pipe to $mailer: $error_message";
}
- return success;
+ }
+ return success;
}
sub _map_exitcode {
- # Returns (error message, is_transient)
- # from the sendmail source (sendmail/sysexit.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/sysexit.h)
+ my $code = shift;
+ if ($code == 64) {
+ return ("Command line usage error (EX_USAGE)", 1);
+ }
+ elsif ($code == 65) {
+ return ("Data format error (EX_DATAERR)", 1);
+ }
+ elsif ($code == 66) {
+ return ("Cannot open input (EX_NOINPUT)", 1);
+ }
+ elsif ($code == 67) {
+ return ("Addressee unknown (EX_NOUSER)", 0);
+ }
+ elsif ($code == 68) {
+ return ("Host name unknown (EX_NOHOST)", 0);
+ }
+ elsif ($code == 69) {
+ return ("Service unavailable (EX_UNAVAILABLE)", 1);
+ }
+ elsif ($code == 70) {
+ return ("Internal software error (EX_SOFTWARE)", 1);
+ }
+ elsif ($code == 71) {
+ return ("System error (EX_OSERR)", 1);
+ }
+ elsif ($code == 72) {
+ return ("Critical OS file missing (EX_OSFILE)", 1);
+ }
+ elsif ($code == 73) {
+ return ("Can't create output file (EX_CANTCREAT)", 1);
+ }
+ elsif ($code == 74) {
+ return ("Input/output error (EX_IOERR)", 1);
+ }
+ elsif ($code == 75) {
+ return ("Temp failure (EX_TEMPFAIL)", 1);
+ }
+ elsif ($code == 76) {
+ return ("Remote error in protocol (EX_PROTOCOL)", 1);
+ }
+ elsif ($code == 77) {
+ return ("Permission denied (EX_NOPERM)", 1);
+ }
+ elsif ($code == 78) {
+ return ("Configuration error (EX_CONFIG)", 1);
+ }
+ else {
+ return ("Unknown Error ($code)", 1);
+ }
}
1;
diff --git a/Bugzilla/Series.pm b/Bugzilla/Series.pm
index be30f6284..d89c2959a 100644
--- a/Bugzilla/Series.pm
+++ b/Bugzilla/Series.pm
@@ -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;
- # If the first parameter is undefined, check if this series already
- # exists and update it series_id accordingly
- $self->{'series_id'} ||= $self->existsInDatabase();
+ (
+ $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();
}
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'} });
- }
+ 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->{'category'}
+ = $cgi->param('category')
+ || $cgi->param('newcategory')
+ || ThrowUserError("missing_category");
- $self->{'subcategory'} = $cgi->param('subcategory')
- || $cgi->param('newsubcategory')
- || ThrowUserError("missing_subcategory");
+ $self->{'subcategory'}
+ = $cgi->param('subcategory')
+ || $cgi->param('newsubcategory')
+ || ThrowUserError("missing_subcategory");
- $self->{'name'} = $cgi->param('name')
- || ThrowUserError("missing_name");
+ $self->{'name'} = $cgi->param('name') || ThrowUserError("missing_name");
- $self->{'creator_id'} = Bugzilla->user->id;
+ $self->{'creator_id'} = Bugzilla->user->id;
- $self->{'frequency'} = $cgi->param('frequency');
- detaint_natural($self->{'frequency'})
- || ThrowUserError("missing_frequency");
+ $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->{'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;
+ $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');
+ # 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'}");
- }
+ 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 });
- }
+ # Is this already in the database?
+ if ($exists) {
- $dbh->bz_commit_transaction();
+ # 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'}));
+ 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);
+ return ($series_id);
}
# Get a category or subcategory IDs, creating the category if it doesn't exist.
sub getCategoryID {
- my ($category) = @_;
- my $category_id;
- my $dbh = Bugzilla->dbh;
+ my ($category) = @_;
+ my $category_id;
+ my $dbh = Bugzilla->dbh;
- # This seems for the best idiom for "Do A. Then maybe do B and A again."
- while (1) {
- # We are quoting this to put it in the DB, so we can remove taint
- trick_taint($category);
+ # This seems for the best idiom for "Do A. Then maybe do B and A again."
+ while (1) {
- $category_id = $dbh->selectrow_array("SELECT id " .
- "from series_categories " .
- "WHERE name =" . $dbh->quote($category));
+ # We are quoting this to put it in the DB, so we can remove taint
+ trick_taint($category);
- last if defined($category_id);
+ $category_id
+ = $dbh->selectrow_array("SELECT id "
+ . "from series_categories "
+ . "WHERE name ="
+ . $dbh->quote($category));
- $dbh->do("INSERT INTO series_categories (name) " .
- "VALUES (" . $dbh->quote($category) . ")");
- }
+ last if defined($category_id);
+
+ $dbh->do("INSERT INTO series_categories (name) "
+ . "VALUES ("
+ . $dbh->quote($category)
+ . ")");
+ }
- return $category_id;
+ return $category_id;
}
##########
@@ -254,20 +285,20 @@ sub id { return $_[0]->{'series_id'}; }
sub name { return $_[0]->{'name'}; }
sub creator {
- my $self = shift;
+ my $self = shift;
- if (!$self->{creator} && $self->{creator_id}) {
- require Bugzilla::User;
- $self->{creator} = new Bugzilla::User($self->{creator_id});
- }
- return $self->{creator};
+ if (!$self->{creator} && $self->{creator_id}) {
+ require Bugzilla::User;
+ $self->{creator} = new Bugzilla::User($self->{creator_id});
+ }
+ return $self->{creator};
}
sub remove_from_db {
- my $self = shift;
- my $dbh = Bugzilla->dbh;
+ my $self = shift;
+ my $dbh = Bugzilla->dbh;
- $dbh->do('DELETE FROM series WHERE series_id = ?', undef, $self->id);
+ $dbh->do('DELETE FROM series WHERE series_id = ?', undef, $self->id);
}
1;
diff --git a/Bugzilla/Status.pm b/Bugzilla/Status.pm
index ae04873a8..619594f5b 100644
--- a/Bugzilla/Status.pm
+++ b/Bugzilla/Status.pm
@@ -17,11 +17,11 @@ use warnings;
# methods.
use base 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,24 +57,25 @@ 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;
- my $dbh = Bugzilla->dbh;
- my $id = $self->id;
- $dbh->bz_start_transaction();
- $self->SUPER::remove_from_db();
- $dbh->do('DELETE FROM status_workflow
- WHERE old_status = ? OR new_status = ?',
- undef, $id, $id);
- $dbh->bz_commit_transaction();
- delete Bugzilla->request_cache->{status_bug_state_open};
+ my $self = shift;
+ my $dbh = Bugzilla->dbh;
+ my $id = $self->id;
+ $dbh->bz_start_transaction();
+ $self->SUPER::remove_from_db();
+ $dbh->do(
+ 'DELETE FROM status_workflow
+ WHERE old_status = ? OR new_status = ?', undef, $id, $id
+ );
+ $dbh->bz_commit_transaction();
+ delete Bugzilla->request_cache->{status_bug_state_open};
}
###############################
@@ -82,16 +83,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;
}
##############
@@ -99,14 +100,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;
}
@@ -115,118 +116,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';
- # For newly-filed bugs
- if ($old_status) {
- $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';
+
+ # For newly-filed bugs
+ if ($old_status) {
+ $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});
- my $missing_statuses = $dbh->selectcol_arrayref('SELECT id
+ # Silently discard invalid bug statuses.
+ $new_status || 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, $new_status->id);
-
- my $sth = $dbh->prepare('INSERT INTO status_workflow
- (old_status, new_status) VALUES (?, ?)');
-
- foreach my $old_status_id (@$missing_statuses) {
- next if ($old_status_id == $new_status->id);
- $sth->execute($old_status_id, $new_status->id);
- }
+ undef, $new_status->id
+ );
+
+ my $sth = $dbh->prepare(
+ 'INSERT INTO status_workflow
+ (old_status, new_status) VALUES (?, ?)'
+ );
+
+ foreach my $old_status_id (@$missing_statuses) {
+ next if ($old_status_id == $new_status->id);
+ $sth->execute($old_status_id, $new_status->id);
+ }
}
1;
diff --git a/Bugzilla/Template.pm b/Bugzilla/Template.pm
index 36435d637..1d597277e 100644
--- a/Bugzilla/Template.pm
+++ b/Bugzilla/Template.pm
@@ -19,7 +19,7 @@ use Bugzilla::Constants;
use Bugzilla::Hook;
use Bugzilla::Install::Requirements;
use Bugzilla::Install::Util qw(install_string template_include_path
- include_languages);
+ include_languages);
use Bugzilla::Keyword;
use Bugzilla::Util;
use Bugzilla::User;
@@ -45,16 +45,16 @@ use JSON::XS qw(encode_json);
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];
my %SHARED_PROVIDERS;
# 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 module into a hash we can
@@ -63,19 +63,19 @@ sub SAFE_URL_REGEXP {
# 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;
}
- return \%constants;
+ else {
+ my @list = (Bugzilla::Constants->$constant);
+ $constants{$constant} = (scalar(@list) == 1) ? $list[0] : \@list;
+ }
+ }
+ return \%constants;
}
# Returns the path to the templates based on the Accept-Language
@@ -83,52 +83,50 @@ 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 ||= '';
-
- # Security - allow letters and a hyphen only
- $ctype =~ s/[^a-zA-Z\-]//g;
- $format =~ s/[^a-zA-Z\-]//g;
- 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} // 'application/octet-stream',
- };
+ my $self = shift;
+ my ($template, $format, $ctype) = @_;
+
+ $ctype ||= 'html';
+ $format ||= '';
+
+ # Security - allow letters and a hyphen only
+ $ctype =~ s/[^a-zA-Z\-]//g;
+ $format =~ s/[^a-zA-Z\-]//g;
+ 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}
+ // 'application/octet-stream',
+ };
}
# This routine quoteUrls contains inspirations from the HTML::FromText CPAN
@@ -139,172 +137,182 @@ sub get_format {
# If you want to modify this routine, read the comments carefully
sub quoteUrls {
- my ($text, $bug, $comment, $user, $bug_link_func) = @_;
- return $text unless $text;
- $user ||= Bugzilla->user;
- $bug_link_func ||= \&get_bug_link;
-
- # We use /g for speed, but uris can have other things inside them
- # (http://foo/bug#3 for example). Filtering that out filters valid
- # bug refs out, so we have to do replacements.
- # mailto can't contain space or #, so we don't have to bother for that
- # Do this by replacing matches with \x{FDD2}$count\x{FDD3}
- # \x{FDDx} is used because it's unlikely to occur in the text
- # and are reserved unicode characters. We disable warnings for now
- # until we require Perl 5.13.9 or newer.
- no warnings 'utf8';
-
- # If the comment is already wrapped, we should ignore newlines when
- # looking for matching regexps. Else we should take them into account.
- my $s = ($comment && $comment->already_wrapped) ? qr/\s/ : qr/\h/;
-
- # However, note that adding the title (for buglinks) can affect things
- # In particular, attachment matches go before bug titles, so that titles
- # with 'attachment 1' don't double match.
- # Dupe checks go afterwards, because that uses ^ and \Z, which won't occur
- # if it was substituted as a bug title (since that always involve leading
- # and trailing text)
-
- # Because of entities, it's easier (and quicker) to do this before escaping
-
- my @things;
- my $count = 0;
- my $tmp;
-
- my @hook_regexes;
- Bugzilla::Hook::process('bug_format_comment',
- { text => \$text, bug => $bug, regexes => \@hook_regexes,
- comment => $comment, user => $user });
-
- foreach my $re (@hook_regexes) {
- my ($match, $replace) = @$re{qw(match replace)};
- if (ref($replace) eq 'CODE') {
- $text =~ s/$match/($things[$count++] = $replace->({matches => [
+ my ($text, $bug, $comment, $user, $bug_link_func) = @_;
+ return $text unless $text;
+ $user ||= Bugzilla->user;
+ $bug_link_func ||= \&get_bug_link;
+
+ # We use /g for speed, but uris can have other things inside them
+ # (http://foo/bug#3 for example). Filtering that out filters valid
+ # bug refs out, so we have to do replacements.
+ # mailto can't contain space or #, so we don't have to bother for that
+ # Do this by replacing matches with \x{FDD2}$count\x{FDD3}
+ # \x{FDDx} is used because it's unlikely to occur in the text
+ # and are reserved unicode characters. We disable warnings for now
+ # until we require Perl 5.13.9 or newer.
+ no warnings 'utf8';
+
+ # If the comment is already wrapped, we should ignore newlines when
+ # looking for matching regexps. Else we should take them into account.
+ my $s = ($comment && $comment->already_wrapped) ? qr/\s/ : qr/\h/;
+
+ # However, note that adding the title (for buglinks) can affect things
+ # In particular, attachment matches go before bug titles, so that titles
+ # with 'attachment 1' don't double match.
+ # Dupe checks go afterwards, because that uses ^ and \Z, which won't occur
+ # if it was substituted as a bug title (since that always involve leading
+ # and trailing text)
+
+ # Because of entities, it's easier (and quicker) to do this before escaping
+
+ my @things;
+ my $count = 0;
+ my $tmp;
+
+ my @hook_regexes;
+ Bugzilla::Hook::process(
+ 'bug_format_comment',
+ {
+ text => \$text,
+ bug => $bug,
+ regexes => \@hook_regexes,
+ comment => $comment,
+ user => $user
+ }
+ );
+
+ foreach my $re (@hook_regexes) {
+ my ($match, $replace) = @$re{qw(match replace)};
+ if (ref($replace) eq 'CODE') {
+ $text =~ s/$match/($things[$count++] = $replace->({matches => [
$1, $2, $3, $4,
$5, $6, $7, $8,
$9, $10]}))
&& ("\x{FDD2}" . ($count-1) . "\x{FDD3}")/egx;
- }
- else {
- $text =~ s/$match/($things[$count++] = $replace)
+ }
+ else {
+ $text =~ s/$match/($things[$count++] = $replace)
&& ("\x{FDD2}" . ($count-1) . "\x{FDD3}")/egx;
- }
}
+ }
- # Provide tooltips for full bug links (Bug 74355)
- my $urlbase_re = '(' . quotemeta(Bugzilla->localconfig->{urlbase}) . ')';
- $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 = '(' . quotemeta(Bugzilla->localconfig->{urlbase}) . ')';
+ $text =~ s~\b(${urlbase_re}\Qshow_bug.cgi?id=\E([0-9]+)(\#c([0-9]+))?)\b
~($things[$count++] = $bug_link_func->($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 rel=\"nofollow\" 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);
- # Color quoted text
- $text =~ s~^(&gt;.+)$~<span class="quote">$1</span >~mg;
- $text =~ s~</span >\n<span class="quote">~\n~g;
+ # 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
+ # 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
- # BMO: don't make diff view the default for patches (Bug 652332)
- $text =~ s~\b(attachment$s*\#?$s*(\d+)(?:$s+\[diff\])?(?:\s+\[details\])?)
+ # attachment links
+ # BMO: don't make diff view the default for patches (Bug 652332)
+ $text =~ s~\b(attachment$s*\#?$s*(\d+)(?:$s+\[diff\])?(?:\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+)/i;
- my $comment_re = qr/comment$s*\#?$s*(\d+)/i;
- $text =~ s~\b($bug_re(?:$s*,?$s*$comment_re)?|$comment_re)
+ # Current bug ID this comment belongs to
+ my $current_bugurl = $bug ? ("show_bug.cgi?id=" . $bug->id) : "";
+
+ # This handles bug a, comment b type stuff. Because we're using /g
+ # we have to do this in one pattern, and so this is semi-messy.
+ # Also, we can't use $bug_re?$comment_re? because that will match the
+ # empty string
+ my $bug_word = template_var('terms')->{bug};
+ my $bug_re = qr/\Q$bug_word\E$s*\#?$s*(\d+)/i;
+ my $comment_re = qr/comment$s*\#?$s*(\d+)/i;
+ $text =~ s~\b($bug_re(?:$s*,?$s*$comment_re)?|$comment_re)
~ # We have several choices. $1 here is the link, and $2-4 are set
# depending on which part matched
(defined($2) ? $bug_link_func->($2, $1, { comment_num => $3, user => $user }) :
"<a href=\"$current_bugurl#c$4\">$1</a>")
~egx;
- # 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)
~$bug_link_func->($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) = @_;
- my $dbh = Bugzilla->dbh;
- $user ||= Bugzilla->user;
-
- my $attachment = new Bugzilla::Attachment({ id => $attachid, cache => 1 });
-
- if ($attachment) {
- my $title = "";
- my $className = "";
- if ($user->can_see_bug($attachment->bug_id)
- && (!$attachment->isprivate || $user->is_insider))
- {
- $title = $attachment->description;
- }
- if ($attachment->isobsolete) {
- $className = "bz_obsolete";
- }
- # Prevent code injection in the title.
- $title = html_quote(clean_text($title));
-
- $link_text =~ s/ \[details\]$//;
- $link_text =~ s/ \[diff\]$//;
- state $urlbase = Bugzilla->localconfig->{urlbase};
- my $linkval = "${urlbase}attachment.cgi?id=$attachid";
-
- # If the attachment is a patch and patch_viewer feature is
- # enabled, add link to the diff.
- my $patchlink = "";
- if ($attachment->ispatch and Bugzilla->feature('patch_viewer')) {
- $patchlink = qq| <a href="${linkval}&amp;action=diff" title="$title">[diff]</a>|;
- }
+ my ($attachid, $link_text, $user) = @_;
+ my $dbh = Bugzilla->dbh;
+ $user ||= Bugzilla->user;
- # Whitespace matters here because these links are in <pre> tags.
- return qq|<span class="$className">|
- . qq|<a href="${linkval}" name="attach_${attachid}" title="$title">$link_text</a>|
- . qq| <a href="${linkval}&amp;action=edit" title="$title">[details]</a>|
- . qq|${patchlink}|
- . qq|</span>|;
+ 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;
}
- 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\]$//;
+ $link_text =~ s/ \[diff\]$//;
+ state $urlbase = Bugzilla->localconfig->{urlbase};
+ my $linkval = "${urlbase}attachment.cgi?id=$attachid";
+
+ # If the attachment is a patch and patch_viewer feature is
+ # enabled, add link to the diff.
+ my $patchlink = "";
+ if ($attachment->ispatch and Bugzilla->feature('patch_viewer')) {
+ $patchlink
+ = qq| <a href="${linkval}&amp;action=diff" title="$title">[diff]</a>|;
}
+
+ # Whitespace matters here because these links are in <pre> tags.
+ return
+ qq|<span class="$className">|
+ . qq|<a href="${linkval}" name="attach_${attachid}" title="$title">$link_text</a>|
+ . qq| <a href="${linkval}&amp;action=edit" title="$title">[details]</a>|
+ . qq|${patchlink}|
+ . qq|</span>|;
+ }
+ else {
+ return qq{$link_text};
+ }
}
# Creates a link to a bug, including its title.
@@ -315,51 +323,57 @@ sub get_attachment_link {
# comment in the bug
sub get_bug_link {
- my ($bug, $link_text, $options) = @_;
- $options ||= {};
- $options->{user} ||= Bugzilla->user;
- my $dbh = Bugzilla->dbh;
-
- if (defined $bug && $bug ne '') {
- $bug = blessed($bug) ? $bug : new Bugzilla::Bug({ id => $bug, cache => 1 });
- return $link_text if $bug->{error};
- }
-
- my $template = Bugzilla->template_inner;
- my $linkified;
- $template->process('bug/link.html.tmpl',
- { bug => $bug, link_text => $link_text, %$options }, \$linkified);
- return $linkified;
+ my ($bug, $link_text, $options) = @_;
+ $options ||= {};
+ $options->{user} ||= Bugzilla->user;
+ my $dbh = Bugzilla->dbh;
+
+ if (defined $bug && $bug ne '') {
+ $bug = blessed($bug) ? $bug : new Bugzilla::Bug({id => $bug, cache => 1});
+ return $link_text if $bug->{error};
+ }
+
+ my $template = Bugzilla->template_inner;
+ my $linkified;
+ $template->process('bug/link.html.tmpl',
+ {bug => $bug, link_text => $link_text, %$options},
+ \$linkified);
+ return $linkified;
}
# We use this instead of format because format doesn't deal well with
# multi-byte languages.
sub multiline_sprintf {
- my ($format, $args, $sizes) = @_;
- my @parts;
- my @my_sizes = @$sizes; # Copy this so we don't modify the input array.
- foreach my $string (@$args) {
- my $size = shift @my_sizes;
- my @pieces = split("\n", wrap_hard($string, $size));
- push(@parts, \@pieces);
- }
-
- my $formatted;
- while (1) {
- # Get the first item of each part.
- my @line = map { shift @$_ } @parts;
- # If they're all undef, we're done.
- last if !grep { defined $_ } @line;
- # Make any single undef item into ''
- @line = map { defined $_ ? $_ : '' } @line;
- # And append a formatted line
- $formatted .= sprintf($format, @line);
- # Remove trailing spaces, or they become lots of =20's in
- # quoted-printable emails.
- $formatted =~ s/\s+$//;
- $formatted .= "\n";
- }
- return $formatted;
+ my ($format, $args, $sizes) = @_;
+ my @parts;
+ my @my_sizes = @$sizes; # Copy this so we don't modify the input array.
+ foreach my $string (@$args) {
+ my $size = shift @my_sizes;
+ my @pieces = split("\n", wrap_hard($string, $size));
+ push(@parts, \@pieces);
+ }
+
+ my $formatted;
+ while (1) {
+
+ # Get the first item of each part.
+ my @line = map { shift @$_ } @parts;
+
+ # If they're all undef, we're done.
+ last if !grep { defined $_ } @line;
+
+ # Make any single undef item into ''
+ @line = map { defined $_ ? $_ : '' } @line;
+
+ # And append a formatted line
+ $formatted .= sprintf($format, @line);
+
+ # Remove trailing spaces, or they become lots of =20's in
+ # quoted-printable emails.
+ $formatted =~ s/\s+$//;
+ $formatted .= "\n";
+ }
+ return $formatted;
}
#####################
@@ -367,8 +381,8 @@ sub multiline_sprintf {
#####################
sub version_filter {
- my ($file_url) = @_;
- return "static/v" . Bugzilla->VERSION . "/$file_url";
+ my ($file_url) = @_;
+ return "static/v" . Bugzilla->VERSION . "/$file_url";
}
# Set up the skin CSS cascade:
@@ -381,65 +395,65 @@ sub version_filter {
# 6. Custom Bugzilla stylesheet set
sub css_files {
- my ($style_urls, $no_yui) = @_;
+ my ($style_urls, $no_yui) = @_;
- # global.css belongs on every page
- my @requested_css = ( 'skins/standard/global.css', @$style_urls );
+ # global.css belongs on every page
+ my @requested_css = ('skins/standard/global.css', @$style_urls);
- unshift @requested_css, "skins/yui.css" unless $no_yui;
+ unshift @requested_css, "skins/yui.css" unless $no_yui;
- my @css_sets = map { _css_link_set($_) } @requested_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 %by_type = (standard => [], skin => [], custom => []);
+ foreach my $set (@css_sets) {
+ foreach my $key (keys %$set) {
+ push(@{$by_type{$key}}, $set->{$key});
}
+ }
- return \%by_type;
+ return \%by_type;
}
sub _css_link_set {
- my ($file_name) = @_;
+ my ($file_name) = @_;
- my %set = (standard => version_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 (-f "$cgi_path/$skin_file_name") {
- $set{skin} = version_filter($skin_file_name);
- }
-
- my $custom_file_name = $file_name;
- $custom_file_name =~ s{(?:^|/)skins/standard/}{skins/custom/};
- if (-f "$cgi_path/$custom_file_name") {
- $set{custom} = version_filter($custom_file_name);
- }
+ my %set = (standard => version_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 (-f "$cgi_path/$skin_file_name") {
+ $set{skin} = version_filter($skin_file_name);
+ }
+
+ my $custom_file_name = $file_name;
+ $custom_file_name =~ s{(?:^|/)skins/standard/}{skins/custom/};
+ if (-f "$cgi_path/$custom_file_name") {
+ $set{custom} = version_filter($custom_file_name);
+ }
+
+ return \%set;
}
# YUI dependency resolution
sub yui_resolve_deps {
- my ($yui, $yui_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_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;
}
###############################################################################
@@ -461,61 +475,57 @@ $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
# 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 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;
+};
# Override the built in .lower() vmethod
-$Template::Stash::SCALAR_OPS->{ lower } =
- sub {
- return lc($_[0]);
- };
+$Template::Stash::SCALAR_OPS->{lower} = sub {
+ return lc($_[0]);
+};
# Create the template object that processes templates and specify
# configuration parameters that apply to all templates.
@@ -525,18 +535,19 @@ $Template::Stash::SCALAR_OPS->{ lower } =
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;
- local $SIG{__DIE__};
- delete $SIG{__DIE__};
- warn "WARNING: CGI::Carp makes templates slow" if $INC{"CGI/Carp.pm"};
- 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;
+ local $SIG{__DIE__};
+ delete $SIG{__DIE__};
+ warn "WARNING: CGI::Carp makes templates slow" if $INC{"CGI/Carp.pm"};
+ my $retval = $self->SUPER::process(@_);
+ shift @$current_langs;
+ return $retval;
}
# Construct the Template object
@@ -545,640 +556,663 @@ 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'}),
-
- # allow PERL/RAWPERL because doing so can boost performance
- EVAL_PERL => 1,
-
- # 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,
-
- ABSOLUTE => 1,
- RELATIVE => 0,
-
- # Only use an on-disk template cache if we're running as the web
- # server. This ensures the permissions of the cache remain correct.
- COMPILE_DIR => is_webserver_group() ? bz_locations()->{'template_cache'} : undef,
-
- # 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/initialize.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 => {
-
- # Render text in required style.
-
- inactive => [
- sub {
- my($context, $isinactive) = @_;
- return sub {
- return $isinactive ? '<span class="bz_inactive">'.$_[0].'</span>' : $_[0];
- }
- }, 1
- ],
-
- closed => [
- sub {
- my($context, $isclosed) = @_;
- return sub {
- return $isclosed ? '<span class="bz_closed">'.$_[0].'</span>' : $_[0];
- }
- }, 1
- ],
-
- obsolete => [
- sub {
- my($context, $isobsolete) = @_;
- return sub {
- return $isobsolete ? '<span class="bz_obsolete">'.$_[0].'</span>' : $_[0];
- }
- }, 1
- ],
-
- # Returns the text with backslashes, single/double quotes,
- # and newlines/carriage returns escaped for use in JS strings.
- js => sub {
- my ($var) = @_;
- no warnings 'utf8';
- $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;
- },
-
- # Sadly, different to the above. See http://www.json.org/
- # for details.
- json => sub {
- my ($var) = @_;
- no warnings 'utf8';
- $var =~ s/([\\\"\/])/\\$1/g;
- $var =~ s/\n/\\n/g;
- $var =~ s/\r/\\r/g;
- $var =~ s/\f/\\f/g;
- $var =~ s/\t/\\t/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) = @_;
- # Only run for utf8 to avoid issues with other multibyte encodings
- # that may be reassigning meaning to ascii characters.
- if (Bugzilla->params->{'utf8'}) {
- $data =~ s/(?![\t\r\n])[[:cntrl:]]//g;
- }
- return $data;
- },
-
- # HTML collapses newlines in element attributes to a single space,
- # so form elements which may have whitespace (ie comments) need
- # 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;
- },
-
- # Prevents line break on hyphens and whitespaces.
- no_break => sub {
- my ($var) = @_;
- $var =~ s/ /\&nbsp;/g;
- $var =~ s/-/\&#8209;/g;
- return $var;
- },
-
- # Insert `<wbr>` HTML tags to camel and snake case words as well as
- # words containing dots in the given string so a long bug summary,
- # for example, will be wrapped in a preferred manner rather than
- # overflowing or expanding the parent element. This conversion
- # should exclude existing HTML tags such as links. Examples:
- # * `test<wbr>_switch<wbr>_window<wbr>_content<wbr>.py`
- # * `Test<wbr>Switch<wbr>To<wbr>Window<wbr>Content`
- # * `<a href="https://www.mozilla.org/">mozilla<wbr>.org</a>`
- wbr => sub {
- my ($var) = @_;
- $var =~ s/([a-z])([A-Z\._])(?![^<]*>)/$1<wbr>$2/g;
- return $var;
- },
-
- xml => \&Bugzilla::Util::xml_quote ,
-
- # This filter is similar to url_quote but used a \ instead of a %
- # as prefix. In addition it replaces a ' ' by a '_'.
- css_class_quote => \&Bugzilla::Util::css_class_quote ,
-
- # Removes control characters and trims extra whitespace.
- clean_text => \&Bugzilla::Util::clean_text ,
-
- quoteUrls => [ sub {
- my ($context, $bug, $comment, $user) = @_;
- return sub {
- my $text = shift;
- return quoteUrls($text, $bug, $comment, $user);
- };
- },
- 1
- ],
-
- bug_link => [ sub {
- my ($context, $bug, $options) = @_;
- return sub {
- my $text = shift;
- return get_bug_link($bug, $text, $options);
- };
- },
- 1
- ],
-
- bug_list_link => sub {
- my ($buglist, $options) = @_;
- return join(", ", map(get_bug_link($_, $_, $options), split(/ *, */, $buglist)));
- },
-
- # In CSV, quotes are doubled, and any value containing a quote or a
- # comma is enclosed in quotes.
- # If a field starts with either "=", "+", "-" or "@", it is preceded
- # by a space to prevent stupid formula execution from Excel & co.
- csv => sub
- {
- my ($var) = @_;
- $var = ' ' . $var if $var =~ /^[+=@-]/;
- # backslash is not special to CSV, but it can be used to confuse some browsers...
- # so we do not allow it to happen. We only do this for logged-in users.
- $var =~ s/\\/\x{FF3C}/g if Bugzilla->user->id;
- $var =~ s/\"/\"\"/g;
- if ($var !~ /^-?(\d+\.)?\d*$/) {
- $var = "\"$var\"";
- }
- return $var;
- } ,
-
- # Format a filesize in bytes to a human readable value
- unitconvert => sub
- {
- my ($data) = @_;
- my $retval = "";
- my %units = (
- 'KB' => 1024,
- 'MB' => 1024 * 1024,
- 'GB' => 1024 * 1024 * 1024,
- );
-
- if ($data < 1024) {
- return "$data bytes";
- }
- else {
- my $u;
- foreach $u ('GB', 'MB', 'KB') {
- if ($data >= $units{$u}) {
- return sprintf("%.2f %s", $data/$units{$u}, $u);
- }
- }
- }
- },
-
- # Format a time for display (more info in Bugzilla::Util)
- time => [ sub {
- my ($context, $format, $timezone) = @_;
- return sub {
- my $time = shift;
- return format_time($time, $format, $timezone);
- };
- },
- 1
- ],
-
- html => \&Bugzilla::Util::html_quote,
-
- html_light => \&Bugzilla::Util::html_light_quote,
-
- email => \&Bugzilla::Util::email_filter,
-
- version => \&version_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],
-
- # We force filtering of every variable in key security-critical
- # places; we have a none filter for people to use when they
- # really, really don't want a variable to be changed.
- none => sub { return $_[0]; } ,
+ my $class = shift;
+ my %opts = @_;
+
+ # IMPORTANT - If you make any FILTER changes here, make sure to
+ # make them in t/004.template.t also, if required.
+
+ my $config = {
+
+ # Colon-separated list of directories containing templates.
+ INCLUDE_PATH => $opts{'include_path'} || _include_path($opts{'language'}),
+
+ # allow PERL/RAWPERL because doing so can boost performance
+ EVAL_PERL => 1,
+
+ # 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,
+
+ ABSOLUTE => 1,
+ RELATIVE => 0,
+
+ # Only use an on-disk template cache if we're running as the web
+ # server. This ensures the permissions of the cache remain correct.
+ COMPILE_DIR => is_webserver_group()
+ ? bz_locations()->{'template_cache'}
+ : undef,
+
+ # 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/initialize.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 => {
+
+ # Render text in required style.
+
+ inactive => [
+ sub {
+ my ($context, $isinactive) = @_;
+ return sub {
+ return $isinactive ? '<span class="bz_inactive">' . $_[0] . '</span>' : $_[0];
+ }
+ },
+ 1
+ ],
+
+ closed => [
+ sub {
+ my ($context, $isclosed) = @_;
+ return sub {
+ return $isclosed ? '<span class="bz_closed">' . $_[0] . '</span>' : $_[0];
+ }
+ },
+ 1
+ ],
+
+ obsolete => [
+ sub {
+ my ($context, $isobsolete) = @_;
+ return sub {
+ return $isobsolete ? '<span class="bz_obsolete">' . $_[0] . '</span>' : $_[0];
+ }
+ },
+ 1
+ ],
+
+ # Returns the text with backslashes, single/double quotes,
+ # and newlines/carriage returns escaped for use in JS strings.
+ js => sub {
+ my ($var) = @_;
+ no warnings 'utf8';
+ $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;
+ },
+
+ # Sadly, different to the above. See http://www.json.org/
+ # for details.
+ json => sub {
+ my ($var) = @_;
+ no warnings 'utf8';
+ $var =~ s/([\\\"\/])/\\$1/g;
+ $var =~ s/\n/\\n/g;
+ $var =~ s/\r/\\r/g;
+ $var =~ s/\f/\\f/g;
+ $var =~ s/\t/\\t/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) = @_;
+
+ # Only run for utf8 to avoid issues with other multibyte encodings
+ # that may be reassigning meaning to ascii characters.
+ if (Bugzilla->params->{'utf8'}) {
+ $data =~ s/(?![\t\r\n])[[:cntrl:]]//g;
+ }
+ return $data;
+ },
+
+ # HTML collapses newlines in element attributes to a single space,
+ # so form elements which may have whitespace (ie comments) need
+ # 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;
+ },
+
+ # Prevents line break on hyphens and whitespaces.
+ no_break => sub {
+ my ($var) = @_;
+ $var =~ s/ /\&nbsp;/g;
+ $var =~ s/-/\&#8209;/g;
+ return $var;
+ },
+
+ # Insert `<wbr>` HTML tags to camel and snake case words as well as
+ # words containing dots in the given string so a long bug summary,
+ # for example, will be wrapped in a preferred manner rather than
+ # overflowing or expanding the parent element. This conversion
+ # should exclude existing HTML tags such as links. Examples:
+ # * `test<wbr>_switch<wbr>_window<wbr>_content<wbr>.py`
+ # * `Test<wbr>Switch<wbr>To<wbr>Window<wbr>Content`
+ # * `<a href="https://www.mozilla.org/">mozilla<wbr>.org</a>`
+ wbr => sub {
+ my ($var) = @_;
+ $var =~ s/([a-z])([A-Z\._])(?![^<]*>)/$1<wbr>$2/g;
+ return $var;
+ },
+
+ xml => \&Bugzilla::Util::xml_quote,
+
+ # This filter is similar to url_quote but used a \ instead of a %
+ # as prefix. In addition it replaces a ' ' by a '_'.
+ css_class_quote => \&Bugzilla::Util::css_class_quote,
+
+ # Removes control characters and trims extra whitespace.
+ clean_text => \&Bugzilla::Util::clean_text,
+
+ quoteUrls => [
+ sub {
+ my ($context, $bug, $comment, $user) = @_;
+ return sub {
+ my $text = shift;
+ return quoteUrls($text, $bug, $comment, $user);
+ };
+ },
+ 1
+ ],
+
+ bug_link => [
+ sub {
+ my ($context, $bug, $options) = @_;
+ return sub {
+ my $text = shift;
+ return get_bug_link($bug, $text, $options);
+ };
+ },
+ 1
+ ],
+
+ bug_list_link => sub {
+ my ($buglist, $options) = @_;
+ return
+ join(", ", map(get_bug_link($_, $_, $options), split(/ *, */, $buglist)));
+ },
+
+ # In CSV, quotes are doubled, and any value containing a quote or a
+ # comma is enclosed in quotes.
+ # If a field starts with either "=", "+", "-" or "@", it is preceded
+ # by a space to prevent stupid formula execution from Excel & co.
+ csv => sub {
+ my ($var) = @_;
+ $var = ' ' . $var if $var =~ /^[+=@-]/;
+
+ # backslash is not special to CSV, but it can be used to confuse some browsers...
+ # so we do not allow it to happen. We only do this for logged-in users.
+ $var =~ s/\\/\x{FF3C}/g if Bugzilla->user->id;
+ $var =~ s/\"/\"\"/g;
+ if ($var !~ /^-?(\d+\.)?\d*$/) {
+ $var = "\"$var\"";
+ }
+ return $var;
+ },
+
+ # Format a filesize in bytes to a human readable value
+ unitconvert => sub {
+ my ($data) = @_;
+ my $retval = "";
+ my %units = ('KB' => 1024, 'MB' => 1024 * 1024, 'GB' => 1024 * 1024 * 1024,);
+
+ if ($data < 1024) {
+ return "$data bytes";
+ }
+ else {
+ my $u;
+ foreach $u ('GB', 'MB', 'KB') {
+ if ($data >= $units{$u}) {
+ return sprintf("%.2f %s", $data / $units{$u}, $u);
+ }
+ }
+ }
+ },
+
+ # Format a time for display (more info in Bugzilla::Util)
+ time => [
+ sub {
+ my ($context, $format, $timezone) = @_;
+ return sub {
+ my $time = shift;
+ return format_time($time, $format, $timezone);
+ };
+ },
+ 1
+ ],
+
+ html => \&Bugzilla::Util::html_quote,
+
+ html_light => \&Bugzilla::Util::html_light_quote,
+
+ email => \&Bugzilla::Util::email_filter,
+
+ version => \&version_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;
- PLUGIN_BASE => 'Bugzilla::Template::Plugin',
-
- # We don't want this feature.
- CONSTANT_NAMESPACE => '__const',
-
- # Default variables for all templates
- VARIABLES => {
- # Some of these are not really constants, and doing this messes up preloading.
- # they are now fake constants.
- constants => _load_constants(),
-
- # Function for retrieving global parameters.
- 'Param' => sub { return Bugzilla->params->{$_[0]}; },
-
- 'bugzilla_version' => sub {
- my $version = Bugzilla->VERSION;
- if (my @ver = $version =~ /^(\d{4})(\d{2})(\d{2})\.(\d+)$/s) {
- if ($ver[3] eq '1') {
- return join('.', @ver[0,1,2]);
- }
- else {
- return join('.', @ver);
- }
- }
- else {
- return $version;
- }
- },
-
- json_encode => sub {
- return encode_json($_[0]);
- },
-
- # Function to create date strings
- 'time2str' => \&Date::Format::time2str,
-
- # Fixed size column formatting for bugmail.
- 'format_columns' => sub {
- my $cols = shift;
- my $format = ($cols == 3) ? FORMAT_TRIPLE : FORMAT_DOUBLE;
- my $col_size = ($cols == 3) ? FORMAT_3_SIZE : FORMAT_2_SIZE;
- return multiline_sprintf($format, \@_, $col_size);
- },
-
- # Generic linear search function
- 'lsearch' => sub {
- my ($array, $item) = @_;
- return firstidx { $_ eq $item } @$array;
- },
-
- # Currently logged in user, if any
- # If an sudo session is in progress, this is the user we're faking
- 'user' => sub { return Bugzilla->user; },
-
- # Currenly active language
- 'current_language' => sub { return Bugzilla->current_language; },
-
- 'script_nonce' => sub {
- my $cgi = Bugzilla->cgi;
- return $cgi->csp_nonce ? sprintf('nonce="%s"', $cgi->csp_nonce) : '';
- },
-
- # 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 "corect" URLBase value
- 'urlbase' => sub { return Bugzilla->localconfig->{urlbase}; },
-
- # Allow templates to access docs url with users' preferred language
- 'docs_urlbase' => sub {
- my $language = Bugzilla->current_language;
- my $docs_urlbase = Bugzilla->params->{'docs_urlbase'};
- $docs_urlbase =~ s/\%lang\%/$language/;
- 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,
-
- # Whether or not keywords are enabled, in this Bugzilla.
- 'use_keywords' => sub { return Bugzilla::Keyword->any_exist; },
-
- # All the keywords
- 'all_keywords' => sub { return Bugzilla::Keyword->get_all(); },
-
- # All the active keywords
- 'active_keywords' => sub {
- return [grep { $_->is_active } Bugzilla::Keyword->get_all()];
- },
-
- 'feature_enabled' => sub { return Bugzilla->feature(@_); },
-
- 'has_extension' => sub { return Bugzilla->has_extension(@_); },
-
- # field_descs can be somewhat slow to generate, so we generate
- # it only once per-language no matter how many times
- # $template->process() is called.
- 'field_descs' => sub { return template_var('field_descs') },
-
- # Calling bug/field-help.none.tmpl once per label is very
- # expensive, so we generate it once per-language.
- 'help_html' => sub { return template_var('help_html') },
-
- # This way we don't have to load field-descs.none.tmpl in
- # many templates.
- 'display_value' => \&Bugzilla::Util::display_value,
-
- 'install_string' => \&Bugzilla::Install::Util::install_string,
-
- 'report_columns' => \&Bugzilla::Search::REPORT_COLUMNS,
-
- # These don't work as normal constants.
- DB_MODULE => \&Bugzilla::Constants::DB_MODULE,
- 'default_authorizer' => sub { return Bugzilla::Auth->new() },
-
- # It is almost always better to do mobile feature detection, client side in js.
- # However, we need to set the meta[name=viewport] server-side or the behavior is
- # not as predictable. It is possible other parts of the frontend may use this feature too.
- 'is_mobile_browser' => sub { return Bugzilla->cgi->user_agent =~ /Mobi/ },
-
- 'socorro_lens_url' => sub {
- my ($sigs) = @_;
-
- # strip [@ ] from sigs
- my @sigs = map { /^\[\@\s*(.+?)\s*\]$/ } @$sigs;
-
- return '' unless @sigs;
- # use a URI object to encode the query string part.
- my $uri = URI->new(Bugzilla->localconfig->{urlbase} . 'metricsgraphics/socorro-lens.html');
- $uri->query_form('s' => join("\\", @sigs));
- return $uri;
- },
+ return $var;
+ },
+
+ # Wrap a displayed comment to the appropriate length
+ wrap_comment => [
+ sub {
+ my ($context, $cols) = @_;
+ return sub { wrap_comment($_[0], $cols) }
},
- };
+ 1
+ ],
+
+ # We force filtering of every variable in key security-critical
+ # places; we have a none filter for people to use when they
+ # really, really don't want a variable to be changed.
+ none => sub { return $_[0]; },
+ },
+
+ PLUGIN_BASE => 'Bugzilla::Template::Plugin',
+
+ # We don't want this feature.
+ CONSTANT_NAMESPACE => '__const',
+
+ # Default variables for all templates
+ VARIABLES => {
+
+ # Some of these are not really constants, and doing this messes up preloading.
+ # they are now fake constants.
+ constants => _load_constants(),
+
+ # Function for retrieving global parameters.
+ 'Param' => sub { return Bugzilla->params->{$_[0]}; },
+
+ 'bugzilla_version' => sub {
+ my $version = Bugzilla->VERSION;
+ if (my @ver = $version =~ /^(\d{4})(\d{2})(\d{2})\.(\d+)$/s) {
+ if ($ver[3] eq '1') {
+ return join('.', @ver[0, 1, 2]);
+ }
+ else {
+ return join('.', @ver);
+ }
+ }
+ else {
+ return $version;
+ }
+ },
+
+ json_encode => sub {
+ return encode_json($_[0]);
+ },
+
+ # Function to create date strings
+ 'time2str' => \&Date::Format::time2str,
+
+ # Fixed size column formatting for bugmail.
+ 'format_columns' => sub {
+ my $cols = shift;
+ my $format = ($cols == 3) ? FORMAT_TRIPLE : FORMAT_DOUBLE;
+ my $col_size = ($cols == 3) ? FORMAT_3_SIZE : FORMAT_2_SIZE;
+ return multiline_sprintf($format, \@_, $col_size);
+ },
+
+ # Generic linear search function
+ 'lsearch' => sub {
+ my ($array, $item) = @_;
+ return firstidx { $_ eq $item } @$array;
+ },
+
+ # Currently logged in user, if any
+ # If an sudo session is in progress, this is the user we're faking
+ 'user' => sub { return Bugzilla->user; },
+
+ # Currenly active language
+ 'current_language' => sub { return Bugzilla->current_language; },
+
+ 'script_nonce' => sub {
+ my $cgi = Bugzilla->cgi;
+ return $cgi->csp_nonce ? sprintf('nonce="%s"', $cgi->csp_nonce) : '';
+ },
+
+ # 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 "corect" URLBase value
+ 'urlbase' => sub { return Bugzilla->localconfig->{urlbase}; },
+
+ # Allow templates to access docs url with users' preferred language
+ 'docs_urlbase' => sub {
+ my $language = Bugzilla->current_language;
+ my $docs_urlbase = Bugzilla->params->{'docs_urlbase'};
+ $docs_urlbase =~ s/\%lang\%/$language/;
+ 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,
+
+ # Whether or not keywords are enabled, in this Bugzilla.
+ 'use_keywords' => sub { return Bugzilla::Keyword->any_exist; },
+
+ # All the keywords
+ 'all_keywords' => sub { return Bugzilla::Keyword->get_all(); },
+
+ # All the active keywords
+ 'active_keywords' => sub {
+ return [grep { $_->is_active } Bugzilla::Keyword->get_all()];
+ },
+
+ 'feature_enabled' => sub { return Bugzilla->feature(@_); },
+
+ 'has_extension' => sub { return Bugzilla->has_extension(@_); },
+
+ # field_descs can be somewhat slow to generate, so we generate
+ # it only once per-language no matter how many times
+ # $template->process() is called.
+ 'field_descs' => sub { return template_var('field_descs') },
+
+ # Calling bug/field-help.none.tmpl once per label is very
+ # expensive, so we generate it once per-language.
+ 'help_html' => sub { return template_var('help_html') },
+
+ # This way we don't have to load field-descs.none.tmpl in
+ # many templates.
+ 'display_value' => \&Bugzilla::Util::display_value,
+
+ 'install_string' => \&Bugzilla::Install::Util::install_string,
+
+ 'report_columns' => \&Bugzilla::Search::REPORT_COLUMNS,
+
+ # These don't work as normal constants.
+ DB_MODULE => \&Bugzilla::Constants::DB_MODULE,
+ 'default_authorizer' => sub { return Bugzilla::Auth->new() },
+
+# It is almost always better to do mobile feature detection, client side in js.
+# However, we need to set the meta[name=viewport] server-side or the behavior is
+# not as predictable. It is possible other parts of the frontend may use this feature too.
+ 'is_mobile_browser' => sub { return Bugzilla->cgi->user_agent =~ /Mobi/ },
+
+ 'socorro_lens_url' => sub {
+ my ($sigs) = @_;
+
+ # strip [@ ] from sigs
+ my @sigs = map {/^\[\@\s*(.+?)\s*\]$/} @$sigs;
+
+ return '' unless @sigs;
+
+ # use a URI object to encode the query string part.
+ my $uri = URI->new(
+ Bugzilla->localconfig->{urlbase} . 'metricsgraphics/socorro-lens.html');
+ $uri->query_form('s' => join("\\", @sigs));
+ return $uri;
+ },
+ },
+ };
- # under mod_perl, use a provider (template loader) that preloads all templates into memory
- my $provider_class
- = $opts{preload}
- ? 'Bugzilla::Template::PreloadProvider'
- : 'Template::Provider';
+# under mod_perl, use a provider (template loader) that preloads all templates into memory
+ my $provider_class
+ = $opts{preload}
+ ? 'Bugzilla::Template::PreloadProvider'
+ : 'Template::Provider';
- # Use a per-process provider to cache compiled templates in memory across
- # requests.
- my $provider_key = join(':', @{ $config->{INCLUDE_PATH} });
- $SHARED_PROVIDERS{$provider_key} ||= $provider_class->new($config);
- $config->{LOAD_TEMPLATES} = [ $SHARED_PROVIDERS{$provider_key} ];
+ # Use a per-process provider to cache compiled templates in memory across
+ # requests.
+ my $provider_key = join(':', @{$config->{INCLUDE_PATH}});
+ $SHARED_PROVIDERS{$provider_key} ||= $provider_class->new($config);
+ $config->{LOAD_TEMPLATES} = [$SHARED_PROVIDERS{$provider_key}];
- local $Template::Config::CONTEXT = 'Bugzilla::Template::Context';
+ local $Template::Config::CONTEXT = 'Bugzilla::Template::Context';
- Bugzilla::Hook::process('template_before_create', { config => $config });
- my $template = $class->new($config)
- || die("Template creation failed: " . $class->error());
+ Bugzilla::Hook::process('template_before_create', {config => $config});
+ my $template = $class->new($config)
+ || die("Template creation failed: " . $class->error());
- # BMO - hook for defining new vmethods, etc
- Bugzilla::Hook::process('template_after_create', { template => $template });
+ # BMO - hook for defining new vmethods, etc
+ Bugzilla::Hook::process('template_after_create', {template => $template});
- # Pass on our current language to any template hooks or inner templates
- # called by this Template object.
- $template->context->{bz_language} = $opts{language} || '';
+ # 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;
+ return $template;
}
# Used as part of the two subroutines below.
our %_templates_to_precompile;
+
sub precompile_templates {
- my ($output) = @_;
+ my ($output) = @_;
+
+ return unless is_webserver_group();
- return unless is_webserver_group();
+ # 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;
- # Remove the compiled templates.
- my $cache_dir = bz_locations()->{'template_cache'};
- my $datadir = bz_locations()->{'datadir'};
+ # 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 = ();
+
+ # 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/}{};
- # 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'});
+ # 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};
+ # 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) {
- # Clear out the cached Provider object
- %SHARED_PROVIDERS = ();
+ # 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};
+
+ # Clear out the cached Provider object
+ %SHARED_PROVIDERS = ();
+
+ print install_string('done') . "\n" if $output;
}
# Helper for precompile_templates
sub _precompile_push {
- my $name = $File::Find::name;
- return if (-d $name);
- return if ($name =~ /\/CVS\//);
- return if ($name !~ /\.tmpl$/);
- $_templates_to_precompile{$name} = 1;
+ my $name = $File::Find::name;
+ return if (-d $name);
+ return if ($name =~ /\/CVS\//);
+ return if ($name !~ /\.tmpl$/);
+ $_templates_to_precompile{$name} = 1;
}
# Helper for precompile_templates
sub _do_template_symlink {
- my $dir_to_symlink = shift;
-
- my $abs_path = abs_path($dir_to_symlink);
-
- # If $dir_to_symlink is already an absolute path (as might happen
- # with packagers who set $libpath to an absolute path), then we don't
- # need to do this symlink.
- return if ($abs_path eq $dir_to_symlink);
-
- my $abs_root = dirname($abs_path);
- my $dir_name = basename($abs_path);
- my $cache_dir = bz_locations()->{'template_cache'};
- my $container = "$cache_dir$abs_root";
- mkpath($container);
- my $target = "$cache_dir/$dir_name";
- # Check if the directory exists, because if there are no extensions,
- # there won't be an "data/template/extensions" directory to link to.
- if (-d $target) {
- # We use abs2rel so that the symlink will look like
- # "../../../../template" which works, while just
- # "data/template/template/" doesn't work.
- my $relative_target = File::Spec->abs2rel($target, $container);
-
- my $link_name = "$container/$dir_name";
- symlink($relative_target, $link_name)
- or warn "Could not make $link_name a symlink to $relative_target: $!";
- }
+ my $dir_to_symlink = shift;
+
+ my $abs_path = abs_path($dir_to_symlink);
+
+ # If $dir_to_symlink is already an absolute path (as might happen
+ # with packagers who set $libpath to an absolute path), then we don't
+ # need to do this symlink.
+ return if ($abs_path eq $dir_to_symlink);
+
+ my $abs_root = dirname($abs_path);
+ my $dir_name = basename($abs_path);
+ my $cache_dir = bz_locations()->{'template_cache'};
+ my $container = "$cache_dir$abs_root";
+ mkpath($container);
+ my $target = "$cache_dir/$dir_name";
+
+ # Check if the directory exists, because if there are no extensions,
+ # there won't be an "data/template/extensions" directory to link to.
+ if (-d $target) {
+
+ # We use abs2rel so that the symlink will look like
+ # "../../../../template" which works, while just
+ # "data/template/template/" doesn't work.
+ my $relative_target = File::Spec->abs2rel($target, $container);
+
+ my $link_name = "$container/$dir_name";
+ symlink($relative_target, $link_name)
+ or warn "Could not make $link_name a symlink to $relative_target: $!";
+ }
}
1;
diff --git a/Bugzilla/Template/Context.pm b/Bugzilla/Template/Context.pm
index 21e2b7ec8..f2db23702 100644
--- a/Bugzilla/Template/Context.pm
+++ b/Bugzilla/Template/Context.pm
@@ -18,23 +18,24 @@ use Bugzilla::Hook;
use Scalar::Util qw(blessed);
sub process {
- my $self = shift;
- # We don't want to run the template_before_process hook for
- # template hooks (but we do want it to run if a hook calls
- # PROCESS inside itself). The problem is that the {component}->{name} of
- # hooks is unreliable--sometimes it starts with ./ and it's the
- # full path to the hook template, and sometimes it's just the relative
- # name (like hook/global/field-descs-end.none.tmpl). Also, calling
- # template_before_process for hook templates doesn't seem too useful,
- # because that's already part of the extension and they should be able
- # to modify their hook if they want (or just modify the variables in the
- # calling template).
- if (not delete $self->{bz_in_hook}) {
- $self->{bz_in_process} = 1;
- }
- my $result = $self->SUPER::process(@_);
- delete $self->{bz_in_process};
- return $result;
+ my $self = shift;
+
+ # We don't want to run the template_before_process hook for
+ # template hooks (but we do want it to run if a hook calls
+ # PROCESS inside itself). The problem is that the {component}->{name} of
+ # hooks is unreliable--sometimes it starts with ./ and it's the
+ # full path to the hook template, and sometimes it's just the relative
+ # name (like hook/global/field-descs-end.none.tmpl). Also, calling
+ # template_before_process for hook templates doesn't seem too useful,
+ # because that's already part of the extension and they should be able
+ # to modify their hook if they want (or just modify the variables in the
+ # calling template).
+ if (not delete $self->{bz_in_hook}) {
+ $self->{bz_in_process} = 1;
+ }
+ my $result = $self->SUPER::process(@_);
+ delete $self->{bz_in_process};
+ return $result;
}
# This method is called by Template-Toolkit exactly once per template or
@@ -46,57 +47,58 @@ 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 $self = shift;
+ my $stash = $self->SUPER::stash(@_);
- my $name = $stash->{component}->{name};
- my $pre_process = $self->config->{PRE_PROCESS};
+ 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 });
- }
+ # 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};
+ # 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;
+ return $stash;
}
sub filter {
- my ($self, $name, $args) = @_;
- # If we pass an alias for the filter name, the filter code is cached
- # instead of looking for it at each call.
- # If the filter has arguments, then we can't cache it.
- $self->SUPER::filter($name, $args, $args ? undef : $name);
+ my ($self, $name, $args) = @_;
+
+ # If we pass an alias for the filter name, the filter code is cached
+ # instead of looking for it at each call.
+ # If the filter has arguments, then we can't cache it.
+ $self->SUPER::filter($name, $args, $args ? undef : $name);
}
# We need a DESTROY sub for the same reason that Bugzilla::CGI does.
sub DESTROY {
- my $self = shift;
- $self->SUPER::DESTROY(@_);
-};
+ my $self = shift;
+ $self->SUPER::DESTROY(@_);
+}
1;
diff --git a/Bugzilla/Template/Plugin/Bugzilla.pm b/Bugzilla/Template/Plugin/Bugzilla.pm
index 752aa9dfa..110d3d352 100644
--- a/Bugzilla/Template/Plugin/Bugzilla.pm
+++ b/Bugzilla/Template/Plugin/Bugzilla.pm
@@ -16,20 +16,20 @@ use base qw(Template::Plugin);
use Bugzilla;
sub new {
- my ($class, $context) = @_;
+ my ($class, $context) = @_;
- return bless {}, $class;
+ return bless {}, $class;
}
sub AUTOLOAD {
- my $class = shift;
- our $AUTOLOAD;
+ my $class = shift;
+ our $AUTOLOAD;
- $AUTOLOAD =~ s/^.*:://;
+ $AUTOLOAD =~ s/^.*:://;
- return if $AUTOLOAD eq 'DESTROY';
+ return if $AUTOLOAD eq 'DESTROY';
- return Bugzilla->$AUTOLOAD(@_);
+ return Bugzilla->$AUTOLOAD(@_);
}
1;
diff --git a/Bugzilla/Template/Plugin/Hook.pm b/Bugzilla/Template/Plugin/Hook.pm
index a2b76a80f..e20ca1016 100644
--- a/Bugzilla/Template/Plugin/Hook.pm
+++ b/Bugzilla/Template/Plugin/Hook.pm
@@ -21,74 +21,73 @@ use Bugzilla::Error;
use File::Spec;
sub new {
- my ($class, $context) = @_;
- return bless { _CONTEXT => $context }, $class;
+ my ($class, $context) = @_;
+ return bless {_CONTEXT => $context}, $class;
}
sub _context { return $_[0]->{_CONTEXT} }
sub process {
- my ($self, $hook_name, $template) = @_;
- my $context = $self->_context();
- $template ||= $context->stash->{component}->{name};
-
- # sanity check:
- if (!$template =~ /[\w\.\/\-_\\]+/) {
- ThrowCodeError('template_invalid', { name => $template });
- }
-
- my (undef, $path, $filename) = File::Spec->splitpath($template);
- $path ||= '';
- $filename =~ m/(.+)\.(.+)\.tmpl$/;
- my $template_name = $1;
- my $type = $2;
-
- # Hooks are named like this:
- my $extension_template = "$path$template_name-$hook_name.$type.tmpl";
-
- # Get the hooks out of the cache if they exist. Otherwise, read them
- # from the disk.
- my $cache = Bugzilla->request_cache->{template_plugin_hook_cache} ||= {};
- my $lang = $context->{bz_language} || '';
- $cache->{"${lang}__$extension_template"}
- ||= $self->_get_hooks($extension_template);
-
- # process() accepts an arrayref of templates, so we just pass the whole
- # arrayref.
- $context->{bz_in_hook} = 1; # See Bugzilla::Template::Context
- return $context->process($cache->{"${lang}__$extension_template"});
+ my ($self, $hook_name, $template) = @_;
+ my $context = $self->_context();
+ $template ||= $context->stash->{component}->{name};
+
+ # sanity check:
+ if (!$template =~ /[\w\.\/\-_\\]+/) {
+ ThrowCodeError('template_invalid', {name => $template});
+ }
+
+ my (undef, $path, $filename) = File::Spec->splitpath($template);
+ $path ||= '';
+ $filename =~ m/(.+)\.(.+)\.tmpl$/;
+ my $template_name = $1;
+ my $type = $2;
+
+ # Hooks are named like this:
+ my $extension_template = "$path$template_name-$hook_name.$type.tmpl";
+
+ # Get the hooks out of the cache if they exist. Otherwise, read them
+ # from the disk.
+ my $cache = Bugzilla->request_cache->{template_plugin_hook_cache} ||= {};
+ my $lang = $context->{bz_language} || '';
+ $cache->{"${lang}__$extension_template"}
+ ||= $self->_get_hooks($extension_template);
+
+ # process() accepts an arrayref of templates, so we just pass the whole
+ # arrayref.
+ $context->{bz_in_hook} = 1; # See Bugzilla::Template::Context
+ return $context->process($cache->{"${lang}__$extension_template"});
}
sub _get_hooks {
- my ($self, $extension_template) = @_;
-
- my $template_sets = $self->_template_hook_include_path();
- my @hooks;
- foreach my $dir_set (@$template_sets) {
- foreach my $template_dir (@$dir_set) {
- my $file = "$template_dir/hook/$extension_template";
- if (-e $file) {
- my $template = $self->_context->template($file);
- push(@hooks, $template);
- # Don't run the hook for more than one language.
- last;
- }
- }
+ my ($self, $extension_template) = @_;
+
+ my $template_sets = $self->_template_hook_include_path();
+ my @hooks;
+ foreach my $dir_set (@$template_sets) {
+ foreach my $template_dir (@$dir_set) {
+ my $file = "$template_dir/hook/$extension_template";
+ if (-e $file) {
+ my $template = $self->_context->template($file);
+ push(@hooks, $template);
+
+ # Don't run the hook for more than one language.
+ last;
+ }
}
+ }
- return \@hooks;
+ return \@hooks;
}
sub _template_hook_include_path {
- my $self = shift;
- my $cache = Bugzilla->request_cache;
- my $language = $self->_context->{bz_language} || '';
- my $cache_key = "template_plugin_hook_include_path_$language";
- $cache->{$cache_key} ||= template_include_path({
- language => $language,
- hook => 1,
- });
- return $cache->{$cache_key};
+ my $self = shift;
+ my $cache = Bugzilla->request_cache;
+ my $language = $self->_context->{bz_language} || '';
+ my $cache_key = "template_plugin_hook_include_path_$language";
+ $cache->{$cache_key}
+ ||= template_include_path({language => $language, hook => 1,});
+ return $cache->{$cache_key};
}
1;
diff --git a/Bugzilla/Template/Plugin/User.pm b/Bugzilla/Template/Plugin/User.pm
index 09452d899..ac22f0861 100644
--- a/Bugzilla/Template/Plugin/User.pm
+++ b/Bugzilla/Template/Plugin/User.pm
@@ -31,20 +31,20 @@ use base qw(Template::Plugin);
use Bugzilla::User;
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::User->$AUTOLOAD(@_);
+ return Bugzilla::User->$AUTOLOAD(@_);
}
1;
diff --git a/Bugzilla/Template/PreloadProvider.pm b/Bugzilla/Template/PreloadProvider.pm
index bddabfa2e..6d963f31f 100644
--- a/Bugzilla/Template/PreloadProvider.pm
+++ b/Bugzilla/Template/PreloadProvider.pm
@@ -15,7 +15,7 @@ use warnings;
use base qw(Template::Provider);
use File::Find ();
-use Cwd ();
+use Cwd ();
use File::Spec;
use Template::Constants qw( STATUS_ERROR );
use Template::Document;
@@ -24,88 +24,88 @@ use Template::Config;
use Bugzilla::Util qw(trick_taint);
sub _init {
- my $self = shift;
- $self->SUPER::_init(@_);
-
- my $path = $self->{INCLUDE_PATH};
- my $cache = $self->{_BZ_CACHE} = {};
- my $search = $self->{_BZ_SEARCH} = {};
-
- foreach my $template_dir (@$path) {
- $template_dir = Cwd::realpath($template_dir);
- my $wanted = sub {
- my ( $name, $dir ) = ($File::Find::name, $File::Find::dir);
- if ( $name =~ /\.tmpl$/ ) {
- my $key = $name;
- $key =~ s/^\Q$template_dir\///;
- unless ($search->{$key}) {
- $search->{$key} = $name;
- }
- trick_taint($name);
- my $data = {
- path => $name,
- name => $key,
- text => do {
- open my $fh, '<:utf8', $name or die "cannot open $name";
- local $/ = undef;
- scalar <$fh>; # $fh is closed it goes out of scope
- },
- time => (stat($name))[9],
- };
- trick_taint($data->{text}) if $data->{text};
- $cache->{$name} = $self->_bz_compile($data) or die "compile error: $name";
- }
+ my $self = shift;
+ $self->SUPER::_init(@_);
+
+ my $path = $self->{INCLUDE_PATH};
+ my $cache = $self->{_BZ_CACHE} = {};
+ my $search = $self->{_BZ_SEARCH} = {};
+
+ foreach my $template_dir (@$path) {
+ $template_dir = Cwd::realpath($template_dir);
+ my $wanted = sub {
+ my ($name, $dir) = ($File::Find::name, $File::Find::dir);
+ if ($name =~ /\.tmpl$/) {
+ my $key = $name;
+ $key =~ s/^\Q$template_dir\///;
+ unless ($search->{$key}) {
+ $search->{$key} = $name;
+ }
+ trick_taint($name);
+ my $data = {
+ path => $name,
+ name => $key,
+ text => do {
+ open my $fh, '<:utf8', $name or die "cannot open $name";
+ local $/ = undef;
+ scalar <$fh>; # $fh is closed it goes out of scope
+ },
+ time => (stat($name))[9],
};
- File::Find::find( { wanted => $wanted, no_chdir => 1 }, $template_dir );
- }
-
- return $self;
+ trick_taint($data->{text}) if $data->{text};
+ $cache->{$name} = $self->_bz_compile($data) or die "compile error: $name";
+ }
+ };
+ File::Find::find({wanted => $wanted, no_chdir => 1}, $template_dir);
+ }
+
+ return $self;
}
sub fetch {
- my ($self, $name, $prefix) = @_;
- my $file;
- if (File::Spec->file_name_is_absolute($name)) {
- $file = $name;
- }
- elsif ($name =~ m#^\./#) {
- $file = File::Spec->rel2abs($name);
- }
- else {
- $file = $self->{_BZ_SEARCH}{$name};
- }
-
- if (not $file) {
- return ("cannot find file - $name ($file)", STATUS_ERROR);
- }
-
- if ($self->{_BZ_CACHE}{$file}) {
- return ($self->{_BZ_CACHE}{$file}, undef);
- }
- else {
- return ("unknown file - $file", STATUS_ERROR);
- }
+ my ($self, $name, $prefix) = @_;
+ my $file;
+ if (File::Spec->file_name_is_absolute($name)) {
+ $file = $name;
+ }
+ elsif ($name =~ m#^\./#) {
+ $file = File::Spec->rel2abs($name);
+ }
+ else {
+ $file = $self->{_BZ_SEARCH}{$name};
+ }
+
+ if (not $file) {
+ return ("cannot find file - $name ($file)", STATUS_ERROR);
+ }
+
+ if ($self->{_BZ_CACHE}{$file}) {
+ return ($self->{_BZ_CACHE}{$file}, undef);
+ }
+ else {
+ return ("unknown file - $file", STATUS_ERROR);
+ }
}
sub _bz_compile {
- my ($self, $data) = @_;
+ my ($self, $data) = @_;
- my $parser = $self->{PARSER} ||= Template::Config->parser( $self->{PARAMS} )
- || return ( Template::Config->error(), STATUS_ERROR );
+ my $parser = $self->{PARSER} ||= Template::Config->parser($self->{PARAMS})
+ || return (Template::Config->error(), STATUS_ERROR);
- # discard the template text - we don't need it any more
- my $text = delete $data->{text};
+ # discard the template text - we don't need it any more
+ my $text = delete $data->{text};
- # call parser to compile template into Perl code
- if (my $parsedoc = $parser->parse($text, $data)) {
- $parsedoc->{METADATA} = {
- 'name' => $data->{name},
- 'modtime' => $data->{time},
- %{ $parsedoc->{METADATA} },
- };
+ # call parser to compile template into Perl code
+ if (my $parsedoc = $parser->parse($text, $data)) {
+ $parsedoc->{METADATA} = {
+ 'name' => $data->{name},
+ 'modtime' => $data->{time},
+ %{$parsedoc->{METADATA}},
+ };
- return Template::Document->new($parsedoc);
- }
+ return Template::Document->new($parsedoc);
+ }
}
1;
diff --git a/Bugzilla/Test/MockDB.pm b/Bugzilla/Test/MockDB.pm
index fb7873ccf..db55b5d1e 100644
--- a/Bugzilla/Test/MockDB.pm
+++ b/Bugzilla/Test/MockDB.pm
@@ -11,110 +11,117 @@ use warnings;
use Try::Tiny;
use Capture::Tiny qw(capture_merged);
-use Bugzilla::Test::MockLocalconfig (
- db_driver => 'sqlite',
- db_name => ':memory:',
-);
+use Bugzilla::Test::MockLocalconfig (db_driver => 'sqlite',
+ db_name => ':memory:',);
use Bugzilla;
-BEGIN { Bugzilla->extensions };
-use Bugzilla::Test::MockParams (
- emailsuffix => '',
- emailregexp => '.+',
-);
+BEGIN { Bugzilla->extensions }
+use Bugzilla::Test::MockParams (emailsuffix => '', emailregexp => '.+',);
sub import {
- require Bugzilla::Install;
- require Bugzilla::Install::DB;
- require Bugzilla::Field;;
+ require Bugzilla::Install;
+ require Bugzilla::Install::DB;
+ require Bugzilla::Field;
- state $first_time = 0;
+ state $first_time = 0;
- return undef if $first_time++;
+ return undef if $first_time++;
- return capture_merged {
- Bugzilla->dbh->bz_setup_database();
+ return capture_merged {
+ Bugzilla->dbh->bz_setup_database();
- # Populate the tables that hold the values for the <select> fields.
- Bugzilla->dbh->bz_populate_enum_tables();
+ # Populate the tables that hold the values for the <select> fields.
+ Bugzilla->dbh->bz_populate_enum_tables();
- Bugzilla::Install::DB::update_fielddefs_definition();
- Bugzilla::Field::populate_field_definitions();
- Bugzilla::Install::init_workflow();
- Bugzilla::Install::DB->update_table_definitions({});
- Bugzilla::Install::update_system_groups();
+ Bugzilla::Install::DB::update_fielddefs_definition();
+ Bugzilla::Field::populate_field_definitions();
+ Bugzilla::Install::init_workflow();
+ Bugzilla::Install::DB->update_table_definitions({});
+ Bugzilla::Install::update_system_groups();
- Bugzilla->set_user(Bugzilla::User->super_user);
+ Bugzilla->set_user(Bugzilla::User->super_user);
- Bugzilla::Install::update_settings();
+ Bugzilla::Install::update_settings();
- my $dbh = Bugzilla->dbh;
- if ( !$dbh->selectrow_array("SELECT 1 FROM priority WHERE value = 'P1'") ) {
- $dbh->do("DELETE FROM priority");
- my $count = 100;
- foreach my $priority (map { "P$_" } 1..5) {
- $dbh->do( "INSERT INTO priority (value, sortkey) VALUES (?, ?)", undef, ( $priority, $count + 100 ) );
- }
- }
- my @flagtypes = (
- {
- name => 'review',
- desc => 'The patch has passed review by a module owner or peer.',
- is_requestable => 1,
- is_requesteeble => 1,
- is_multiplicable => 1,
- grant_group => '',
- target_type => 'a',
- cc_list => '',
- inclusions => ['']
- },
- {
- name => 'feedback',
- desc => 'A particular person\'s input is requested for a patch, ' .
- 'but that input does not amount to an official review.',
- is_requestable => 1,
- is_requesteeble => 1,
- is_multiplicable => 1,
- grant_group => '',
- target_type => 'a',
- cc_list => '',
- inclusions => ['']
- }
- );
+ my $dbh = Bugzilla->dbh;
+ if (!$dbh->selectrow_array("SELECT 1 FROM priority WHERE value = 'P1'")) {
+ $dbh->do("DELETE FROM priority");
+ my $count = 100;
+ foreach my $priority (map {"P$_"} 1 .. 5) {
+ $dbh->do("INSERT INTO priority (value, sortkey) VALUES (?, ?)",
+ undef, ($priority, $count + 100));
+ }
+ }
+ my @flagtypes = (
+ {
+ name => 'review',
+ desc => 'The patch has passed review by a module owner or peer.',
+ is_requestable => 1,
+ is_requesteeble => 1,
+ is_multiplicable => 1,
+ grant_group => '',
+ target_type => 'a',
+ cc_list => '',
+ inclusions => ['']
+ },
+ {
+ name => 'feedback',
+ desc => 'A particular person\'s input is requested for a patch, '
+ . 'but that input does not amount to an official review.',
+ is_requestable => 1,
+ is_requesteeble => 1,
+ is_multiplicable => 1,
+ grant_group => '',
+ target_type => 'a',
+ cc_list => '',
+ inclusions => ['']
+ }
+ );
- foreach my $flag (@flagtypes) {
- 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;
+ foreach my $flag (@flagtypes) {
+ 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,
+ $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));
+ 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');
+ 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));
- }
+ 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)
+ );
+ }
+ }
+ };
}
1;
diff --git a/Bugzilla/Test/MockLocalconfig.pm b/Bugzilla/Test/MockLocalconfig.pm
index a32aea0d4..080fdeef8 100644
--- a/Bugzilla/Test/MockLocalconfig.pm
+++ b/Bugzilla/Test/MockLocalconfig.pm
@@ -10,9 +10,9 @@ use strict;
use warnings;
sub import {
- my ($class, %lc) = @_;
- $ENV{LOCALCONFIG_ENV} = 'BMO';
- $ENV{"BMO_$_"} = $lc{$_} for keys %lc;
+ my ($class, %lc) = @_;
+ $ENV{LOCALCONFIG_ENV} = 'BMO';
+ $ENV{"BMO_$_"} = $lc{$_} for keys %lc;
}
1;
diff --git a/Bugzilla/Test/MockParams.pm b/Bugzilla/Test/MockParams.pm
index 2d064c616..8738f78d4 100644
--- a/Bugzilla/Test/MockParams.pm
+++ b/Bugzilla/Test/MockParams.pm
@@ -16,56 +16,48 @@ use Bugzilla::Config;
use Safe;
our $Params;
+
BEGIN {
- our $Mock = mock 'Bugzilla::Config' => (
- override => [
- 'read_param_file' => sub {
- my ($class) = @_;
- return {} unless $Params;
- my $s = Safe->new;
- $s->reval($Params);
- die "Error evaluating params: $@" if $@;
- return { %{ $s->varglob('param') } };
- },
- '_write_file' => sub {
- my ($class, $str) = @_;
- $Params = $str;
- },
- ],
- );
+ our $Mock = mock 'Bugzilla::Config' => (
+ override => [
+ 'read_param_file' => sub {
+ my ($class) = @_;
+ return {} unless $Params;
+ my $s = Safe->new;
+ $s->reval($Params);
+ die "Error evaluating params: $@" if $@;
+ return {%{$s->varglob('param')}};
+ },
+ '_write_file' => sub {
+ my ($class, $str) = @_;
+ $Params = $str;
+ },
+ ],
+ );
}
sub import {
- my ($self, %answers) = @_;
- state $first_time = 0;
+ my ($self, %answers) = @_;
+ state $first_time = 0;
- require Bugzilla::Field;
- require Bugzilla::Status;
- require Bugzilla;
- my $Bugzilla = mock 'Bugzilla' => (
- override => [
- installation_answers => sub { \%answers },
- ],
- );
- my $BugzillaField = mock 'Bugzilla::Field' => (
- override => [
- get_legal_field_values => sub { [] },
- ],
- );
- my $BugzillaStatus = mock 'Bugzilla::Status' => (
- override => [
- closed_bug_statuses => sub { die "no database" },
- ],
- );
+ require Bugzilla::Field;
+ require Bugzilla::Status;
+ require Bugzilla;
+ my $Bugzilla = mock 'Bugzilla' =>
+ (override => [installation_answers => sub { \%answers },],);
+ my $BugzillaField = mock 'Bugzilla::Field' =>
+ (override => [get_legal_field_values => sub { [] },],);
+ my $BugzillaStatus = mock 'Bugzilla::Status' =>
+ (override => [closed_bug_statuses => sub { die "no database" },],);
- if ($first_time++) {
- capture_merged {
- Bugzilla::Config::update_params();
- };
- }
- else {
- Bugzilla::Config::SetParam($_, $answers{$_}) for keys %answers;
- }
+ if ($first_time++) {
+ capture_merged {
+ Bugzilla::Config::update_params();
+ };
+ }
+ else {
+ Bugzilla::Config::SetParam($_, $answers{$_}) for keys %answers;
+ }
}
-1; \ No newline at end of file
+1;
diff --git a/Bugzilla/Test/Util.pm b/Bugzilla/Test/Util.pm
index 9fbc151f7..995cff0be 100644
--- a/Bugzilla/Test/Util.pm
+++ b/Bugzilla/Test/Util.pm
@@ -20,50 +20,51 @@ use Mojo::Message::Response;
use Test2::Tools::Mock qw(mock);
sub create_user {
- my ($login, $password, %extra) = @_;
- require Bugzilla;
- return Bugzilla::User->create({
- login_name => $login,
- cryptpassword => $password,
- disabledtext => "",
- disable_mail => 0,
- extern_id => undef,
- %extra,
- });
+ my ($login, $password, %extra) = @_;
+ require Bugzilla;
+ return Bugzilla::User->create({
+ login_name => $login,
+ cryptpassword => $password,
+ disabledtext => "",
+ disable_mail => 0,
+ extern_id => undef,
+ %extra,
+ });
}
sub issue_api_key {
- my ($login, $given_api_key) = @_;
- my $user = Bugzilla::User->check({ name => $login });
+ my ($login, $given_api_key) = @_;
+ my $user = Bugzilla::User->check({name => $login});
- my $params = {
- user_id => $user->id,
- description => 'Bugzilla::Test::Util::issue_api_key',
- api_key => $given_api_key,
- };
+ my $params = {
+ user_id => $user->id,
+ description => 'Bugzilla::Test::Util::issue_api_key',
+ api_key => $given_api_key,
+ };
- if ($given_api_key) {
- return Bugzilla::User::APIKey->create_special($params);
- } else {
- return Bugzilla::User::APIKey->create($params);
- }
+ if ($given_api_key) {
+ return Bugzilla::User::APIKey->create_special($params);
+ }
+ else {
+ return Bugzilla::User::APIKey->create($params);
+ }
}
sub _json_content_type { $_->headers->content_type('application/json') }
sub mock_useragent_tx {
- my ($body, $modify) = @_;
- $modify //= \&_json_content_type;
+ my ($body, $modify) = @_;
+ $modify //= \&_json_content_type;
- my $res = Mojo::Message::Response->new;
- $res->code(200);
- $res->body($body);
- if ($modify) {
- local $_ = $res;
- $modify->($res);
- }
+ my $res = Mojo::Message::Response->new;
+ $res->code(200);
+ $res->body($body);
+ if ($modify) {
+ local $_ = $res;
+ $modify->($res);
+ }
- return mock({result => $res});
+ return mock({result => $res});
}
1;
diff --git a/Bugzilla/Token.pm b/Bugzilla/Token.pm
index 8e51db45d..3398e236a 100644
--- a/Bugzilla/Token.pm
+++ b/Bugzilla/Token.pm
@@ -27,12 +27,12 @@ use JSON qw(encode_json decode_json);
use base qw(Exporter);
@Bugzilla::Token::EXPORT = qw(issue_api_token issue_session_token
- issue_short_lived_session_token
- issue_auth_delegation_token check_auth_delegation_token
- check_token_data delete_token
- issue_hash_token check_hash_token
- issue_hash_sig check_hash_sig
- set_token_extra_data get_token_extra_data);
+ issue_short_lived_session_token
+ issue_auth_delegation_token check_auth_delegation_token
+ check_token_data delete_token
+ issue_hash_token check_hash_token
+ issue_hash_sig check_hash_sig
+ set_token_extra_data get_token_extra_data);
# 128 bits password:
# 128 * log10(2) / log10(62) = 21.49, round up to 22.
@@ -45,407 +45,439 @@ use constant TOKEN_LENGTH => 22;
# Create a token used for internal API authentication
sub issue_api_token {
- # Generates a random token, adds it to the tokens table if one does not
- # already exist, and returns the token to the caller.
- my $dbh = Bugzilla->dbh;
- my $user = Bugzilla->user;
- my ($token) = $dbh->selectrow_array("
+
+ # Generates a random token, adds it to the tokens table if one does not
+ # already exist, and returns the token to the caller.
+ my $dbh = Bugzilla->dbh;
+ my $user = Bugzilla->user;
+ my ($token) = $dbh->selectrow_array("
SELECT token FROM tokens
WHERE userid = ? AND tokentype = 'api_token'
- AND (" . $dbh->sql_date_math('issuedate', '+', (MAX_TOKEN_AGE * 24 - 12), 'HOUR') . ") > NOW()",
- undef, $user->id);
- return $token // _create_token($user->id, 'api_token', '');
+ AND ("
+ . $dbh->sql_date_math('issuedate', '+', (MAX_TOKEN_AGE * 24 - 12), 'HOUR')
+ . ") > NOW()", undef, $user->id);
+ return $token // _create_token($user->id, 'api_token', '');
}
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_name = shift;
- my $dbh = Bugzilla->dbh;
- my $template = Bugzilla->template;
- my $vars = {};
-
- # Is there already a pending request for this login name? If yes, do not throw
- # an error because the user may have lost his email with the token inside.
- # But to prevent using this way to mailbomb an email address, make sure
- # the last request is at least 10 minutes old before sending a new email.
-
- my $pending_requests = $dbh->selectrow_array(
- 'SELECT COUNT(*)
+ my $login_name = shift;
+ my $dbh = Bugzilla->dbh;
+ my $template = Bugzilla->template;
+ my $vars = {};
+
+ # Is there already a pending request for this login name? If yes, do not throw
+ # an error because the user may have lost his email with the token inside.
+ # But to prevent using this way to mailbomb an email address, make sure
+ # the last request is at least 10 minutes old before sending a new email.
+
+ my $pending_requests = $dbh->selectrow_array(
+ 'SELECT COUNT(*)
FROM tokens
WHERE tokentype = ?
AND ' . $dbh->sql_istrcmp('eventdata', '?') . '
AND issuedate > '
- . $dbh->sql_date_math('NOW()', '-', 10, 'MINUTE'),
- undef, ('account', $login_name));
+ . $dbh->sql_date_math('NOW()', '-', 10, 'MINUTE'), undef,
+ ('account', $login_name)
+ );
- ThrowUserError('too_soon_for_new_token', {'type' => 'account'}) if $pending_requests;
+ ThrowUserError('too_soon_for_new_token', {'type' => 'account'})
+ if $pending_requests;
- my ($token, $token_ts) = _create_token(undef, 'account', $login_name);
+ my ($token, $token_ts) = _create_token(undef, 'account', $login_name);
- $vars->{'email'} = $login_name . Bugzilla->params->{'emailsuffix'};
- $vars->{'expiration_ts'} = ctime($token_ts + MAX_TOKEN_AGE * 86400);
- $vars->{'token'} = $token;
+ $vars->{'email'} = $login_name . Bugzilla->params->{'emailsuffix'};
+ $vars->{'expiration_ts'} = ctime($token_ts + MAX_TOKEN_AGE * 86400);
+ $vars->{'token'} = $token;
- my $message;
- $template->process('account/email/request-new.txt.tmpl', $vars, \$message)
- || ThrowTemplateError($template->error());
+ my $message;
+ $template->process('account/email/request-new.txt.tmpl', $vars, \$message)
+ || ThrowTemplateError($template->error());
- # In 99% of cases, the user getting the confirmation email is the same one
- # who made the request, and so it is reasonable to send the email in the same
- # language used to view the "Create a New Account" page (we cannot use his
- # user prefs as the user has no account yet!).
- MessageToMTA($message);
+ # 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 his
+ # user prefs as the user has no account yet!).
+ MessageToMTA($message);
}
sub IssueEmailChangeToken {
- my ($user, $new_email) = @_;
- my $email_suffix = Bugzilla->params->{'emailsuffix'};
- my $old_email = $user->login;
+ my ($user, $new_email) = @_;
+ my $email_suffix = Bugzilla->params->{'emailsuffix'};
+ my $old_email = $user->login;
- my ($token, $token_ts) = _create_token($user->id, 'emailold', $old_email . ":" . $new_email);
+ my ($token, $token_ts)
+ = _create_token($user->id, 'emailold', $old_email . ":" . $new_email);
- my $newtoken = _create_token($user->id, 'emailnew', $old_email . ":" . $new_email);
+ my $newtoken
+ = _create_token($user->id, 'emailnew', $old_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->{'oldemailaddress'} = $old_email . $email_suffix;
- $vars->{'newemailaddress'} = $new_email . $email_suffix;
- $vars->{'expiration_ts'} = ctime($token_ts + MAX_TOKEN_AGE * 86400);
- $vars->{'token'} = $token;
- # For SecureMail extension
- $vars->{'to_user'} = $user;
- $vars->{'emailaddress'} = $old_email . $email_suffix;
+ $vars->{'oldemailaddress'} = $old_email . $email_suffix;
+ $vars->{'newemailaddress'} = $new_email . $email_suffix;
+ $vars->{'expiration_ts'} = ctime($token_ts + MAX_TOKEN_AGE * 86400);
+ $vars->{'token'} = $token;
- my $message;
- $template->process("account/email/change-old.txt.tmpl", $vars, \$message)
- || ThrowTemplateError($template->error());
+ # For SecureMail extension
+ $vars->{'to_user'} = $user;
+ $vars->{'emailaddress'} = $old_email . $email_suffix;
- MessageToMTA($message);
+ my $message;
+ $template->process("account/email/change-old.txt.tmpl", $vars, \$message)
+ || ThrowTemplateError($template->error());
- $vars->{'token'} = $newtoken;
- $vars->{'emailaddress'} = $new_email . $email_suffix;
+ MessageToMTA($message);
- $message = "";
- $template->process("account/email/change-new.txt.tmpl", $vars, \$message)
- || ThrowTemplateError($template->error());
+ $vars->{'token'} = $newtoken;
+ $vars->{'emailaddress'} = $new_email . $email_suffix;
- MessageToMTA($message);
+ $message = "";
+ $template->process("account/email/change-new.txt.tmpl", $vars, \$message)
+ || ThrowTemplateError($template->error());
+
+ MessageToMTA($message);
}
# 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()', '-', 10, 'MINUTE'),
- undef, ($user->id, 'password'));
+ . $dbh->sql_date_math('NOW()', '-', 10, '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 ($token, $token_ts) = _create_token($user->id, 'password', remote_ip());
+ my ($token, $token_ts) = _create_token($user->id, 'password', remote_ip());
- # 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->{'emailaddress'} = $user->email;
- $vars->{'expiration_ts'} = ctime($token_ts + MAX_TOKEN_AGE * 86400);
- # The user is not logged in (else he wouldn't request a new password).
- # So we have to pass this information to the template.
- $vars->{'timezone'} = $user->timezone;
+ $vars->{'token'} = $token;
+ $vars->{'emailaddress'} = $user->email;
+ $vars->{'expiration_ts'} = ctime($token_ts + MAX_TOKEN_AGE * 86400);
- my $message = "";
- $template->process("account/password/forgotten-password.txt.tmpl",
- $vars, \$message)
- || ThrowTemplateError($template->error());
+ # The user is not logged in (else he wouldn't request a new password).
+ # So we have to pass this information to the template.
+ $vars->{'timezone'} = $user->timezone;
- MessageToMTA($message);
+ my $message = "";
+ $template->process("account/password/forgotten-password.txt.tmpl",
+ $vars, \$message)
+ || ThrowTemplateError($template->error());
+
+ MessageToMTA($message);
}
sub issue_session_token {
- my ($data, $user) = @_;
- # Generates a random token, adds it to the tokens table, and returns
- # the token to the caller.
+ my ($data, $user) = @_;
+
+ # Generates a random token, adds it to the tokens table, and returns
+ # the token to the caller.
- $user //= Bugzilla->user;
- return _create_token($user->id, 'session', $data);
+ $user //= Bugzilla->user;
+ return _create_token($user->id, 'session', $data);
}
sub issue_short_lived_session_token {
- my ($data, $user) = @_;
- # Generates a random token, adds it to the tokens table, and returns
- # the token to the caller.
+ my ($data, $user) = @_;
+
+ # Generates a random token, adds it to the tokens table, and returns
+ # the token to the caller.
- $user //= Bugzilla->user;
- return _create_token($user->id ? $user->id : undef, 'session.short', $data);
+ $user //= Bugzilla->user;
+ return _create_token($user->id ? $user->id : undef, 'session.short', $data);
}
sub issue_hash_sig {
- my ($type, $data, $salt) = @_;
- $data //= "";
- $salt //= generate_random_password(16);
-
- my $hmac = hmac_sha256_base64(
- $salt,
- $type,
- $data,
- Bugzilla->localconfig->{site_wide_secret}
- );
- return sprintf("%s|%s|%x", $salt, $hmac, length($data));
+ my ($type, $data, $salt) = @_;
+ $data //= "";
+ $salt //= generate_random_password(16);
+
+ my $hmac = hmac_sha256_base64($salt, $type, $data,
+ Bugzilla->localconfig->{site_wide_secret});
+ return sprintf("%s|%s|%x", $salt, $hmac, length($data));
}
sub check_hash_sig {
- my ($type, $sig, $data) = @_;
- return 0 unless defined $sig && defined $data;
- my ($salt, undef, $len) = split(/\|/, $sig, 3);
- return length($data) == hex($len) && $sig eq issue_hash_sig($type, $data, $salt);
+ my ($type, $sig, $data) = @_;
+ return 0 unless defined $sig && defined $data;
+ my ($salt, undef, $len) = split(/\|/, $sig, 3);
+ return
+ length($data) == hex($len) && $sig eq issue_hash_sig($type, $data, $salt);
}
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);
- # $token needs to be a byte string.
- utf8::encode($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);
+
+ # $token needs to be a byte string.
+ utf8::encode($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_date_math('issuedate', '+', '?', 'HOUR') . " <= NOW()",
- undef, MAX_TOKEN_AGE * 24);
- $dbh->do("DELETE FROM tokens WHERE tokentype = ? AND " .
- $dbh->sql_date_math('issuedate', '+', '?', 'HOUR') . " <= NOW()",
- undef, 'session.short', MAX_SHORT_TOKEN_HOURS);
+ my $dbh = Bugzilla->dbh;
+ $dbh->do(
+ "DELETE FROM tokens WHERE "
+ . $dbh->sql_date_math('issuedate', '+', '?', 'HOUR')
+ . " <= NOW()",
+ undef,
+ MAX_TOKEN_AGE * 24
+ );
+ $dbh->do(
+ "DELETE FROM tokens WHERE tokentype = ? AND "
+ . $dbh->sql_date_math('issuedate', '+', '?', 'HOUR')
+ . " <= NOW()",
+ undef, 'session.short', MAX_SHORT_TOKEN_HOURS
+ );
}
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);
-
- # Some DBs such as MySQL are case-insensitive by default so we do
- # a quick comparison to make sure the tokens are indeed the same.
- (defined $db_token && $db_token eq $token)
- || ThrowCodeError("cancel_token_does_not_exist");
-
- # If we are canceling the creation of a new user account, then there
- # is no entry in the 'profiles' table.
- my $user = new Bugzilla::User($userid);
-
- $vars->{'emailaddress'} = $userid ? $user->email : $eventdata;
- $vars->{'remoteaddress'} = remote_ip();
- $vars->{'token'} = $token;
- $vars->{'tokentype'} = $tokentype;
- $vars->{'issuedate'} = $issuedate;
- # The user is probably not logged in.
- # So we have to pass this information to the template.
- $vars->{'timezone'} = $user->timezone;
- $vars->{'eventdata'} = $eventdata;
- $vars->{'cancelaction'} = $cancelaction;
-
- # Notify the user via email about the cancellation.
- my $template = Bugzilla->template_inner($user->setting('lang'));
-
- my $message;
- $template->process("account/cancel-token.txt.tmpl", $vars, \$message)
- || ThrowTemplateError($template->error());
+ WHERE token = ?', undef, $token
+ );
- MessageToMTA($message);
+ # Some DBs such as MySQL are case-insensitive by default so we do
+ # a quick comparison to make sure the tokens are indeed the same.
+ (defined $db_token && $db_token eq $token)
+ || ThrowCodeError("cancel_token_does_not_exist");
- # Delete the token from the database.
- delete_token($token);
+ # If we are canceling the creation of a new user account, then there
+ # is no entry in the 'profiles' table.
+ my $user = new Bugzilla::User($userid);
+
+ $vars->{'emailaddress'} = $userid ? $user->email : $eventdata;
+ $vars->{'remoteaddress'} = remote_ip();
+ $vars->{'token'} = $token;
+ $vars->{'tokentype'} = $tokentype;
+ $vars->{'issuedate'} = $issuedate;
+
+ # The user is probably not logged in.
+ # So we have to pass this information to the template.
+ $vars->{'timezone'} = $user->timezone;
+ $vars->{'eventdata'} = $eventdata;
+ $vars->{'cancelaction'} = $cancelaction;
+
+ # Notify the user via email about the cancellation.
+ my $template = Bugzilla->template_inner($user->setting('lang'));
+
+ my $message;
+ $template->process("account/cancel-token.txt.tmpl", $vars, \$message)
+ || ThrowTemplateError($template->error());
+
+ MessageToMTA($message);
+
+ # Delete the token from the database.
+ delete_token($token);
}
sub DeletePasswordTokens {
- my ($userid, $reason) = @_;
- my $dbh = Bugzilla->dbh;
+ my ($userid, $reason) = @_;
+ my $dbh = Bugzilla->dbh;
- detaint_natural($userid);
- my $tokens = $dbh->selectcol_arrayref('SELECT token FROM tokens
+ detaint_natural($userid);
+ my $tokens = $dbh->selectcol_arrayref(
+ 'SELECT token FROM tokens
WHERE userid = ? AND tokentype = ?',
- undef, ($userid, 'password'));
+ undef, ($userid, 'password')
+ );
- foreach my $token (@$tokens) {
- Bugzilla::Token::Cancel($token, $reason);
- }
+ foreach my $token (@$tokens) {
+ Bugzilla::Token::Cancel($token, $reason);
+ }
}
# Returns an email change token if the user has one.
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
@@ -453,73 +485,73 @@ sub delete_token {
# Note: this routine must not be called while tables are locked as it will try
# to lock some tables itself, see CleanTokenTable().
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 his 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 his 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;
}
sub set_token_extra_data {
- my ($token, $data) = @_;
+ my ($token, $data) = @_;
- $data = encode_json($data) if ref($data);
+ $data = encode_json($data) if ref($data);
- # extra_data is MEDIUMTEXT, max 16M
- if (length($data) > 16_777_215) {
- ThrowCodeError('token_data_too_big');
- }
+ # extra_data is MEDIUMTEXT, max 16M
+ if (length($data) > 16_777_215) {
+ ThrowCodeError('token_data_too_big');
+ }
- Bugzilla->dbh->do(
- "INSERT INTO token_data (token, extra_data) VALUES (?, ?) ON DUPLICATE KEY UPDATE extra_data = ?",
- undef, $token, $data, $data);
+ Bugzilla->dbh->do(
+ "INSERT INTO token_data (token, extra_data) VALUES (?, ?) ON DUPLICATE KEY UPDATE extra_data = ?",
+ undef, $token, $data, $data
+ );
}
sub get_token_extra_data {
- my ($token) = @_;
- trick_taint($token);
- my ($data) = Bugzilla->dbh->selectrow_array(
- "SELECT extra_data FROM token_data WHERE token = ?",
- undef, $token);
- return undef unless defined $data;
- $data = encode('UTF-8', $data);
- eval {
- $data = decode_json($data);
- };
- return $data;
+ my ($token) = @_;
+ trick_taint($token);
+ my ($data)
+ = Bugzilla->dbh->selectrow_array(
+ "SELECT extra_data FROM token_data WHERE token = ?",
+ undef, $token);
+ return undef unless defined $data;
+ $data = encode('UTF-8', $data);
+ eval { $data = decode_json($data); };
+ return $data;
}
################################################################################
@@ -529,34 +561,38 @@ sub get_token_extra_data {
# Generates a unique token and inserts it into the database
# Returns the token and the token timestamp
sub _create_token {
- my ($userid, $tokentype, $eventdata) = @_;
- my $dbh = Bugzilla->dbh;
+ my ($userid, $tokentype, $eventdata) = @_;
+ my $dbh = Bugzilla->dbh;
- detaint_natural($userid) if defined $userid;
- trick_taint($tokentype);
- trick_taint($eventdata);
+ detaint_natural($userid) if defined $userid;
+ trick_taint($tokentype);
+ trick_taint($eventdata);
- my $is_shadow = Bugzilla->is_shadow_db;
- $dbh = Bugzilla->switch_to_main_db() if $is_shadow;
+ my $is_shadow = Bugzilla->is_shadow_db;
+ $dbh = Bugzilla->switch_to_main_db() if $is_shadow;
- $dbh->bz_start_transaction();
+ $dbh->bz_start_transaction();
- my $token = GenerateUniqueToken();
+ my $token = GenerateUniqueToken();
- $dbh->do("INSERT INTO tokens (userid, issuedate, token, tokentype, eventdata)
- VALUES (?, NOW(), ?, ?, ?)", undef, ($userid, $token, $tokentype, $eventdata));
+ $dbh->do(
+ "INSERT INTO tokens (userid, issuedate, token, tokentype, eventdata)
+ VALUES (?, NOW(), ?, ?, ?)", undef,
+ ($userid, $token, $tokentype, $eventdata)
+ );
- $dbh->bz_commit_transaction();
+ $dbh->bz_commit_transaction();
- if (wantarray) {
- my (undef, $token_ts, undef) = GetTokenData($token);
- $token_ts = str2time($token_ts);
- Bugzilla->switch_to_shadow_db() if $is_shadow;
- return ($token, $token_ts);
- } else {
- Bugzilla->switch_to_shadow_db() if $is_shadow;
- return $token;
- }
+ if (wantarray) {
+ my (undef, $token_ts, undef) = GetTokenData($token);
+ $token_ts = str2time($token_ts);
+ Bugzilla->switch_to_shadow_db() if $is_shadow;
+ return ($token, $token_ts);
+ }
+ else {
+ Bugzilla->switch_to_shadow_db() if $is_shadow;
+ return $token;
+ }
}
1;
diff --git a/Bugzilla/Types.pm b/Bugzilla/Types.pm
index 93d699f49..e4868d227 100644
--- a/Bugzilla/Types.pm
+++ b/Bugzilla/Types.pm
@@ -11,17 +11,16 @@ use 5.10.1;
use strict;
use warnings;
-use Type::Library
- -base,
- -declare => qw( Bug User Group Attachment Comment JSONBool );
+use Type::Library -base,
+ -declare => qw( Bug User Group Attachment Comment JSONBool );
use Type::Utils -all;
use Types::Standard -types;
-class_type Bug, { class => 'Bugzilla::Bug' };
-class_type User, { class => 'Bugzilla::User' };
-class_type Group, { class => 'Bugzilla::Group' };
-class_type Attachment, { class => 'Bugzilla::Attachment' };
-class_type Comment, { class => 'Bugzilla::Comment' };
-class_type JSONBool, { class => 'JSON::PP::Boolean' };
+class_type Bug, {class => 'Bugzilla::Bug'};
+class_type User, {class => 'Bugzilla::User'};
+class_type Group, {class => 'Bugzilla::Group'};
+class_type Attachment, {class => 'Bugzilla::Attachment'};
+class_type Comment, {class => 'Bugzilla::Comment'};
+class_type JSONBool, {class => 'JSON::PP::Boolean'};
1;
diff --git a/Bugzilla/Update.pm b/Bugzilla/Update.pm
index 72a7108a8..9f9288162 100644
--- a/Bugzilla/Update.pm
+++ b/Bugzilla/Update.pm
@@ -13,149 +13,159 @@ use warnings;
use Bugzilla::Constants;
-use constant TIME_INTERVAL => 86400; # Default is one day, in seconds.
-use constant TIMEOUT => 5; # Number of seconds before timeout.
+use constant TIME_INTERVAL => 86400; # Default is one day, in seconds.
+use constant TIMEOUT => 5; # Number of seconds before timeout.
# Look for new releases and notify logged in administrators about them.
sub get_notifications {
- return if !Bugzilla->feature('updates');
- return if (Bugzilla->params->{'upgrade_notification'} eq 'disabled');
-
- my $local_file = bz_locations()->{'datadir'} . '/' . LOCAL_FILE;
- # Update the local XML file if this one doesn't exist or if
- # the last modification time (stat[9]) is older than TIME_INTERVAL.
- if (!-e $local_file || (time() - (stat($local_file))[9] > TIME_INTERVAL)) {
- unlink $local_file; # Make sure the old copy is away.
- return { 'error' => 'no_update' } if (-e $local_file);
-
- my $error = _synchronize_data();
- # If an error is returned, leave now.
- return $error if $error;
+ return if !Bugzilla->feature('updates');
+ return if (Bugzilla->params->{'upgrade_notification'} eq 'disabled');
+
+ my $local_file = bz_locations()->{'datadir'} . '/' . LOCAL_FILE;
+
+ # Update the local XML file if this one doesn't exist or if
+ # the last modification time (stat[9]) is older than TIME_INTERVAL.
+ if (!-e $local_file || (time() - (stat($local_file))[9] > TIME_INTERVAL)) {
+ unlink $local_file; # Make sure the old copy is away.
+ return {'error' => 'no_update'} if (-e $local_file);
+
+ my $error = _synchronize_data();
+
+ # If an error is returned, leave now.
+ return $error if $error;
+ }
+
+ # If we cannot access the local XML file, ignore it.
+ return {'error' => 'no_access'} unless (-r $local_file);
+
+ my $twig = XML::Twig->new();
+ $twig->safe_parsefile($local_file);
+
+ # If the XML file is invalid, return.
+ return {'error' => 'corrupted'} if $@;
+ my $root = $twig->root;
+
+ my @releases;
+ foreach my $branch ($root->children('branch')) {
+ my $release = {
+ 'branch_ver' => $branch->{'att'}->{'id'},
+ 'latest_ver' => $branch->{'att'}->{'vid'},
+ 'status' => $branch->{'att'}->{'status'},
+ 'url' => $branch->{'att'}->{'url'},
+ 'date' => $branch->{'att'}->{'date'}
+ };
+ push(@releases, $release);
+ }
+
+ # On which branch is the current installation running?
+ my @current_version
+ = (BUGZILLA_VERSION =~ m/^(\d+)\.(\d+)(?:(rc|\.)(\d+))?\+?$/);
+
+ my @release;
+ if (Bugzilla->params->{'upgrade_notification'} eq 'development_snapshot') {
+ @release = grep { $_->{'status'} eq 'development' } @releases;
+
+ # If there is no development snapshot available, then we are in the
+ # process of releasing a release candidate. That's the release we want.
+ unless (scalar(@release)) {
+ @release = grep { $_->{'status'} eq 'release-candidate' } @releases;
}
-
- # If we cannot access the local XML file, ignore it.
- return { 'error' => 'no_access' } unless (-r $local_file);
-
- my $twig = XML::Twig->new();
- $twig->safe_parsefile($local_file);
- # If the XML file is invalid, return.
- return { 'error' => 'corrupted' } if $@;
- my $root = $twig->root;
-
- my @releases;
- foreach my $branch ($root->children('branch')) {
- my $release = {
- 'branch_ver' => $branch->{'att'}->{'id'},
- 'latest_ver' => $branch->{'att'}->{'vid'},
- 'status' => $branch->{'att'}->{'status'},
- 'url' => $branch->{'att'}->{'url'},
- 'date' => $branch->{'att'}->{'date'}
- };
- push(@releases, $release);
- }
-
- # On which branch is the current installation running?
- my @current_version =
- (BUGZILLA_VERSION =~ m/^(\d+)\.(\d+)(?:(rc|\.)(\d+))?\+?$/);
-
- my @release;
- if (Bugzilla->params->{'upgrade_notification'} eq 'development_snapshot') {
- @release = grep {$_->{'status'} eq 'development'} @releases;
- # If there is no development snapshot available, then we are in the
- # process of releasing a release candidate. That's the release we want.
- unless (scalar(@release)) {
- @release = grep {$_->{'status'} eq 'release-candidate'} @releases;
- }
- }
- elsif (Bugzilla->params->{'upgrade_notification'} eq 'latest_stable_release') {
- @release = grep {$_->{'status'} eq 'stable'} @releases;
- }
- elsif (Bugzilla->params->{'upgrade_notification'} eq 'stable_branch_release') {
- # We want the latest stable version for the current branch.
- # If we are running a development snapshot, we won't match anything.
- my $branch_version = $current_version[0] . '.' . $current_version[1];
-
- # We do a string comparison instead of a numerical one, because
- # e.g. 2.2 == 2.20, but 2.2 ne 2.20 (and 2.2 is indeed much older).
- @release = grep {$_->{'branch_ver'} eq $branch_version} @releases;
-
- # If the branch is now closed, we should strongly suggest
- # to upgrade to the latest stable release available.
- if (scalar(@release) && $release[0]->{'status'} eq 'closed') {
- @release = grep {$_->{'status'} eq 'stable'} @releases;
- return {'data' => $release[0], 'deprecated' => $branch_version};
- }
+ }
+ elsif (Bugzilla->params->{'upgrade_notification'} eq 'latest_stable_release') {
+ @release = grep { $_->{'status'} eq 'stable' } @releases;
+ }
+ elsif (Bugzilla->params->{'upgrade_notification'} eq 'stable_branch_release') {
+
+ # We want the latest stable version for the current branch.
+ # If we are running a development snapshot, we won't match anything.
+ my $branch_version = $current_version[0] . '.' . $current_version[1];
+
+ # We do a string comparison instead of a numerical one, because
+ # e.g. 2.2 == 2.20, but 2.2 ne 2.20 (and 2.2 is indeed much older).
+ @release = grep { $_->{'branch_ver'} eq $branch_version } @releases;
+
+ # If the branch is now closed, we should strongly suggest
+ # to upgrade to the latest stable release available.
+ if (scalar(@release) && $release[0]->{'status'} eq 'closed') {
+ @release = grep { $_->{'status'} eq 'stable' } @releases;
+ return {'data' => $release[0], 'deprecated' => $branch_version};
}
- else {
- # Unknown parameter.
- return {'error' => 'unknown_parameter'};
- }
-
- # Return if no new release is available.
- return unless scalar(@release);
-
- # Only notify the administrator if the latest version available
- # is newer than the current one.
- my @new_version =
- ($release[0]->{'latest_ver'} =~ m/^(\d+)\.(\d+)(?:(rc|\.)(\d+))?\+?$/);
-
- # We convert release candidates 'rc' to integers (rc ? 0 : 1) in order
- # to compare versions easily.
- $current_version[2] = ($current_version[2] && $current_version[2] eq 'rc') ? 0 : 1;
- $new_version[2] = ($new_version[2] && $new_version[2] eq 'rc') ? 0 : 1;
-
- my $is_newer = _compare_versions(\@current_version, \@new_version);
- return ($is_newer == 1) ? {'data' => $release[0]} : undef;
+ }
+ else {
+ # Unknown parameter.
+ return {'error' => 'unknown_parameter'};
+ }
+
+ # Return if no new release is available.
+ return unless scalar(@release);
+
+ # Only notify the administrator if the latest version available
+ # is newer than the current one.
+ my @new_version
+ = ($release[0]->{'latest_ver'} =~ m/^(\d+)\.(\d+)(?:(rc|\.)(\d+))?\+?$/);
+
+ # We convert release candidates 'rc' to integers (rc ? 0 : 1) in order
+ # to compare versions easily.
+ $current_version[2]
+ = ($current_version[2] && $current_version[2] eq 'rc') ? 0 : 1;
+ $new_version[2] = ($new_version[2] && $new_version[2] eq 'rc') ? 0 : 1;
+
+ my $is_newer = _compare_versions(\@current_version, \@new_version);
+ return ($is_newer == 1) ? {'data' => $release[0]} : undef;
}
sub _synchronize_data {
- my $local_file = bz_locations()->{'datadir'} . '/' . LOCAL_FILE;
-
- my $ua = LWP::UserAgent->new();
- $ua->timeout(TIMEOUT);
- $ua->protocols_allowed(['http', 'https']);
- # If the URL of the proxy is given, use it, else get this information
- # from the environment variable.
- my $proxy_url = Bugzilla->params->{'proxy_url'};
- if ($proxy_url) {
- $ua->proxy(['http', 'https'], $proxy_url);
- }
- else {
- $ua->env_proxy;
- }
- my $response = eval { $ua->mirror(REMOTE_FILE, $local_file) };
-
- # $ua->mirror() forces the modification time of the local XML file
- # to match the modification time of the remote one.
- # So we have to update it manually to reflect that a newer version
- # of the file has effectively been requested. This will avoid
- # any new download for the next TIME_INTERVAL.
- if (-e $local_file) {
- # Try to alter its last modification time.
- my $can_alter = utime(undef, undef, $local_file);
- # This error should never happen.
- $can_alter || return { 'error' => 'no_update' };
- }
- elsif ($response && $response->is_error) {
- # We have been unable to download the file.
- return { 'error' => 'cannot_download', 'reason' => $response->status_line };
- }
- else {
- return { 'error' => 'no_write', 'reason' => $@ };
- }
-
- # Everything went well.
- return 0;
+ my $local_file = bz_locations()->{'datadir'} . '/' . LOCAL_FILE;
+
+ my $ua = LWP::UserAgent->new();
+ $ua->timeout(TIMEOUT);
+ $ua->protocols_allowed(['http', 'https']);
+
+ # If the URL of the proxy is given, use it, else get this information
+ # from the environment variable.
+ my $proxy_url = Bugzilla->params->{'proxy_url'};
+ if ($proxy_url) {
+ $ua->proxy(['http', 'https'], $proxy_url);
+ }
+ else {
+ $ua->env_proxy;
+ }
+ my $response = eval { $ua->mirror(REMOTE_FILE, $local_file) };
+
+ # $ua->mirror() forces the modification time of the local XML file
+ # to match the modification time of the remote one.
+ # So we have to update it manually to reflect that a newer version
+ # of the file has effectively been requested. This will avoid
+ # any new download for the next TIME_INTERVAL.
+ if (-e $local_file) {
+
+ # Try to alter its last modification time.
+ my $can_alter = utime(undef, undef, $local_file);
+
+ # This error should never happen.
+ $can_alter || return {'error' => 'no_update'};
+ }
+ elsif ($response && $response->is_error) {
+
+ # We have been unable to download the file.
+ return {'error' => 'cannot_download', 'reason' => $response->status_line};
+ }
+ else {
+ return {'error' => 'no_write', 'reason' => $@};
+ }
+
+ # Everything went well.
+ return 0;
}
sub _compare_versions {
- my ($old_ver, $new_ver) = @_;
- while (scalar(@$old_ver) && scalar(@$new_ver)) {
- my $old = shift(@$old_ver) || 0;
- my $new = shift(@$new_ver) || 0;
- return $new <=> $old if ($new <=> $old);
- }
- return scalar(@$new_ver) <=> scalar(@$old_ver);
+ my ($old_ver, $new_ver) = @_;
+ while (scalar(@$old_ver) && scalar(@$new_ver)) {
+ my $old = shift(@$old_ver) || 0;
+ my $new = shift(@$new_ver) || 0;
+ return $new <=> $old if ($new <=> $old);
+ }
+ return scalar(@$new_ver) <=> scalar(@$old_ver);
}
diff --git a/Bugzilla/User.pm b/Bugzilla/User.pm
index afd310eb0..fdfc7f8d0 100644
--- a/Bugzilla/User.pm
+++ b/Bugzilla/User.pm
@@ -34,9 +34,9 @@ use Role::Tiny::With;
use base qw(Bugzilla::Object Exporter);
@Bugzilla::User::EXPORT = qw(is_available_username
- login_to_id user_id_to_login
- USER_MATCH_MULTIPLE USER_MATCH_FAILED USER_MATCH_SUCCESS
- MATCH_SKIP_CONFIRM
+ login_to_id user_id_to_login
+ USER_MATCH_MULTIPLE USER_MATCH_FAILED USER_MATCH_SUCCESS
+ MATCH_SKIP_CONFIRM
);
#####################################################################
@@ -47,16 +47,16 @@ use constant USER_MATCH_MULTIPLE => -1;
use constant USER_MATCH_FAILED => 0;
use constant USER_MATCH_SUCCESS => 1;
-use constant MATCH_SKIP_CONFIRM => 1;
+use constant MATCH_SKIP_CONFIRM => 1;
use constant DEFAULT_USER => {
- 'userid' => 0,
- 'realname' => '',
- 'login_name' => '',
- 'showmybugslink' => 0,
- 'disabledtext' => '',
- 'disable_mail' => 0,
- 'is_enabled' => 1,
+ 'userid' => 0,
+ 'realname' => '',
+ 'login_name' => '',
+ 'showmybugslink' => 0,
+ 'disabledtext' => '',
+ 'disable_mail' => 0,
+ 'is_enabled' => 1,
};
use constant DB_TABLE => 'profiles';
@@ -66,23 +66,24 @@ use constant DB_TABLE => 'profiles';
# Bugzilla::User used "name" for the realname field. This should be
# fixed one day.
sub DB_COLUMNS {
- my $dbh = Bugzilla->dbh;
- return (
- 'profiles.userid',
- 'profiles.login_name',
- 'profiles.realname',
- 'profiles.mybugslink AS showmybugslink',
- 'profiles.disabledtext',
- 'profiles.disable_mail',
- 'profiles.extern_id',
- 'profiles.is_enabled',
- $dbh->sql_date_format('last_seen_date', '%Y-%m-%d') . ' AS last_seen_date',
- 'profiles.password_change_required',
- 'profiles.password_change_reason',
- 'profiles.mfa',
- 'profiles.mfa_required_date',
- 'profiles.nickname'
+ my $dbh = Bugzilla->dbh;
+ return (
+ 'profiles.userid',
+ 'profiles.login_name',
+ 'profiles.realname',
+ 'profiles.mybugslink AS showmybugslink',
+ 'profiles.disabledtext',
+ 'profiles.disable_mail',
+ 'profiles.extern_id',
+ 'profiles.is_enabled',
+ $dbh->sql_date_format('last_seen_date', '%Y-%m-%d') . ' AS last_seen_date',
+ 'profiles.password_change_required',
+ 'profiles.password_change_reason',
+ 'profiles.mfa',
+ 'profiles.mfa_required_date',
+ 'profiles.nickname'
),
+ ;
}
use constant NAME_FIELD => 'login_name';
@@ -90,41 +91,41 @@ 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_for_creation,
- realname => \&_check_realname,
- nickname => \&_check_realname,
- extern_id => \&_check_extern_id,
- is_enabled => \&_check_is_enabled,
- password_change_required => \&Bugzilla::Object::check_boolean,
- password_change_reason => \&_check_password_change_reason,
- mfa => \&_check_mfa,
+ cryptpassword => \&_check_password,
+ disable_mail => \&_check_disable_mail,
+ disabledtext => \&_check_disabledtext,
+ login_name => \&check_login_name_for_creation,
+ realname => \&_check_realname,
+ nickname => \&_check_realname,
+ extern_id => \&_check_extern_id,
+ is_enabled => \&_check_is_enabled,
+ password_change_required => \&Bugzilla::Object::check_boolean,
+ password_change_reason => \&_check_password_change_reason,
+ mfa => \&_check_mfa,
};
sub UPDATE_COLUMNS {
- my $self = shift;
- my @cols = qw(
- disable_mail
- disabledtext
- login_name
- realname
- extern_id
- is_enabled
- password_change_required
- password_change_reason
- mfa
- mfa_required_date
- nickname
- );
- push(@cols, 'cryptpassword') if exists $self->{cryptpassword};
- return @cols;
-};
+ my $self = shift;
+ my @cols = qw(
+ disable_mail
+ disabledtext
+ login_name
+ realname
+ extern_id
+ is_enabled
+ password_change_required
+ password_change_reason
+ mfa
+ mfa_required_date
+ nickname
+ );
+ push(@cols, 'cryptpassword') if exists $self->{cryptpassword};
+ return @cols;
+}
use constant VALIDATOR_DEPENDENCIES => {
- is_enabled => [ 'disabledtext' ],
- password_change_reason => [ 'password_change_required' ],
+ is_enabled => ['disabledtext'],
+ password_change_reason => ['password_change_required'],
};
use constant EXTRA_REQUIRED_FIELDS => qw(is_enabled);
@@ -132,18 +133,18 @@ use constant EXTRA_REQUIRED_FIELDS => qw(is_enabled);
with 'Bugzilla::Elastic::Role::Object';
sub ES_INDEX {
- my ($class) = @_;
- sprintf("%s_%s", Bugzilla->params->{elasticsearch_index}, $class->ES_TYPE);
+ my ($class) = @_;
+ sprintf("%s_%s", Bugzilla->params->{elasticsearch_index}, $class->ES_TYPE);
}
-sub ES_TYPE { 'user' }
+sub ES_TYPE {'user'}
-sub ES_OBJECTS_AT_ONCE { 5000 }
+sub ES_OBJECTS_AT_ONCE {5000}
sub ES_SELECT_UPDATED_SQL {
- my ($class, $mtime) = @_;
+ my ($class, $mtime) = @_;
- my $sql = q{
+ my $sql = q{
SELECT DISTINCT
object_id
FROM
@@ -151,221 +152,227 @@ sub ES_SELECT_UPDATED_SQL {
WHERE
class = 'Bugzilla::User' AND at_time > FROM_UNIXTIME(?)
};
- return ($sql, [$mtime]);
+ return ($sql, [$mtime]);
}
sub ES_SELECT_ALL_SQL {
- my ($class, $last_id) = @_;
+ my ($class, $last_id) = @_;
- my $id = $class->ID_FIELD;
- my $table = $class->DB_TABLE;
+ my $id = $class->ID_FIELD;
+ my $table = $class->DB_TABLE;
- return ("SELECT $id FROM $table WHERE $id > ? AND is_enabled AND NOT disabledtext ORDER BY $id", [$last_id // 0]);
+ return (
+ "SELECT $id FROM $table WHERE $id > ? AND is_enabled AND NOT disabledtext ORDER BY $id",
+ [$last_id // 0]
+ );
}
sub ES_SETTINGS {
- return {
- number_of_shards => 2,
- analysis => {
- filter => {
- asciifolding_original => {
- type => "asciifolding",
- preserve_original => \1,
- },
- },
- analyzer => {
- autocomplete => {
- type => 'custom',
- tokenizer => 'keyword',
- filter => [ 'lowercase', 'asciifolding_original' ],
- },
- folding => {
- tokenizer => 'standard',
- filter => [ 'standard', 'lowercase', 'asciifolding_original' ],
- },
- }
- }
- };
+ return {
+ number_of_shards => 2,
+ analysis => {
+ filter => {
+ asciifolding_original => {type => "asciifolding", preserve_original => \1,},
+ },
+ analyzer => {
+ autocomplete => {
+ type => 'custom',
+ tokenizer => 'keyword',
+ filter => ['lowercase', 'asciifolding_original'],
+ },
+ folding => {
+ tokenizer => 'standard',
+ filter => ['standard', 'lowercase', 'asciifolding_original'],
+ },
+ }
+ }
+ };
}
sub ES_PROPERTIES {
- return {
- suggest_user => {
- type => 'completion',
- analyzer => 'folding',
- search_analyzer => 'folding',
- payloads => \1,
- },
- suggest_nick => {
- type => 'completion',
- analyzer => 'autocomplete',
- payloads => \1,
- },
- login => { type => 'string' },
- name => { type => 'string' },
- is_enabled => { type => 'boolean' },
- };
+ return {
+ suggest_user => {
+ type => 'completion',
+ analyzer => 'folding',
+ search_analyzer => 'folding',
+ payloads => \1,
+ },
+ suggest_nick =>
+ {type => 'completion', analyzer => 'autocomplete', payloads => \1,},
+ login => {type => 'string'},
+ name => {type => 'string'},
+ is_enabled => {type => 'boolean'},
+ };
}
sub es_document {
- my ( $self, $timestamp ) = @_;
- my $doc = {
- login => $self->login,
- name => $self->name,
- is_enabled => $self->is_enabled,
- suggest_user => {
- input => [ $self->login, $self->name ],
- output => $self->identity,
- payload => { name => $self->login, real_name => $self->name },
- },
+ my ($self, $timestamp) = @_;
+ my $doc = {
+ login => $self->login,
+ name => $self->name,
+ is_enabled => $self->is_enabled,
+ suggest_user => {
+ input => [$self->login, $self->name],
+ output => $self->identity,
+ payload => {name => $self->login, real_name => $self->name},
+ },
+ };
+ my $name = $self->name;
+ my @nicks = extract_nicks($name);
+
+ if (@nicks) {
+ $doc->{suggest_nick} = {
+ input => \@nicks,
+ output => $self->login,
+ payload => {name => $self->login, real_name => $self->name},
};
- my $name = $self->name;
- my @nicks = extract_nicks($name);
-
- if (@nicks) {
- $doc->{suggest_nick} = {
- input => \@nicks,
- output => $self->login,
- payload => { name => $self->login, real_name => $self->name },
- };
- }
+ }
- return $doc;
+ return $doc;
}
################################################################################
# Functions
################################################################################
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;
- my $user = Bugzilla->user;
-
- # 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;
+ my $user = Bugzilla->user;
+
+ # 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);
- Bugzilla->audit(sprintf('%s <%s> removed group %s from %s', $user->login, remote_ip(), $group->name, $self->login));
- }
- foreach my $group (@$added) {
- $sth_add_mapping->execute($self->id, $group->id, $is_bless, GRANT_DIRECT);
- Bugzilla->audit(sprintf('%s <%s> added group %s to %s', $user->login, remote_ip(), $group->name, $self->login));
- }
+ foreach my $group (@$removed) {
+ $sth_remove_mapping->execute($self->id, $group->id, $is_bless, GRANT_DIRECT);
+ Bugzilla->audit(sprintf(
+ '%s <%s> removed group %s from %s',
+ $user->login, remote_ip(), $group->name, $self->login
+ ));
+ }
+ foreach my $group (@$added) {
+ $sth_add_mapping->execute($self->id, $group->id, $is_bless, GRANT_DIRECT);
+ Bugzilla->audit(sprintf(
+ '%s <%s> added group %s to %s',
+ $user->login, remote_ip(), $group->name, $self->login
+ ));
+ }
- 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, $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, $user->id,
+ get_field_id('bug_group'),
+ join(', ', map { $_->name } @$removed),
+ join(', ', map { $_->name } @$added)
+ );
+ }
+ else {
+ # XXX: should create profiles_activity entries for blesser changes.
+ }
- Bugzilla->memcached->clear_config({ key => 'user_groups.' . $self->id });
+ Bugzilla->memcached->clear_config({key => 'user_groups.' . $self->id});
- my $type = $is_bless ? 'bless_groups' : 'groups';
- $changes->{$type} = [
- [ map { $_->name } @$removed ],
- [ map { $_->name } @$added ],
- ];
- }
+ my $type = $is_bless ? 'bless_groups' : 'groups';
+ $changes->{$type} = [[map { $_->name } @$removed], [map { $_->name } @$added],];
+ }
}
sub update {
- my $self = shift;
- my $options = shift;
+ my $self = shift;
+ my $options = shift;
- my $group_changes = delete $self->{_group_changes};
+ my $group_changes = delete $self->{_group_changes};
- my $changes = $self->SUPER::update(@_);
- my $dbh = Bugzilla->dbh;
- $self->_update_groups($group_changes, $changes);
+ my $changes = $self->SUPER::update(@_);
+ my $dbh = Bugzilla->dbh;
+ $self->_update_groups($group_changes, $changes);
- if (exists $changes->{login_name}) {
- # Delete all the tokens related to the userid
- $dbh->do('DELETE FROM tokens WHERE userid = ?', undef, $self->id)
- unless $options->{keep_tokens};
- # And rederive regex groups
- $self->derive_regexp_groups();
- }
+ if (exists $changes->{login_name}) {
- if (exists $changes->{mfa} && $self->mfa eq '') {
- if (Bugzilla->user->id != $self->id) {
- Bugzilla->audit(sprintf('%s disabled 2FA for %s', Bugzilla->user->login, $self->login));
- }
- $dbh->do("DELETE FROM profile_mfa WHERE user_id = ?", undef, $self->id);
+ # 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->{mfa} && $self->mfa eq '') {
+ if (Bugzilla->user->id != $self->id) {
+ Bugzilla->audit(
+ sprintf('%s disabled 2FA for %s', Bugzilla->user->login, $self->login));
}
+ $dbh->do("DELETE FROM profile_mfa WHERE user_id = ?", undef, $self->id);
+ }
- # Logout the user if necessary.
- Bugzilla->logout_user($self)
- if (!$options->{keep_session}
- && (exists $changes->{login_name}
- || exists $changes->{disabledtext}
- || exists $changes->{cryptpassword}));
+ # Logout the user if necessary.
+ Bugzilla->logout_user($self)
+ if (
+ !$options->{keep_session}
+ && ( exists $changes->{login_name}
+ || exists $changes->{disabledtext}
+ || exists $changes->{cryptpassword})
+ );
- # XXX Can update profiles_activity here as soon as it understands
- # field names like login_name.
+ # XXX Can update profiles_activity here as soon as it understands
+ # field names like login_name.
- return $changes;
+ return $changes;
}
################################################################################
@@ -373,288 +380,294 @@ sub update {
################################################################################
sub _check_disable_mail {
- my ($invocant, $value) = @_;
- return 1 if ref($invocant) && !$invocant->is_enabled;
- return $value ? 1 : 0;
+ my ($invocant, $value) = @_;
+ return 1 if ref($invocant) && !$invocant->is_enabled;
+ return $value ? 1 : 0;
}
sub _check_disabledtext { return trim($_[1]) || ''; }
# Check whether the extern_id is unique.
sub _check_extern_id {
- my ($invocant, $extern_id) = @_;
- $extern_id = trim($extern_id);
- return undef unless defined($extern_id) && $extern_id ne "";
- if (!ref($invocant) || $invocant->extern_id ne $extern_id) {
- my $existing_login = $invocant->new({ extern_id => $extern_id });
- if ($existing_login) {
- ThrowUserError( 'extern_id_exists',
- { extern_id => $extern_id,
- existing_login_name => $existing_login->login });
- }
+ my ($invocant, $extern_id) = @_;
+ $extern_id = trim($extern_id);
+ return undef unless defined($extern_id) && $extern_id ne "";
+ if (!ref($invocant) || $invocant->extern_id ne $extern_id) {
+ my $existing_login = $invocant->new({extern_id => $extern_id});
+ if ($existing_login) {
+ ThrowUserError('extern_id_exists',
+ {extern_id => $extern_id, existing_login_name => $existing_login->login});
}
- return $extern_id;
+ }
+ return $extern_id;
}
# This is public since createaccount.cgi needs to use it before issuing
# a token for account creation.
sub check_login_name_for_creation {
- my ($invocant, $name) = @_;
- $name = trim($name);
- $name || ThrowUserError('user_login_required');
- validate_email_syntax($name)
- || ThrowUserError('illegal_email_address', { addr => $name });
-
- # Check the name if it's a new user, or if we're changing the name.
- if (!ref($invocant) || $invocant->login ne $name) {
- is_available_username($name)
- || ThrowUserError('account_exists', { email => $name });
- }
+ my ($invocant, $name) = @_;
+ $name = trim($name);
+ $name || ThrowUserError('user_login_required');
+ validate_email_syntax($name)
+ || ThrowUserError('illegal_email_address', {addr => $name});
- return $name;
+ # Check the name if it's a new user, or if we're changing the name.
+ if (!ref($invocant) || $invocant->login ne $name) {
+ is_available_username($name)
+ || ThrowUserError('account_exists', {email => $name});
+ }
+
+ return $name;
}
sub _check_password {
- my ($self, $pass) = @_;
+ my ($self, $pass) = @_;
- # If the password is '*', do not encrypt it or validate it further--we
- # are creating a user who should not be able to log in using DB
- # authentication.
- return $pass if $pass eq '*';
+ # If the password is '*', do not encrypt it or validate it further--we
+ # are creating a user who should not be able to log in using DB
+ # authentication.
+ return $pass if $pass eq '*';
- Bugzilla->assert_password_is_secure($pass);
- my $cryptpassword = bz_crypt($pass);
- return $cryptpassword;
+ Bugzilla->assert_password_is_secure($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;
}
sub _check_password_change_reason {
- my ($self, $value) = @_;
- return $self->password_change_required
- ? trim($_[1]) || ''
- : '';
+ my ($self, $value) = @_;
+ return $self->password_change_required ? trim($_[1]) || '' : '';
}
sub _check_mfa {
- my ($self, $provider) = @_;
- $provider = lc($provider // '');
- return 'TOTP' if $provider eq 'totp';
- return 'Duo' if $provider eq 'duo';
-
- # you must be member of the bz_can_disable_mfa group to disable mfa for
- # other accounts.
- if ($provider eq '') {
- my $user = Bugzilla->user;
- if ($user->id != $self->id && !$user->in_group('bz_can_disable_mfa')) {
- ThrowUserError('mfa_disable_denied');
- }
+ my ($self, $provider) = @_;
+ $provider = lc($provider // '');
+ return 'TOTP' if $provider eq 'totp';
+ return 'Duo' if $provider eq 'duo';
+
+ # you must be member of the bz_can_disable_mfa group to disable mfa for
+ # other accounts.
+ if ($provider eq '') {
+ my $user = Bugzilla->user;
+ if ($user->id != $self->id && !$user->in_group('bz_can_disable_mfa')) {
+ ThrowUserError('mfa_disable_denied');
}
+ }
- return '';
+ return '';
}
################################################################################
# Mutators
################################################################################
-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_password_change_required { $_[0]->set('password_change_required', $_[1]); }
-sub set_password_change_reason { $_[0]->set('password_change_reason', $_[1]); }
+sub set_disable_mail { $_[0]->set('disable_mail', $_[1]); }
+sub set_email_enabled { $_[0]->set('disable_mail', !$_[1]); }
+sub set_extern_id { $_[0]->set('extern_id', $_[1]); }
+
+sub set_password_change_required {
+ $_[0]->set('password_change_required', $_[1]);
+}
+sub set_password_change_reason { $_[0]->set('password_change_reason', $_[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 _generate_nickname {
- my ($name, $login, $id) = @_;
- my ($nick) = extract_nicks($name);
- if (!$nick) {
- $nick = "";
- }
- my ($count) = Bugzilla->dbh->selectrow_array('SELECT COUNT(*) FROM profiles WHERE nickname = ? AND userid != ?', undef, $nick, $id);
- if ($count) {
- $nick = "";
- }
- return $nick;
+ my ($name, $login, $id) = @_;
+ my ($nick) = extract_nicks($name);
+ if (!$nick) {
+ $nick = "";
+ }
+ my ($count)
+ = Bugzilla->dbh->selectrow_array(
+ 'SELECT COUNT(*) FROM profiles WHERE nickname = ? AND userid != ?',
+ undef, $nick, $id);
+ if ($count) {
+ $nick = "";
+ }
+ return $nick;
}
sub set_name {
- my ($self, $name) = @_;
- $self->set('realname', $name);
- delete $self->{identity};
- $self->set('nickname', _generate_nickname($name, $self->login, $self->id));
+ my ($self, $name) = @_;
+ $self->set('realname', $name);
+ delete $self->{identity};
+ $self->set('nickname', _generate_nickname($name, $self->login, $self->id));
}
sub set_nick {
- my ($self, $nick) = @_;
- $self->set('nickname', $nick);
+ my ($self, $nick) = @_;
+ $self->set('nickname', $nick);
}
sub set_password {
- my ($self, $password) = @_;
- $self->set('cryptpassword', $password);
- $self->set('password_change_required', 0);
- $self->set('password_change_reason', '');
+ my ($self, $password) = @_;
+ $self->set('cryptpassword', $password);
+ $self->set('password_change_required', 0);
+ $self->set('password_change_reason', '');
}
sub set_disabledtext {
- my ($self, $text) = @_;
- $self->set('disabledtext', $text);
- $self->set('is_enabled', trim($text) eq '' ? 0 : 1);
- $self->set('disable_mail', 1) if !$self->is_enabled;
+ my ($self, $text) = @_;
+ $self->set('disabledtext', $text);
+ $self->set('is_enabled', trim($text) eq '' ? 0 : 1);
+ $self->set('disable_mail', 1) if !$self->is_enabled;
}
sub set_mfa {
- my ($self, $value) = @_;
- $self->set('mfa', $value);
- delete $self->{mfa_provider};
+ my ($self, $value) = @_;
+ $self->set('mfa', $value);
+ delete $self->{mfa_provider};
}
sub set_mfa_required_date {
- my ($self, $value) = @_;
- $self->set('mfa_required_date', $value);
+ my ($self, $value) = @_;
+ $self->set('mfa_required_date', $value);
}
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;
- use Data::Dumper;
+ use Data::Dumper;
- # 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;
+ 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 }
- );
- }
+ foreach my $key (keys %$changes) {
- # 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;
+ # Check we were given an array
+ unless (ref($changes->{$key}) eq 'ARRAY') {
+ ThrowCodeError('param_invalid', {param => $changes->{$key}, function => $key});
}
- return $changes;
+ # 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;
}
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});
+ }
}
################################################################################
@@ -662,201 +675,212 @@ sub update_last_seen_date {
################################################################################
# Accessors for user attributes
-sub name { $_[0]->{realname}; }
-sub login { $_[0]->{login_name}; }
-sub extern_id { $_[0]->{extern_id}; }
-sub email { $_[0]->login . Bugzilla->params->{'emailsuffix'}; }
-sub disabledtext { $_[0]->{'disabledtext'}; }
-sub is_enabled { $_[0]->{'is_enabled'} ? 1 : 0; }
+sub name { $_[0]->{realname}; }
+sub login { $_[0]->{login_name}; }
+sub extern_id { $_[0]->{extern_id}; }
+sub email { $_[0]->login . Bugzilla->params->{'emailsuffix'}; }
+sub disabledtext { $_[0]->{'disabledtext'}; }
+sub is_enabled { $_[0]->{'is_enabled'} ? 1 : 0; }
sub showmybugslink { $_[0]->{showmybugslink}; }
sub email_disabled { $_[0]->{disable_mail} || !$_[0]->{is_enabled}; }
-sub email_enabled { !$_[0]->email_disabled; }
+sub email_enabled { !$_[0]->email_disabled; }
sub last_seen_date { $_[0]->{last_seen_date}; }
sub password_change_required { $_[0]->{password_change_required}; }
-sub password_change_reason { $_[0]->{password_change_reason}; }
+sub password_change_reason { $_[0]->{password_change_reason}; }
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};
}
sub mfa { $_[0]->{mfa} }
sub mfa_required_date {
- my $self = shift;
- return $self->{mfa_required_date} ? datetime_from($self->{mfa_required_date}, @_) : undef;
+ my $self = shift;
+ return $self->{mfa_required_date}
+ ? datetime_from($self->{mfa_required_date}, @_)
+ : undef;
}
sub mfa_provider {
- my ($self) = @_;
- my $mfa = $self->{mfa} || return undef;
- return $self->{mfa_provider} if exists $self->{mfa_provider};
- require Bugzilla::MFA;
- $self->{mfa_provider} = Bugzilla::MFA->new_from($self, $mfa);
- return $self->{mfa_provider};
+ my ($self) = @_;
+ my $mfa = $self->{mfa} || return undef;
+ return $self->{mfa_provider} if exists $self->{mfa_provider};
+ require Bugzilla::MFA;
+ $self->{mfa_provider} = Bugzilla::MFA->new_from($self, $mfa);
+ return $self->{mfa_provider};
}
sub in_mfa_group {
- my $self = shift;
- return $self->{in_mfa_group} if exists $self->{in_mfa_group};
+ my $self = shift;
+ return $self->{in_mfa_group} if exists $self->{in_mfa_group};
- my $mfa_group = Bugzilla->params->{mfa_group};
- return $self->{in_mfa_group} = ($mfa_group && $self->in_group($mfa_group));
+ my $mfa_group = Bugzilla->params->{mfa_group};
+ return $self->{in_mfa_group} = ($mfa_group && $self->in_group($mfa_group));
}
sub name_or_login {
- my $self = shift;
+ my $self = shift;
- return $self->name || $self->login;
+ return $self->name || $self->login;
}
# Generate a string to identify the user by name + login if the user
# has a name or by login only if she doesn't.
sub identity {
- my $self = shift;
+ my $self = shift;
- return "" unless $self->id;
+ return "" unless $self->id;
- if (!defined $self->{identity}) {
- $self->{identity} =
- $self->name ? $self->name . " <" . $self->login. ">" : $self->login;
- }
+ if (!defined $self->{identity}) {
+ $self->{identity}
+ = $self->name ? $self->name . " <" . $self->login . ">" : $self->login;
+ }
- return $self->{identity};
+ return $self->{identity};
}
sub nick {
- my $self = shift;
+ my $self = shift;
- return "" unless $self->id;
- return $self->{nickname} if $self->{nickname};
- return $self->{nick} //= (split(/@/, $self->login, 2))[0];
+ return "" unless $self->id;
+ return $self->{nickname} if $self->{nickname};
+ return $self->{nick} //= (split(/@/, $self->login, 2))[0];
}
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;
}
##########################
@@ -864,280 +888,288 @@ sub is_bug_ignored {
##########################
sub recent_searches {
- my $self = shift;
- $self->{recent_searches} ||=
- Bugzilla::Search::Recent->match({ user_id => $self->id });
- return $self->{recent_searches};
+ my $self = shift;
+ $self->{recent_searches}
+ ||= Bugzilla::Search::Recent->match({user_id => $self->id});
+ return $self->{recent_searches};
}
sub recent_search_containing {
- my ($self, $bug_id) = @_;
- my $searches = $self->recent_searches;
+ my ($self, $bug_id) = @_;
+ my $searches = $self->recent_searches;
- foreach my $search (@$searches) {
- return $search if grep($_ == $bug_id, @{ $search->bug_list });
- }
+ foreach my $search (@$searches) {
+ return $search if grep($_ == $bug_id, @{$search->bug_list});
+ }
- return undef;
+ return undef;
}
sub recent_search_for {
- my ($self, $bug) = @_;
- my $params = Bugzilla->input_params;
- my $cgi = Bugzilla->cgi;
-
- if ($self->id) {
- # First see if there's a list_id parameter in the query string.
- my $list_id = $params->{list_id};
- if (!$list_id) {
- # If not, check for "list_id" in the query string of the referer.
- my $referer = $cgi->referer;
- if ($referer) {
- my $uri = URI->new($referer);
- if ($uri->path =~ /buglist\.cgi$/) {
- $list_id = $uri->query_param('list_id')
- || $uri->query_param('regetlastlist');
- }
- }
+ my ($self, $bug) = @_;
+ my $params = Bugzilla->input_params;
+ my $cgi = Bugzilla->cgi;
+
+ if ($self->id) {
+
+ # First see if there's a list_id parameter in the query string.
+ my $list_id = $params->{list_id};
+ if (!$list_id) {
+
+ # If not, check for "list_id" in the query string of the referer.
+ my $referer = $cgi->referer;
+ if ($referer) {
+ my $uri = URI->new($referer);
+ if ($uri->path =~ /buglist\.cgi$/) {
+ $list_id = $uri->query_param('list_id') || $uri->query_param('regetlastlist');
}
+ }
+ }
- if ($list_id && $list_id ne 'cookie') {
- # If we got a bad list_id (either some other user's or an expired
- # one) don't crash, just don't return that list.
- my $search = Bugzilla::Search::Recent->check_quietly(
- { id => $list_id });
- return $search if $search;
- }
+ if ($list_id && $list_id ne 'cookie') {
- # If there's no list_id, see if the current bug's id is contained
- # in any of the user's saved lists.
- my $search = $self->recent_search_containing($bug->id);
- return $search if $search;
+ # If we got a bad list_id (either some other user's or an expired
+ # one) don't crash, just don't return that list.
+ my $search = Bugzilla::Search::Recent->check_quietly({id => $list_id});
+ return $search if $search;
}
- # Finally (or always, if we're logged out), if there's a BUGLIST cookie
- # and the selected bug is in the list, then return the cookie as a fake
- # Search::Recent object.
- if (my $list = $cgi->cookie('BUGLIST')) {
- # Also split on colons, which was used as a separator in old cookies.
- my @bug_ids = split(/[:-]/, $list);
- if (grep { $_ == $bug->id } @bug_ids) {
- my $search = Bugzilla::Search::Recent->new_from_cookie(\@bug_ids);
- return $search;
- }
+ # If there's no list_id, see if the current bug's id is contained
+ # in any of the user's saved lists.
+ my $search = $self->recent_search_containing($bug->id);
+ return $search if $search;
+ }
+
+ # Finally (or always, if we're logged out), if there's a BUGLIST cookie
+ # and the selected bug is in the list, then return the cookie as a fake
+ # Search::Recent object.
+ if (my $list = $cgi->cookie('BUGLIST')) {
+
+ # Also split on colons, which was used as a separator in old cookies.
+ my @bug_ids = split(/[:-]/, $list);
+ if (grep { $_ == $bug->id } @bug_ids) {
+ my $search = Bugzilla::Search::Recent->new_from_cookie(\@bug_ids);
+ return $search;
}
+ }
- return undef;
+ return undef;
}
sub save_last_search {
- my ($self, $params) = @_;
- my ($bug_ids, $order, $vars, $list_id) =
- @$params{qw(bugs order vars list_id)};
-
- my $cgi = Bugzilla->cgi;
- if ($order) {
- $cgi->send_cookie(-name => 'LASTORDER',
- -value => $order,
- -expires => 'Fri, 01-Jan-2038 00:00:00 GMT');
- }
+ my ($self, $params) = @_;
+ my ($bug_ids, $order, $vars, $list_id) = @$params{qw(bugs order vars list_id)};
+
+ my $cgi = Bugzilla->cgi;
+ if ($order) {
+ $cgi->send_cookie(
+ -name => 'LASTORDER',
+ -value => $order,
+ -expires => 'Fri, 01-Jan-2038 00:00:00 GMT'
+ );
+ }
- return if !@$bug_ids;
-
- my $search;
- if ($self->id) {
- on_main_db {
- if ($list_id) {
- $search = Bugzilla::Search::Recent->check_quietly({ id => $list_id });
- }
-
- if ($search) {
- if (join(',', @{$search->bug_list}) ne join(',', @$bug_ids)) {
- $search->set_bug_list($bug_ids);
- }
- if (!$search->list_order || $order ne $search->list_order) {
- $search->set_list_order($order);
- }
- $search->update();
- }
- else {
- # If we already have an existing search with a totally
- # identical bug list, then don't create a new one. This
- # prevents people from writing over their whole
- # recent-search list by just refreshing a saved search
- # (which doesn't have list_id in the header) over and over.
- my $list_string = join(',', @$bug_ids);
- my $existing_search = Bugzilla::Search::Recent->match({
- user_id => $self->id, bug_list => $list_string });
-
- if (!scalar(@$existing_search)) {
- $search = Bugzilla::Search::Recent->create({
- user_id => $self->id,
- bug_list => $bug_ids,
- list_order => $order });
- }
- else {
- $search = $existing_search->[0];
- }
- }
- };
- delete $self->{recent_searches};
- }
- # Logged-out users use a cookie to store a single last search. We don't
- # override that cookie with the logged-in user's latest search, because
- # if they did one search while logged out and another while logged in,
- # they may still want to navigate through the search they made while
- # logged out.
- else {
- my $bug_list = join('-', @$bug_ids);
- if (length($bug_list) < 4000) {
- $cgi->send_cookie(-name => 'BUGLIST',
- -value => $bug_list,
- -expires => 'Fri, 01-Jan-2038 00:00:00 GMT');
+ return if !@$bug_ids;
+
+ my $search;
+ if ($self->id) {
+ on_main_db {
+ if ($list_id) {
+ $search = Bugzilla::Search::Recent->check_quietly({id => $list_id});
+ }
+
+ if ($search) {
+ if (join(',', @{$search->bug_list}) ne join(',', @$bug_ids)) {
+ $search->set_bug_list($bug_ids);
+ }
+ if (!$search->list_order || $order ne $search->list_order) {
+ $search->set_list_order($order);
+ }
+ $search->update();
+ }
+ else {
+ # If we already have an existing search with a totally
+ # identical bug list, then don't create a new one. This
+ # prevents people from writing over their whole
+ # recent-search list by just refreshing a saved search
+ # (which doesn't have list_id in the header) over and over.
+ my $list_string = join(',', @$bug_ids);
+ my $existing_search = Bugzilla::Search::Recent->match(
+ {user_id => $self->id, bug_list => $list_string});
+
+ if (!scalar(@$existing_search)) {
+ $search
+ = Bugzilla::Search::Recent->create({
+ user_id => $self->id, bug_list => $bug_ids, list_order => $order
+ });
}
else {
- $cgi->remove_cookie('BUGLIST');
- $vars->{'toolong'} = 1;
+ $search = $existing_search->[0];
}
+ }
+ };
+ delete $self->{recent_searches};
+ }
+
+ # Logged-out users use a cookie to store a single last search. We don't
+ # override that cookie with the logged-in user's latest search, because
+ # if they did one search while logged out and another while logged in,
+ # they may still want to navigate through the search they made while
+ # logged out.
+ else {
+ my $bug_list = join('-', @$bug_ids);
+ if (length($bug_list) < 4000) {
+ $cgi->send_cookie(
+ -name => 'BUGLIST',
+ -value => $bug_list,
+ -expires => 'Fri, 01-Jan-2038 00:00:00 GMT'
+ );
}
- return $search;
+ else {
+ $cgi->remove_cookie('BUGLIST');
+ $vars->{'toolong'} = 1;
+ }
+ }
+ return $search;
}
sub 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;
+
+ # Skip the group if we have already checked it
+ if (!$checked_groups{$member_id}) {
- Bugzilla->memcached->set_config({
- key => $user_groups_key,
- data => $groups,
- });
+ # 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];
+
+ Bugzilla->memcached->set_config({key => $user_groups_key, data => $groups,});
+ }
- $self->{groups} = Bugzilla::Group->new_from_list($groups);
- return $self->{groups};
+ $self->{groups} = Bugzilla::Group->new_from_list($groups);
+ return $self->{groups};
}
sub force_bug_dissociation {
- my ($self, $nobody, $groups, $timestamp) = @_;
- my $dbh = Bugzilla->dbh;
- my $auto_user = Bugzilla->user;
- $timestamp //= $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
-
- my $group_marks = join(", ", ('?') x @$groups);
- my $user_id = $self->id;
- my @params = ($user_id, $user_id, $user_id, $user_id,
- map { blessed $_ ? $_->id : $_ } @$groups);
- my $bugs = $dbh->selectall_arrayref(qq{
+ my ($self, $nobody, $groups, $timestamp) = @_;
+ my $dbh = Bugzilla->dbh;
+ my $auto_user = Bugzilla->user;
+ $timestamp //= $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
+
+ my $group_marks = join(", ", ('?') x @$groups);
+ my $user_id = $self->id;
+ my @params = (
+ $user_id, $user_id, $user_id, $user_id,
+ map { blessed $_ ? $_->id : $_ } @$groups
+ );
+ my $bugs = $dbh->selectall_arrayref(
+ qq{
SELECT
bugs.bug_id,
bugs.reporter_accessible,
@@ -1157,99 +1189,103 @@ sub force_bug_dissociation {
OR match_assignee
OR match_qa_contact
OR match_cc
- }, { Slice => {} }, @params);
-
- my @reporter_bugs = map { $_->{bug_id} } grep { $_->{match_reporter} } @$bugs;
- my @assignee_bugs = map { $_->{bug_id} } grep { $_->{match_assignee} } @$bugs;
- my @qa_bugs = map { $_->{bug_id} } grep { $_->{match_qa_contact} } @$bugs;
- my @cc_bugs = map { $_->{bug_id} } grep { $_->{match_cc} } @$bugs;
-
- # Reporter - set reporter_accessible to false
- my $reporter_accessible_field_id = get_field_id('reporter_accessible');
- foreach my $bug_id (@reporter_bugs) {
- $dbh->do(
- q{INSERT INTO bugs_activity (bug_id, who, bug_when, fieldid, removed, added)
- VALUES (?, ?, ?, ?, ?, ?)},
- undef, $bug_id, $auto_user->id, $timestamp, $reporter_accessible_field_id, 1, 0);
- $dbh->do(
- q{UPDATE bugs SET reporter_accessible = 0, delta_ts = ?, lastdiffed = ?
- WHERE bug_id = ?},
- undef, $timestamp, $timestamp, $bug_id);
- }
-
- # Assignee
- my $assigned_to_field_id = get_field_id('assigned_to');
- foreach my $bug_id (@assignee_bugs) {
- $dbh->do(
- q{INSERT INTO bugs_activity (bug_id, who, bug_when, fieldid, removed, added)
- VALUES (?, ?, ?, ?, ?, ?)},
- undef, $bug_id, $auto_user->id, $timestamp, $assigned_to_field_id,
- $self->login, $auto_user->login);
- $dbh->do(
- q{UPDATE bugs SET assigned_to = ?, delta_ts = ?, lastdiffed = ?
- WHERE bug_id = ?},
- undef, $nobody->id, $timestamp, $timestamp, $bug_id);
- }
-
- # QA Contact
- my $qa_field_id = get_field_id('qa_contact');
- foreach my $bug_id (@qa_bugs) {
- $dbh->do(
- q{INSERT INTO bugs_activity (bug_id, who, bug_when, fieldid, removed, added)
- VALUES (?, ?, ?, ?, ?, '')},
- undef, $bug_id, $auto_user->id, $timestamp, $qa_field_id, $self->login);
- $dbh->do(
- q{UPDATE bugs SET qa_contact = NULL, delta_ts = ?, lastdiffed = ?
- WHERE bug_id = ?},
- undef, $timestamp, $timestamp, $bug_id);
- }
-
- # CC list
- my $cc_field_id = get_field_id('cc');
- foreach my $bug_id (@cc_bugs) {
- $dbh->do(
- q{INSERT INTO bugs_activity (bug_id, who, bug_when, fieldid, removed, added)
- VALUES (?, ?, ?, ?, ?, '')},
- undef, $bug_id, $auto_user->id, $timestamp, $cc_field_id, $self->login);
- $dbh->do(q{DELETE FROM cc WHERE bug_id = ? AND who = ?},
- undef, $bug_id, $self->id);
- }
+ }, {Slice => {}}, @params
+ );
+
+ my @reporter_bugs = map { $_->{bug_id} } grep { $_->{match_reporter} } @$bugs;
+ my @assignee_bugs = map { $_->{bug_id} } grep { $_->{match_assignee} } @$bugs;
+ my @qa_bugs = map { $_->{bug_id} } grep { $_->{match_qa_contact} } @$bugs;
+ my @cc_bugs = map { $_->{bug_id} } grep { $_->{match_cc} } @$bugs;
+
+ # Reporter - set reporter_accessible to false
+ my $reporter_accessible_field_id = get_field_id('reporter_accessible');
+ foreach my $bug_id (@reporter_bugs) {
+ $dbh->do(
+ q{INSERT INTO bugs_activity (bug_id, who, bug_when, fieldid, removed, added)
+ VALUES (?, ?, ?, ?, ?, ?)}, undef, $bug_id, $auto_user->id, $timestamp,
+ $reporter_accessible_field_id, 1, 0
+ );
+ $dbh->do(
+ q{UPDATE bugs SET reporter_accessible = 0, delta_ts = ?, lastdiffed = ?
+ WHERE bug_id = ?}, undef, $timestamp, $timestamp, $bug_id
+ );
+ }
+
+ # Assignee
+ my $assigned_to_field_id = get_field_id('assigned_to');
+ foreach my $bug_id (@assignee_bugs) {
+ $dbh->do(
+ q{INSERT INTO bugs_activity (bug_id, who, bug_when, fieldid, removed, added)
+ VALUES (?, ?, ?, ?, ?, ?)}, undef, $bug_id, $auto_user->id, $timestamp,
+ $assigned_to_field_id, $self->login, $auto_user->login
+ );
+ $dbh->do(
+ q{UPDATE bugs SET assigned_to = ?, delta_ts = ?, lastdiffed = ?
+ WHERE bug_id = ?}, undef, $nobody->id, $timestamp, $timestamp, $bug_id
+ );
+ }
+
+ # QA Contact
+ my $qa_field_id = get_field_id('qa_contact');
+ foreach my $bug_id (@qa_bugs) {
+ $dbh->do(
+ q{INSERT INTO bugs_activity (bug_id, who, bug_when, fieldid, removed, added)
+ VALUES (?, ?, ?, ?, ?, '')}, undef, $bug_id, $auto_user->id, $timestamp,
+ $qa_field_id, $self->login
+ );
+ $dbh->do(
+ q{UPDATE bugs SET qa_contact = NULL, delta_ts = ?, lastdiffed = ?
+ WHERE bug_id = ?}, undef, $timestamp, $timestamp, $bug_id
+ );
+ }
+
+ # CC list
+ my $cc_field_id = get_field_id('cc');
+ foreach my $bug_id (@cc_bugs) {
+ $dbh->do(
+ q{INSERT INTO bugs_activity (bug_id, who, bug_when, fieldid, removed, added)
+ VALUES (?, ?, ?, ?, ?, '')}, undef, $bug_id, $auto_user->id, $timestamp,
+ $cc_field_id, $self->login
+ );
+ $dbh->do(q{DELETE FROM cc WHERE bug_id = ? AND who = ?},
+ undef, $bug_id, $self->id);
+ }
- if (@reporter_bugs || @assignee_bugs || @qa_bugs || @cc_bugs) {
- $self->clear_last_statistics_ts();
+ if (@reporter_bugs || @assignee_bugs || @qa_bugs || @cc_bugs) {
+ $self->clear_last_statistics_ts();
- # It's complex to determine which items now need to be flushed from memcached.
- # As this is expected to be a rare event, we just flush the entire cache.
- Bugzilla->memcached->clear_all();
- }
+ # It's complex to determine which items now need to be flushed from memcached.
+ # As this is expected to be a rare event, we just flush the entire cache.
+ Bugzilla->memcached->clear_all();
+ }
- return $bugs;
+ return $bugs;
}
sub last_visited {
- my ($self) = @_;
+ my ($self) = @_;
- return Bugzilla::BugUserLastVisit->match({ user_id => $self->id });
+ return Bugzilla::BugUserLastVisit->match({user_id => $self->id});
}
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;
+ }
- # BMO - Bug mentors are considered involved with the bug
- return 1 if $bug->is_mentor($self);
+ # BMO - Bug mentors are considered involved with the bug
+ return 1 if $bug->is_mentor($self);
- return unless $bug->cc;
- return any { $user_login eq $_ } @{ $bug->cc };
+ return unless $bug->cc;
+ return any { $user_login eq $_ } @{$bug->cc};
}
# It turns out that calling ->id on objects a few hundred thousand
@@ -1257,228 +1293,239 @@ 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 groups_owned {
- my $self = shift;
- return $self->{groups_owned} //= Bugzilla::Group->match({ owner_user_id => $self->id });
+ my $self = shift;
+ return $self->{groups_owned}
+ //= Bugzilla::Group->match({owner_user_id => $self->id});
}
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('admin')) {
- # Users having admin permissions may bless all groups.
- $self->{'bless_groups'} = [Bugzilla::Group->get_all];
- return $self->{'bless_groups'};
- }
+ if ($self->in_group('admin')) {
- if (Bugzilla->params->{usevisibilitygroups}
- && !$self->visible_groups_inherited) {
- return [];
- }
+ # Users having admin 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;
- $self->{in_group} //= { map { $_->name => $_ } @{ $self->groups } };
+ my ($self, $group, $product_id) = @_;
+ $group = $group->name if blessed $group;
+ $self->{in_group} //= {map { $_->name => $_ } @{$self->groups}};
- if ($self->{in_group}{$group}) {
- 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
+ if ($self->{in_group}{$group}) {
+ 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) = @_;
- my $product_ids = Bugzilla->dbh->selectcol_arrayref(
- "SELECT DISTINCT product_id
+ # 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
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;
- my $has_external_groups =
- $dbh->selectrow_array('SELECT 1
+ 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) {
- my $dbh = Bugzilla->dbh;
- my $user_id = $self->id;
-
- 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) = @_;
- 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,
+ # 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) {
+ my $dbh = Bugzilla->dbh;
+ my $user_id = $self->id;
+
+ 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 $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
@@ -1488,1107 +1535,1173 @@ sub visible_bugs {
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;
- }
+ 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;
- }
+ $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;
}
+ }
- return [grep { $visible_cache->{blessed $_ ? $_->id : $_} } @$bugs];
+ return [grep { $visible_cache->{blessed $_ ? $_->id : $_} } @$bugs];
}
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 any { $_->name eq $product_name } @{$self->get_selectable_products};
+ return any { $_->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;
-
- 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 .
- " AND group_id NOT IN(" . $self->groups_as_string . ") " .
- " WHERE group_id IS NULL " .
- "ORDER BY name";
-
- 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 $self = shift;
+ my $class_id = shift;
+ my $class_restricted = Bugzilla->params->{'useclassification'} && $class_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
+ . " AND group_id NOT IN("
+ . $self->groups_as_string . ") "
+ . " WHERE group_id IS NULL "
+ . "ORDER BY name";
+
+ my $prod_ids = Bugzilla->dbh->selectcol_arrayref($query);
+ $self->{selectable_products} = Bugzilla::Product->new_from_list($prod_ids);
+ }
+
+ # Restrict the list of products to those being in the classification, if any.
+ if ($class_restricted) {
+ return [grep { $_->classification_id == $class_id }
+ @{$self->{selectable_products}}];
+ }
+
+ # If we come here, then we want all selectable products.
+ return $self->{selectable_products};
}
sub get_selectable_classifications {
- my ($self) = @_;
+ my ($self) = @_;
- if (!defined $self->{selectable_classifications}) {
- my $products = $self->get_selectable_products;
- my %class_ids = map { $_->classification_id => 1 } @$products;
+ if (!defined $self->{selectable_classifications}) {
+ my $products = $self->get_selectable_products;
+ my %class_ids = map { $_->classification_id => 1 } @$products;
- $self->{selectable_classifications} = Bugzilla::Classification->new_from_list([keys %class_ids]);
- }
- return $self->{selectable_classifications};
+ $self->{selectable_classifications}
+ = Bugzilla::Classification->new_from_list([keys %class_ids]);
+ }
+ return $self->{selectable_classifications};
}
sub can_enter_product {
- my ($self, $input, $warn) = @_;
- my $dbh = Bugzilla->dbh;
- $warn ||= 0;
-
- $input = trim($input) if !ref $input;
- if (!defined $input or $input eq '') {
- return unless $warn == THROW_ERROR;
- ThrowUserError('object_not_specified',
- { class => 'Bugzilla::Product' });
- }
+ my ($self, $input, $warn) = @_;
+ my $dbh = Bugzilla->dbh;
+ $warn ||= 0;
- if (!scalar @{ $self->get_enterable_products }) {
- return unless $warn == THROW_ERROR;
- ThrowUserError('no_products');
- }
+ $input = trim($input) if !ref $input;
+ if (!defined $input or $input eq '') {
+ return unless $warn == THROW_ERROR;
+ ThrowUserError('object_not_specified', {class => 'Bugzilla::Product'});
+ }
- my $product = blessed($input) ? $input
- : new Bugzilla::Product({ name => $input });
- my $can_enter =
- $product && grep($_->name eq $product->name,
- @{ $self->get_enterable_products });
+ if (!scalar @{$self->get_enterable_products}) {
+ return unless $warn == THROW_ERROR;
+ ThrowUserError('no_products');
+ }
- return $product if $can_enter;
+ my $product
+ = blessed($input) ? $input : new Bugzilla::Product({name => $input});
+ my $can_enter = $product
+ && grep($_->name eq $product->name, @{$self->get_enterable_products});
- return 0 unless $warn == THROW_ERROR;
+ return $product if $can_enter;
- # Check why access was denied. These checks are slow,
- # but that's fine, because they only happen if we fail.
+ return 0 unless $warn == THROW_ERROR;
- # We don't just use $product->name for error messages, because if it
- # changes case from $input, then that's a clue that the product does
- # exist but is hidden.
- my $name = blessed($input) ? $input->name : $input;
+ # Check why access was denied. These checks are slow,
+ # but that's fine, because they only happen if we fail.
- # The product could not exist or you could be denied...
- if (!$product || !$product->user_has_access($self)) {
- ThrowUserError('entry_access_denied', { product => $name });
- }
- # It could be closed for bug entry...
- elsif (!$product->is_active) {
- ThrowUserError('product_disabled', { product => $product });
- }
- # It could have no components...
- elsif (!@{$product->components}
- || !grep { $_->is_active } @{$product->components})
- {
- ThrowUserError('missing_component', { product => $product });
- }
- # It could have no versions...
- elsif (!@{$product->versions}
- || !grep { $_->is_active } @{$product->versions})
- {
- ThrowUserError ('missing_version', { product => $product });
- }
+ # We don't just use $product->name for error messages, because if it
+ # changes case from $input, then that's a clue that the product does
+ # exist but is hidden.
+ my $name = blessed($input) ? $input->name : $input;
+
+ # The product could not exist or you could be denied...
+ if (!$product || !$product->user_has_access($self)) {
+ ThrowUserError('entry_access_denied', {product => $name});
+ }
+
+ # It could be closed for bug entry...
+ elsif (!$product->is_active) {
+ ThrowUserError('product_disabled', {product => $product});
+ }
+
+ # 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.";
+ 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 $enterable_ids = $dbh->selectcol_arrayref(
- 'SELECT products.id FROM products
+ # All products which the user has "Entry" access to.
+ my $enterable_ids = $dbh->selectcol_arrayref(
+ 'SELECT products.id 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 (' . $self->groups_as_string . ')
WHERE group_id IS NULL
- AND products.isactive = 1');
-
- 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
+ AND products.isactive = 1'
+ );
+
+ if (scalar @$enterable_ids) {
+
+ # And all of these products must have at least one component
+ # and one version.
+ $enterable_ids = $dbh->selectcol_arrayref(
+ 'SELECT DISTINCT products.id FROM products
+ WHERE '
+ . $dbh->sql_in('products.id', $enterable_ids)
+ . ' AND products.id IN (SELECT DISTINCT components.product_id
FROM components
WHERE components.isactive = 1)
AND products.id IN (SELECT DISTINCT versions.product_id
FROM versions
- WHERE versions.isactive = 1)');
- }
+ WHERE versions.isactive = 1)'
+ );
+ }
- $self->{enterable_products} =
- Bugzilla::Product->new_from_list($enterable_ids);
- return $self->{enterable_products};
+ $self->{enterable_products} = Bugzilla::Product->new_from_list($enterable_ids);
+ return $self->{enterable_products};
}
sub can_access_product {
- my ($self, $product) = @_;
- my $product_name = blessed($product) ? $product->name : $product;
- return scalar(grep {$_->name eq $product_name} @{$self->get_accessible_products});
+ my ($self, $product) = @_;
+ my $product_name = blessed($product) ? $product->name : $product;
+ return
+ scalar(grep { $_->name eq $product_name } @{$self->get_accessible_products});
}
sub get_accessible_products {
- my $self = shift;
+ 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};
+ # 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 ];
+ return [sort { $a->name cmp $b->name } values %products];
}
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 him 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 him 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_change_flag {
- my ($self, $flag_type, $old_status, $new_status) = @_;
+ my ($self, $flag_type, $old_status, $new_status) = @_;
- # "old_status:new_status" => [OR conditions
- state $flag_transitions = {
- 'X:-' => ['grant_group'],
- 'X:+' => ['grant_group'],
- 'X:?' => ['request_group'],
+ # "old_status:new_status" => [OR conditions
+ state $flag_transitions = {
+ 'X:-' => ['grant_group'],
+ 'X:+' => ['grant_group'],
+ 'X:?' => ['request_group'],
- '?:X' => ['request_group', 'is_setter'],
- '?:-' => ['grant_group'],
- '?:+' => ['grant_group'],
+ '?:X' => ['request_group', 'is_setter'],
+ '?:-' => ['grant_group'],
+ '?:+' => ['grant_group'],
- '+:X' => ['grant_group'],
- '+:-' => ['grant_group'],
- '+:?' => ['grant_group'],
+ '+:X' => ['grant_group'],
+ '+:-' => ['grant_group'],
+ '+:?' => ['grant_group'],
- '-:X' => ['grant_group'],
- '-:+' => ['grant_group'],
- '-:?' => ['grant_group'],
- };
+ '-:X' => ['grant_group'],
+ '-:+' => ['grant_group'],
+ '-:?' => ['grant_group'],
+ };
- return 1 if $new_status eq $old_status;
+ return 1 if $new_status eq $old_status;
- my $action = "$old_status:$new_status";
- my %bool = (
- request_group => $self->can_request_flag($flag_type),
- grant_group => $self->can_set_flag($flag_type),
- is_setter => $self->id == Bugzilla->user->id,
- );
+ my $action = "$old_status:$new_status";
+ my %bool = (
+ request_group => $self->can_request_flag($flag_type),
+ grant_group => $self->can_set_flag($flag_type),
+ is_setter => $self->id == Bugzilla->user->id,
+ );
- my $cond = $flag_transitions->{$action};
- if ($cond) {
- if (any { $bool{ $_ } } @$cond) {
- return 1;
- }
- else {
- return 0;
- }
+ my $cond = $flag_transitions->{$action};
+ if ($cond) {
+ if (any { $bool{$_} } @$cond) {
+ return 1;
}
else {
- warn "unknown flag transition blocked: $action";
- return 0;
+ return 0;
}
+ }
+ else {
+ warn "unknown flag transition blocked: $action";
+ return 0;
+ }
}
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;
}
sub can_unset_flag {
- my ($self, $flag_type, $flag_status) = @_;
- return 1 if !$flag_type->grant_group_id;
- return 1 if ($flag_status ne '+' && $flag_status ne '-');
- return $self->in_group_id($flag_type->grant_group_id) ? 1 : 0;
+ my ($self, $flag_type, $flag_status) = @_;
+ return 1 if !$flag_type->grant_group_id;
+ return 1 if ($flag_status ne '+' && $flag_status ne '-');
+ return $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 $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;
+ my $dbh = Bugzilla->dbh;
+ my $sth;
- if (Bugzilla->params->{'usevisibilitygroups'}) {
- $sth = $dbh->prepare("SELECT DISTINCT grantor_id
+ 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));
-
- unless ($list) {
- $self->{'product_resp'} = [];
- return $self->{'product_resp'};
- }
+ {Slice => {}}, ($self->id, $self->id, $self->id)
+ );
- 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(@_)) {
- # Otherwise, we're checking a specific group
- my $group_id = shift;
- return grep($_->id == $group_id, @{ $self->bless_groups }) ? 1 : 0;
+ # 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;
}
sub match {
- # Generates a list of users whose login name (email address) or real name
- # matches a substring or wildcard.
- # This is also called if matches are disabled (for error checking), but
- # in this case only the exact match code will end up running.
-
- # $str contains the string to match, while $limit contains the
- # maximum number of records to retrieve.
- my ($str, $limit, $exclude_disabled) = @_;
- my $user = Bugzilla->user;
- my $dbh = Bugzilla->dbh;
- $str = trim($str);
+ # Generates a list of users whose login name (email address) or real name
+ # matches a substring or wildcard.
+ # This is also called if matches are disabled (for error checking), but
+ # in this case only the exact match code will end up running.
- my @users = ();
- return \@users if $str =~ /^\s*$/;
+ # $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;
- # 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.
+ $str = trim($str);
- # first try wildcards
- my $wildstr = $str;
+ my @users = ();
+ return \@users if $str =~ /^\s*$/;
- # 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;
+ # 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.
- # Execute the query, retrieve the results, and make them into
- # User objects.
- my $user_ids = $dbh->selectcol_arrayref($query, undef, ($wildstr, $wildstr));
- @users = @{Bugzilla::User->new_from_list($user_ids)};
- }
- else { # try an exact match
- # Exact matches don't care if a user is disabled.
- trick_taint($str);
- my $user_id = $dbh->selectrow_array('SELECT userid FROM profiles
- WHERE ' . $dbh->sql_istrcmp('login_name', '?'),
- undef, $str);
-
- push(@users, new Bugzilla::User($user_id)) if $user_id;
+ # first try wildcards
+ my $wildstr = $str;
+
+ # Do not do wildcards if there is no '*' in the string.
+ if ($wildstr =~ s/\*/\%/g && $user->id) {
+
+ # Build the query.
+ trick_taint($wildstr);
+ my $query = "SELECT DISTINCT userid FROM profiles ";
+ if (Bugzilla->params->{'usevisibilitygroups'}) {
+ $query .= "INNER JOIN user_group_map
+ ON user_group_map.user_id = profiles.userid ";
}
+ $query
+ .= "WHERE ("
+ . $dbh->sql_istrcmp('login_name', '?', "LIKE") . " OR "
+ . $dbh->sql_istrcmp('realname', '?', "LIKE") . ") ";
+ if (Bugzilla->params->{'usevisibilitygroups'}) {
+ $query
+ .= "AND isbless = 0 "
+ . "AND group_id IN("
+ . join(', ', (-1, @{$user->visible_groups_inherited})) . ") ";
+ }
+ $query .= " AND is_enabled = 1 " if $exclude_disabled;
+ $query .= $dbh->sql_limit($limit) if $limit;
+
+ # Execute the query, retrieve the results, and make them into
+ # User objects.
+ my $user_ids = $dbh->selectcol_arrayref($query, undef, ($wildstr, $wildstr));
+ @users = @{Bugzilla::User->new_from_list($user_ids)};
+ }
+ else { # try an exact match
+ # Exact matches don't care if a user is disabled.
+ trick_taint($str);
+ my $user_id = $dbh->selectrow_array(
+ 'SELECT userid FROM profiles
+ WHERE '
+ . $dbh->sql_istrcmp('login_name', '?'), undef, $str
+ );
+
+ push(@users, new Bugzilla::User($user_id)) if $user_id;
+ }
- # then try substring search
- if (!scalar(@users) && length($str) >= 3 && $user->id) {
- trick_taint($str);
+ # then try substring search
+ if (!scalar(@users) && length($str) >= 3 && $user->id) {
+ trick_taint($str);
- my $query = "SELECT DISTINCT userid FROM profiles ";
- if (Bugzilla->params->{'usevisibilitygroups'}) {
- $query .= "INNER JOIN user_group_map
+ my $query = "SELECT DISTINCT userid FROM profiles ";
+ if (Bugzilla->params->{'usevisibilitygroups'}) {
+ $query .= "INNER JOIN user_group_map
ON user_group_map.user_id = profiles.userid ";
- }
- $query .= " WHERE (" .
- $dbh->sql_iposition('?', 'login_name') . " > 0" . " OR " .
- $dbh->sql_iposition('?', 'realname') . " > 0) ";
- if (Bugzilla->params->{'usevisibilitygroups'}) {
- $query .= " AND isbless = 0" .
- " AND group_id IN(" .
- join(', ', (-1, @{$user->visible_groups_inherited})) . ") ";
- }
- $query .= " AND is_enabled = 1 " if $exclude_disabled;
- $query .= $dbh->sql_limit($limit) if $limit;
- my $user_ids = $dbh->selectcol_arrayref($query, undef, ($str, $str));
- @users = @{Bugzilla::User->new_from_list($user_ids)};
}
- return \@users;
+ $query
+ .= " WHERE ("
+ . $dbh->sql_iposition('?', 'login_name') . " > 0" . " OR "
+ . $dbh->sql_iposition('?', 'realname')
+ . " > 0) ";
+ if (Bugzilla->params->{'usevisibilitygroups'}) {
+ $query
+ .= " AND isbless = 0"
+ . " AND group_id IN("
+ . join(', ', (-1, @{$user->visible_groups_inherited})) . ") ";
+ }
+ $query .= " AND is_enabled = 1 " if $exclude_disabled;
+ $query .= $dbh->sql_limit($limit) if $limit;
+ my $user_ids = $dbh->selectcol_arrayref($query, undef, ($str, $str));
+ @users = @{Bugzilla::User->new_from_list($user_ids)};
+ }
+ return \@users;
}
sub match_field {
- my $fields = shift; # arguments as a hash
- my $data = shift || Bugzilla->input_params; # hash to look up fields in
- my $behavior = shift || 0; # A constant that tells us how to act
- my $matches = {}; # the values sent to the template
- my $matchsuccess = 1; # did the match fail?
- my $need_confirm = 0; # whether to display confirmation screen
- my $match_multiple = 0; # whether we ever matched more than one user
- my @non_conclusive_fields; # fields which don't have a unique user.
-
- my $params = Bugzilla->params;
-
- # prepare default form values
-
- # Fields can be regular expressions matching multiple form fields
- # (f.e. "requestee-(\d+)"), so expand each non-literal field
- # into the list of form fields it matches.
- my $expanded_fields = {};
- foreach my $field_pattern (keys %{$fields}) {
- # Check if the field has any non-word characters. Only those fields
- # can be regular expressions, so don't expand the field if it doesn't
- # have any of those characters.
- if ($field_pattern =~ /^\w+$/) {
- $expanded_fields->{$field_pattern} = $fields->{$field_pattern};
- }
- else {
- my @field_names = grep(/$field_pattern/, keys %$data);
-
- foreach my $field_name (@field_names) {
- $expanded_fields->{$field_name} =
- { type => $fields->{$field_pattern}->{'type'} };
-
- # The field is a requestee field; in order for its name
- # to show up correctly on the confirmation page, we need
- # to find out the name of its flag type.
- if ($field_name =~ /^requestee(_type)?-(\d+)$/) {
- my $flag_type;
- if ($1) {
- require Bugzilla::FlagType;
- $flag_type = new Bugzilla::FlagType($2);
- }
- else {
- require Bugzilla::Flag;
- my $flag = new Bugzilla::Flag($2);
- $flag_type = $flag->type if $flag;
- }
- if ($flag_type) {
- $expanded_fields->{$field_name}->{'flag_type'} = $flag_type;
- }
- else {
- # No need to look for a valid requestee if the flag(type)
- # has been deleted (may occur in race conditions).
- delete $expanded_fields->{$field_name};
- delete $data->{$field_name};
- }
- }
- }
+ my $fields = shift; # arguments as a hash
+ my $data = shift || Bugzilla->input_params; # hash to look up fields in
+ my $behavior = shift || 0; # A constant that tells us how to act
+ my $matches = {}; # the values sent to the template
+ my $matchsuccess = 1; # did the match fail?
+ my $need_confirm = 0; # whether to display confirmation screen
+ my $match_multiple = 0; # whether we ever matched more than one user
+ my @non_conclusive_fields; # fields which don't have a unique user.
+
+ my $params = Bugzilla->params;
+
+ # prepare default form values
+
+ # Fields can be regular expressions matching multiple form fields
+ # (f.e. "requestee-(\d+)"), so expand each non-literal field
+ # into the list of form fields it matches.
+ my $expanded_fields = {};
+ foreach my $field_pattern (keys %{$fields}) {
+
+ # Check if the field has any non-word characters. Only those fields
+ # can be regular expressions, so don't expand the field if it doesn't
+ # have any of those characters.
+ if ($field_pattern =~ /^\w+$/) {
+ $expanded_fields->{$field_pattern} = $fields->{$field_pattern};
+ }
+ else {
+ my @field_names = grep(/$field_pattern/, keys %$data);
+
+ foreach my $field_name (@field_names) {
+ $expanded_fields->{$field_name} = {type => $fields->{$field_pattern}->{'type'}};
+
+ # The field is a requestee field; in order for its name
+ # to show up correctly on the confirmation page, we need
+ # to find out the name of its flag type.
+ if ($field_name =~ /^requestee(_type)?-(\d+)$/) {
+ my $flag_type;
+ if ($1) {
+ require Bugzilla::FlagType;
+ $flag_type = new Bugzilla::FlagType($2);
+ }
+ else {
+ require Bugzilla::Flag;
+ my $flag = new Bugzilla::Flag($2);
+ $flag_type = $flag->type if $flag;
+ }
+ if ($flag_type) {
+ $expanded_fields->{$field_name}->{'flag_type'} = $flag_type;
+ }
+ else {
+ # No need to look for a valid requestee if the flag(type)
+ # has been deleted (may occur in race conditions).
+ delete $expanded_fields->{$field_name};
+ delete $data->{$field_name};
+ }
}
+ }
}
- $fields = $expanded_fields;
+ }
+ $fields = $expanded_fields;
- foreach my $field (keys %{$fields}) {
- next unless defined $data->{$field};
+ foreach my $field (keys %{$fields}) {
+ next unless defined $data->{$field};
- #Concatenate login names, so that we have a common way to handle them.
- my $raw_field;
- if (ref $data->{$field}) {
- $raw_field = join(",", @{$data->{$field}});
- }
- else {
- $raw_field = $data->{$field};
- }
- $raw_field = clean_text($raw_field || '');
-
- # Now we either split $raw_field by spaces/commas and put the list
- # into @queries, or in the case of fields which only accept single
- # entries, we simply use the verbatim text.
- my @queries;
- if ($fields->{$field}->{'type'} eq 'single') {
- @queries = ($raw_field);
- # We will repopulate it later if a match is found, else it must
- # be set to an empty string so that the field remains defined.
- $data->{$field} = '';
- }
- elsif ($fields->{$field}->{'type'} eq 'multi') {
- @queries = split(/[,;]+/, $raw_field);
- # We will repopulate it later if a match is found, else it must
- # be undefined.
- delete $data->{$field};
- }
- else {
- # bad argument
- ThrowCodeError('bad_arg',
- { argument => $fields->{$field}->{'type'},
- function => 'Bugzilla::User::match_field',
- });
- }
+ #Concatenate login names, so that we have a common way to handle them.
+ my $raw_field;
+ if (ref $data->{$field}) {
+ $raw_field = join(",", @{$data->{$field}});
+ }
+ else {
+ $raw_field = $data->{$field};
+ }
+ $raw_field = clean_text($raw_field || '');
- # Tolerate fields that do not exist (in case you specify
- # e.g. the QA contact, and it's currently not in use).
- next unless (defined $raw_field && $raw_field ne '');
+ # Now we either split $raw_field by spaces/commas and put the list
+ # into @queries, or in the case of fields which only accept single
+ # entries, we simply use the verbatim text.
+ my @queries;
+ if ($fields->{$field}->{'type'} eq 'single') {
+ @queries = ($raw_field);
- my $limit = 0;
- if ($params->{'maxusermatches'}) {
- $limit = $params->{'maxusermatches'} + 1;
- }
+ # We will repopulate it later if a match is found, else it must
+ # be set to an empty string so that the field remains defined.
+ $data->{$field} = '';
+ }
+ elsif ($fields->{$field}->{'type'} eq 'multi') {
+ @queries = split(/[,;]+/, $raw_field);
- my @logins;
- for my $query (@queries) {
- $query = trim($query);
- next if $query eq '';
-
- my $users = match(
- $query, # match string
- $limit, # match limit
- 1 # exclude_disabled
- );
-
- # here is where it checks for multiple matches
- if (scalar(@{$users}) == 1) { # exactly one match
- push(@logins, @{$users}[0]->login);
-
- # skip confirmation for exact matches
- next if (lc(@{$users}[0]->login) eq lc($query));
-
- $matches->{$field}->{$query}->{'status'} = 'success';
- $need_confirm = 1 if $params->{'confirmuniqueusermatch'};
-
- }
- elsif ((scalar(@{$users}) > 1)
- && ($params->{'maxusermatches'} != 1)) {
- $need_confirm = 1;
- $match_multiple = 1;
- push(@non_conclusive_fields, $field);
-
- if (($params->{'maxusermatches'})
- && (scalar(@{$users}) > $params->{'maxusermatches'}))
- {
- $matches->{$field}->{$query}->{'status'} = 'trunc';
- pop @{$users}; # take the last one out
- }
- else {
- $matches->{$field}->{$query}->{'status'} = 'success';
- }
-
- }
- else {
- # everything else fails
- $matchsuccess = 0; # fail
- push(@non_conclusive_fields, $field);
- $matches->{$field}->{$query}->{'status'} = 'fail';
- $need_confirm = 1; # confirmation screen shows failures
- }
-
- $matches->{$field}->{$query}->{'users'} = $users;
+ # We will repopulate it later if a match is found, else it must
+ # be undefined.
+ delete $data->{$field};
+ }
+ else {
+ # bad argument
+ ThrowCodeError(
+ 'bad_arg',
+ {
+ argument => $fields->{$field}->{'type'},
+ function => 'Bugzilla::User::match_field',
}
+ );
+ }
+
+ # Tolerate fields that do not exist (in case you specify
+ # e.g. the QA contact, and it's currently not in use).
+ next unless (defined $raw_field && $raw_field ne '');
+
+ my $limit = 0;
+ if ($params->{'maxusermatches'}) {
+ $limit = $params->{'maxusermatches'} + 1;
+ }
+
+ my @logins;
+ for my $query (@queries) {
+ $query = trim($query);
+ next if $query eq '';
+
+ my $users = match(
+ $query, # match string
+ $limit, # match limit
+ 1 # exclude_disabled
+ );
- # 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] || '';
+ # 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';
}
+
+ }
+ 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;
}
- my $retval;
- if (!$matchsuccess) {
- $retval = USER_MATCH_FAILED;
+ # 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] || '';
}
- elsif ($match_multiple) {
- $retval = USER_MATCH_MULTIPLE;
- }
- else {
- $retval = USER_MATCH_SUCCESS;
+ elsif (scalar @logins) {
+ $data->{$field} = \@logins;
}
+ }
- # Skip confirmation if we were told to, or if we don't need to confirm.
- if ($behavior == MATCH_SKIP_CONFIRM || !$need_confirm) {
- return wantarray ? ($retval, \@non_conclusive_fields) : $retval;
- }
+ my $retval;
+ if (!$matchsuccess) {
+ $retval = USER_MATCH_FAILED;
+ }
+ elsif ($match_multiple) {
+ $retval = USER_MATCH_MULTIPLE;
+ }
+ else {
+ $retval = USER_MATCH_SUCCESS;
+ }
- my $template = Bugzilla->template;
- my $cgi = Bugzilla->cgi;
- my $vars = {};
+ # Skip confirmation if we were told to, or if we don't need to confirm.
+ if ($behavior == MATCH_SKIP_CONFIRM || !$need_confirm) {
+ return wantarray ? ($retval, \@non_conclusive_fields) : $retval;
+ }
- $vars->{'script'} = $cgi->url(-relative => 1); # for self-referencing URLs
- $vars->{'fields'} = $fields; # fields being matched
- $vars->{'matches'} = $matches; # matches that were made
- $vars->{'matchsuccess'} = $matchsuccess; # continue or fail
- $vars->{'matchmultiple'} = $match_multiple;
+ my $template = Bugzilla->template;
+ my $cgi = Bugzilla->cgi;
+ my $vars = {};
- print $cgi->header();
+ $vars->{'script'} = $cgi->url(-relative => 1); # for self-referencing URLs
+ $vars->{'fields'} = $fields; # fields being matched
+ $vars->{'matches'} = $matches; # matches that were made
+ $vars->{'matchsuccess'} = $matchsuccess; # continue or fail
+ $vars->{'matchmultiple'} = $match_multiple;
- $template->process("global/confirm-user-match.html.tmpl", $vars)
- || ThrowTemplateError($template->error());
- exit;
+ print $cgi->header();
+
+ $template->process("global/confirm-user-match.html.tmpl", $vars)
+ || ThrowTemplateError($template->error());
+ exit;
}
# Changes in some fields automatically trigger events. The field names are
# from the fielddefs table.
our %names_to_events = (
- 'resolution' => EVT_OPENED_CLOSED,
- 'keywords' => EVT_KEYWORD,
- 'cc' => EVT_CC,
- 'bug_severity' => EVT_PROJ_MANAGEMENT,
- 'priority' => EVT_PROJ_MANAGEMENT,
- 'bug_status' => EVT_PROJ_MANAGEMENT,
- 'target_milestone' => EVT_PROJ_MANAGEMENT,
- 'attachments.description' => EVT_ATTACHMENT_DATA,
- 'attachments.mimetype' => EVT_ATTACHMENT_DATA,
- 'attachments.ispatch' => EVT_ATTACHMENT_DATA,
- 'dependson' => EVT_DEPEND_BLOCK,
- 'blocked' => EVT_DEPEND_BLOCK,
- 'product' => EVT_COMPONENT,
- 'component' => EVT_COMPONENT);
+ 'resolution' => EVT_OPENED_CLOSED,
+ 'keywords' => EVT_KEYWORD,
+ 'cc' => EVT_CC,
+ 'bug_severity' => EVT_PROJ_MANAGEMENT,
+ 'priority' => EVT_PROJ_MANAGEMENT,
+ 'bug_status' => EVT_PROJ_MANAGEMENT,
+ 'target_milestone' => EVT_PROJ_MANAGEMENT,
+ 'attachments.description' => EVT_ATTACHMENT_DATA,
+ 'attachments.mimetype' => EVT_ATTACHMENT_DATA,
+ 'attachments.ispatch' => EVT_ATTACHMENT_DATA,
+ 'dependson' => EVT_DEPEND_BLOCK,
+ 'blocked' => EVT_DEPEND_BLOCK,
+ 'product' => EVT_COMPONENT,
+ 'component' => EVT_COMPONENT
+);
# Returns true if the user wants mail for a given bug change.
# Note: the "+" signs before the constants suppress bareword quoting.
sub wants_bug_mail {
- my $self = shift;
- my ($bug, $relationship, $fieldDiffs, $comments, $dep_mail, $changer) = @_;
-
- # is_silent_user is true if the username is mentioned in the param `silent_users`
- return 0 if $changer && $changer->is_silent_user;
-
- # Make a list of the events which have happened during this bug change,
- # from the point of view of this user.
- my %events;
- foreach my $change (@$fieldDiffs) {
- my $fieldName = $change->{field_name};
- # A change to any of the above fields sets the corresponding event
- if (defined($names_to_events{$fieldName})) {
- $events{$names_to_events{$fieldName}} = 1;
- }
- else {
- # Catch-all for any change not caught by a more specific event
- $events{+EVT_OTHER} = 1;
- }
+ my $self = shift;
+ my ($bug, $relationship, $fieldDiffs, $comments, $dep_mail, $changer) = @_;
- # If the user is in a particular role and the value of that role
- # changed, we need the ADDED_REMOVED event.
- if (($fieldName eq "assigned_to" && $relationship == REL_ASSIGNEE) ||
- ($fieldName eq "qa_contact" && $relationship == REL_QA))
- {
- $events{+EVT_ADDED_REMOVED} = 1;
- }
+ # is_silent_user is true if the username is mentioned in the param `silent_users`
+ return 0 if $changer && $changer->is_silent_user;
- 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;
- }
- }
- }
+ # 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};
- if (!$bug->lastdiffed) {
- # 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;
- }
- }
-
- if (grep { $_->type == CMT_ATTACHMENT_CREATED } @$comments) {
- $events{+EVT_ATTACHMENT} = 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;
}
- 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;
+ 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;
+ }
+ }
+ }
- my $wants_mail = $self->wants_mail(\@event_list, $relationship);
+ if (!$bug->lastdiffed) {
- # 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);
- }
+ # Notify about new bugs.
+ $events{+EVT_BUG_CREATED} = 1;
- if ($wants_mail && $bug->bug_status eq 'UNCONFIRMED') {
- $wants_mail &= $self->wants_mail([EVT_UNCONFIRMED], $relationship);
+ # You role is new if the bug itself is.
+ # Only makes sense for the assignee, QA contact and the CC list.
+ if ( $relationship == REL_ASSIGNEE
+ || $relationship == REL_QA
+ || $relationship == REL_CC)
+ {
+ $events{+EVT_ADDED_REMOVED} = 1;
+ }
+ }
+
+ 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);
+ }
+
+ # BMO: add a hook to allow custom bugmail filtering
+ Bugzilla::Hook::process(
+ "user_wants_mail",
+ {
+ user => $self,
+ wants_mail => \$wants_mail,
+ bug => $bug,
+ relationship => $relationship,
+ fieldDiffs => $fieldDiffs,
+ comments => $comments,
+ dep_mail => $dep_mail,
+ changer => $changer,
}
+ );
- # BMO: add a hook to allow custom bugmail filtering
- Bugzilla::Hook::process("user_wants_mail", {
- user => $self,
- wants_mail => \$wants_mail,
- bug => $bug,
- relationship => $relationship,
- fieldDiffs => $fieldDiffs,
- comments => $comments,
- dep_mail => $dep_mail,
- changer => $changer,
- });
-
- return $wants_mail;
+ 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) = @_;
+ 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'};
+ # 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);
+ # 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;
- }
+ # 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;
+ # 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;
+ 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 (!exists $self->{'is_global_watcher'}) {
- my @watchers = split(/\s*,\s*/, Bugzilla->params->{'globalwatchers'});
- $self->{'is_global_watcher'} = (any { $_ eq $self->login } @watchers) ? 1 : 0;
- }
- return $self->{'is_global_watcher'};
+ if (!exists $self->{'is_global_watcher'}) {
+ my @watchers = split(/\s*,\s*/, Bugzilla->params->{'globalwatchers'});
+ $self->{'is_global_watcher'} = (any { $_ eq $self->login } @watchers) ? 1 : 0;
+ }
+ return $self->{'is_global_watcher'};
}
sub is_silent_user {
- my $self = shift;
+ my $self = shift;
- if (!exists $self->{'is_silent_user'}) {
- my @users = split(/\s*,\s*/, Bugzilla->params->{'silent_users'});
- $self->{'is_silent_user'} = (any { $self->login eq $_ } @users) ? 1 : 0;
- }
+ if (!exists $self->{'is_silent_user'}) {
+ my @users = split(/\s*,\s*/, Bugzilla->params->{'silent_users'});
+ $self->{'is_silent_user'} = (any { $self->login eq $_ } @users) ? 1 : 0;
+ }
- return $self->{'is_silent_user'};
+ return $self->{'is_silent_user'};
}
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 ($class, $params) = @_;
- my $dbh = Bugzilla->dbh;
-
- $dbh->bz_start_transaction();
- $params->{nickname} = _generate_nickname($params->{realname}, $params->{login_name}, 0);
- my $user = $class->SUPER::create($params);
-
- # 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 ($class, $params) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ $dbh->bz_start_transaction();
+ $params->{nickname}
+ = _generate_nickname($params->{realname}, $params->{login_name}, 0);
+ my $user = $class->SUPER::create($params);
+
+ # 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;
}
sub check_required_create_fields {
- my ($invocant, $params) = @_;
- my $class = ref($invocant) || $invocant;
- # ensure disabled users also have their email disabled
- $params->{disable_mail} = 1 if
- exists $params->{disabledtext}
- && defined($params->{disabledtext})
- && trim($params->{disabledtext}) ne '';
- $class->SUPER::check_required_create_fields($params);
+ my ($invocant, $params) = @_;
+ my $class = ref($invocant) || $invocant;
+
+ # ensure disabled users also have their email disabled
+ $params->{disable_mail} = 1
+ if exists $params->{disabledtext}
+ && defined($params->{disabledtext})
+ && trim($params->{disabledtext}) ne '';
+ $class->SUPER::check_required_create_fields($params);
}
###########################
@@ -2596,44 +2709,45 @@ sub check_required_create_fields {
###########################
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};
}
###############
@@ -2641,116 +2755,126 @@ sub account_ip_login_failures {
###############
sub is_available_username {
- my ($username, $old_username) = @_;
+ my ($username, $old_username) = @_;
- if(login_to_id($username) != 0) {
- return 0;
- }
-
- my $dbh = Bugzilla->dbh;
- # $username is safe because it is only used in SELECT placeholders.
- trick_taint($username);
- # Reject if the new login is part of an email change which is
- # still in progress
- #
- # substring/locate stuff: bug 165221; this used to use regexes, but that
- # was unsafe and required weird escaping; using substring to pull out
- # the new/old email addresses and sql_position() to find the delimiter (':')
- # is cleaner/safer
- my $eventdata = $dbh->selectrow_array(
- "SELECT eventdata
+ if (login_to_id($username) != 0) {
+ return 0;
+ }
+
+ my $dbh = Bugzilla->dbh;
+
+ # $username is safe because it is only used in SELECT placeholders.
+ trick_taint($username);
+
+ # Reject if the new login is part of an email change which is
+ # still in progress
+ #
+ # substring/locate stuff: bug 165221; this used to use regexes, but that
+ # was unsafe and required weird escaping; using substring to pull out
+ # the new/old email addresses and sql_position() to find the delimiter (':')
+ # is cleaner/safer
+ my $eventdata = $dbh->selectrow_array(
+ "SELECT eventdata
FROM tokens
WHERE (tokentype = 'emailold'
- AND SUBSTRING(eventdata, 1, (" .
- $dbh->sql_position(q{':'}, 'eventdata') . "- 1)) = ?)
+ AND SUBSTRING(eventdata, 1, ("
+ . $dbh->sql_position(q{':'}, 'eventdata') . "- 1)) = ?)
OR (tokentype = 'emailnew'
- AND SUBSTRING(eventdata, (" .
- $dbh->sql_position(q{':'}, 'eventdata') . "+ 1), LENGTH(eventdata)) = ?)",
- undef, ($username, $username));
-
- if ($eventdata) {
- # Allow thru owner of token
- if($old_username && ($eventdata eq "$old_username:$username")) {
- return 1;
- }
- return 0;
+ AND SUBSTRING(eventdata, ("
+ . $dbh->sql_position(q{':'}, 'eventdata')
+ . "+ 1), LENGTH(eventdata)) = ?)",
+ undef, ($username, $username)
+ );
+
+ if ($eventdata) {
+
+ # Allow thru owner of token
+ if ($old_username && ($eventdata eq "$old_username:$username")) {
+ return 1;
}
+ return 0;
+ }
- return 1;
+ return 1;
}
sub check_account_creation_enabled {
- my $self = shift;
+ my $self = shift;
- # If we're using e.g. LDAP for login, then we can't create a new account.
- $self->authorizer->user_can_create_account
- || ThrowUserError('auth_cant_create_account');
+ # If we're using e.g. LDAP for login, then we can't create a new account.
+ $self->authorizer->user_can_create_account
+ || ThrowUserError('auth_cant_create_account');
- Bugzilla->params->{'createemailregexp'}
- || ThrowUserError('account_creation_disabled');
+ Bugzilla->params->{'createemailregexp'}
+ || ThrowUserError('account_creation_disabled');
}
sub check_and_send_account_creation_confirmation {
- my ($self, $login) = @_;
+ my ($self, $login) = @_;
- $login = $self->check_login_name_for_creation($login);
- my $creation_regexp = Bugzilla->params->{'createemailregexp'};
+ $login = $self->check_login_name_for_creation($login);
+ my $creation_regexp = Bugzilla->params->{'createemailregexp'};
- if ($login !~ /$creation_regexp/i) {
- ThrowUserError('account_creation_restricted');
- }
+ if ($login !~ /$creation_regexp/i) {
+ ThrowUserError('account_creation_restricted');
+ }
- # BMO - add a hook to allow extra validation prior to account creation.
- Bugzilla::Hook::process("user_verify_login", { login => $login });
+ # BMO - add a hook to allow extra validation prior to account creation.
+ Bugzilla::Hook::process("user_verify_login", {login => $login});
- # Create and send a token for this new account.
- require Bugzilla::Token;
- Bugzilla::Token::issue_new_user_account_token($login);
+ # Create and send a token for this new account.
+ require Bugzilla::Token;
+ Bugzilla::Token::issue_new_user_account_token($login);
}
# This is used in a few performance-critical areas where we don't want to
# do check() and pull all the user data from the database.
sub login_to_id {
- my ($login, $throw_error) = @_;
- my $dbh = Bugzilla->dbh;
- my $cache = Bugzilla->request_cache->{user_login_to_id} ||= {};
-
- # We cache lookups because this function showed up as taking up a
- # significant amount of time in profiles of xt/search.t. However,
- # for users that don't exist, we re-do the check every time, because
- # otherwise we break is_available_username.
- my $user_id;
- if (defined $cache->{$login}) {
- $user_id = $cache->{$login};
- }
- else {
- # No need to validate $login -- it will be used by the following SELECT
- # statement only, so it's safe to simply trick_taint.
- trick_taint($login);
- $user_id = $dbh->selectrow_array(
- "SELECT userid FROM profiles
- WHERE " . $dbh->sql_istrcmp('login_name', '?'), undef, $login);
- $cache->{$login} = $user_id;
- }
-
- if ($user_id) {
- return $user_id;
- } elsif ($throw_error) {
- ThrowUserError('invalid_username', { name => $login });
- } else {
- return 0;
- }
+ my ($login, $throw_error) = @_;
+ my $dbh = Bugzilla->dbh;
+ my $cache = Bugzilla->request_cache->{user_login_to_id} ||= {};
+
+ # We cache lookups because this function showed up as taking up a
+ # significant amount of time in profiles of xt/search.t. However,
+ # for users that don't exist, we re-do the check every time, because
+ # otherwise we break is_available_username.
+ my $user_id;
+ if (defined $cache->{$login}) {
+ $user_id = $cache->{$login};
+ }
+ else {
+ # No need to validate $login -- it will be used by the following SELECT
+ # statement only, so it's safe to simply trick_taint.
+ trick_taint($login);
+ $user_id = $dbh->selectrow_array(
+ "SELECT userid FROM profiles
+ WHERE " . $dbh->sql_istrcmp('login_name', '?'), undef, $login
+ );
+ $cache->{$login} = $user_id;
+ }
+
+ if ($user_id) {
+ return $user_id;
+ }
+ elsif ($throw_error) {
+ ThrowUserError('invalid_username', {name => $login});
+ }
+ else {
+ return 0;
+ }
}
sub user_id_to_login {
- my $user_id = shift;
- my $dbh = Bugzilla->dbh;
+ my $user_id = shift;
+ my $dbh = Bugzilla->dbh;
- return '' unless ($user_id && detaint_natural($user_id));
+ return '' unless ($user_id && detaint_natural($user_id));
- my $login = $dbh->selectrow_array('SELECT login_name FROM profiles
- WHERE userid = ?', undef, $user_id);
- return $login || '';
+ my $login = $dbh->selectrow_array(
+ 'SELECT login_name FROM profiles
+ WHERE userid = ?', undef, $user_id
+ );
+ return $login || '';
}
1;
diff --git a/Bugzilla/User/APIKey.pm b/Bugzilla/User/APIKey.pm
index c1a4ed572..a30c6718f 100644
--- a/Bugzilla/User/APIKey.pm
+++ b/Bugzilla/User/APIKey.pm
@@ -21,58 +21,60 @@ use Bugzilla::Error;
# Overriden Constants that are used as methods
#####################################################################
-use constant DB_TABLE => 'user_api_keys';
-use constant DB_COLUMNS => qw(
- id
- user_id
- api_key
- app_id
- description
- revoked
- last_used
- last_used_ip
+use constant DB_TABLE => 'user_api_keys';
+use constant DB_COLUMNS => qw(
+ id
+ user_id
+ api_key
+ app_id
+ description
+ revoked
+ last_used
+ last_used_ip
);
use constant UPDATE_COLUMNS => qw(description revoked last_used last_used_ip);
use constant VALIDATORS => {
- api_key => \&_check_api_key,
- app_id => \&_check_app_id,
- description => \&_check_description,
- revoked => \&Bugzilla::Object::check_boolean,
+ api_key => \&_check_api_key,
+ app_id => \&_check_app_id,
+ 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 app_id { return $_[0]->{app_id} }
-sub description { return $_[0]->{description} }
-sub revoked { return $_[0]->{revoked} }
-sub last_used { return $_[0]->{last_used} }
-sub last_used_ip { return $_[0]->{last_used_ip} }
+sub id { return $_[0]->{id} }
+sub user_id { return $_[0]->{user_id} }
+sub api_key { return $_[0]->{api_key} }
+sub app_id { return $_[0]->{app_id} }
+sub description { return $_[0]->{description} }
+sub revoked { return $_[0]->{revoked} }
+sub last_used { return $_[0]->{last_used} }
+sub last_used_ip { return $_[0]->{last_used_ip} }
# 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->set('last_used_ip', remote_ip());
- $self->update;
+ my $self = shift;
+ my $timestamp
+ = shift || Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
+ $self->set('last_used', $timestamp);
+ $self->set('last_used_ip', remote_ip());
+ $self->update;
}
# Setters
@@ -80,20 +82,22 @@ 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]) || ''; }
+
sub _check_app_id {
- my ($invocant, $app_id) = @_;
+ my ($invocant, $app_id) = @_;
- ThrowCodeError("invalid_app_id", { app_id => $app_id }) unless $app_id =~ /^[[:xdigit:]]+$/;
+ ThrowCodeError("invalid_app_id", {app_id => $app_id})
+ unless $app_id =~ /^[[:xdigit:]]+$/;
- return $app_id;
+ return $app_id;
}
sub create_special {
- my ($class, @args) = @_;
- local VALIDATORS->{api_key} = sub { return $_[1] };
- return $class->create(@args);
+ my ($class, @args) = @_;
+ local VALIDATORS->{api_key} = sub { return $_[1] };
+ return $class->create(@args);
}
1;
diff --git a/Bugzilla/User/Session.pm b/Bugzilla/User/Session.pm
index 56e1cd07a..652a866c1 100644
--- a/Bugzilla/User/Session.pm
+++ b/Bugzilla/User/Session.pm
@@ -17,14 +17,14 @@ use base qw(Bugzilla::Object);
# Overriden Constants that are used as methods
#####################################################################
-use constant DB_TABLE => 'logincookies';
-use constant DB_COLUMNS => qw(
- cookie
- userid
- lastused
- ipaddr
- id
- restrict_ipaddr
+use constant DB_TABLE => 'logincookies';
+use constant DB_COLUMNS => qw(
+ cookie
+ userid
+ lastused
+ ipaddr
+ id
+ restrict_ipaddr
);
use constant UPDATE_COLUMNS => qw();
@@ -33,17 +33,19 @@ use constant LIST_ORDER => 'lastused DESC';
use constant NAME_FIELD => 'cookie';
# 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 userid { return $_[0]->{userid} }
-sub cookie { return $_[0]->{cookie} }
-sub lastused { return $_[0]->{lastused} }
-sub ipaddr { return $_[0]->{ipaddr} }
+sub id { return $_[0]->{id} }
+sub userid { return $_[0]->{userid} }
+sub cookie { return $_[0]->{cookie} }
+sub lastused { return $_[0]->{lastused} }
+sub ipaddr { return $_[0]->{ipaddr} }
sub restrict_ipaddr { return $_[0]->{restrict_ipaddr} }
1;
diff --git a/Bugzilla/User/Setting.pm b/Bugzilla/User/Setting.pm
index ac53fbb32..e08f3bd8c 100644
--- a/Bugzilla/User/Setting.pm
+++ b/Bugzilla/User/Setting.pm
@@ -13,12 +13,13 @@ use strict;
use warnings;
use base 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;
@@ -30,89 +31,86 @@ use Module::Runtime qw(require_module);
###############################
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, $category);
- ($default, $is_enabled, $value, $subclass, $category) =
- $dbh->selectrow_array(
- q{SELECT default_value, is_enabled, setting_value, subclass, category
+ 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, $category);
+ ($default, $is_enabled, $value, $subclass, $category) = $dbh->selectrow_array(
+ q{SELECT default_value, is_enabled, setting_value, subclass, category
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, $category) =
- $dbh->selectrow_array(
- q{SELECT default_value, is_enabled, subclass, category
+ 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, $category) = $dbh->selectrow_array(
+ q{SELECT default_value, is_enabled, subclass, category
FROM setting
- WHERE name = ?},
- undef,
- $setting_name);
- }
-
- $self->{'is_enabled'} = $is_enabled;
- $self->{'default_value'} = $default;
- $self->{'category'} = $category;
-
- # 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;
- $self->{'category'} = shift;
+ WHERE name = ?}, undef, $setting_name
+ );
}
- if ($subclass) {
- eval { require_module( $class . '::' . $subclass ) }
- || ThrowCodeError( 'setting_subclass_invalid', { 'subclass' => $subclass } );
- $class = $class . '::' . $subclass;
- }
- bless($self, $class);
- $self->{'_setting_name'} = $setting_name;
- $self->{'_user_id'} = $user_id;
+ $self->{'is_enabled'} = $is_enabled;
+ $self->{'default_value'} = $default;
+ $self->{'category'} = $category;
- 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;
+ $self->{'category'} = shift;
+ }
+ if ($subclass) {
+ eval { require_module($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,205 +118,220 @@ sub new {
###############################
sub add_setting {
- my ($params) = @_;
- my ($name, $options, $default, $subclass, $force_check, $silently, $category)
- = @$params{qw( name options default subclass force_check silently category )};
- my $dbh = Bugzilla->dbh;
-
- # Categories were added later, so we need to check if the old
- # setting has the correct category if provided
- my $exists = _setting_exists($name);
- if ($exists && $category) {
- my $old_category = $dbh->selectrow_arrayref(
- "SELECT category FROM setting WHERE name = ?", undef, $name);
- if ($old_category ne $category) {
- $dbh->do('UPDATE setting SET category = ? WHERE name = ?',
- undef, $category, $name);
- Bugzilla->memcached->clear_config();
- }
+ my ($params) = @_;
+ my ($name, $options, $default, $subclass, $force_check, $silently, $category)
+ = @$params{qw( name options default subclass force_check silently category )};
+ my $dbh = Bugzilla->dbh;
+
+ # Categories were added later, so we need to check if the old
+ # setting has the correct category if provided
+ my $exists = _setting_exists($name);
+ if ($exists && $category) {
+ my $old_category
+ = $dbh->selectrow_arrayref("SELECT category FROM setting WHERE name = ?",
+ undef, $name);
+ if ($old_category ne $category) {
+ $dbh->do('UPDATE setting SET category = ? WHERE name = ?',
+ undef, $category, $name);
+ Bugzilla->memcached->clear_config();
}
+ }
- return if ($exists && !$force_check);
-
- ($name && $default)
- || 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 $options && scalar(@$options)) {
- my $list = join(', ', map {$dbh->quote($_)} @$options);
- $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, category)
- VALUES (?, ?, 1, ?, ?)},
- undef, ($name, $default, $subclass, $category));
+ return if ($exists && !$force_check);
+
+ ($name && $default) || 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 (@$options){
- $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 $options && scalar(@$options)) {
+ my $list = join(', ', map { $dbh->quote($_) } @$options);
+ $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, category)
+ VALUES (?, ?, 1, ?, ?)}, undef,
+ ($name, $default, $subclass, $category)
+ );
+
+ my $sth = $dbh->prepare(
+ q{INSERT INTO setting_value (name, value, sortindex)
+ VALUES (?, ?, ?)}
+ );
+
+ my $sortindex = 5;
+ foreach my $key (@$options) {
+ $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, category
+ 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, category
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, $category) = @$row;
+ foreach my $row (@$rows) {
+ my ($name, $default_value, $is_enabled, $value, $subclass, $category) = @$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, $category);
+ 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, $category);
+ }
- 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, category
- FROM setting});
+ my $rows = $dbh->selectall_arrayref(
+ q{SELECT name, default_value, is_enabled, subclass, category
+ FROM setting}
+ );
- foreach my $row (@$rows) {
- my ($name, $default_value, $is_enabled, $subclass, $category) = @$row;
+ foreach my $row (@$rows) {
+ my ($name, $default_value, $is_enabled, $subclass, $category) = @$row;
- $default_settings->{$name} = new Bugzilla::User::Setting(
- $name, $user_id, $is_enabled, $default_value, $default_value, 1,
- $subclass, $category);
- }
+ $default_settings->{$name}
+ = new Bugzilla::User::Setting($name, $user_id, $is_enabled, $default_value,
+ $default_value, 1, $subclass, $category);
+ }
- return $default_settings;
+ return $default_settings;
}
sub set_default {
- my ($setting_name, $default_value, $is_enabled) = @_;
- my $dbh = Bugzilla->dbh;
+ my ($setting_name, $default_value, $is_enabled) = @_;
+ my $dbh = Bugzilla->dbh;
- my $sth = $dbh->prepare(q{UPDATE setting
+ my $sth = $dbh->prepare(
+ q{UPDATE setting
SET default_value = ?, is_enabled = ?
- WHERE name = ?});
- $sth->execute($default_value, $is_enabled, $setting_name);
+ WHERE name = ?}
+ );
+ $sth->execute($default_value, $is_enabled, $setting_name);
}
sub _setting_exists {
- my ($setting_name) = @_;
- my $dbh = Bugzilla->dbh;
- return $dbh->selectrow_arrayref(
- "SELECT 1 FROM setting WHERE name = ?", undef, $setting_name) || 0;
+ my ($setting_name) = @_;
+ my $dbh = Bugzilla->dbh;
+ return $dbh->selectrow_arrayref("SELECT 1 FROM setting WHERE name = ?",
+ undef, $setting_name)
+ || 0;
}
sub legal_values {
- my ($self) = @_;
+ my ($self) = @_;
- return $self->{'legal_values'} if defined $self->{'legal_values'};
+ return $self->{'legal_values'} if defined $self->{'legal_values'};
- my $dbh = Bugzilla->dbh;
- $self->{'legal_values'} = $dbh->selectcol_arrayref(
- q{SELECT value
+ my $dbh = Bugzilla->dbh;
+ $self->{'legal_values'} = $dbh->selectcol_arrayref(
+ q{SELECT value
FROM setting_value
WHERE name = ?
- ORDER BY sortindex},
- undef, $self->{'_setting_name'});
+ ORDER BY sortindex}, undef, $self->{'_setting_name'}
+ );
- return $self->{'legal_values'};
+ return $self->{'legal_values'};
}
sub validate_value {
- my $self = shift;
-
- if (grep(/^$_[0]$/, @{$self->legal_values()})) {
- trick_taint($_[0]);
- }
- else {
- ThrowCodeError('setting_value_invalid',
- {'name' => $self->{'_setting_name'},
- 'value' => $_[0]});
- }
+ my $self = shift;
+
+ if (grep(/^$_[0]$/, @{$self->legal_values()})) {
+ trick_taint($_[0]);
+ }
+ else {
+ ThrowCodeError('setting_value_invalid',
+ {'name' => $self->{'_setting_name'}, 'value' => $_[0]});
+ }
}
sub reset_to_default {
- my ($self) = @_;
+ my ($self) = @_;
- my $dbh = Bugzilla->dbh;
- my $sth = $dbh->do(q{ DELETE
+ my $dbh = Bugzilla->dbh;
+ my $sth = $dbh->do(
+ q{ DELETE
FROM profile_setting
WHERE setting_name = ?
- AND user_id = ?},
- undef, $self->{'_setting_name'}, $self->{'_user_id'});
- $self->{'value'} = $self->{'default_value'};
- $self->{'is_default'} = 1;
+ AND user_id = ?}, undef, $self->{'_setting_name'},
+ $self->{'_user_id'}
+ );
+ $self->{'value'} = $self->{'default_value'};
+ $self->{'is_default'} = 1;
}
sub set {
- my ($self, $value) = @_;
- my $dbh = Bugzilla->dbh;
- my $query;
+ my ($self, $value) = @_;
+ my $dbh = Bugzilla->dbh;
+ my $query;
- if ($self->{'is_default'}) {
- $query = q{INSERT INTO profile_setting
+ if ($self->{'is_default'}) {
+ $query = q{INSERT INTO profile_setting
(setting_value, setting_name, user_id)
VALUES (?,?,?)};
- } else {
- $query = q{UPDATE profile_setting
+ }
+ else {
+ $query = q{UPDATE profile_setting
SET setting_value = ?
WHERE setting_name = ?
AND user_id = ?};
- }
- $dbh->do($query, undef, $value, $self->{'_setting_name'}, $self->{'_user_id'});
+ }
+ $dbh->do($query, undef, $value, $self->{'_setting_name'}, $self->{'_user_id'});
- $self->{'value'} = $value;
- $self->{'is_default'} = 0;
+ $self->{'value'} = $value;
+ $self->{'is_default'} = 0;
}
-
1;
__END__
diff --git a/Bugzilla/User/Setting/Lang.pm b/Bugzilla/User/Setting/Lang.pm
index a3032b9fc..5f49bc6e4 100644
--- a/Bugzilla/User/Setting/Lang.pm
+++ b/Bugzilla/User/Setting/Lang.pm
@@ -16,11 +16,11 @@ use base qw(Bugzilla::User::Setting);
use Bugzilla::Constants;
sub legal_values {
- my ($self) = @_;
+ my ($self) = @_;
- return $self->{'legal_values'} if defined $self->{'legal_values'};
+ return $self->{'legal_values'} if defined $self->{'legal_values'};
- return $self->{'legal_values'} = Bugzilla->languages;
+ return $self->{'legal_values'} = Bugzilla->languages;
}
1;
diff --git a/Bugzilla/User/Setting/Skin.pm b/Bugzilla/User/Setting/Skin.pm
index f0ea502ef..f4d82007b 100644
--- a/Bugzilla/User/Setting/Skin.pm
+++ b/Bugzilla/User/Setting/Skin.pm
@@ -21,28 +21,31 @@ use File::Basename;
use constant BUILTIN_SKIN_NAMES => ['standard'];
sub legal_values {
- my ($self) = @_;
-
- 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)};
-
- foreach my $direntry (glob(catdir($dirbase, '*'))) {
- if (-d $direntry) {
- # Stylesheet set
- next if basename($direntry) =~ /^cvs$/i;
- push(@legal_values, basename($direntry));
- }
- elsif ($direntry =~ /\.css$/) {
- # Single-file stylesheet
- push(@legal_values, basename($direntry));
- }
+ my ($self) = @_;
+
+ 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)};
+
+ foreach my $direntry (glob(catdir($dirbase, '*'))) {
+ if (-d $direntry) {
+
+ # Stylesheet set
+ next if basename($direntry) =~ /^cvs$/i;
+ push(@legal_values, basename($direntry));
+ }
+ elsif ($direntry =~ /\.css$/) {
+
+ # Single-file stylesheet
+ push(@legal_values, basename($direntry));
}
+ }
- return $self->{'legal_values'} = \@legal_values;
+ return $self->{'legal_values'} = \@legal_values;
}
1;
diff --git a/Bugzilla/User/Setting/Timezone.pm b/Bugzilla/User/Setting/Timezone.pm
index a9515259e..6e20f1fc2 100644
--- a/Bugzilla/User/Setting/Timezone.pm
+++ b/Bugzilla/User/Setting/Timezone.pm
@@ -18,19 +18,21 @@ use base qw(Bugzilla::User::Setting);
use Bugzilla::Constants;
sub legal_values {
- my ($self) = @_;
+ my ($self) = @_;
- return $self->{'legal_values'} if defined $self->{'legal_values'};
+ return $self->{'legal_values'} if defined $self->{'legal_values'};
- my @timezones = DateTime::TimeZone->all_names;
- # Remove old formats, such as CST6CDT, EST, EST5EDT.
- @timezones = grep { $_ =~ m#.+/.+#} @timezones;
- # Append 'local' to the list, which will use the timezone
- # given by the server.
- push(@timezones, 'local');
- push(@timezones, 'UTC');
+ my @timezones = DateTime::TimeZone->all_names;
- return $self->{'legal_values'} = \@timezones;
+ # Remove old formats, such as CST6CDT, EST, EST5EDT.
+ @timezones = grep { $_ =~ m#.+/.+# } @timezones;
+
+ # Append 'local' to the list, which will use the timezone
+ # given by the server.
+ push(@timezones, 'local');
+ push(@timezones, 'UTC');
+
+ return $self->{'legal_values'} = \@timezones;
}
1;
diff --git a/Bugzilla/UserAgent.pm b/Bugzilla/UserAgent.pm
index bd31a2a13..ec75a6131 100644
--- a/Bugzilla/UserAgent.pm
+++ b/Bugzilla/UserAgent.pm
@@ -20,177 +20,202 @@ 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"],
- qr/\(.*Intel Mac OS X.*\)/ => ["x86_64"],
- # 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/\(.*(?: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"],
+ qr/\(.*Intel Mac OS X.*\)/ => ["x86_64"],
+
+ # 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/\(.*(?: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/\(.*(?:iPad|iPhone).*\)/ => ["iOS"],
- 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")],
- # Firefox OS
- qr/\(Mobile;.*Gecko.*Firefox/ => ["Gonk (Firefox OS)"],
- # 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/\(.*(?:iPad|iPhone).*\)/ => ["iOS"],
+ 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")],
+
+ # Firefox OS
+ qr/\(Mobile;.*Gecko.*Firefox/ => ["Gonk (Firefox OS)"],
+
+ # 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 = shift || Bugzilla->cgi->user_agent || '';
- my @detected;
- my $iterator = natatime(2, PLATFORMS_MAP);
- while (my($re, $ra) = $iterator->()) {
- if ($userAgent =~ $re) {
- push @detected, @$ra;
- }
+ my $userAgent = shift || Bugzilla->cgi->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 = shift || Bugzilla->cgi->user_agent || '';
- my @detected;
- my $iterator = natatime(2, OS_MAP);
- while (my($re, $ra) = $iterator->()) {
- if ($userAgent =~ $re) {
- push @detected, @$ra;
- }
+ my $userAgent = shift || Bugzilla->cgi->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.
@@ -198,11 +223,11 @@ sub detect_op_sys {
# field.
# Returns 'Other' if none of the values match.
sub _pick_valid_field_value {
- my ($field, @values) = @_;
- foreach my $value (@values) {
- return $value if check_field($field, $value, undef, 1);
- }
- return DEFAULT_VALUE;
+ my ($field, @values) = @_;
+ foreach my $value (@values) {
+ return $value if check_field($field, $value, undef, 1);
+ }
+ return DEFAULT_VALUE;
}
1;
diff --git a/Bugzilla/Util.pm b/Bugzilla/Util.pm
index aa524b263..ab7e2189b 100644
--- a/Bugzilla/Util.pm
+++ b/Bugzilla/Util.pm
@@ -13,22 +13,22 @@ use warnings;
use base qw(Exporter);
@Bugzilla::Util::EXPORT = qw(trick_taint detaint_natural
- detaint_signed
- with_writable_database with_readonly_database
- html_quote url_quote xml_quote
- css_class_quote html_light_quote
- i_am_cgi i_am_webservice is_webserver_group
- correct_urlbase remote_ip
- validate_ip do_ssl_redirect_if_required use_attachbase
- diff_arrays on_main_db css_url_rewrite
- trim wrap_hard wrap_comment find_wrap_point
- format_time validate_date validate_time datetime_from time_ago
- file_mod_time is_7bit_clean
- bz_crypt generate_random_password
- validate_email_syntax clean_text
- get_text template_var disable_utf8
- enable_utf8 detect_encoding email_filter
- round extract_nicks);
+ detaint_signed
+ with_writable_database with_readonly_database
+ html_quote url_quote xml_quote
+ css_class_quote html_light_quote
+ i_am_cgi i_am_webservice is_webserver_group
+ correct_urlbase remote_ip
+ validate_ip do_ssl_redirect_if_required use_attachbase
+ diff_arrays on_main_db css_url_rewrite
+ trim wrap_hard wrap_comment find_wrap_point
+ format_time validate_date validate_time datetime_from time_ago
+ file_mod_time is_7bit_clean
+ bz_crypt generate_random_password
+ validate_email_syntax clean_text
+ get_text template_var disable_utf8
+ enable_utf8 detect_encoding email_filter
+ round extract_nicks);
use Bugzilla::Logging;
use Bugzilla::Constants;
use Bugzilla::RNG qw(irand);
@@ -50,663 +50,704 @@ use Text::Wrap;
use Try::Tiny;
sub with_writable_database(&) {
- my ($code) = @_;
- my $dbh = Bugzilla->dbh_main;
- local Bugzilla->request_cache->{dbh} = $dbh;
- local Bugzilla->request_cache->{error_mode} = ERROR_MODE_DIE;
- try {
- $dbh->bz_start_transaction;
- $code->();
- $dbh->bz_commit_transaction;
- } catch {
- $dbh->bz_rollback_transaction;
- # re-throw
- die $_;
- };
+ my ($code) = @_;
+ my $dbh = Bugzilla->dbh_main;
+ local Bugzilla->request_cache->{dbh} = $dbh;
+ local Bugzilla->request_cache->{error_mode} = ERROR_MODE_DIE;
+ try {
+ $dbh->bz_start_transaction;
+ $code->();
+ $dbh->bz_commit_transaction;
+ }
+ catch {
+ $dbh->bz_rollback_transaction;
+
+ # re-throw
+ die $_;
+ };
}
sub with_readonly_database(&) {
- my ($code) = @_;
- local Bugzilla->request_cache->{dbh} = undef;
- local Bugzilla->request_cache->{error_mode} = ERROR_MODE_DIE;
- Bugzilla->switch_to_shadow_db();
- $code->();
+ my ($code) = @_;
+ local Bugzilla->request_cache->{dbh} = undef;
+ local Bugzilla->request_cache->{error_mode} = ERROR_MODE_DIE;
+ Bugzilla->switch_to_shadow_db();
+ $code->();
}
sub trick_taint {
- untaint($_[0]);
+ untaint($_[0]);
- return defined $_[0];
+ return defined $_[0];
}
sub detaint_natural {
- my $match = $_[0] =~ /^(\d+)$/;
- $_[0] = $match ? int($1) : undef;
- return (defined($_[0]));
+ my $match = $_[0] =~ /^(\d+)$/;
+ $_[0] = $match ? int($1) : undef;
+ return (defined($_[0]));
}
sub detaint_signed {
- my $match = $_[0] =~ /^([-+]?\d+)$/;
- # The "int()" call removes any leading plus sign.
- $_[0] = $match ? int($1) : undef;
- return (defined($_[0]));
+ my $match = $_[0] =~ /^([-+]?\d+)$/;
+
+ # The "int()" call removes any leading plus sign.
+ $_[0] = $match ? int($1) : undef;
+ return (defined($_[0]));
}
my %html_quote = (
- q{&} => '&amp;',
- q{<} => '&lt;',
- q{>} => '&gt;',
- q{"} => '&quot;',
- q{@} => '&#64;', # Obscure '@'.
+ q{&} => '&amp;',
+ q{<} => '&lt;',
+ q{>} => '&gt;',
+ q{"} => '&quot;',
+ q{@} => '&#64;', # Obscure '@'.
);
# Bug 120030: Override html filter to obscure the '@' in user
# visible strings.
# Bug 319331: Handle BiDi disruptions.
sub html_quote {
- my $var = shift;
- no warnings 'utf8';
- $var =~ s/([&<>"@])/$html_quote{$1}/g;
-
- state $use_utf8 = Bugzilla->params->{'utf8'};
-
- if ($use_utf8) {
- # Remove control characters if the encoding is utf8.
- # Other multibyte encodings may be using this range; so ignore if not utf8.
- $var =~ s/(?![\t\r\n])[[:cntrl:]]//g;
-
- # Remove the following characters because they're
- # influencing BiDi:
- # --------------------------------------------------------
- # |Code |Name |UTF-8 representation|
- # |------|--------------------------|--------------------|
- # |U+202a|Left-To-Right Embedding |0xe2 0x80 0xaa |
- # |U+202b|Right-To-Left Embedding |0xe2 0x80 0xab |
- # |U+202c|Pop Directional Formatting|0xe2 0x80 0xac |
- # |U+202d|Left-To-Right Override |0xe2 0x80 0xad |
- # |U+202e|Right-To-Left Override |0xe2 0x80 0xae |
- # --------------------------------------------------------
- #
- # The following are characters influencing BiDi, too, but
- # they can be spared from filtering because they don't
- # influence more than one character right or left:
- # --------------------------------------------------------
- # |Code |Name |UTF-8 representation|
- # |------|--------------------------|--------------------|
- # |U+200e|Left-To-Right Mark |0xe2 0x80 0x8e |
- # |U+200f|Right-To-Left Mark |0xe2 0x80 0x8f |
- # --------------------------------------------------------
- $var =~ tr/\x{202a}-\x{202e}//d;
- }
- return $var;
+ my $var = shift;
+ no warnings 'utf8';
+ $var =~ s/([&<>"@])/$html_quote{$1}/g;
+
+ state $use_utf8 = Bugzilla->params->{'utf8'};
+
+ if ($use_utf8) {
+
+ # Remove control characters if the encoding is utf8.
+ # Other multibyte encodings may be using this range; so ignore if not utf8.
+ $var =~ s/(?![\t\r\n])[[:cntrl:]]//g;
+
+ # Remove the following characters because they're
+ # influencing BiDi:
+ # --------------------------------------------------------
+ # |Code |Name |UTF-8 representation|
+ # |------|--------------------------|--------------------|
+ # |U+202a|Left-To-Right Embedding |0xe2 0x80 0xaa |
+ # |U+202b|Right-To-Left Embedding |0xe2 0x80 0xab |
+ # |U+202c|Pop Directional Formatting|0xe2 0x80 0xac |
+ # |U+202d|Left-To-Right Override |0xe2 0x80 0xad |
+ # |U+202e|Right-To-Left Override |0xe2 0x80 0xae |
+ # --------------------------------------------------------
+ #
+ # The following are characters influencing BiDi, too, but
+ # they can be spared from filtering because they don't
+ # influence more than one character right or left:
+ # --------------------------------------------------------
+ # |Code |Name |UTF-8 representation|
+ # |------|--------------------------|--------------------|
+ # |U+200e|Left-To-Right Mark |0xe2 0x80 0x8e |
+ # |U+200f|Right-To-Left Mark |0xe2 0x80 0x8f |
+ # --------------------------------------------------------
+ $var =~ tr/\x{202a}-\x{202e}//d;
+ }
+ return $var;
}
sub 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,
- 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,
+ title => 1,
+ id => 1,
+ name => 1,
+ class => 1,
+ '*' => 0, # Reject all other attributes.
+ },
+ blockquote => {
+ cite => $protocol_regexp,
+ id => 1,
+ name => 1,
+ class => 1,
+ '*' => 0, # Reject all other attributes.
+ },
+ 'q' => {
+ cite => $protocol_regexp,
+ id => 1,
+ name => 1,
+ class => 1,
+ '*' => 0, # Reject all other attributes.
+ },
+ );
+
+ Bugzilla->process_cache->{html_scrubber} = $scrubber = HTML::Scrubber->new(
+ default => \@default,
+ allow => \@allow,
+ rules => \@rules,
+ comment => 0,
+ process => 0
+ );
+ }
+ return $scrubber->scrub($text);
}
sub email_filter {
- my ($toencode) = @_;
- if (!Bugzilla->user->id) {
- my @emails = Email::Address->parse($toencode);
- if (scalar @emails) {
- my @hosts = map { quotemeta($_->host) } @emails;
- my $hosts_re = join('|', @hosts);
- $toencode =~ s/\@(?:$hosts_re)//g;
- return $toencode;
- }
+ my ($toencode) = @_;
+ if (!Bugzilla->user->id) {
+ my @emails = Email::Address->parse($toencode);
+ if (scalar @emails) {
+ my @hosts = map { quotemeta($_->host) } @emails;
+ my $hosts_re = join('|', @hosts);
+ $toencode =~ s/\@(?:$hosts_re)//g;
+ return $toencode;
}
- return $toencode;
+ }
+ return $toencode;
}
# This originally came from CGI.pm, by Lincoln D. Stein
sub url_quote {
- my ($toencode) = (@_);
- utf8::encode($toencode) # The below regex works only on bytes
- if Bugzilla->params->{'utf8'} && utf8::is_utf8($toencode);
- $toencode =~ s/([^a-zA-Z0-9_\-.])/uc sprintf("%%%02x",ord($1))/eg;
- return $toencode;
+ my ($toencode) = (@_);
+ utf8::encode($toencode) # The below regex works only on bytes
+ if Bugzilla->params->{'utf8'} && utf8::is_utf8($toencode);
+ $toencode =~ s/([^a-zA-Z0-9_\-.])/uc sprintf("%%%02x",ord($1))/eg;
+ return $toencode;
}
sub css_class_quote {
- my ($toencode) = (@_);
- $toencode =~ s#[ /]#_#g;
- $toencode =~ s/([^a-zA-Z0-9_\-.])/uc sprintf("&#x%x;",ord($1))/eg;
- return $toencode;
+ my ($toencode) = (@_);
+ $toencode =~ s#[ /]#_#g;
+ $toencode =~ s/([^a-zA-Z0-9_\-.])/uc sprintf("&#x%x;",ord($1))/eg;
+ return $toencode;
}
sub xml_quote {
- my ($var) = (@_);
- $var =~ s/\&/\&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;
}
sub is_webserver_group {
- my @effective_gids = split(/ /, $EGID);
+ my @effective_gids = split(/ /, $EGID);
- state $web_server_gid;
- if (!defined $web_server_gid) {
- my $web_server_group = Bugzilla->localconfig->{webservergroup};
+ state $web_server_gid;
+ if (!defined $web_server_gid) {
+ my $web_server_group = Bugzilla->localconfig->{webservergroup};
- if ($web_server_group eq '' || ON_WINDOWS) {
- $web_server_gid = $effective_gids[0];
- }
+ if ($web_server_group eq '' || ON_WINDOWS) {
+ $web_server_gid = $effective_gids[0];
+ }
- elsif ($web_server_group =~ /^\d+$/) {
- $web_server_gid = $web_server_group;
- }
+ elsif ($web_server_group =~ /^\d+$/) {
+ $web_server_gid = $web_server_group;
+ }
- else {
- $web_server_gid = eval { getgrnam($web_server_group) };
- $web_server_gid //= 0;
- }
+ else {
+ $web_server_gid = eval { getgrnam($web_server_group) };
+ $web_server_gid //= 0;
}
+ }
- return any { $web_server_gid == $_ } @effective_gids;
+ return any { $web_server_gid == $_ } @effective_gids;
}
# 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();
- my $uri = URI->new(Bugzilla->localconfig->{'urlbase'});
- return if $uri->scheme ne 'https';
-
- # If we're already running under SSL, never redirect.
- return if $ENV{HTTPS} && $ENV{HTTPS} eq 'on';
- DEBUG("Redirect to HTTPS because \$ENV{HTTPS}=$ENV{HTTPS}");
- Bugzilla->cgi->redirect_to_https();
+ return if !i_am_cgi();
+ my $uri = URI->new(Bugzilla->localconfig->{'urlbase'});
+ return if $uri->scheme ne 'https';
+
+ # If we're already running under SSL, never redirect.
+ return if $ENV{HTTPS} && $ENV{HTTPS} eq 'on';
+ DEBUG("Redirect to HTTPS because \$ENV{HTTPS}=$ENV{HTTPS}");
+ Bugzilla->cgi->redirect_to_https();
}
# 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->localconfig->{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->localconfig->{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;
- # 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);
+ my $ipv6 = join(':', @chunks);
- my $ipv6 = join(':', @chunks);
- # The IP address is valid and can now be detainted.
- untaint($ipv6);
+ # The IP address is valid and can now be detainted.
+ untaint($ipv6);
- # Need to handle the exception of trailing :: being valid.
- return "${ipv6}::" if $ip =~ /::$/;
- return $ipv6;
+ # Need to handle the exception of trailing :: being valid.
+ return "${ipv6}::" if $ip =~ /::$/;
+ return $ipv6;
}
sub use_attachbase {
- my $attachbase = Bugzilla->localconfig->{'attachment_base'};
- my $urlbase = Bugzilla->localconfig->{'urlbase'};
- return ($attachbase ne '' && $attachbase ne $urlbase);
+ my $attachbase = Bugzilla->localconfig->{'attachment_base'};
+ my $urlbase = Bugzilla->localconfig->{'urlbase'};
+ return ($attachbase ne '' && $attachbase ne $urlbase);
}
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 css_url_rewrite {
- my ($content, $callback) = @_;
- $content =~ s{(?<!=)url\((["']?)([^\)]+?)\1\)}{$callback->($2)}eig;
- return $content;
+ my ($content, $callback) = @_;
+ $content =~ s{(?<!=)url\((["']?)([^\)]+?)\1\)}{$callback->($2)}eig;
+ return $content;
}
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 {
- # Due to a segfault in Text::Tabs::expand() when processing tabs with
- # Unicode (see http://rt.perl.org/rt3/Public/Bug/Display.html?id=52104),
- # we have to remove tabs before processing the comment. This restriction
- # can go away when we require Perl 5.8.9 or newer.
- $line =~ s/\t/ /g;
- $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 {
+ # Due to a segfault in Text::Tabs::expand() when processing tabs with
+ # Unicode (see http://rt.perl.org/rt3/Public/Bug/Display.html?id=52104),
+ # we have to remove tabs before processing the comment. This restriction
+ # can go away when we require Perl 5.8.9 or newer.
+ $line =~ s/\t/ /g;
+ $wrappedcomment .= (wrap('', '', $line) . "\n");
}
+ }
- chomp($wrappedcomment); # Text::Wrap adds an extra newline at the end.
- return $wrappedcomment;
+ chomp($wrappedcomment); # Text::Wrap adds an extra newline at the end.
+ return $wrappedcomment;
}
sub find_wrap_point {
- my ($string, $maxpos) = @_;
- if (!$string) { return 0 }
- if (length($string) < $maxpos) { return length($string) }
- my $wrappoint = rindex($string, ",", $maxpos); # look for comma
- if ($wrappoint <= 0) { # can't find comma
- $wrappoint = rindex($string, " ", $maxpos); # look for space
- if ($wrappoint <= 0) { # can't find space
- $wrappoint = rindex($string, "-", $maxpos); # look for hyphen
- if ($wrappoint <= 0) { # can't find hyphen
- $wrappoint = $maxpos; # just truncate it
- } else {
- $wrappoint++; # leave hyphen on the left side
- }
- }
+ my ($string, $maxpos) = @_;
+ if (!$string) { return 0 }
+ if (length($string) < $maxpos) { return length($string) }
+ my $wrappoint = rindex($string, ",", $maxpos); # look for comma
+ if ($wrappoint <= 0) { # can't find comma
+ $wrappoint = rindex($string, " ", $maxpos); # look for space
+ if ($wrappoint <= 0) { # can't find space
+ $wrappoint = rindex($string, "-", $maxpos); # look for hyphen
+ if ($wrappoint <= 0) { # can't find hyphen
+ $wrappoint = $maxpos; # just truncate it
+ }
+ else {
+ $wrappoint++; # leave hyphen on the left side
+ }
}
- return $wrappoint;
+ }
+ return $wrappoint;
}
sub 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);
-}
+ my ($date, $format, $timezone) = @_;
-sub datetime_from {
- my ($date, $timezone) = @_;
-
- # In the database, this is the "0" date.
- use Carp qw(cluck);
- cluck("undefined date") unless defined $date;
- return undef unless defined $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);
+ # If $format is not set, try to guess the correct date format.
+ if (!$format) {
+ if (!ref $date
+ && $date =~ /^(\d{4})[-\.](\d{2})[-\.](\d{2}) (\d{2}):(\d{2})(:(\d{2}))?$/)
+ {
+ my $sec = $7;
+ if (defined $sec) {
+ $format = "%Y-%m-%d %T %Z";
+ }
+ else {
+ $format = "%Y-%m-%d %R %Z";
+ }
}
else {
- @time = strptime($date);
- }
-
- unless (scalar @time) {
- # If an unknown timezone is passed (such as MSK, for Moskow),
- # strptime() is unable to parse the date. We try again, but we first
- # remove the timezone.
- $date =~ s/\s+\S+$//;
- @time = strptime($date);
- }
-
- 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 => Bugzilla->local_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};
+ # Default date format. See DateTime for other formats available.
+ $format = "%Y-%m-%d %R %Z";
}
+ }
- 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.
+ use Carp qw(cluck);
+ cluck("undefined date") unless defined $date;
+ return undef unless defined $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 => Bugzilla->local_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};
+ }
+
+ 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 time_ago {
- my ($param) = @_;
- # DateTime object or seconds
- my $ss = ref($param) ? time() - $param->epoch : $param;
- my $mm = round($ss / 60);
- my $hh = round($mm / 60);
- my $dd = round($hh / 24);
- my $mo = round($dd / 30);
- my $yy = round($mo / 12);
-
- return 'just now' if $ss < 10;
- return $ss . ' seconds ago' if $ss < 45;
- return 'a minute ago' if $ss < 90;
- return $mm . ' minutes ago' if $mm < 45;
- return 'an hour ago' if $mm < 90;
- return $hh . ' hours ago' if $hh < 24;
- return 'a day ago' if $hh < 36;
- return $dd . ' days ago' if $dd < 30;
- return 'a month ago' if $dd < 45;
- return $mo . ' months ago' if $mo < 12;
- return 'a year ago' if $mo < 18;
- return $yy . ' years ago';
+ my ($param) = @_;
+
+ # DateTime object or seconds
+ my $ss = ref($param) ? time() - $param->epoch : $param;
+ my $mm = round($ss / 60);
+ my $hh = round($mm / 60);
+ my $dd = round($hh / 24);
+ my $mo = round($dd / 30);
+ my $yy = round($mo / 12);
+
+ return 'just now' if $ss < 10;
+ return $ss . ' seconds ago' if $ss < 45;
+ return 'a minute ago' if $ss < 90;
+ return $mm . ' minutes ago' if $mm < 45;
+ return 'an hour ago' if $mm < 90;
+ return $hh . ' hours ago' if $hh < 24;
+ return 'a day ago' if $hh < 36;
+ return $dd . ' days ago' if $dd < 30;
+ return 'a month ago' if $dd < 45;
+ return $mo . ' months ago' if $mo < 12;
+ return 'a year ago' if $mo < 18;
+ return $yy . ' years ago';
}
sub file_mod_time {
- my ($filename) = (@_);
- my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size,
- $atime,$mtime,$ctime,$blksize,$blocks)
- = stat($filename);
- return $mtime;
+ my ($filename) = (@_);
+ my (
+ $dev, $ino, $mode, $nlink, $uid, $gid, $rdev,
+ $size, $atime, $mtime, $ctime, $blksize, $blocks
+ ) = stat($filename);
+ return $mtime;
}
sub bz_crypt {
- my ($password, $salt) = @_;
-
- my $algorithm;
- if (!defined $salt) {
- # If you don't use a salt, then people can create tables of
- # hashes that map to particular passwords, and then break your
- # hashing very easily if they have a large-enough table of common
- # (or even uncommon) passwords. So we generate a unique salt for
- # each password in the database, and then just prepend it to
- # the hash.
- $salt = generate_random_password(PASSWORD_SALT_LENGTH);
- $algorithm = PASSWORD_DIGEST_ALGORITHM;
- }
-
- # We append the algorithm used to the string. This is good because then
- # we can change the algorithm being used, in the future, without
- # disrupting the validation of existing passwords. Also, this tells
- # us if a password is using the old "crypt" method of hashing passwords,
- # because the algorithm will be missing from the string.
- if ($salt =~ /{([^}]+)}$/) {
- $algorithm = $1;
- }
-
- # Wide characters cause crypt and Digest to die.
- if (Bugzilla->params->{'utf8'}) {
- utf8::encode($password) if utf8::is_utf8($password);
- }
-
- my $crypted_password;
- if (!$algorithm) {
- # Crypt the password.
- $crypted_password = crypt($password, $salt);
-
- # HACK: Perl has bug where returned crypted password is considered
- # tainted. See http://rt.perl.org/rt3/Public/Bug/Display.html?id=59998
- unless(tainted($password) || tainted($salt)) {
- untaint($crypted_password);
- }
- }
- else {
- my $hasher = Digest->new($algorithm);
- # We only want to use the first characters of the salt, no
- # matter how long of a salt we may have been passed.
- $salt = substr($salt, 0, PASSWORD_SALT_LENGTH);
- $hasher->add($password, $salt);
- $crypted_password = $salt . $hasher->b64digest . "{$algorithm}";
+ my ($password, $salt) = @_;
+
+ my $algorithm;
+ if (!defined $salt) {
+
+ # If you don't use a salt, then people can create tables of
+ # hashes that map to particular passwords, and then break your
+ # hashing very easily if they have a large-enough table of common
+ # (or even uncommon) passwords. So we generate a unique salt for
+ # each password in the database, and then just prepend it to
+ # the hash.
+ $salt = generate_random_password(PASSWORD_SALT_LENGTH);
+ $algorithm = PASSWORD_DIGEST_ALGORITHM;
+ }
+
+ # We append the algorithm used to the string. This is good because then
+ # we can change the algorithm being used, in the future, without
+ # disrupting the validation of existing passwords. Also, this tells
+ # us if a password is using the old "crypt" method of hashing passwords,
+ # because the algorithm will be missing from the string.
+ if ($salt =~ /{([^}]+)}$/) {
+ $algorithm = $1;
+ }
+
+ # Wide characters cause crypt and Digest to die.
+ if (Bugzilla->params->{'utf8'}) {
+ utf8::encode($password) if utf8::is_utf8($password);
+ }
+
+ my $crypted_password;
+ if (!$algorithm) {
+
+ # Crypt the password.
+ $crypted_password = crypt($password, $salt);
+
+ # HACK: Perl has bug where returned crypted password is considered
+ # tainted. See http://rt.perl.org/rt3/Public/Bug/Display.html?id=59998
+ unless (tainted($password) || tainted($salt)) {
+ untaint($crypted_password);
}
-
- # Return the crypted password.
- return $crypted_password;
+ }
+ else {
+ my $hasher = Digest->new($algorithm);
+
+ # We only want to use the first characters of the salt, no
+ # matter how long of a salt we may have been passed.
+ $salt = substr($salt, 0, PASSWORD_SALT_LENGTH);
+ $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
@@ -716,211 +757,218 @@ sub bz_crypt {
# by the number of characters you generate, and that gets you the equivalent
# strength of the string in bits.
sub generate_random_password {
- my $size = shift || 10; # default to 10 chars if nothing specified
- return join("", map{ ('0'..'9','a'..'z','A'..'Z')[irand 62] } (1..$size));
+ my $size = shift || 10; # default to 10 chars if nothing specified
+ return
+ join("", map { ('0' .. '9', 'a' .. 'z', 'A' .. 'Z')[irand 62] } (1 .. $size));
}
sub validate_email_syntax {
- my ($addr) = @_;
- my $match = Bugzilla->params->{'emailregexp'};
- my $email = $addr . Bugzilla->params->{'emailsuffix'};
- # This regexp follows RFC 2822 section 3.4.1.
- my $addr_spec = $Email::Address::addr_spec;
- # RFC 2822 section 2.1 specifies that email addresses must
- # be made of US-ASCII characters only.
- # Email::Address::addr_spec doesn't enforce this.
- if ($addr =~ /$match/
- && $email !~ /\P{ASCII}/
- && $email =~ /^$addr_spec$/
- && length($email) <= 127)
- {
- # We assume these checks to suffice to consider the address untainted.
- untaint($_[0]);
- return 1;
- }
- return 0;
+ my ($addr) = @_;
+ my $match = Bugzilla->params->{'emailregexp'};
+ my $email = $addr . Bugzilla->params->{'emailsuffix'};
+
+ # This regexp follows RFC 2822 section 3.4.1.
+ my $addr_spec = $Email::Address::addr_spec;
+
+ # RFC 2822 section 2.1 specifies that email addresses must
+ # be made of US-ASCII characters only.
+ # Email::Address::addr_spec doesn't enforce this.
+ if ( $addr =~ /$match/
+ && $email !~ /\P{ASCII}/
+ && $email =~ /^$addr_spec$/
+ && length($email) <= 127)
+ {
+ # We assume these checks to suffice to consider the address untainted.
+ untaint($_[0]);
+ return 1;
+ }
+ return 0;
}
sub validate_date {
- my ($date) = @_;
- my $date2;
-
- # $ts is undefined if the parser fails.
- my $ts = str2time($date);
- if ($ts) {
- $date2 = time2str("%Y-%m-%d", $ts);
-
- $date =~ s/(\d+)-0*(\d+?)-0*(\d+?)/$1-$2-$3/;
- $date2 =~ s/(\d+)-0*(\d+?)-0*(\d+?)/$1-$2-$3/;
- }
- my $ret = ($ts && $date eq $date2);
- return $ret ? 1 : 0;
+ my ($date) = @_;
+ my $date2;
+
+ # $ts is undefined if the parser fails.
+ my $ts = str2time($date);
+ if ($ts) {
+ $date2 = time2str("%Y-%m-%d", $ts);
+
+ $date =~ s/(\d+)-0*(\d+?)-0*(\d+?)/$1-$2-$3/;
+ $date2 =~ s/(\d+)-0*(\d+?)-0*(\d+?)/$1-$2-$3/;
+ }
+ my $ret = ($ts && $date eq $date2);
+ return $ret ? 1 : 0;
}
sub validate_time {
- my ($time) = @_;
- my $time2;
-
- # $ts is undefined if the parser fails.
- my $ts = str2time($time);
- if ($ts) {
- $time2 = time2str("%H:%M:%S", $ts);
- if ($time =~ /^(\d{1,2}):(\d\d)(?::(\d\d))?$/) {
- $time = sprintf("%02d:%02d:%02d", $1, $2, $3 || 0);
- }
+ my ($time) = @_;
+ my $time2;
+
+ # $ts is undefined if the parser fails.
+ my $ts = str2time($time);
+ if ($ts) {
+ $time2 = time2str("%H:%M:%S", $ts);
+ if ($time =~ /^(\d{1,2}):(\d\d)(?::(\d\d))?$/) {
+ $time = sprintf("%02d:%02d:%02d", $1, $2, $3 || 0);
}
- my $ret = ($ts && $time eq $time2);
- return $ret ? 1 : 0;
+ }
+ my $ret = ($ts && $time eq $time2);
+ return $ret ? 1 : 0;
}
sub is_7bit_clean {
- return $_[0] !~ /[^\x20-\x7E\x0A\x0D]/;
+ return $_[0] !~ /[^\x20-\x7E\x0A\x0D]/;
}
sub clean_text {
- my $dtext = shift;
- if ($dtext) {
- # change control characters into a space
- $dtext =~ s/[\x00-\x1F\x7F]+/ /g;
- }
- return trim($dtext);
+ my $dtext = shift;
+ if ($dtext) {
+
+ # change control characters into a space
+ $dtext =~ s/[\x00-\x1F\x7F]+/ /g;
+ }
+ return trim($dtext);
}
sub on_main_db (&) {
- my $code = shift;
- my $original_dbh = Bugzilla->dbh;
- Bugzilla->request_cache->{dbh} = Bugzilla->dbh_main;
- $code->();
- Bugzilla->request_cache->{dbh} = $original_dbh;
+ my $code = shift;
+ my $original_dbh = Bugzilla->dbh;
+ Bugzilla->request_cache->{dbh} = Bugzilla->dbh_main;
+ $code->();
+ Bugzilla->request_cache->{dbh} = $original_dbh;
}
sub get_text {
- my ($name, $vars) = @_;
- my $template = Bugzilla->template_inner;
- $vars ||= {};
- $vars->{'message'} = $name;
- my $message;
- if (!$template->process('global/message.txt.tmpl', $vars, \$message)) {
- require Bugzilla::Error;
- Bugzilla::Error::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;
+ if (!$template->process('global/message.txt.tmpl', $vars, \$message)) {
+ require Bugzilla::Error;
+ Bugzilla::Error::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.
- my $result = $template->process('global/field-descs.none.tmpl',
- { vars => \%vars, in_template_var => 1 });
- # Bugzilla::Error can't be "use"d in Bugzilla::Util.
- if (!$result) {
- require Bugzilla::Error;
- Bugzilla::Error::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.
+ my $result = $template->process('global/field-descs.none.tmpl',
+ {vars => \%vars, in_template_var => 1});
+
+ # Bugzilla::Error can't be "use"d in Bugzilla::Util.
+ if (!$result) {
+ require Bugzilla::Error;
+ Bugzilla::Error::ThrowTemplateError($template->error);
+ }
+ $cache->{$lang} = \%vars;
+ return $vars{$name};
}
sub display_value {
- my ($field, $value) = @_;
- return template_var('value_descs')->{$field}->{$value} // $value;
+ my ($field, $value) = @_;
+ return template_var('value_descs')->{$field}->{$value} // $value;
}
sub disable_utf8 {
- if (Bugzilla->params->{'utf8'}) {
- binmode STDOUT, ':bytes'; # Turn off UTF8 encoding.
- }
+ if (Bugzilla->params->{'utf8'}) {
+ binmode STDOUT, ':bytes'; # Turn off UTF8 encoding.
+ }
}
sub enable_utf8 {
- if (Bugzilla->params->{'utf8'}) {
- binmode STDOUT, ':utf8'; # Turn on UTF8 encoding.
- }
+ if (Bugzilla->params->{'utf8'}) {
+ binmode STDOUT, ':utf8'; # Turn on UTF8 encoding.
+ }
}
use constant UTF8_ACCIDENTAL => qw(shiftjis big5-eten euc-kr euc-jp);
sub detect_encoding {
- my $data = shift;
-
- if (!Bugzilla->feature('detect_charset')) {
- require Bugzilla::Error;
- Bugzilla::Error::ThrowCodeError('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,
- # but Encode::Guess can usually tell which one it is.
- if ($encoding && $encoding eq 'iso-8859-8') {
- my $decoded_as = _guess_iso($data, 'iso-8859-8',
- # These are ordered this way because it gives the most
- # accurate results.
- qw(iso-8859-7 iso-8859-2));
- $encoding = $decoded_as if $decoded_as;
- }
+ my $data = shift;
+
+ if (!Bugzilla->feature('detect_charset')) {
+ require Bugzilla::Error;
+ Bugzilla::Error::ThrowCodeError('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,
+ # but Encode::Guess can usually tell which one it is.
+ if ($encoding && $encoding eq 'iso-8859-8') {
+ my $decoded_as = _guess_iso(
+ $data, 'iso-8859-8',
+
+ # These are ordered this way because it gives the most
+ # accurate results.
+ qw(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;
}
# From Math::Round
use constant ROUND_HALF => 0.50000000000008;
+
sub round {
- my @res = map {
- $_ >= 0
- ? floor($_ + ROUND_HALF)
- : ceil($_ - ROUND_HALF);
- } @_;
- return (wantarray) ? @res : $res[0];
+ my @res = map { $_ >= 0 ? floor($_ + ROUND_HALF) : ceil($_ - ROUND_HALF); } @_;
+ return (wantarray) ? @res : $res[0];
}
sub extract_nicks {
- my ($name) = @_;
- return () unless defined $name;
- my @nicks = (
- $name =~ /
+ my ($name) = @_;
+ return () unless defined $name;
+ my @nicks = (
+ $name =~ /
# This negative lookbehind lets us
# match colons that are not followed by numbers.
(?<!\d)
@@ -933,9 +981,9 @@ sub extract_nicks {
# can be the end of the string or some punctuation.
\b
/mgx
- );
+ );
- return grep { defined $_ } @nicks;
+ return grep { defined $_ } @nicks;
}
diff --git a/Bugzilla/Version.pm b/Bugzilla/Version.pm
index a078cb4fc..d8c77671a 100644
--- a/Bugzilla/Version.pm
+++ b/Bugzilla/Version.pm
@@ -26,121 +26,118 @@ 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) {
- $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) {
+ $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 ($changes, $old_self) = $self->SUPER::update(@_);
-
- if (exists $changes->{value}) {
- my $dbh = Bugzilla->dbh;
- $dbh->do('UPDATE bugs SET version = ?
- WHERE version = ? AND product_id = ?',
- undef, ($self->name, $old_self->name, $self->product_id));
- }
- return $changes;
-}
+ my $self = shift;
+ my ($changes, $old_self) = $self->SUPER::update(@_);
-sub remove_from_db {
- my $self = shift;
+ if (exists $changes->{value}) {
my $dbh = Bugzilla->dbh;
+ $dbh->do(
+ 'UPDATE bugs SET version = ?
+ WHERE version = ? AND product_id = ?', undef,
+ ($self->name, $old_self->name, $self->product_id)
+ );
+ }
+ return $changes;
+}
- # 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();
+sub remove_from_db {
+ my $self = shift;
+ my $dbh = Bugzilla->dbh;
+
+ # 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();
}
###############################
@@ -148,45 +145,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_name { $_[0]->set('value', $_[1]); }
+sub set_name { $_[0]->set('value', $_[1]); }
sub set_is_active { $_[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);
}
###############################
@@ -196,44 +196,52 @@ sub _check_product {
# This is taken straight from Sort::Versions 1.5, which is not included
# with perl by default.
sub vers_cmp {
- my ($a, $b) = @_;
-
- # Remove leading zeroes - Bug 344661
- $a =~ s/^0*(\d.+)/$1/;
- $b =~ s/^0*(\d.+)/$1/;
-
- my @A = ($a =~ /([-.]|\d+|[^-.\d]+)/g);
- my @B = ($b =~ /([-.]|\d+|[^-.\d]+)/g);
-
- my ($A, $B);
- while (@A and @B) {
- $A = shift @A;
- $B = shift @B;
- if ($A eq '-' and $B eq '-') {
- next;
- } elsif ( $A eq '-' ) {
- return -1;
- } elsif ( $B eq '-') {
- return 1;
- } elsif ($A eq '.' and $B eq '.') {
- next;
- } elsif ( $A eq '.' ) {
- return -1;
- } elsif ( $B eq '.' ) {
- return 1;
- } elsif ($A =~ /^\d+$/ and $B =~ /^\d+$/) {
- if ($A =~ /^0/ || $B =~ /^0/) {
- return $A cmp $B if $A cmp $B;
- } else {
- return $A <=> $B if $A <=> $B;
- }
- } else {
- $A = uc $A;
- $B = uc $B;
- return $A cmp $B if $A cmp $B;
- }
+ my ($a, $b) = @_;
+
+ # Remove leading zeroes - Bug 344661
+ $a =~ s/^0*(\d.+)/$1/;
+ $b =~ s/^0*(\d.+)/$1/;
+
+ my @A = ($a =~ /([-.]|\d+|[^-.\d]+)/g);
+ my @B = ($b =~ /([-.]|\d+|[^-.\d]+)/g);
+
+ my ($A, $B);
+ while (@A and @B) {
+ $A = shift @A;
+ $B = shift @B;
+ if ($A eq '-' and $B eq '-') {
+ next;
+ }
+ elsif ($A eq '-') {
+ return -1;
+ }
+ elsif ($B eq '-') {
+ return 1;
+ }
+ elsif ($A eq '.' and $B eq '.') {
+ next;
+ }
+ elsif ($A eq '.') {
+ return -1;
+ }
+ elsif ($B eq '.') {
+ return 1;
+ }
+ elsif ($A =~ /^\d+$/ and $B =~ /^\d+$/) {
+ if ($A =~ /^0/ || $B =~ /^0/) {
+ return $A cmp $B if $A cmp $B;
+ }
+ else {
+ return $A <=> $B if $A <=> $B;
+ }
+ }
+ else {
+ $A = uc $A;
+ $B = uc $B;
+ return $A cmp $B if $A cmp $B;
}
- return @A <=> @B;
+ }
+ return @A <=> @B;
}
1;
diff --git a/Bugzilla/WebService.pm b/Bugzilla/WebService.pm
index 6ff821a38..da0fbfd32 100644
--- a/Bugzilla/WebService.pm
+++ b/Bugzilla/WebService.pm
@@ -17,11 +17,12 @@ use Bugzilla::WebService::Server;
# Used by the JSON-RPC server to convert incoming date fields apprpriately.
use constant DATE_FIELDS => {};
+
# Used by the JSON-RPC server to convert incoming base64 fields appropriately.
use constant BASE64_FIELDS => {};
# For some methods, we shouldn't call Bugzilla->login before we call them
-use constant LOGIN_EXEMPT => { };
+use constant LOGIN_EXEMPT => {};
# Used to allow methods to be called in the JSON-RPC WebService via GET.
# Methods that can modify data MUST not be listed here.
@@ -32,8 +33,8 @@ use constant READ_ONLY => ();
use constant PUBLIC_METHODS => ();
sub login_exempt {
- my ($class, $method) = @_;
- return $class->LOGIN_EXEMPT->{$method};
+ my ($class, $method) = @_;
+ return $class->LOGIN_EXEMPT->{$method};
}
1;
diff --git a/Bugzilla/WebService/Bug.pm b/Bugzilla/WebService/Bug.pm
index 61a95e07d..5b6c31063 100644
--- a/Bugzilla/WebService/Bug.pm
+++ b/Bugzilla/WebService/Bug.pm
@@ -19,7 +19,8 @@ use Bugzilla::Constants;
use Bugzilla::Error;
use Bugzilla::Field;
use Bugzilla::WebService::Constants;
-use Bugzilla::WebService::Util qw(extract_flags filter filter_wants validate translate);
+use Bugzilla::WebService::Util
+ qw(extract_flags filter filter_wants validate translate);
use Bugzilla::Bug;
use Bugzilla::BugMail;
use Bugzilla::Util qw(trick_taint trim detaint_natural remote_ip);
@@ -45,71 +46,68 @@ use Type::Utils;
use constant PRODUCT_SPECIFIC_FIELDS => qw(version target_milestone component);
sub DATE_FIELDS {
- my $fields = {
- comments => ['new_since'],
- create => [],
- history => ['new_since'],
- search => ['last_change_time', 'creation_time'],
- update => []
- };
-
- # Add date related custom fields
- foreach my $field (Bugzilla->active_custom_fields) {
- next unless ($field->type == FIELD_TYPE_DATETIME
- || $field->type == FIELD_TYPE_DATE);
- push(@{ $fields->{create} }, $field->name);
- push(@{ $fields->{update} }, $field->name);
- }
-
- return $fields;
+ my $fields = {
+ comments => ['new_since'],
+ create => [],
+ history => ['new_since'],
+ search => ['last_change_time', 'creation_time'],
+ update => []
+ };
+
+ # Add date related custom fields
+ foreach my $field (Bugzilla->active_custom_fields) {
+ next
+ unless ($field->type == FIELD_TYPE_DATETIME
+ || $field->type == FIELD_TYPE_DATE);
+ push(@{$fields->{create}}, $field->name);
+ push(@{$fields->{update}}, $field->name);
+ }
+
+ return $fields;
}
-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
+ 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
);
-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',
};
######################################################
@@ -119,6 +117,7 @@ use constant ATTACHMENT_MAPPED_RETURNS => {
BEGIN {
# In 3.0, get was called get_bugs
*get_bugs = \&get;
+
# Before 3.4rc1, "history" was get_history.
*get_history = \&history;
}
@@ -128,1216 +127,1240 @@ BEGIN {
###########
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)],
- });
- }
- }
+ 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)],
+ }
+ );
+ }
}
+ }
+
+ elsif ($field_name eq 'bug_status') {
+ my @status_all = Bugzilla::Status->get_all;
+ 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;
- 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 $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 $html = Bugzilla::Template::quoteUrls($params->{text}, $bug);
+ my $html = Bugzilla::Template::quoteUrls($params->{text}, $bug);
- return { html => $html };
+ return {html => $html};
}
# Helper for Bug.comments
sub _translate_comment {
- my ($self, $comment, $filters, $types, $prefix) = @_;
- my $attach_id = $comment->is_about_attachment ? $comment->extra_data
- : undef;
-
- my $comment_hash = {
- id => $self->type('int', $comment->id),
- bug_id => $self->type('int', $comment->bug_id),
- creator => $self->type('email', $comment->author->login),
- author => $self->type('email', $comment->author->login),
- time => $self->type('dateTime', $comment->creation_ts),
- creation_time => $self->type('dateTime', $comment->creation_ts),
- is_private => $self->type('boolean', $comment->is_private),
- text => $self->type('string', $comment->body_full),
- attachment_id => $self->type('int', $attach_id),
- count => $self->type('int', $comment->count),
- };
-
- # Don't load comment tags unless enabled
- if (Bugzilla->params->{'comment_taggers_group'}) {
- $comment_hash->{tags} = [
- map { $self->type('string', $_) }
- @{ $comment->tags }
- ];
- }
-
- return filter($filters, $comment_hash, $types, $prefix);
+ my ($self, $comment, $filters, $types, $prefix) = @_;
+ my $attach_id = $comment->is_about_attachment ? $comment->extra_data : undef;
+
+ my $comment_hash = {
+ id => $self->type('int', $comment->id),
+ bug_id => $self->type('int', $comment->bug_id),
+ creator => $self->type('email', $comment->author->login),
+ author => $self->type('email', $comment->author->login),
+ time => $self->type('dateTime', $comment->creation_ts),
+ creation_time => $self->type('dateTime', $comment->creation_ts),
+ is_private => $self->type('boolean', $comment->is_private),
+ text => $self->type('string', $comment->body_full),
+ attachment_id => $self->type('int', $attach_id),
+ count => $self->type('int', $comment->count),
+ };
+
+ # Don't load comment tags unless enabled
+ if (Bugzilla->params->{'comment_taggers_group'}) {
+ $comment_hash->{tags} = [map { $self->type('string', $_) } @{$comment->tags}];
+ }
+
+ return filter($filters, $comment_hash, $types, $prefix);
}
sub get {
- my ($self, $params) = validate(@_, 'ids');
-
- unless (Bugzilla->user->id) {
- Bugzilla->check_rate_limit("get_bug", remote_ip());
+ my ($self, $params) = validate(@_, 'ids');
+
+ unless (Bugzilla->user->id) {
+ Bugzilla->check_rate_limit("get_bug", remote_ip());
+ }
+ Bugzilla->switch_to_shadow_db() unless Bugzilla->user->id;
+
+ my $ids = $params->{ids};
+ (defined $ids && scalar @$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;
+ }
}
- Bugzilla->switch_to_shadow_db() unless Bugzilla->user->id;
-
- my $ids = $params->{ids};
- (defined $ids && scalar @$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));
+ 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);
- if (Bugzilla->user->id) {
- foreach my $bug (@bugs) {
- Bugzilla->log_user_request($bug->id, undef, 'bug-get');
- }
+ if (Bugzilla->user->id) {
+ foreach my $bug (@bugs) {
+ Bugzilla->log_user_request($bug->id, undef, 'bug-get');
}
- 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} = $self->type('int', $bug_id);
-
- my ($activity) = Bugzilla::Bug::GetBugActivity($bug_id, 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('email', $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;
-
- # 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
- if (Bugzilla->params->{'usebugaliases'}) {
- $item{alias} = $self->type('string', $bug->alias);
+ 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)
+ = Bugzilla::Bug::GetBugActivity($bug_id, 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('email', $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);
}
- else {
- # For API reasons, we always want the value to appear, we just
- # don't want it to have a value if aliases are turned off.
- $item{alias} = undef;
- }
-
- push(@return, \%item);
- }
-
- return { bugs => \@return };
-}
-
-sub search {
- my ($self, $params) = @_;
- my $user = Bugzilla->user;
- my $dbh = Bugzilla->dbh;
-
- Bugzilla->switch_to_shadow_db();
-
- my $match_params = dclone($params);
- delete $match_params->{include_fields};
- delete $match_params->{exclude_fields};
-
- # Determine whether this is a quicksearch query
- if (exists $match_params->{quicksearch}) {
- my $quicksearch = quicksearch($match_params->{'quicksearch'});
- my $cgi = Bugzilla::CGI->new($quicksearch);
- $match_params = $cgi->Vars;
+ $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);
}
- if ( defined($match_params->{offset}) and !defined($match_params->{limit}) ) {
- ThrowCodeError('param_required',
- { param => 'limit', function => 'Bug.search()' });
- }
+ $item{history} = \@history;
- 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;
- }
+ # 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
+ if (Bugzilla->params->{'usebugaliases'}) {
+ $item{alias} = $self->type('string', $bug->alias);
}
else {
- delete $match_params->{limit};
- delete $match_params->{offset};
+ # For API reasons, we always want the value to appear, we just
+ # don't want it to have a value if aliases are turned off.
+ $item{alias} = undef;
}
- $match_params = Bugzilla::Bug::map_fields($match_params);
-
- my %options = ( fields => ['bug_id'] );
+ push(@return, \%item);
+ }
- # Find the highest custom field id
- my @field_ids = grep(/^f(\d+)$/, keys %$match_params);
- my $last_field_id = @field_ids ? max @field_ids + 1 : 1;
+ return {bugs => \@return};
+}
- # 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++;
+sub search {
+ my ($self, $params) = @_;
+ my $user = Bugzilla->user;
+ my $dbh = Bugzilla->dbh;
+
+ Bugzilla->switch_to_shadow_db();
+
+ my $match_params = dclone($params);
+ delete $match_params->{include_fields};
+ delete $match_params->{exclude_fields};
+
+ # Determine whether this is a quicksearch query
+ if (exists $match_params->{quicksearch}) {
+ my $quicksearch = quicksearch($match_params->{'quicksearch'});
+ my $cgi = Bugzilla::CGI->new($quicksearch);
+ $match_params = $cgi->Vars;
+ }
+
+ if (defined($match_params->{offset}) and !defined($match_params->{limit})) {
+ ThrowCodeError('param_required',
+ {param => 'limit', function => 'Bug.search()'});
+ }
+
+ my $max_results = Bugzilla->params->{max_search_results};
+ unless (defined $match_params->{limit} && $match_params->{limit} == 0) {
+ if (!defined $match_params->{limit} || $match_params->{limit} > $max_results) {
+ $match_params->{limit} = $max_results;
}
- 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++;
+ }
+ else {
+ delete $match_params->{limit};
+ delete $match_params->{offset};
+ }
+
+ $match_params = Bugzilla::Bug::map_fields($match_params);
+
+ my %options = (fields => ['bug_id']);
+
+ # Find the highest custom field id
+ my @field_ids = grep(/^f(\d+)$/, keys %$match_params);
+ my $last_field_id = @field_ids ? max @field_ids + 1 : 1;
+
+ # Do special search types for certain fields.
+ if (my $change_when = delete $match_params->{'delta_ts'}) {
+ $match_params->{"f${last_field_id}"} = 'delta_ts';
+ $match_params->{"o${last_field_id}"} = 'greaterthaneq';
+ $match_params->{"v${last_field_id}"} = $change_when;
+ $last_field_id++;
+ }
+ if (my $creation_when = delete $match_params->{'creation_ts'}) {
+ $match_params->{"f${last_field_id}"} = 'creation_ts';
+ $match_params->{"o${last_field_id}"} = 'greaterthaneq';
+ $match_params->{"v${last_field_id}"} = $creation_when;
+ $last_field_id++;
+ }
+
+ # Some fields require a search type such as short desc, keywords, etc.
+ foreach my $param (qw(short_desc longdesc status_whiteboard bug_file_loc)) {
+ if (defined $match_params->{$param}
+ && !defined $match_params->{$param . '_type'})
+ {
+ $match_params->{$param . '_type'} = 'allwordssubstr';
}
-
- # 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';
+ }
+
+ # 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 commenter 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}};
}
- if (defined $match_params->{'keywords'} && !defined $match_params->{'keywords_type'}) {
- $match_params->{'keywords_type'} = 'allwords';
+ else {
+ $options{order} = [split(/\s*,\s*/, $match_params->{order})];
}
+ }
- # 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 commenter 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++;
- }
+ $options{params} = $match_params;
- # 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');
- }
+ my $search = new Bugzilla::Search(%options);
+ my ($data) = $search->data;
- # 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}) ];
- }
+ # BMO if the caller only wants the count, that's all we need to return
+ if ($params->{count_only}) {
+ if (Bugzilla->usage_mode == USAGE_MODE_XMLRPC) {
+ return $data;
}
-
- $options{params} = $match_params;
-
- my $search = new Bugzilla::Search(%options);
- my ($data) = $search->data;
-
- # BMO if the caller only wants the count, that's all we need to return
- if ($params->{count_only}) {
- if (Bugzilla->usage_mode == USAGE_MODE_XMLRPC) {
- return $data;
- }
- else {
- return { bug_count => $data };
- }
+ else {
+ return {bug_count => $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;
- # BzAPI
- Bugzilla->request_cache->{bzapi_search_bugs} = [ map { $bug_objects{$_} } @bug_ids ];
+ # BzAPI
+ Bugzilla->request_cache->{bzapi_search_bugs}
+ = [map { $bug_objects{$_} } @bug_ids];
- return { bugs => \@bugs };
+ return {bugs => \@bugs};
}
sub possible_duplicates {
- my ($self, $params) = validate(@_, 'product');
- my $user = Bugzilla->user;
-
- Bugzilla->switch_to_shadow_db();
-
- state $params_type = Dict [
- id => Optional [Int],
- product => Optional [ ArrayRef [Str] ],
- limit => Optional [Int],
- summary => Optional [Str],
- include_fields => Optional [ ArrayRef [Str] ],
- Bugzilla_api_token => Optional [Str]
- ];
-
- ThrowCodeError( 'param_invalid', { function => 'Bug.possible_duplicates', param => 'A param' } )
- if !$params_type->check($params);
-
- my $summary;
- if ($params->{id}) {
- my $bug = Bugzilla::Bug->check({ id => $params->{id}, cache => 1 });
- $summary = $bug->short_desc;
- }
- elsif ($params->{summary}) {
- $summary = $params->{summary};
- }
- else {
- ThrowCodeError('param_required',
- { function => 'Bug.possible_duplicates', param => 'id or summary' });
- }
-
- my @products;
- foreach my $name (@{ $params->{'product'} || [] }) {
- my $object = $user->can_enter_product($name, THROW_ERROR);
- push(@products, $object);
- }
-
- my $possible_dupes = Bugzilla::Bug->possible_duplicates(
- {
- summary => $summary,
- products => \@products,
- limit => $params->{limit}
- }
- );
+ my ($self, $params) = validate(@_, 'product');
+ my $user = Bugzilla->user;
+
+ Bugzilla->switch_to_shadow_db();
+
+ state $params_type = Dict [
+ id => Optional [Int],
+ product => Optional [ArrayRef [Str]],
+ limit => Optional [Int],
+ summary => Optional [Str],
+ include_fields => Optional [ArrayRef [Str]],
+ Bugzilla_api_token => Optional [Str]
+ ];
+
+ ThrowCodeError('param_invalid',
+ {function => 'Bug.possible_duplicates', param => 'A param'})
+ if !$params_type->check($params);
+
+ my $summary;
+ if ($params->{id}) {
+ my $bug = Bugzilla::Bug->check({id => $params->{id}, cache => 1});
+ $summary = $bug->short_desc;
+ }
+ elsif ($params->{summary}) {
+ $summary = $params->{summary};
+ }
+ else {
+ ThrowCodeError('param_required',
+ {function => 'Bug.possible_duplicates', param => 'id or summary'});
+ }
+
+ my @products;
+ foreach my $name (@{$params->{'product'} || []}) {
+ my $object = $user->can_enter_product($name, THROW_ERROR);
+ push(@products, $object);
+ }
+
+ my $possible_dupes
+ = Bugzilla::Bug->possible_duplicates({
+ summary => $summary, products => \@products, limit => $params->{limit}
+ });
- # If a bug id was used, remove the bug with the same id from the list.
- if ($params->{id}) {
- @$possible_dupes = grep { $_->id != $params->{id} } @$possible_dupes;
- }
+ # If a bug id was used, remove the bug with the same id from the list.
+ if ($params->{id}) {
+ @$possible_dupes = grep { $_->id != $params->{id} } @$possible_dupes;
+ }
- my @hashes = map { $self->_bug_to_hash($_, $params) } @$possible_dupes;
- $self->_add_update_tokens($params, $possible_dupes, \@hashes);
- return { bugs => \@hashes };
+ 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');
- # BMO: Don't allow updating of bugs if disabled
- if (Bugzilla->params->{disable_bug_updates}) {
- ThrowErrorPage('bug/process/updates-disabled.html.tmpl',
- 'Bug updates are currently disabled.');
- }
+ # BMO: Don't allow updating of bugs if disabled
+ if (Bugzilla->params->{disable_bug_updates}) {
+ ThrowErrorPage(
+ 'bug/process/updates-disabled.html.tmpl',
+ 'Bug updates are currently disabled.'
+ );
+ }
- 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($_) } @$ids;
+ my @bugs = map { Bugzilla::Bug->check($_) } @$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};
- my $flags = delete $values{flags};
+ my $flags = delete $values{flags};
- foreach my $bug (@bugs) {
- if (!$user->can_edit_product($bug->product_obj->id) ) {
- ThrowUserError("product_edit_denied",
- { product => $bug->product });
- }
-
- $bug->set_all(\%values);
- if ($flags) {
- my ($old_flags, $new_flags) = extract_flags($flags, $bug);
- $bug->set_flags($old_flags, $new_flags);
- }
+ foreach my $bug (@bugs) {
+ if (!$user->can_edit_product($bug->product_obj->id)) {
+ ThrowUserError("product_edit_denied", {product => $bug->product});
}
- my %all_changes;
- $dbh->bz_start_transaction();
- foreach my $bug (@bugs) {
- $all_changes{$bug->id} = $bug->update();
+ $bug->set_all(\%values);
+ if ($flags) {
+ my ($old_flags, $new_flags) = extract_flags($flags, $bug);
+ $bug->set_flags($old_flags, $new_flags);
}
- $dbh->bz_commit_transaction();
+ }
+
+ my %all_changes;
+ $dbh->bz_start_transaction();
+ foreach my $bug (@bugs) {
+ $all_changes{$bug->id} = $bug->update();
+ }
+ $dbh->bz_commit_transaction();
+
+ foreach my $bug (@bugs) {
+ $bug->send_changes($all_changes{$bug->id});
+ }
+
+ my %api_name = reverse %{Bugzilla::Bug::FIELD_MAP()};
+
+ # This doesn't normally belong in FIELD_MAP, but we do want to translate
+ # "bug_group" back into "groups".
+ $api_name{'bug_group'} = 'groups';
+
+ my @result;
+ foreach my $bug (@bugs) {
+ my %hash = (
+ id => $self->type('int', $bug->id),
+ last_change_time => $self->type('dateTime', $bug->delta_ts),
+ changes => {},
+ );
- foreach my $bug (@bugs) {
- $bug->send_changes($all_changes{$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.
+ if (Bugzilla->params->{'usebugaliases'}) {
+ $hash{alias} = $self->type('string', $bug->alias);
+ }
+ else {
+ # For API reasons, we always want the alias field to appear, we
+ # just don't want it to have a value if aliases are turned off.
+ $hash{alias} = $self->type('string', '');
}
- my %api_name = reverse %{ Bugzilla::Bug::FIELD_MAP() };
- # This doesn't normally belong in FIELD_MAP, but we do want to translate
- # "bug_group" back into "groups".
- $api_name{'bug_group'} = 'groups';
-
- my @result;
- foreach my $bug (@bugs) {
- my %hash = (
- id => $self->type('int', $bug->id),
- last_change_time => $self->type('dateTime', $bug->delta_ts),
- changes => {},
- );
-
- # alias is returned in case users pass a mixture of ids and aliases,
- # so that they can know which set of changes relates to which value
- # they passed.
- if (Bugzilla->params->{'usebugaliases'}) {
- $hash{alias} = $self->type('string', $bug->alias);
- }
- else {
- # For API reasons, we always want the alias field to appear, we
- # just don't want it to have a value if aliases are turned off.
- $hash{alias} = $self->type('string', '');
- }
-
- my %changes = %{ $all_changes{$bug->id} };
- foreach my $field (keys %changes) {
- my $change = $changes{$field};
- my $api_field = $api_name{$field} || $field;
- # We normalize undef to an empty string, so that the API
- # stays consistent for things like Deadline that can become
- # empty.
- $change->[0] = '' if !defined $change->[0];
- $change->[1] = '' if !defined $change->[1];
- $hash{changes}->{$api_field} = {
- removed => $self->type('string', $change->[0]),
- added => $self->type('string', $change->[1])
- };
- }
-
- push(@result, \%hash);
+ my %changes = %{$all_changes{$bug->id}};
+ foreach my $field (keys %changes) {
+ my $change = $changes{$field};
+ my $api_field = $api_name{$field} || $field;
+
+ # We normalize undef to an empty string, so that the API
+ # stays consistent for things like Deadline that can become
+ # empty.
+ $change->[0] = '' if !defined $change->[0];
+ $change->[1] = '' if !defined $change->[1];
+ $hash{changes}->{$api_field} = {
+ removed => $self->type('string', $change->[0]),
+ added => $self->type('string', $change->[1])
+ };
}
- return { bugs => \@result };
+ push(@result, \%hash);
+ }
+
+ return {bugs => \@result};
}
sub create {
- my ($self, $params) = @_;
- my $dbh = Bugzilla->dbh;
-
- # BMO: Don't allow updating of bugs if disabled
- if (Bugzilla->params->{disable_bug_updates}) {
- ThrowErrorPage('bug/process/updates-disabled.html.tmpl',
- 'Bug updates are currently disabled.');
- }
+ my ($self, $params) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ # BMO: Don't allow updating of bugs if disabled
+ if (Bugzilla->params->{disable_bug_updates}) {
+ ThrowErrorPage(
+ 'bug/process/updates-disabled.html.tmpl',
+ 'Bug updates are currently disabled.'
+ );
+ }
- Bugzilla->login(LOGIN_REQUIRED);
+ Bugzilla->login(LOGIN_REQUIRED);
- # Some fields cannot be sent to Bugzilla::Bug->create
- foreach my $key (qw(login password token)) {
- delete $params->{$key};
- }
+ # Some fields cannot be sent to Bugzilla::Bug->create
+ foreach my $key (qw(login password token)) {
+ delete $params->{$key};
+ }
- $params = Bugzilla::Bug::map_fields($params);
+ $params = Bugzilla::Bug::map_fields($params);
- my $flags = delete $params->{flags};
+ my $flags = delete $params->{flags};
- # We start a nested transaction in case flag setting fails
- # we want the bug creation to roll back as well.
- $dbh->bz_start_transaction();
+ # We start a nested transaction in case flag setting fails
+ # we want the bug creation to roll back as well.
+ $dbh->bz_start_transaction();
- my $bug = Bugzilla::Bug->create($params);
+ my $bug = Bugzilla::Bug->create($params);
- # Set bug flags
- if ($flags) {
- my ($flags, $new_flags) = extract_flags($flags, $bug);
- $bug->set_flags($flags, $new_flags);
- $bug->update($bug->creation_ts);
- }
+ # Set bug flags
+ if ($flags) {
+ my ($flags, $new_flags) = extract_flags($flags, $bug);
+ $bug->set_flags($flags, $new_flags);
+ $bug->update($bug->creation_ts);
+ }
- $dbh->bz_commit_transaction();
+ $dbh->bz_commit_transaction();
- $bug->send_changes();
+ $bug->send_changes();
- return { id => $self->type('int', $bug->bug_id) };
+ return {id => $self->type('int', $bug->bug_id)};
}
sub legal_values {
- my ($self, $params) = @_;
+ my ($self, $params) = @_;
- Bugzilla->switch_to_shadow_db();
+ Bugzilla->switch_to_shadow_db();
- defined $params->{field}
- or ThrowCodeError('param_required', { param => 'field' });
+ defined $params->{field}
+ or ThrowCodeError('param_required', {param => 'field'});
- my $field = Bugzilla::Bug::FIELD_MAP->{$params->{field}}
- || $params->{field};
+ my $field = Bugzilla::Bug::FIELD_MAP->{$params->{field}} || $params->{field};
- my @global_selects =
- @{ Bugzilla->fields({ is_select => 1, is_abnormal => 0 }) };
+ my @global_selects = @{Bugzilla->fields({is_select => 1, is_abnormal => 0})};
- my $values;
- if (grep($_->name eq $field, @global_selects)) {
- # The field is a valid one.
- trick_taint($field);
- $values = get_legal_field_values($field);
- }
- elsif (grep($_ eq $field, PRODUCT_SPECIFIC_FIELDS)) {
- my $id = $params->{product_id};
- defined $id || ThrowCodeError('param_required',
- { function => 'Bug.legal_values', param => 'product_id' });
- grep($_->id eq $id, @{Bugzilla->user->get_accessible_products})
- || ThrowUserError('product_access_denied', { id => $id });
-
- my $product = new Bugzilla::Product($id);
- my @objects;
- if ($field eq 'version') {
- @objects = @{$product->versions};
- }
- elsif ($field eq 'target_milestone') {
- @objects = @{$product->milestones};
- }
- elsif ($field eq 'component') {
- @objects = @{$product->components};
- }
+ my $values;
+ if (grep($_->name eq $field, @global_selects)) {
- $values = [map { $_->name } @objects];
+ # The field is a valid one.
+ trick_taint($field);
+ $values = get_legal_field_values($field);
+ }
+ elsif (grep($_ eq $field, PRODUCT_SPECIFIC_FIELDS)) {
+ my $id = $params->{product_id};
+ defined $id
+ || ThrowCodeError('param_required',
+ {function => 'Bug.legal_values', param => 'product_id'});
+ grep($_->id eq $id, @{Bugzilla->user->get_accessible_products})
+ || ThrowUserError('product_access_denied', {id => $id});
+
+ my $product = new Bugzilla::Product($id);
+ my @objects;
+ if ($field eq 'version') {
+ @objects = @{$product->versions};
}
- else {
- ThrowCodeError('invalid_field_name', { field => $params->{field} });
+ elsif ($field eq 'target_milestone') {
+ @objects = @{$product->milestones};
}
-
- my @result;
- foreach my $val (@$values) {
- push(@result, $self->type('string', $val));
+ elsif ($field eq 'component') {
+ @objects = @{$product->components};
}
- return { values => \@result };
+ $values = [map { $_->name } @objects];
+ }
+ else {
+ ThrowCodeError('invalid_field_name', {field => $params->{field}});
+ }
+
+ my @result;
+ foreach my $val (@$values) {
+ push(@result, $self->type('string', $val));
+ }
+
+ return {values => \@result};
}
sub add_attachment {
- my ($self, $params) = validate(@_, 'ids');
- my $dbh = Bugzilla->dbh;
+ my ($self, $params) = validate(@_, 'ids');
+ my $dbh = Bugzilla->dbh;
+
+ # BMO: Don't allow updating of bugs if disabled
+ if (Bugzilla->params->{disable_bug_updates}) {
+ ThrowErrorPage(
+ 'bug/process/updates-disabled.html.tmpl',
+ 'Bug updates are currently disabled.'
+ );
+ }
- # BMO: Don't allow updating of bugs if disabled
- if (Bugzilla->params->{disable_bug_updates}) {
- ThrowErrorPage('bug/process/updates-disabled.html.tmpl',
- 'Bug updates are currently disabled.');
- }
+ Bugzilla->login(LOGIN_REQUIRED);
+ defined $params->{ids} || ThrowCodeError('param_required', {param => 'ids'});
+ defined $params->{data} || ThrowCodeError('param_required', {param => 'data'});
- 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($_) } @{$params->{ids}};
+ foreach my $bug (@bugs) {
+ Bugzilla->user->can_edit_product($bug->product_id)
+ || ThrowUserError("product_edit_denied", {product => $bug->product});
+ }
+
+ 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 @bugs = map { Bugzilla::Bug->check($_) } @{ $params->{ids} };
- foreach my $bug (@bugs) {
- Bugzilla->user->can_edit_product($bug->product_id)
- || ThrowUserError("product_edit_denied", {product => $bug->product});
+ if ($flags) {
+ my ($old_flags, $new_flags) = extract_flags($flags, $bug, $attachment);
+ $attachment->set_flags($old_flags, $new_flags);
}
- my @created;
- $dbh->bz_start_transaction();
- my $timestamp = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
-
- my $flags = delete $params->{flags};
-
- foreach my $bug (@bugs) {
- my $attachment = Bugzilla::Attachment->create({
- bug => $bug,
- creation_ts => $timestamp,
- data => $params->{data},
- description => $params->{summary},
- filename => $params->{file_name},
- mimetype => $params->{content_type},
- ispatch => $params->{is_patch},
- isprivate => $params->{is_private},
- });
-
- if ($flags) {
- my ($old_flags, $new_flags) = extract_flags($flags, $bug, $attachment);
- $attachment->set_flags($old_flags, $new_flags);
- }
-
- $attachment->update($timestamp);
- my $comment = $params->{comment} || '';
- $attachment->bug->add_comment($comment,
- { isprivate => $attachment->isprivate,
- type => CMT_ATTACHMENT_CREATED,
- extra_data => $attachment->id });
- push(@created, $attachment);
- }
- $_->bug->update($timestamp) foreach @created;
- $dbh->bz_commit_transaction();
+ $attachment->update($timestamp);
+ my $comment = $params->{comment} || '';
+ $attachment->bug->add_comment(
+ $comment,
+ {
+ isprivate => $attachment->isprivate,
+ type => CMT_ATTACHMENT_CREATED,
+ extra_data => $attachment->id
+ }
+ );
+ push(@created, $attachment);
+ }
+ $_->bug->update($timestamp) foreach @created;
+ $dbh->bz_commit_transaction();
- $_->send_changes() foreach @bugs;
+ $_->send_changes() foreach @bugs;
- my %attachments = map { $_->id => $self->_attachment_to_hash($_, $params) }
- @created;
+ my %attachments
+ = map { $_->id => $self->_attachment_to_hash($_, $params) } @created;
- return { attachments => \%attachments };
+ return {attachments => \%attachments};
}
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};
- }
-
- $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 ($self, $params) = validate(@_, 'ids');
+
+ my $user = Bugzilla->login(LOGIN_REQUIRED);
+ my $dbh = Bugzilla->dbh;
+
+ my $ids = delete $params->{ids};
+ defined $ids || ThrowCodeError('param_required', {param => 'ids'});
+
+ # Some fields cannot be sent to set_all
+ foreach my $key (qw(login password token)) {
+ delete $params->{$key};
+ }
+
+ $params = translate($params, ATTACHMENT_MAPPED_SETTERS);
+
+ # Get all the attachments, after verifying that they exist and are editable
+ my @attachments = ();
+ my %bugs = ();
+ foreach my $id (@$ids) {
+ my $attachment = Bugzilla::Attachment->new($id)
+ || ThrowUserError("invalid_attach_id", {attach_id => $id});
+ my $bug = $attachment->bug;
+ $attachment->_check_bug;
+
+ push @attachments, $attachment;
+ $bugs{$bug->id} = $bug;
+ }
+
+ my $flags = delete $params->{flags};
+ my $comment = delete $params->{comment};
+
+ # Update the values
+ foreach my $attachment (@attachments) {
+ my ($update_flags, $new_flags)
+ = $flags ? extract_flags($flags, $attachment->bug, $attachment) : ([], []);
+ if ($attachment->validate_can_edit) {
+ $attachment->set_all($params);
+ $attachment->set_flags($update_flags, $new_flags) if $flags;
}
-
- my $flags = delete $params->{flags};
- my $comment = delete $params->{comment};
-
- # Update the values
- foreach my $attachment (@attachments) {
- my ($update_flags, $new_flags) = $flags
- ? extract_flags($flags, $attachment->bug, $attachment)
- : ([], []);
- if ($attachment->validate_can_edit) {
- $attachment->set_all($params);
- $attachment->set_flags($update_flags, $new_flags) if $flags;
- }
- elsif (scalar @$update_flags && !scalar(@$new_flags) && !scalar keys %$params) {
- # Requestees can set flags targetted to them, even if they cannot
- # edit the attachment. Flag setters can edit their own flags too.
- my %flag_list = map { $_->{id} => $_ } @$update_flags;
- my $flag_objs = Bugzilla::Flag->new_from_list([ keys %flag_list ]);
- my @editable_flags;
- foreach my $flag_obj (@$flag_objs) {
- if ($flag_obj->setter_id == $user->id
- || ($flag_obj->requestee_id && $flag_obj->requestee_id == $user->id))
- {
- push(@editable_flags, $flag_list{$flag_obj->id});
- }
- }
- if (!scalar @editable_flags) {
- ThrowUserError("illegal_attachment_edit", { attach_id => $attachment->id });
- }
- $attachment->set_flags(\@editable_flags, []);
- }
- else {
- ThrowUserError("illegal_attachment_edit", { attach_id => $attachment->id });
+ elsif (scalar @$update_flags && !scalar(@$new_flags) && !scalar keys %$params) {
+
+ # Requestees can set flags targetted to them, even if they cannot
+ # edit the attachment. Flag setters can edit their own flags too.
+ my %flag_list = map { $_->{id} => $_ } @$update_flags;
+ my $flag_objs = Bugzilla::Flag->new_from_list([keys %flag_list]);
+ my @editable_flags;
+ foreach my $flag_obj (@$flag_objs) {
+ if ($flag_obj->setter_id == $user->id
+ || ($flag_obj->requestee_id && $flag_obj->requestee_id == $user->id))
+ {
+ push(@editable_flags, $flag_list{$flag_obj->id});
}
+ }
+ if (!scalar @editable_flags) {
+ ThrowUserError("illegal_attachment_edit", {attach_id => $attachment->id});
+ }
+ $attachment->set_flags(\@editable_flags, []);
}
+ else {
+ ThrowUserError("illegal_attachment_edit", {attach_id => $attachment->id});
+ }
+ }
- $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();
-
- if ($comment = trim($comment)) {
- $attachment->bug->add_comment($comment,
- { isprivate => $attachment->isprivate,
- type => CMT_ATTACHMENT_UPDATED,
- extra_data => $attachment->id });
- }
+ # Do the actual update and get information to return to user
+ my @result;
+ foreach my $attachment (@attachments) {
+ my $changes = $attachment->update();
- $changes = translate($changes, ATTACHMENT_MAPPED_RETURNS);
+ if ($comment = trim($comment)) {
+ $attachment->bug->add_comment(
+ $comment,
+ {
+ isprivate => $attachment->isprivate,
+ type => CMT_ATTACHMENT_UPDATED,
+ extra_data => $attachment->id
+ }
+ );
+ }
- my %hash = (
- id => $self->type('int', $attachment->id),
- last_change_time => $self->type('dateTime', $attachment->modification_time),
- changes => {},
- );
+ $changes = translate($changes, ATTACHMENT_MAPPED_RETURNS);
- foreach my $field (keys %$changes) {
- my $change = $changes->{$field};
+ my %hash = (
+ id => $self->type('int', $attachment->id),
+ last_change_time => $self->type('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 => $self->type('string', $change->[0] // ''),
- added => $self->type('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 => $self->type('string', $change->[0] // ''),
+ added => $self->type('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();
- # Return the information to the user
- return { attachments => \@result };
+ # Email users about the change
+ foreach my $bug (values %bugs) {
+ $bug->update();
+ $bug->send_changes();
+ }
+
+ # Return the information to the user
+ return {attachments => \@result};
}
sub add_comment {
- my ($self, $params) = @_;
+ my ($self, $params) = @_;
- # BMO: Don't allow updating of bugs if disabled
- if (Bugzilla->params->{disable_bug_updates}) {
- ThrowErrorPage('bug/process/updates-disabled.html.tmpl',
- 'Bug updates are currently disabled.');
- }
+ # BMO: Don't allow updating of bugs if disabled
+ if (Bugzilla->params->{disable_bug_updates}) {
+ ThrowErrorPage(
+ 'bug/process/updates-disabled.html.tmpl',
+ 'Bug updates are currently disabled.'
+ );
+ }
- #The user must login in order add a comment
- Bugzilla->login(LOGIN_REQUIRED);
+ #The user must login in order add a comment
+ 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($params->{id});
+ my $bug = Bugzilla::Bug->check($params->{id});
- 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});
- # Backwards-compatibility for versions before 3.6
- if (defined $params->{private}) {
- $params->{is_private} = delete $params->{private};
- }
- # Append comment
- $bug->add_comment($comment, { isprivate => $params->{is_private},
- work_time => $params->{work_time} });
+ # Backwards-compatibility for versions before 3.6
+ if (defined $params->{private}) {
+ $params->{is_private} = delete $params->{private};
+ }
- # Add comment tags
- $bug->set_all({ comment_tags => $params->{comment_tags} })
- if defined $params->{comment_tags};
+ # Append comment
+ $bug->add_comment($comment,
+ {isprivate => $params->{is_private}, work_time => $params->{work_time}});
- # Capture the call to bug->update (which creates the new comment) in
- # a transaction so we're sure to get the correct comment_id.
+ # Add comment tags
+ $bug->set_all({comment_tags => $params->{comment_tags}})
+ if defined $params->{comment_tags};
- my $dbh = Bugzilla->dbh;
- $dbh->bz_start_transaction();
+ # Capture the call to bug->update (which creates the new comment) in
+ # a transaction so we're sure to get the correct comment_id.
- $bug->update();
+ my $dbh = Bugzilla->dbh;
+ $dbh->bz_start_transaction();
- my $new_comment_id = $dbh->bz_last_key('longdescs', 'comment_id');
+ $bug->update();
- $dbh->bz_commit_transaction();
+ my $new_comment_id = $dbh->bz_last_key('longdescs', 'comment_id');
- # Send mail.
- Bugzilla::BugMail::Send($bug->bug_id, { changer => Bugzilla->user });
+ $dbh->bz_commit_transaction();
- return { id => $self->type('int', $new_comment_id) };
+ # Send mail.
+ Bugzilla::BugMail::Send($bug->bug_id, {changer => Bugzilla->user});
+
+ return {id => $self->type('int', $new_comment_id)};
}
sub update_see_also {
- my ($self, $params) = @_;
+ my ($self, $params) = @_;
- # BMO: Don't allow updating of bugs if disabled
- if (Bugzilla->params->{disable_bug_updates}) {
- ThrowErrorPage('bug/process/updates-disabled.html.tmpl',
- 'Bug updates are currently disabled.');
+ # BMO: Don't allow updating of bugs if disabled
+ if (Bugzilla->params->{disable_bug_updates}) {
+ ThrowErrorPage(
+ 'bug/process/updates-disabled.html.tmpl',
+ 'Bug updates are currently disabled.'
+ );
+ }
+
+ 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($id);
+ $user->can_edit_product($bug->product_id)
+ || ThrowUserError("product_edit_denied", {product => $bug->product});
+ push(@bugs, $bug);
+ if ($remove) {
+ $bug->remove_see_also($_) foreach @$remove;
}
-
- 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($id);
- $user->can_edit_product($bug->product_id)
- || ThrowUserError("product_edit_denied",
- { product => $bug->product });
- push(@bugs, $bug);
- if ($remove) {
- $bug->remove_see_also($_) foreach @$remove;
- }
- if ($add) {
- $bug->add_see_also($_) foreach @$add;
- }
+ 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 });
+ }
+
+ 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 $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 %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;
- my @log_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});
- }
- push @log_attachments, $attach;
-
- $attachments{$attach->id} =
- $self->_attachment_to_hash($attach, $params);
+ }
+
+ my %attachments;
+ my @log_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});
}
+ push @log_attachments, $attach;
- if (Bugzilla->user->id) {
- foreach my $attachment (@log_attachments) {
- Bugzilla->log_user_request($attachment->bug_id, $attachment->id, "attachment-get");
- }
+ $attachments{$attach->id} = $self->_attachment_to_hash($attach, $params);
+ }
+
+ if (Bugzilla->user->id) {
+ foreach my $attachment (@log_attachments) {
+ Bugzilla->log_user_request($attachment->bug_id, $attachment->id,
+ "attachment-get");
}
+ }
- return { bugs => \%bugs, attachments => \%attachments };
+ return {bugs => \%bugs, attachments => \%attachments};
}
sub 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;
+ 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 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' });
-
- 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'});
+
+ 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];
}
##############################
@@ -1350,304 +1373,320 @@ 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, {
- alias => $self->type('string', $bug->alias),
- 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),
- } };
-
- state $voting_enabled //= $bug->can('votes') ? 1 : 0;
- if ($voting_enabled && filter_wants $params, 'votes') {
- $item{votes} = $self->type('int', $bug->votes);
- }
+ my ($self, $bug, $params) = @_;
- # First we handle any fields that require extra work (such as date parsing
- # or SQL calls).
- if (filter_wants $params, 'assigned_to') {
- $item{'assigned_to'} = $self->type('email', $bug->assigned_to->login);
- $item{'assigned_to_detail'} = $self->_user_to_hash($bug->assigned_to, $params, undef, 'assigned_to');
- }
- if (filter_wants $params, 'blocks') {
- my @blocks = map { $self->type('int', $_) } @{ $bug->blocked };
- $item{'blocks'} = \@blocks;
- }
- if (filter_wants $params, 'classification') {
- $item{classification} = $self->type('string', $bug->classification);
- }
- if (filter_wants $params, 'component') {
- $item{component} = $self->type('string', $bug->component);
- }
- if (filter_wants $params, 'cc') {
- my @cc = map { $self->type('email', $_) } @{ $bug->cc || [] };
- $item{'cc'} = \@cc;
- $item{'cc_detail'} = [ map { $self->_user_to_hash($_, $params, undef, 'cc') } @{ $bug->cc_users } ];
- }
- if (filter_wants $params, 'creation_time') {
- $item{'creation_time'} = $self->type('dateTime', $bug->creation_ts);
- }
- if (filter_wants $params, 'creator') {
- $item{'creator'} = $self->type('email', $bug->reporter->login);
- $item{'creator_detail'} = $self->_user_to_hash($bug->reporter, $params, undef, 'creator');
- }
- if (filter_wants $params, 'depends_on') {
- my @depends_on = map { $self->type('int', $_) } @{ $bug->dependson };
- $item{'depends_on'} = \@depends_on;
- }
- if (filter_wants $params, 'dupe_of') {
- $item{'dupe_of'} = $self->type('int', $bug->dup_id);
- }
- if (filter_wants $params, 'duplicates') {
- $item{'duplicates'} = [ map { $self->type('int', $_->id) } @{ $bug->duplicates } ];
- }
- 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);
+ # 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,
+ {
+ alias => $self->type('string', $bug->alias),
+ 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, 'product') {
- $item{product} = $self->type('string', $bug->product);
+ };
+
+ state $voting_enabled //= $bug->can('votes') ? 1 : 0;
+ if ($voting_enabled && filter_wants $params, 'votes') {
+ $item{votes} = $self->type('int', $bug->votes);
+ }
+
+ # First we handle any fields that require extra work (such as date parsing
+ # or SQL calls).
+ if (filter_wants $params, 'assigned_to') {
+ $item{'assigned_to'} = $self->type('email', $bug->assigned_to->login);
+ $item{'assigned_to_detail'}
+ = $self->_user_to_hash($bug->assigned_to, $params, undef, 'assigned_to');
+ }
+ if (filter_wants $params, 'blocks') {
+ my @blocks = map { $self->type('int', $_) } @{$bug->blocked};
+ $item{'blocks'} = \@blocks;
+ }
+ if (filter_wants $params, 'classification') {
+ $item{classification} = $self->type('string', $bug->classification);
+ }
+ if (filter_wants $params, 'component') {
+ $item{component} = $self->type('string', $bug->component);
+ }
+ if (filter_wants $params, 'cc') {
+ my @cc = map { $self->type('email', $_) } @{$bug->cc || []};
+ $item{'cc'} = \@cc;
+ $item{'cc_detail'}
+ = [map { $self->_user_to_hash($_, $params, undef, 'cc') } @{$bug->cc_users}];
+ }
+ if (filter_wants $params, 'creation_time') {
+ $item{'creation_time'} = $self->type('dateTime', $bug->creation_ts);
+ }
+ if (filter_wants $params, 'creator') {
+ $item{'creator'} = $self->type('email', $bug->reporter->login);
+ $item{'creator_detail'}
+ = $self->_user_to_hash($bug->reporter, $params, undef, 'creator');
+ }
+ if (filter_wants $params, 'depends_on') {
+ my @depends_on = map { $self->type('int', $_) } @{$bug->dependson};
+ $item{'depends_on'} = \@depends_on;
+ }
+ if (filter_wants $params, 'dupe_of') {
+ $item{'dupe_of'} = $self->type('int', $bug->dup_id);
+ }
+ if (filter_wants $params, 'duplicates') {
+ $item{'duplicates'} = [map { $self->type('int', $_->id) } @{$bug->duplicates}];
+ }
+ if (filter_wants $params, 'groups') {
+ my @groups = map { $self->type('string', $_->name) } @{$bug->groups_in};
+ $item{'groups'} = \@groups;
+ }
+ if (filter_wants $params, 'is_open') {
+ $item{'is_open'} = $self->type('boolean', $bug->status->is_open);
+ }
+ if (filter_wants $params, 'keywords') {
+ my @keywords = map { $self->type('string', $_->name) } @{$bug->keyword_objects};
+ $item{'keywords'} = \@keywords;
+ }
+ if (filter_wants $params, 'last_change_time') {
+ $item{'last_change_time'} = $self->type('dateTime', $bug->delta_ts);
+ }
+ if (filter_wants $params, 'product') {
+ $item{product} = $self->type('string', $bug->product);
+ }
+ if (filter_wants $params, 'qa_contact') {
+ my $qa_login = $bug->qa_contact ? $bug->qa_contact->login : '';
+ $item{'qa_contact'} = $self->type('email', $qa_login);
+ if ($bug->qa_contact) {
+ $item{'qa_contact_detail'}
+ = $self->_user_to_hash($bug->qa_contact, $params, undef, 'qa_contact');
}
- if (filter_wants $params, 'qa_contact') {
- my $qa_login = $bug->qa_contact ? $bug->qa_contact->login : '';
- $item{'qa_contact'} = $self->type('email', $qa_login);
- if ($bug->qa_contact) {
- $item{'qa_contact_detail'} = $self->_user_to_hash($bug->qa_contact, $params, undef, 'qa_contact');
- }
+ }
+ if (filter_wants $params, 'see_also') {
+ my @see_also = map { $self->type('string', $_->name) } @{$bug->see_also};
+ $item{'see_also'} = \@see_also;
+ }
+ if (filter_wants $params, 'flags') {
+ $item{'flags'} = [map { $self->_flag_to_hash($_) } @{$bug->flags}];
+ }
+
+ # And now custom fields
+ my @custom_fields = Bugzilla->active_custom_fields({
+ product => $bug->product_obj,
+ component => $bug->component_obj,
+ bug_id => $bug->id
+ });
+ 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, 'see_also') {
- my @see_also = map { $self->type('string', $_->name) }
- @{ $bug->see_also };
- $item{'see_also'} = \@see_also;
+ elsif ($field->type == FIELD_TYPE_DATETIME || $field->type == FIELD_TYPE_DATE) {
+ my $value = $bug->$name;
+ $item{$name} = defined($value) ? $self->type('dateTime', $value) : undef;
}
- if (filter_wants $params, 'flags') {
- $item{'flags'} = [ map { $self->_flag_to_hash($_) } @{$bug->flags} ];
+ 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({
- product => $bug->product_obj, component => $bug->component_obj, bug_id => $bug->id });
- 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)
- {
- my $value = $bug->$name;
- $item{$name} = defined($value) ? $self->type('dateTime', $value) : undef;
- }
- 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);
- }
- }
-
- # 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, 'deadline') {
- # No need to format $bug->deadline specially, because Bugzilla::Bug
- # already does it for us.
- $item{'deadline'} = $self->type('string', $bug->deadline);
- }
- if (filter_wants $params, 'actual_time') {
- $item{'actual_time'} = $self->type('double', $bug->actual_time);
- }
+ else {
+ $item{$name} = $self->type('string', $bug->$name);
}
+ }
- # 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);
+ # 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, 'is_creator_accessible') {
- $item{'is_creator_accessible'} = $self->type('boolean', $bug->reporter_accessible);
+ if (filter_wants $params, 'remaining_time') {
+ $item{'remaining_time'} = $self->type('double', $bug->remaining_time);
}
+ if (filter_wants $params, 'deadline') {
- # BMO - support for special mentors field
- if (filter_wants $params, 'mentors') {
- $item{'mentors'}
- = [ map { $self->type('email', $_->login) } @{ $bug->mentors || [] } ];
- $item{'mentors_detail'}
- = [ map { $self->_user_to_hash($_, $params, undef, 'mentors') } @{ $bug->mentors } ];
+ # No need to format $bug->deadline specially, because Bugzilla::Bug
+ # already does it for us.
+ $item{'deadline'} = $self->type('string', $bug->deadline);
}
-
- if (filter_wants $params, 'comment_count') {
- $item{'comment_count'} = $self->type('int', $bug->comment_count);
+ 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);
+ }
+
+ # BMO - support for special mentors field
+ if (filter_wants $params, 'mentors') {
+ $item{'mentors'}
+ = [map { $self->type('email', $_->login) } @{$bug->mentors || []}];
+ $item{'mentors_detail'}
+ = [map { $self->_user_to_hash($_, $params, undef, 'mentors') }
+ @{$bug->mentors}];
+ }
+
+ if (filter_wants $params, 'comment_count') {
+ $item{'comment_count'} = $self->type('int', $bug->comment_count);
+ }
+
+ return \%item;
}
sub _user_to_hash {
- my ($self, $user, $filters, $types, $prefix) = @_;
- my $item = filter $filters, {
- id => $self->type('int', $user->id),
- real_name => $self->type('string', $user->name),
- name => $self->type('email', $user->login),
- email => $self->type('email', $user->email),
- }, $types, $prefix;
- return $item;
+ my ($self, $user, $filters, $types, $prefix) = @_;
+ my $item = filter $filters,
+ {
+ id => $self->type('int', $user->id),
+ real_name => $self->type('string', $user->name),
+ name => $self->type('email', $user->login),
+ email => $self->type('email', $user->email),
+ },
+ $types, $prefix;
+ return $item;
}
sub _attachment_to_hash {
- my ($self, $attach, $filters, $types, $prefix) = @_;
-
- my $item = filter $filters, {
- creation_time => $self->type('dateTime', $attach->attached),
- last_change_time => $self->type('dateTime', $attach->modification_time),
- id => $self->type('int', $attach->id),
- bug_id => $self->type('int', $attach->bug_id),
- file_name => $self->type('string', $attach->filename),
- summary => $self->type('string', $attach->description),
- description => $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/attacher require an extra lookup, so we only send them if
- # the filter wants them.
- foreach my $field (qw(creator attacher)) {
- if (filter_wants $filters, $field, $types, $prefix) {
- $item->{$field} = $self->type('email', $attach->attacher->login);
- }
- }
+ my ($self, $attach, $filters, $types, $prefix) = @_;
- if (filter_wants $filters, 'data', $types, $prefix) {
- $item->{'data'} = $self->type('base64', $attach->data);
+ 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),
+ description => $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/attacher require an extra lookup, so we only send them if
+ # the filter wants them.
+ foreach my $field (qw(creator attacher)) {
+ if (filter_wants $filters, $field, $types, $prefix) {
+ $item->{$field} = $self->type('email', $attach->attacher->login);
}
+ }
- if (filter_wants $filters, 'size', $types, $prefix) {
- $item->{'size'} = $self->type('int', $attach->datasize);
- }
+ if (filter_wants $filters, 'data', $types, $prefix) {
+ $item->{'data'} = $self->type('base64', $attach->data);
+ }
- if (filter_wants $filters, 'flags', $types, $prefix) {
- $item->{'flags'} = [ map { $self->_flag_to_hash($_) } @{$attach->flags} ];
- }
+ 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;
+ return $item;
}
sub _flag_to_hash {
- my ($self, $flag) = @_;
-
- my $item = {
- id => $self->type('int', $flag->id),
- name => $self->type('string', $flag->name),
- type_id => $self->type('int', $flag->type_id),
- creation_date => $self->type('dateTime', $flag->creation_date),
- modification_date => $self->type('dateTime', $flag->modification_date),
- status => $self->type('string', $flag->status)
- };
-
- foreach my $field (qw(setter requestee)) {
- my $field_id = $field . "_id";
- $item->{$field} = $self->type('email', $flag->$field->login)
- if $flag->$field_id;
- }
-
- return $item;
+ my ($self, $flag) = @_;
+
+ my $item = {
+ id => $self->type('int', $flag->id),
+ name => $self->type('string', $flag->name),
+ type_id => $self->type('int', $flag->type_id),
+ creation_date => $self->type('dateTime', $flag->creation_date),
+ modification_date => $self->type('dateTime', $flag->modification_date),
+ status => $self->type('string', $flag->status)
+ };
+
+ foreach my $field (qw(setter requestee)) {
+ my $field_id = $field . "_id";
+ $item->{$field} = $self->type('email', $flag->$field->login)
+ if $flag->$field_id;
+ }
+
+ return $item;
}
sub _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 _add_update_tokens {
- my ($self, $params, $bugs, $hashes) = @_;
+ my ($self, $params, $bugs, $hashes) = @_;
- return if !Bugzilla->user->id;
- return if !filter_wants($params, 'update_token');
+ return if !Bugzilla->user->id;
+ return if !filter_wants($params, 'update_token');
- for(my $i = 0; $i < @$bugs; $i++) {
- my $token = issue_hash_token([$bugs->[$i]->id, $bugs->[$i]->delta_ts]);
- $hashes->[$i]->{'update_token'} = $self->type('string', $token);
- }
+ for (my $i = 0; $i < @$bugs; $i++) {
+ my $token = issue_hash_token([$bugs->[$i]->id, $bugs->[$i]->delta_ts]);
+ $hashes->[$i]->{'update_token'} = $self->type('string', $token);
+ }
}
1;
diff --git a/Bugzilla/WebService/BugUserLastVisit.pm b/Bugzilla/WebService/BugUserLastVisit.pm
index 5e4c0d2ba..9b4261bc3 100644
--- a/Bugzilla/WebService/BugUserLastVisit.pm
+++ b/Bugzilla/WebService/BugUserLastVisit.pm
@@ -19,84 +19,84 @@ use Bugzilla::WebService::Util qw( validate filter );
use Bugzilla::Constants;
use constant PUBLIC_METHODS => qw(
- get
- update
+ get
+ update
);
sub update {
- my ($self, $params) = validate(@_, 'ids');
- my $user = Bugzilla->user;
- my $dbh = Bugzilla->dbh;
+ my ($self, $params) = validate(@_, 'ids');
+ my $user = Bugzilla->user;
+ my $dbh = Bugzilla->dbh;
- $user->login(LOGIN_REQUIRED);
+ $user->login(LOGIN_REQUIRED);
- my $ids = $params->{ids} // [];
- ThrowCodeError('param_required', { param => 'ids' }) unless @$ids;
+ my $ids = $params->{ids} // [];
+ ThrowCodeError('param_required', {param => 'ids'}) unless @$ids;
- # Cache permissions for bugs. This highly reduces the number of calls to the
- # DB. visible_bugs() is only able to handle bug IDs, so we have to skip
- # aliases.
- $user->visible_bugs([grep /^[0-9]+$/, @$ids]);
+ # Cache permissions for bugs. This highly reduces the number of calls to the
+ # DB. visible_bugs() is only able to handle bug IDs, so we have to skip
+ # aliases.
+ $user->visible_bugs([grep /^[0-9]+$/, @$ids]);
- $dbh->bz_start_transaction();
- my @results;
- my $last_visit_ts = $dbh->selectrow_array('SELECT NOW()');
- foreach my $bug_id (@$ids) {
- my $bug = Bugzilla::Bug->check({ id => $bug_id, cache => 1 });
+ $dbh->bz_start_transaction();
+ my @results;
+ my $last_visit_ts = $dbh->selectrow_array('SELECT NOW()');
+ foreach my $bug_id (@$ids) {
+ my $bug = Bugzilla::Bug->check({id => $bug_id, cache => 1});
- ThrowUserError('user_not_involved', { bug_id => $bug->id })
- unless $user->is_involved_in_bug($bug);
+ ThrowUserError('user_not_involved', {bug_id => $bug->id})
+ unless $user->is_involved_in_bug($bug);
- $bug->update_user_last_visit($user, $last_visit_ts);
+ $bug->update_user_last_visit($user, $last_visit_ts);
- push(
- @results,
- $self->_bug_user_last_visit_to_hash(
- $bug_id, $last_visit_ts, $params
- ));
- }
- $dbh->bz_commit_transaction();
+ push(@results,
+ $self->_bug_user_last_visit_to_hash($bug_id, $last_visit_ts, $params));
+ }
+ $dbh->bz_commit_transaction();
- return \@results;
+ return \@results;
}
sub get {
- my ($self, $params) = validate(@_, 'ids');
- my $user = Bugzilla->user;
- my $ids = $params->{ids};
-
- $user->login(LOGIN_REQUIRED);
-
- 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_visits = @{ $user->last_visited };
-
- if ($ids) {
- # remove bugs that we are not interested in if ids is passed in.
- my %id_set = map { ($_ => 1) } @$ids;
- @last_visits = grep { $id_set{ $_->bug_id } } @last_visits;
- }
-
- return [
- map {
- $self->_bug_user_last_visit_to_hash($_->bug_id, $_->last_visit_ts,
- $params)
- } @last_visits
- ];
+ my ($self, $params) = validate(@_, 'ids');
+ my $user = Bugzilla->user;
+ my $ids = $params->{ids};
+
+ $user->login(LOGIN_REQUIRED);
+
+ 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_visits = @{$user->last_visited};
+
+ if ($ids) {
+
+ # remove bugs that we are not interested in if ids is passed in.
+ my %id_set = map { ($_ => 1) } @$ids;
+ @last_visits = grep { $id_set{$_->bug_id} } @last_visits;
+ }
+
+ return [
+ map {
+ $self->_bug_user_last_visit_to_hash($_->bug_id, $_->last_visit_ts, $params)
+ } @last_visits
+ ];
}
sub _bug_user_last_visit_to_hash {
- my ($self, $bug_id, $last_visit_ts, $params) = @_;
+ my ($self, $bug_id, $last_visit_ts, $params) = @_;
- my %result = (id => $self->type('int', $bug_id),
- last_visit_ts => $self->type('dateTime', $last_visit_ts));
+ my %result = (
+ id => $self->type('int', $bug_id),
+ last_visit_ts => $self->type('dateTime', $last_visit_ts)
+ );
- return filter($params, \%result);
+ return filter($params, \%result);
}
1;
diff --git a/Bugzilla/WebService/Bugzilla.pm b/Bugzilla/WebService/Bugzilla.pm
index 8e95028cb..f8f745a55 100644
--- a/Bugzilla/WebService/Bugzilla.pm
+++ b/Bugzilla/WebService/Bugzilla.pm
@@ -21,77 +21,76 @@ use Try::Tiny;
use DateTime;
# Basic info that is needed before logins
-use constant LOGIN_EXEMPT => {
- timezone => 1,
- version => 1,
-};
+use constant LOGIN_EXEMPT => {timezone => 1, version => 1,};
use constant READ_ONLY => qw(
- extensions
- timezone
- time
- version
- jobqueue_status
+ extensions
+ timezone
+ time
+ version
+ jobqueue_status
);
use constant PUBLIC_METHODS => qw(
- extensions
- time
- timezone
- version
- jobqueue_status
+ extensions
+ time
+ timezone
+ version
+ jobqueue_status
);
sub version {
- my $self = shift;
- return { version => $self->type('string', BUGZILLA_VERSION) };
+ my $self = shift;
+ return {version => $self->type('string', BUGZILLA_VERSION)};
}
sub extensions {
- my $self = shift;
-
- my %retval;
- foreach my $extension (@{ Bugzilla->extensions }) {
- my $version = $extension->VERSION || 0;
- my $name = $extension->NAME;
- $retval{$name}->{version} = $self->type('string', $version);
- }
- return { extensions => \%retval };
+ my $self = shift;
+
+ my %retval;
+ foreach my $extension (@{Bugzilla->extensions}) {
+ my $version = $extension->VERSION || 0;
+ my $name = $extension->NAME;
+ $retval{$name}->{version} = $self->type('string', $version);
+ }
+ return {extensions => \%retval};
}
sub timezone {
- my $self = shift;
- # All Webservices return times in UTC; Use UTC here for backwards compat.
- return { timezone => $self->type('string', "+0000") };
+ my $self = shift;
+
+ # All Webservices return times in UTC; Use UTC here for backwards compat.
+ return {timezone => $self->type('string', "+0000")};
}
sub time {
- my ($self) = @_;
- # All Webservices return times in UTC; Use UTC here for backwards compat.
- # Hardcode values where appropriate
- my $dbh = Bugzilla->dbh;
-
- my $db_time = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
- $db_time = datetime_from($db_time, 'UTC');
- my $now_utc = DateTime->now();
-
- return {
- db_time => $self->type('dateTime', $db_time),
- web_time => $self->type('dateTime', $now_utc),
- web_time_utc => $self->type('dateTime', $now_utc),
- tz_name => $self->type('string', 'UTC'),
- tz_offset => $self->type('string', '+0000'),
- tz_short_name => $self->type('string', 'UTC'),
- };
+ my ($self) = @_;
+
+ # All Webservices return times in UTC; Use UTC here for backwards compat.
+ # Hardcode values where appropriate
+ my $dbh = Bugzilla->dbh;
+
+ my $db_time = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
+ $db_time = datetime_from($db_time, 'UTC');
+ my $now_utc = DateTime->now();
+
+ return {
+ db_time => $self->type('dateTime', $db_time),
+ web_time => $self->type('dateTime', $now_utc),
+ web_time_utc => $self->type('dateTime', $now_utc),
+ tz_name => $self->type('string', 'UTC'),
+ tz_offset => $self->type('string', '+0000'),
+ tz_short_name => $self->type('string', 'UTC'),
+ };
}
sub jobqueue_status {
- my ( $self, $params ) = @_;
+ my ($self, $params) = @_;
- Bugzilla->login(LOGIN_REQUIRED);
+ Bugzilla->login(LOGIN_REQUIRED);
- my $dbh = Bugzilla->dbh;
- my $query = q{
+ my $dbh = Bugzilla->dbh;
+ my $query = q{
SELECT
COUNT(*) AS total,
COALESCE(
@@ -105,17 +104,18 @@ sub jobqueue_status {
ON f.funcid = j.funcid;
};
- my $status;
- try {
- $status = $dbh->selectrow_hashref($query);
- $status->{errors} = 0 + $status->{errors};
- $status->{total} = 0 + $status->{total};
- } catch {
- ERROR($_);
- ThrowCodeError('jobqueue_status_error');
- };
-
- return $status;
+ my $status;
+ try {
+ $status = $dbh->selectrow_hashref($query);
+ $status->{errors} = 0 + $status->{errors};
+ $status->{total} = 0 + $status->{total};
+ }
+ catch {
+ ERROR($_);
+ ThrowCodeError('jobqueue_status_error');
+ };
+
+ return $status;
}
1;
diff --git a/Bugzilla/WebService/Classification.pm b/Bugzilla/WebService/Classification.pm
index 32139ff3f..35e67ba08 100644
--- a/Bugzilla/WebService/Classification.pm
+++ b/Bugzilla/WebService/Classification.pm
@@ -18,65 +18,76 @@ use Bugzilla::Error;
use Bugzilla::WebService::Util qw(filter validate params_to_objects);
use constant READ_ONLY => qw(
- get
+ get
);
use constant PUBLIC_METHODS => qw(
- get
+ get
);
sub get {
- my ($self, $params) = validate(@_, 'names', 'ids');
+ my ($self, $params) = validate(@_, 'names', 'ids');
- defined $params->{names} || defined $params->{ids}
- || ThrowCodeError('params_required', { function => 'Classification.get',
- params => ['names', 'ids'] });
+ defined $params->{names}
+ || defined $params->{ids}
+ || ThrowCodeError('params_required',
+ {function => 'Classification.get', params => ['names', 'ids']});
- my $user = Bugzilla->user;
+ my $user = Bugzilla->user;
- Bugzilla->params->{'useclassification'}
- || $user->in_group('editclassifications')
- || ThrowUserError('auth_classification_not_enabled');
+ Bugzilla->params->{'useclassification'}
+ || $user->in_group('editclassifications')
+ || ThrowUserError('auth_classification_not_enabled');
- Bugzilla->switch_to_shadow_db;
+ Bugzilla->switch_to_shadow_db;
- my @classification_objs = @{ params_to_objects($params, 'Bugzilla::Classification') };
- unless ($user->in_group('editclassifications')) {
- my %selectable_class = map { $_->id => 1 } @{$user->get_selectable_classifications};
- @classification_objs = grep { $selectable_class{$_->id} } @classification_objs;
- }
+ my @classification_objs
+ = @{params_to_objects($params, 'Bugzilla::Classification')};
+ unless ($user->in_group('editclassifications')) {
+ my %selectable_class
+ = map { $_->id => 1 } @{$user->get_selectable_classifications};
+ @classification_objs = grep { $selectable_class{$_->id} } @classification_objs;
+ }
- my @classifications = map { $self->_classification_to_hash($_, $params) } @classification_objs;
+ my @classifications
+ = map { $self->_classification_to_hash($_, $params) } @classification_objs;
- return { classifications => \@classifications };
+ return {classifications => \@classifications};
}
sub _classification_to_hash {
- my ($self, $classification, $params) = @_;
-
- my $user = Bugzilla->user;
- return unless (Bugzilla->params->{'useclassification'} || $user->in_group('editclassifications'));
-
- my $products = $user->in_group('editclassifications') ?
- $classification->products : $user->get_selectable_products($classification->id);
-
- return filter $params, {
- id => $self->type('int', $classification->id),
- name => $self->type('string', $classification->name),
- description => $self->type('string', $classification->description),
- sort_key => $self->type('int', $classification->sortkey),
- products => [ map { $self->_product_to_hash($_, $params) } @$products ],
+ my ($self, $classification, $params) = @_;
+
+ my $user = Bugzilla->user;
+ return
+ unless (Bugzilla->params->{'useclassification'}
+ || $user->in_group('editclassifications'));
+
+ my $products
+ = $user->in_group('editclassifications')
+ ? $classification->products
+ : $user->get_selectable_products($classification->id);
+
+ return filter $params,
+ {
+ id => $self->type('int', $classification->id),
+ name => $self->type('string', $classification->name),
+ description => $self->type('string', $classification->description),
+ sort_key => $self->type('int', $classification->sortkey),
+ products => [map { $self->_product_to_hash($_, $params) } @$products],
};
}
sub _product_to_hash {
- my ($self, $product, $params) = @_;
-
- return filter $params, {
- id => $self->type('int', $product->id),
- name => $self->type('string', $product->name),
- description => $self->type('string', $product->description),
- }, undef, 'products';
+ my ($self, $product, $params) = @_;
+
+ return filter $params,
+ {
+ id => $self->type('int', $product->id),
+ name => $self->type('string', $product->name),
+ description => $self->type('string', $product->description),
+ },
+ undef, 'products';
}
1;
diff --git a/Bugzilla/WebService/Constants.pm b/Bugzilla/WebService/Constants.pm
index 71435c13a..71fcccd6e 100644
--- a/Bugzilla/WebService/Constants.pm
+++ b/Bugzilla/WebService/Constants.pm
@@ -14,27 +14,27 @@ use warnings;
use base 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,161 +56,184 @@ 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,
- # 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,
- # Invalid Product
- no_products => 106,
- entry_access_denied => 106,
- product_access_denied => 106,
- product_disabled => 106,
- # Invalid Summary
- require_summary => 107,
- # Invalid field name
- invalid_field_name => 108,
- # Not authorized to edit the bug
- product_edit_denied => 109,
- # Comment-related errors
- comment_is_private => 110,
- comment_id_invalid => 111,
- comment_too_long => 114,
- comment_invalid_isprivate => 117,
- # Comment tagging
- comment_tag_disabled => 125,
- comment_tag_invalid => 126,
- comment_tag_too_long => 127,
- comment_tag_too_short => 128,
- # See Also errors
- bug_url_invalid => 112,
- bug_url_too_long => 112,
- # Insidergroup Errors
- user_not_insider => 113,
- # Note: 114 is above in the Comment-related section.
- # Bug update errors
- illegal_change => 115,
- # Dependency errors
- dependency_loop_single => 116,
- dependency_loop_multi => 116,
- # Note: 117 is above in the Comment-related section.
- # Dup errors
- dupe_loop_detected => 118,
- dupe_id_required => 119,
- # Bug-related group errors
- group_invalid_removal => 120,
- group_restriction_not_allowed => 120,
- # Status/Resolution errors
- missing_resolution => 121,
- resolution_not_allowed => 122,
- illegal_bug_status_transition => 123,
- # Flag errors
- flag_status_invalid => 129,
- flag_update_denied => 130,
- flag_type_requestee_disabled => 131,
- flag_not_unique => 132,
- flag_type_not_unique => 133,
- flag_type_inactive => 134,
-
- # Authentication errors are usually 300-400.
- invalid_username_or_password => 300,
- account_disabled => 301,
- auth_invalid_email => 302,
- extern_id_conflict => -303,
- auth_failure => 304,
- password_insecure => 305,
- api_key_not_valid => 306,
- api_key_revoked => 306,
- auth_invalid_token => 307,
- invalid_cookies_or_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,
- # Error 502 password_too_short no longer exists.
- # Error 503 password_too_long no longer exists.
- invalid_username => 504,
- # This is from strict_isolation, but it also basically means
- # "invalid user."
- invalid_user_group => 504,
- user_access_by_id_denied => 505,
- user_access_by_match_denied => 505,
-
- # Attachment errors are 600-700.
- file_too_large => 600,
- invalid_content_type => 601,
- # Error 602 attachment_illegal_url no longer exists.
- file_not_specified => 603,
- missing_attachment_description => 604,
- # Error 605 attachment_url_disabled no longer exists.
- zero_length_file => 606,
-
- # Product erros are 700-800
- product_blank_name => 700,
- product_name_too_long => 701,
- product_name_already_in_use => 702,
- product_name_diff_in_case => 702,
- product_must_have_description => 703,
- product_must_have_version => 704,
- product_must_define_defaultmilestone => 705,
-
- # Group errors are 800-900
- empty_group_name => 800,
- group_exists => 801,
- empty_group_description => 802,
- invalid_regexp => 803,
- invalid_group_name => 804,
- group_cannot_view => 805,
-
- # Search errors are 1000-1100
- buglist_parameters_required => 1000,
-
- # BugUserLastVisited errors
- user_not_involved => 1300,
-
- # Job queue errors 1400-1500
- jobqueue_status_error => 1400,
-
- # 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,
+
+ # 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,
+
+ # Invalid Product
+ no_products => 106,
+ entry_access_denied => 106,
+ product_access_denied => 106,
+ product_disabled => 106,
+
+ # Invalid Summary
+ require_summary => 107,
+
+ # Invalid field name
+ invalid_field_name => 108,
+
+ # Not authorized to edit the bug
+ product_edit_denied => 109,
+
+ # Comment-related errors
+ comment_is_private => 110,
+ comment_id_invalid => 111,
+ comment_too_long => 114,
+ comment_invalid_isprivate => 117,
+
+ # Comment tagging
+ comment_tag_disabled => 125,
+ comment_tag_invalid => 126,
+ comment_tag_too_long => 127,
+ comment_tag_too_short => 128,
+
+ # See Also errors
+ bug_url_invalid => 112,
+ bug_url_too_long => 112,
+
+ # Insidergroup Errors
+ user_not_insider => 113,
+
+ # Note: 114 is above in the Comment-related section.
+ # Bug update errors
+ illegal_change => 115,
+
+ # Dependency errors
+ dependency_loop_single => 116,
+ dependency_loop_multi => 116,
+
+ # Note: 117 is above in the Comment-related section.
+ # Dup errors
+ dupe_loop_detected => 118,
+ dupe_id_required => 119,
+
+ # Bug-related group errors
+ group_invalid_removal => 120,
+ group_restriction_not_allowed => 120,
+
+ # Status/Resolution errors
+ missing_resolution => 121,
+ resolution_not_allowed => 122,
+ illegal_bug_status_transition => 123,
+
+ # Flag errors
+ flag_status_invalid => 129,
+ flag_update_denied => 130,
+ flag_type_requestee_disabled => 131,
+ flag_not_unique => 132,
+ flag_type_not_unique => 133,
+ flag_type_inactive => 134,
+
+ # Authentication errors are usually 300-400.
+ invalid_username_or_password => 300,
+ account_disabled => 301,
+ auth_invalid_email => 302,
+ extern_id_conflict => -303,
+ auth_failure => 304,
+ password_insecure => 305,
+ api_key_not_valid => 306,
+ api_key_revoked => 306,
+ auth_invalid_token => 307,
+ invalid_cookies_or_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,
+
+ # Error 502 password_too_short no longer exists.
+ # Error 503 password_too_long no longer exists.
+ invalid_username => 504,
+
+ # This is from strict_isolation, but it also basically means
+ # "invalid user."
+ invalid_user_group => 504,
+ user_access_by_id_denied => 505,
+ user_access_by_match_denied => 505,
+
+ # Attachment errors are 600-700.
+ file_too_large => 600,
+ invalid_content_type => 601,
+
+ # Error 602 attachment_illegal_url no longer exists.
+ file_not_specified => 603,
+ missing_attachment_description => 604,
+
+ # Error 605 attachment_url_disabled no longer exists.
+ zero_length_file => 606,
+
+ # Product erros are 700-800
+ product_blank_name => 700,
+ product_name_too_long => 701,
+ product_name_already_in_use => 702,
+ product_name_diff_in_case => 702,
+ product_must_have_description => 703,
+ product_must_have_version => 704,
+ product_must_define_defaultmilestone => 705,
+
+ # Group errors are 800-900
+ empty_group_name => 800,
+ group_exists => 801,
+ empty_group_description => 802,
+ invalid_regexp => 803,
+ invalid_group_name => 804,
+ group_cannot_view => 805,
+
+ # Search errors are 1000-1100
+ buglist_parameters_required => 1000,
+
+ # BugUserLastVisited errors
+ user_not_involved => 1300,
+
+ # Job queue errors 1400-1500
+ jobqueue_status_error => 1400,
+
+ # 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
@@ -231,81 +254,82 @@ 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',
- 'User' => 'Bugzilla::WebService::User',
- 'Product' => 'Bugzilla::WebService::Product',
- 'Group' => 'Bugzilla::WebService::Group',
- 'BugUserLastVisit' => 'Bugzilla::WebService::BugUserLastVisit',
- 'Elastic' => 'Bugzilla::WebService::Elastic',
- %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',
+ 'User' => 'Bugzilla::WebService::User',
+ 'Product' => 'Bugzilla::WebService::Product',
+ 'Group' => 'Bugzilla::WebService::Group',
+ 'BugUserLastVisit' => 'Bugzilla::WebService::BugUserLastVisit',
+ 'Elastic' => 'Bugzilla::WebService::Elastic',
+ %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;
diff --git a/Bugzilla/WebService/Elastic.pm b/Bugzilla/WebService/Elastic.pm
index 3a33a1dba..373f6db58 100644
--- a/Bugzilla/WebService/Elastic.pm
+++ b/Bugzilla/WebService/Elastic.pm
@@ -30,30 +30,28 @@ use Bugzilla::Error;
use Bugzilla::WebService::Util qw(validate);
use Bugzilla::Util qw(trim detaint_natural trick_taint);
-use constant READ_ONLY => qw( suggest_users );
+use constant READ_ONLY => qw( suggest_users );
use constant PUBLIC_METHODS => qw( suggest_users );
sub suggest_users {
- my ($self, $params) = @_;
+ my ($self, $params) = @_;
- Bugzilla->switch_to_shadow_db();
+ Bugzilla->switch_to_shadow_db();
- ThrowCodeError('params_required', { function => 'Elastic.suggest_users', params => ['match'] })
- unless defined $params->{match};
+ ThrowCodeError('params_required',
+ {function => 'Elastic.suggest_users', params => ['match']})
+ unless defined $params->{match};
- ThrowUserError('user_access_by_match_denied')
- unless Bugzilla->user->id;
+ ThrowUserError('user_access_by_match_denied') unless Bugzilla->user->id;
- trick_taint($params->{match});
- my $results = Bugzilla->elastic->suggest_users($params->{match} . "");
- my @users = map {
- {
- real_name => $self->type(string => $_->{real_name}),
- name => $self->type(email => $_->{name}),
- }
- } @$results;
+ trick_taint($params->{match});
+ my $results = Bugzilla->elastic->suggest_users($params->{match} . "");
+ my @users = map { {
+ real_name => $self->type(string => $_->{real_name}),
+ name => $self->type(email => $_->{name}),
+ } } @$results;
- return { users => \@users };
+ return {users => \@users};
}
-1; \ No newline at end of file
+1;
diff --git a/Bugzilla/WebService/Group.pm b/Bugzilla/WebService/Group.pm
index b13003e08..4467883a4 100644
--- a/Bugzilla/WebService/Group.pm
+++ b/Bugzilla/WebService/Group.pm
@@ -17,207 +17,210 @@ use Bugzilla::Error;
use Bugzilla::WebService::Util qw(validate translate params_to_objects);
use constant PUBLIC_METHODS => qw(
- create
- get
- update
+ create
+ get
+ update
);
-use constant MAPPED_RETURNS => {
- userregexp => 'user_regexp',
- isactive => 'is_active'
-};
+use constant MAPPED_RETURNS =>
+ {userregexp => 'user_regexp', isactive => 'is_active'};
sub create {
- my ($self, $params) = @_;
-
- Bugzilla->login(LOGIN_REQUIRED);
- Bugzilla->user->in_group('creategroups')
- || ThrowUserError("auth_failure", { group => "creategroups",
- action => "add",
- object => "group"});
- # Create group
- my $group = Bugzilla::Group->create({
- name => $params->{name},
- description => $params->{description},
- userregexp => $params->{user_regexp},
- isactive => $params->{is_active},
- isbuggroup => 1,
- icon_url => $params->{icon_url}
- });
- return { id => $self->type('int', $group->id) };
+ my ($self, $params) = @_;
+
+ Bugzilla->login(LOGIN_REQUIRED);
+ Bugzilla->user->in_group('creategroups')
+ || ThrowUserError("auth_failure",
+ {group => "creategroups", action => "add", object => "group"});
+
+ # Create group
+ my $group = Bugzilla::Group->create({
+ name => $params->{name},
+ description => $params->{description},
+ userregexp => $params->{user_regexp},
+ isactive => $params->{is_active},
+ isbuggroup => 1,
+ icon_url => $params->{icon_url}
+ });
+ return {id => $self->type('int', $group->id)};
}
sub update {
- my ($self, $params) = @_;
-
- my $dbh = Bugzilla->dbh;
+ 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);
+ }
- Bugzilla->login(LOGIN_REQUIRED);
- Bugzilla->user->in_group('creategroups')
- || ThrowUserError("auth_failure", { group => "creategroups",
- action => "edit",
- object => "group" });
+ return {groups => \@result};
+}
- defined($params->{names}) || defined($params->{ids})
- || ThrowCodeError('params_required',
- { function => 'Group.update', params => ['ids', 'names'] });
+sub get {
+ my ($self, $params) = validate(@_, 'ids', 'names', 'type');
- my $group_objects = params_to_objects($params, 'Bugzilla::Group');
+ Bugzilla->login(LOGIN_REQUIRED);
- my %values = %$params;
+ # 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');
+ }
- # 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);
- }
-
- return { groups => \@result };
-}
-
-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 => $self->type('int', $group->id),
- name => $self->type('string', $group->name),
- description => $self->type('string', $group->description),
- };
-
- if ($user->in_group('creategroups')) {
- $field_data->{is_active} = $self->type('boolean', $group->is_active);
- $field_data->{is_bug_group} = $self->type('boolean', $group->is_bug_group);
- $field_data->{user_regexp} = $self->type('string', $group->user_regexp);
- }
-
- if ($params->{membership}) {
- $field_data->{membership} = $self->_get_group_membership($group, $params);
- }
- return $field_data;
+ my ($self, $params, $group) = @_;
+ my $user = Bugzilla->user;
+
+ my $field_data = {
+ id => $self->type('int', $group->id),
+ name => $self->type('string', $group->name),
+ description => $self->type('string', $group->description),
+ };
+
+ if ($user->in_group('creategroups')) {
+ $field_data->{is_active} = $self->type('boolean', $group->is_active);
+ $field_data->{is_bug_group} = $self->type('boolean', $group->is_bug_group);
+ $field_data->{user_regexp} = $self->type('string', $group->user_regexp);
+ }
+
+ if ($params->{membership}) {
+ $field_data->{membership} = $self->_get_group_membership($group, $params);
+ }
+ return $field_data;
}
sub _get_group_membership {
- my ($self, $group, $params) = @_;
- my $user = Bugzilla->user;
+ my ($self, $group, $params) = @_;
+ my $user = Bugzilla->user;
- my %users_only;
- my $dbh = Bugzilla->dbh;
- my $editusers = $user->in_group('editusers');
+ my %users_only;
+ my $dbh = Bugzilla->dbh;
+ my $editusers = $user->in_group('editusers');
- my $query = 'SELECT userid FROM profiles';
- my $visibleGroups;
+ my $query = 'SELECT userid FROM profiles';
+ my $visibleGroups;
- if (!$editusers && Bugzilla->params->{'usevisibilitygroups'}) {
- # Show only users in visible groups.
- $visibleGroups = $user->visible_groups_inherited;
+ if (!$editusers && Bugzilla->params->{'usevisibilitygroups'}) {
- if (scalar @$visibleGroups) {
- $query .= qq{, user_group_map AS ugm
+ # Show only users in visible groups.
+ $visibleGroups = $user->visible_groups_inherited;
+
+ if (scalar @$visibleGroups) {
+ $query .= qq{, user_group_map AS ugm
WHERE ugm.user_id = profiles.userid
AND ugm.isbless = 0
AND } . $dbh->sql_in('ugm.group_id', $visibleGroups);
- }
- } elsif ($editusers || $user->can_bless($group->id) || $user->in_group('creategroups')) {
- $visibleGroups = 1;
- $query .= qq{, user_group_map AS ugm
+ }
+ }
+ elsif ($editusers
+ || $user->can_bless($group->id)
+ || $user->in_group('creategroups'))
+ {
+ $visibleGroups = 1;
+ $query .= qq{, user_group_map AS ugm
WHERE ugm.user_id = profiles.userid
AND ugm.isbless = 0
};
- }
- if (!$visibleGroups) {
- ThrowUserError('group_not_visible', { group => $group });
- }
-
- my $grouplist = Bugzilla::Group->flatten_group_membership($group->id);
- $query .= ' AND ' . $dbh->sql_in('ugm.group_id', $grouplist);
-
- my $userids = $dbh->selectcol_arrayref($query);
- my $user_objects = Bugzilla::User->new_from_list($userids);
- my @users =
- map {{
- id => $self->type('int', $_->id),
- real_name => $self->type('string', $_->name),
- name => $self->type('string', $_->login),
- email => $self->type('string', $_->email),
- can_login => $self->type('boolean', $_->is_enabled),
- email_enabled => $self->type('boolean', $_->email_enabled),
- login_denied_text => $self->type('string', $_->disabledtext),
- }} @$user_objects;
-
- return \@users;
+ }
+ if (!$visibleGroups) {
+ ThrowUserError('group_not_visible', {group => $group});
+ }
+
+ my $grouplist = Bugzilla::Group->flatten_group_membership($group->id);
+ $query .= ' AND ' . $dbh->sql_in('ugm.group_id', $grouplist);
+
+ my $userids = $dbh->selectcol_arrayref($query);
+ my $user_objects = Bugzilla::User->new_from_list($userids);
+ my @users = map { {
+ id => $self->type('int', $_->id),
+ real_name => $self->type('string', $_->name),
+ name => $self->type('string', $_->login),
+ email => $self->type('string', $_->email),
+ can_login => $self->type('boolean', $_->is_enabled),
+ email_enabled => $self->type('boolean', $_->email_enabled),
+ login_denied_text => $self->type('string', $_->disabledtext),
+ } } @$user_objects;
+
+ return \@users;
}
1;
diff --git a/Bugzilla/WebService/JSON.pm b/Bugzilla/WebService/JSON.pm
index 5c28b20f4..f670d1fd9 100644
--- a/Bugzilla/WebService/JSON.pm
+++ b/Bugzilla/WebService/JSON.pm
@@ -39,7 +39,7 @@ sub decode {
}
}
-sub _build_json { JSON::MaybeXS->new }
+sub _build_json { JSON::MaybeXS->new }
# delegation all the json options to the real json encoder.
{
diff --git a/Bugzilla/WebService/Product.pm b/Bugzilla/WebService/Product.pm
index cdd8a0a92..0726c371d 100644
--- a/Bugzilla/WebService/Product.pm
+++ b/Bugzilla/WebService/Product.pm
@@ -20,24 +20,22 @@ use Bugzilla::WebService::Constants;
use Bugzilla::WebService::Util qw(validate filter filter_wants);
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
+ create
+ get
+ get_accessible_products
+ get_enterable_products
+ get_selectable_products
);
-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 #
@@ -47,256 +45,240 @@ BEGIN { *get_products = \&get }
# 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
our %FLAG_CACHE;
+
sub get {
- my ($self, $params) = validate(@_, 'ids', 'names', 'type');
- my $user = Bugzilla->user;
-
- Bugzilla->request_cache->{bz_etag_disable} = 1;
-
- 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;
+
+ Bugzilla->request_cache->{bz_etag_disable} = 1;
+
+ 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
- # accessible 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
- # accessible 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
+ # accessible products.
+ push(@requested_products, grep { $ids{$_->id} } @$products);
+ }
- # 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;
- }
+ if (defined $params->{names}) {
+
+ # Create a hash with the names the user wants
+ my %names = map { lc($_) => 1 } @{$params->{names}};
- # Now create a result entry for each.
- local %FLAG_CACHE = ();
- my @products = map { $self->_product_to_hash($params, $_) }
- @requested_products;
- return { products => \@products };
+ # Return the intersection of this, by grepping the names from
+ # accessible 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.
+ local %FLAG_CACHE = ();
+ 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 _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}];
- }
- # BMO - add default hw/os
- $field_data->{default_platform} = $self->type('string', $product->default_platform);
- $field_data->{default_op_sys} = $self->type('string', $product->default_op_sys);
- # BMO - add default security group
- $field_data->{default_security_group} = $self->type('string', $product->default_security_group);
- 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}];
+ }
+
+ # BMO - add default hw/os
+ $field_data->{default_platform}
+ = $self->type('string', $product->default_platform);
+ $field_data->{default_op_sys} = $self->type('string', $product->default_op_sys);
+
+ # BMO - add default security group
+ $field_data->{default_security_group}
+ = $self->type('string', $product->default_security_group);
+ return filter($params, $field_data);
}
sub _component_to_hash {
- my ($self, $component, $params) = @_;
- my $field_data = filter $params, {
- id =>
- $self->type('int', $component->id),
- name =>
- $self->type('string', $component->name),
- description =>
- $self->type('string', $component->description),
- default_assigned_to =>
- $self->type('email', $component->default_assignee->login),
- default_qa_contact =>
- $self->type('email', $component->default_qa_contact->login),
- triage_owner =>
- $self->type('email', $component->triage_owner->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 {
- $FLAG_CACHE{ $_->id } //= $self->_flag_type_to_hash($_)
- } @{$component->flag_types->{'bug'}}],
- attachment =>
- [map {
- $FLAG_CACHE{ $_->id } //= $self->_flag_type_to_hash($_)
- } @{$component->flag_types->{'attachment'}}],
- };
- }
+ my ($self, $component, $params) = @_;
+ my $field_data = filter $params, {
+ id => $self->type('int', $component->id),
+ name => $self->type('string', $component->name),
+ description => $self->type('string', $component->description),
+ default_assigned_to =>
+ $self->type('email', $component->default_assignee->login),
+ default_qa_contact =>
+ $self->type('email', $component->default_qa_contact->login),
+ triage_owner => $self->type('email', $component->triage_owner->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 { $FLAG_CACHE{$_->id} //= $self->_flag_type_to_hash($_) }
+ @{$component->flag_types->{'bug'}}
+ ],
+ attachment => [
+ map { $FLAG_CACHE{$_->id} //= $self->_flag_type_to_hash($_) }
+ @{$component->flag_types->{'attachment'}}
+ ],
+ };
+ }
- return $field_data;
+ return $field_data;
}
sub _flag_type_to_hash {
- my ($self, $flag_type) = @_;
- return {
- 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),
- };
+ my ($self, $flag_type) = @_;
+ return {
+ 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),
+ };
}
sub _version_to_hash {
- my ($self, $version, $params) = @_;
- return filter $params, {
- id =>
- $self->type('int', $version->id),
- name =>
- $self->type('string', $version->name),
- sort_key => # sort_key is returened to match Bug.fields
- 0,
- is_active =>
- $self->type('boolean', $version->is_active),
- }, undef, 'versions';
+ my ($self, $version, $params) = @_;
+ return filter $params, {
+ id => $self->type('int', $version->id),
+ name => $self->type('string', $version->name),
+ sort_key => # sort_key is returened to match Bug.fields
+ 0,
+ is_active => $self->type('boolean', $version->is_active),
+ },
+ undef, 'versions';
}
sub _milestone_to_hash {
- my ($self, $milestone, $params) = @_;
- return filter $params, {
- id =>
- $self->type('int', $milestone->id),
- name =>
- $self->type('string', $milestone->name),
- sort_key =>
- $self->type('int', $milestone->sortkey),
- is_active =>
- $self->type('boolean', $milestone->is_active),
- }, undef, 'milestones';
+ my ($self, $milestone, $params) = @_;
+ return filter $params,
+ {
+ id => $self->type('int', $milestone->id),
+ name => $self->type('string', $milestone->name),
+ sort_key => $self->type('int', $milestone->sortkey),
+ is_active => $self->type('boolean', $milestone->is_active),
+ },
+ undef, 'milestones';
}
1;
diff --git a/Bugzilla/WebService/Server.pm b/Bugzilla/WebService/Server.pm
index c4bd3e605..2aed48e22 100644
--- a/Bugzilla/WebService/Server.pm
+++ b/Bugzilla/WebService/Server.pm
@@ -22,88 +22,93 @@ use Module::Runtime qw(require_module);
use Try::Tiny;
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 =~ /^_/);
-
- # We never want to create a new session unless the user is calling the
- # login method. Setting dont_persist_session makes
- # Bugzilla::Auth::_handle_login_result() skip calling persist_login().
- if ($full_method ne 'User.login') {
- Bugzilla->request_cache->{dont_persist_session} = 1;
- }
-
- try {
- require_module($class);
- }
- catch {
- ThrowCodeError('unknown_method', {method => $full_method});
- FATAL($_);
- };
- 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 =~ /^_/);
+
+ # We never want to create a new session unless the user is calling the
+ # login method. Setting dont_persist_session makes
+ # Bugzilla::Auth::_handle_login_result() skip calling persist_login().
+ if ($full_method ne 'User.login') {
+ Bugzilla->request_cache->{dont_persist_session} = 1;
+ }
+
+ try {
+ require_module($class);
+ }
+ catch {
+ ThrowCodeError('unknown_method', {method => $full_method});
+ FATAL($_);
+ };
+ 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 (Bugzilla->request_cache->{bz_etag_disable}) {
- return undef;
+ my ($self, $data) = @_;
+ my $cache = Bugzilla->request_cache;
+
+ if (Bugzilla->request_cache->{bz_etag_disable}) {
+ return undef;
+ }
+ elsif (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;
}
- elsif (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);
- }
- return $cache->{'bz_etag'};
+
+ $cache->{'bz_etag'} = md5_base64($data);
+ }
+ return $cache->{'bz_etag'};
}
1;
diff --git a/Bugzilla/WebService/Server/JSONRPC.pm b/Bugzilla/WebService/Server/JSONRPC.pm
index 12a3143cc..ef00737ad 100644
--- a/Bugzilla/WebService/Server/JSONRPC.pm
+++ b/Bugzilla/WebService/Server/JSONRPC.pm
@@ -12,16 +12,17 @@ use strict;
use warnings;
use Bugzilla::WebService::Server;
-BEGIN {
- our @ISA = qw(Bugzilla::WebService::Server);
- if (eval { require JSON::RPC::Server::CGI }) {
- unshift(@ISA, 'JSON::RPC::Server::CGI');
- }
- else {
- require JSON::RPC::Legacy::Server::CGI;
- unshift(@ISA, 'JSON::RPC::Legacy::Server::CGI');
- }
+BEGIN {
+ our @ISA = qw(Bugzilla::WebService::Server);
+
+ if (eval { require JSON::RPC::Server::CGI }) {
+ unshift(@ISA, 'JSON::RPC::Server::CGI');
+ }
+ else {
+ require JSON::RPC::Legacy::Server::CGI;
+ unshift(@ISA, 'JSON::RPC::Legacy::Server::CGI');
+ }
}
use Bugzilla::Error;
@@ -40,88 +41,92 @@ use Bugzilla::WebService::JSON;
#####################################
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 = Bugzilla::WebService::JSON->new;
- $json->allow_blessed(1);
- $json->convert_blessed(1);
- $json->allow_nonref(1);
- # This may seem a little backwards, but what this really means is
- # "don't convert our utf8 into byte strings, just leave it as a
- # utf8 string."
- $json->utf8(0) if Bugzilla->params->{'utf8'};
- return $json;
+ my $self = shift;
+ my $json = Bugzilla::WebService::JSON->new;
+ $json->allow_blessed(1);
+ $json->convert_blessed(1);
+ $json->allow_nonref(1);
+
+ # This may seem a little backwards, but what this really means is
+ # "don't convert our utf8 into byte strings, just leave it as a
+ # utf8 string."
+ $json->utf8(0) if Bugzilla->params->{'utf8'};
+ return $json;
}
# Override the JSON::RPC method to return our CGI object instead of theirs.
sub cgi { return Bugzilla->cgi; }
sub response_header {
- my $self = shift;
- # The HTTP body needs to be bytes (not a utf8 string) for recent
- # versions of HTTP::Message, but JSON::RPC::Server doesn't handle this
- # properly. $_[1] is the HTTP body content we're going to be sending.
- if (utf8::is_utf8($_[1])) {
- utf8::encode($_[1]);
- # Since we're going to just be sending raw bytes, we need to
- # set STDOUT to not expect utf8.
- disable_utf8();
- }
- return $self->SUPER::response_header(@_);
+ my $self = shift;
+
+ # The HTTP body needs to be bytes (not a utf8 string) for recent
+ # versions of HTTP::Message, but JSON::RPC::Server doesn't handle this
+ # properly. $_[1] is the HTTP body content we're going to be sending.
+ if (utf8::is_utf8($_[1])) {
+ utf8::encode($_[1]);
+
+ # Since we're going to just be sending raw bytes, we need to
+ # set STDOUT to not expect utf8.
+ disable_utf8();
+ }
+ return $self->SUPER::response_header(@_);
}
sub response {
- my ($self, $response) = @_;
- my $cgi = $self->cgi;
-
- # Implement JSONP.
- if (my $callback = $self->_bz_callback) {
- my $content = $response->content;
- if (blessed $content) {
- $content = $content->encode;
- }
- # 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);
- my $content = $response->content;
- if (blessed $content) {
- $content = $content->encode;
- utf8::encode($content);
- }
- print $content;
- }
+ my ($self, $response) = @_;
+ my $cgi = $self->cgi;
+
+ # Implement JSONP.
+ if (my $callback = $self->_bz_callback) {
+ my $content = $response->content;
+ if (blessed $content) {
+ $content = $content->encode;
+ }
+
+ # 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);
+ my $content = $response->content;
+ if (blessed $content) {
+ $content = $content->encode;
+ utf8::encode($content);
+ }
+ print $content;
+ }
}
# The JSON-RPC 1.1 GET specification is not so great--you can't specify
@@ -133,70 +138,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 = Bugzilla->localconfig->{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 = Bugzilla->localconfig->{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;
}
#######################################
@@ -204,72 +208,76 @@ sub retrieve_json_from_get {
#######################################
sub type {
- my ($self, $type, $value) = @_;
-
- # This is the only type that does something special with undef.
- if ($type eq 'boolean') {
- return $value ? JSON::true : JSON::false;
- }
-
- return JSON::null if !defined $value;
-
- my $retval = $value;
-
- if ($type eq 'int') {
- $retval = int($value);
- }
- if ($type eq 'double') {
- $retval = 0.0 + $value;
- }
- elsif ($type eq 'string') {
- # Forces string context, so that JSON will make it a string.
- $retval = "$value";
- }
- elsif ($type eq 'dateTime') {
- # ISO-8601 "YYYYMMDDTHH:MM:SS" with a literal T
- $retval = $self->datetime_format_outbound($value);
- }
- elsif ($type eq 'base64') {
- utf8::encode($value) if utf8::is_utf8($value);
- $retval = encode_base64($value, '');
- }
- elsif ($type eq 'email' && Bugzilla->params->{'webservice_email_filter'}) {
- $retval = email_filter($value);
- }
-
- return $retval;
+ my ($self, $type, $value) = @_;
+
+ # This is the only type that does something special with undef.
+ if ($type eq 'boolean') {
+ return $value ? JSON::true : JSON::false;
+ }
+
+ return JSON::null if !defined $value;
+
+ my $retval = $value;
+
+ if ($type eq 'int') {
+ $retval = int($value);
+ }
+ if ($type eq 'double') {
+ $retval = 0.0 + $value;
+ }
+ elsif ($type eq 'string') {
+
+ # Forces string context, so that JSON will make it a string.
+ $retval = "$value";
+ }
+ elsif ($type eq 'dateTime') {
+
+ # ISO-8601 "YYYYMMDDTHH:MM:SS" with a literal T
+ $retval = $self->datetime_format_outbound($value);
+ }
+ elsif ($type eq 'base64') {
+ utf8::encode($value) if utf8::is_utf8($value);
+ $retval = encode_base64($value, '');
+ }
+ elsif ($type eq 'email' && Bugzilla->params->{'webservice_email_filter'}) {
+ $retval = email_filter($value);
+ }
+
+ return $retval;
}
sub datetime_format_outbound {
- my $self = shift;
- # YUI expects ISO8601 in UTC time; including TZ specifier
- return $self->SUPER::datetime_format_outbound(@_) . 'Z';
+ my $self = shift;
+
+ # YUI expects ISO8601 in UTC time; including TZ specifier
+ return $self->SUPER::datetime_format_outbound(@_) . 'Z';
}
sub handle_login {
- my $self = shift;
-
- # If we're being called using GET, we don't allow cookie-based or Env
- # login, because GET requests can be done cross-domain, and we don't
- # want private data showing up on another site unless the user
- # explicitly gives that site their username and password. (This is
- # particularly important for JSONP, which would allow a remote site
- # to use private data without the user's knowledge, unless we had this
- # protection in place.)
- if ($self->request->method ne 'POST') {
- # XXX There's no particularly good way for us to get a parameter
- # to Bugzilla->login at this point, so we pass this information
- # around using request_cache, which is a bit of a hack. The
- # implementation of it is in Bugzilla::Auth::Login::Stack.
- Bugzilla->request_cache->{auth_no_automatic_login} = 1;
- }
-
- my $path = $self->path_info;
- my $class = $self->{dispatch_path}->{$path};
- my $full_method = $self->_bz_method_name;
- $full_method =~ /^\S+\.(\S+)/;
- my $method = $1;
- $self->SUPER::handle_login($class, $method, $full_method);
+ my $self = shift;
+
+ # If we're being called using GET, we don't allow cookie-based or Env
+ # login, because GET requests can be done cross-domain, and we don't
+ # want private data showing up on another site unless the user
+ # explicitly gives that site their username and password. (This is
+ # particularly important for JSONP, which would allow a remote site
+ # to use private data without the user's knowledge, unless we had this
+ # protection in place.)
+ if ($self->request->method ne 'POST') {
+
+ # XXX There's no particularly good way for us to get a parameter
+ # to Bugzilla->login at this point, so we pass this information
+ # around using request_cache, which is a bit of a hack. The
+ # implementation of it is in Bugzilla::Auth::Login::Stack.
+ Bugzilla->request_cache->{auth_no_automatic_login} = 1;
+ }
+
+ my $path = $self->path_info;
+ my $class = $self->{dispatch_path}->{$path};
+ my $full_method = $self->_bz_method_name;
+ $full_method =~ /^\S+\.(\S+)/;
+ my $method = $1;
+ $self->SUPER::handle_login($class, $method, $full_method);
}
######################################
@@ -278,165 +286,165 @@ sub handle_login {
# Store the ID of the current call, because Bugzilla::Error will need it.
sub _handle {
- my $self = shift;
- my ($obj) = @_;
- $self->{_bz_request_id} = $obj->{id};
+ my $self = shift;
+ my ($obj) = @_;
+ $self->{_bz_request_id} = $obj->{id};
- my $result = $self->SUPER::_handle(@_);
+ my $result = $self->SUPER::_handle(@_);
- # Set the ETag if not already set in the webservice methods.
- my $etag = $self->bz_etag;
- if (!$etag && ref $result) {
- my $data = $self->json->decode($result)->{'result'};
- $self->bz_etag($data);
- }
+ # Set the ETag if not already set in the webservice methods.
+ my $etag = $self->bz_etag;
+ if (!$etag && ref $result) {
+ my $data = $self->json->decode($result)->{'result'};
+ $self->bz_etag($data);
+ }
- return $result;
+ return $result;
}
# Make all error messages returned by JSON::RPC go into the 100000
# range, and bring down all our errors into the normal range.
sub _error {
- my ($self, $id, $code) = (shift, shift, shift);
- # All JSON::RPC errors are less than 1000.
- if ($code < 1000) {
- $code += 100000;
- }
- # Bugzilla::Error adds 100,000 to all *our* errors, so
- # we know they came from us.
- elsif ($code > 100000) {
- $code -= 100000;
- }
-
- # We can't just set $_[1] because it's not always settable,
- # in JSON::RPC::Server.
- unshift(@_, $id, $code);
- my $json = $self->SUPER::_error(@_);
-
- # We want to always send the JSON-RPC 1.1 error format, although
- # If we're not in JSON-RPC 1.1, we don't need the silly "name" parameter.
- if (!$self->version or $self->version ne '1.1') {
- my $object = $self->json->decode($json);
- my $message = $object->{error};
- # Just assure that future versions of JSON::RPC don't change the
- # JSON-RPC 1.0 error format.
- if (!ref $message) {
- $object->{error} = {
- code => $code,
- message => $message,
- };
- $json = $self->json->encode($object);
- }
- }
- return $json;
+ my ($self, $id, $code) = (shift, shift, shift);
+
+ # All JSON::RPC errors are less than 1000.
+ if ($code < 1000) {
+ $code += 100000;
+ }
+
+ # Bugzilla::Error adds 100,000 to all *our* errors, so
+ # we know they came from us.
+ elsif ($code > 100000) {
+ $code -= 100000;
+ }
+
+ # We can't just set $_[1] because it's not always settable,
+ # in JSON::RPC::Server.
+ unshift(@_, $id, $code);
+ my $json = $self->SUPER::_error(@_);
+
+ # We want to always send the JSON-RPC 1.1 error format, although
+ # If we're not in JSON-RPC 1.1, we don't need the silly "name" parameter.
+ if (!$self->version or $self->version ne '1.1') {
+ my $object = $self->json->decode($json);
+ my $message = $object->{error};
+
+ # Just assure that future versions of JSON::RPC don't change the
+ # JSON-RPC 1.0 error format.
+ if (!ref $message) {
+ $object->{error} = {code => $code, message => $message,};
+ $json = $self->json->encode($object);
+ }
+ }
+ return $json;
}
# This handles dispatching our calls to the appropriate class based on
# the name of the method.
sub _find_procedure {
- my $self = shift;
+ my $self = shift;
- my $method = shift;
- $self->{_bz_method_name} = $method;
+ my $method = shift;
+ $self->{_bz_method_name} = $method;
- # This tricks SUPER::_find_procedure into finding the right class.
- $method =~ /^(\S+)\.(\S+)$/;
- $self->path_info($1);
- unshift(@_, $2);
+ # This tricks SUPER::_find_procedure into finding the right class.
+ $method =~ /^(\S+)\.(\S+)$/;
+ $self->path_info($1);
+ unshift(@_, $2);
- return $self->SUPER::_find_procedure(@_);
+ return $self->SUPER::_find_procedure(@_);
}
# This is a hacky way to do something right before methods are called.
# This is the last thing that JSON::RPC::Server::_handle calls right before
# the method is actually called.
sub _argument_type_check {
- my $self = shift;
- my $params = $self->SUPER::_argument_type_check(@_);
-
- # JSON-RPC 1.0 requires all parameters to be passed as an array, so
- # we just pull out the first item and assume it's an object.
- my $params_is_array;
- if (ref $params eq 'ARRAY') {
- $params = $params->[0];
- $params_is_array = 1;
- }
-
- taint_data($params);
-
- # Now, convert dateTime fields on input.
- $self->_bz_method_name =~ /^(\S+)\.(\S+)$/;
- my ($class, $method) = ($1, $2);
- my $pkg = $self->{dispatch_path}->{$class};
- my @date_fields = @{ $pkg->DATE_FIELDS->{$method} || [] };
- foreach my $field (@date_fields) {
- if (defined $params->{$field}) {
- my $value = $params->{$field};
- if (ref $value eq 'ARRAY') {
- $params->{$field} =
- [ map { $self->datetime_format_inbound($_) } @$value ];
- }
- else {
- $params->{$field} = $self->datetime_format_inbound($value);
- }
- }
- }
- my @base64_fields = @{ $pkg->BASE64_FIELDS->{$method} || [] };
- foreach my $field (@base64_fields) {
- if (defined $params->{$field}) {
- $params->{$field} = decode_base64($params->{$field});
- }
- }
-
- # Update the params to allow for several convenience key/values
- # use for authentication
- fix_credentials($params, $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];
- }
-
- 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];
+ }
+
+ return $params;
}
##########################
@@ -445,22 +453,24 @@ sub _argument_type_check {
# _bz_method_name is stored by _find_procedure for later use.
sub _bz_method_name {
- return $_[0]->{_bz_method_name};
+ return $_[0]->{_bz_method_name};
}
sub _bz_callback {
- my ($self, $value) = @_;
- if (defined $value) {
- $value = trim($value);
- # We don't use \w because we don't want to allow Unicode here.
- if ($value !~ /^[A-Za-z0-9_\.\[\]]+$/) {
- ThrowUserError('json_rpc_invalid_callback', { callback => $value });
- }
- $self->{_bz_callback} = $value;
- # JSONP needs to be parsed by a JS parser, not by a JSON parser.
- $self->content_type('text/javascript');
+ my ($self, $value) = @_;
+ if (defined $value) {
+ $value = trim($value);
+
+ # We don't use \w because we don't want to allow Unicode here.
+ if ($value !~ /^[A-Za-z0-9_\.\[\]]+$/) {
+ ThrowUserError('json_rpc_invalid_callback', {callback => $value});
}
- return $self->{_bz_callback};
+ $self->{_bz_callback} = $value;
+
+ # JSONP needs to be parsed by a JS parser, not by a JSON parser.
+ $self->content_type('text/javascript');
+ }
+ return $self->{_bz_callback};
}
1;
diff --git a/Bugzilla/WebService/Server/REST.pm b/Bugzilla/WebService/Server/REST.pm
index 5d8367410..781960c68 100644
--- a/Bugzilla/WebService/Server/REST.pm
+++ b/Bugzilla/WebService/Server/REST.pm
@@ -41,146 +41,146 @@ use Module::Runtime qw(require_module);
###########################
sub handle {
- my ($self) = @_;
-
- # Determine how the data should be represented. We do this early so
- # errors will also be returned with the proper content type.
- # If no accept header was sent or the content types specified were not
- # matched, we default to the first type in the whitelist.
- $self->content_type($self->_best_content_type(REST_CONTENT_TYPE_WHITELIST()));
-
- # Using current path information, decide which class/method to
- # use to serve the request. Throw error if no resource was found
- # unless we were looking for OPTIONS
- if (!$self->_find_resource($self->cgi->path_info)) {
- if ($self->request->method eq 'OPTIONS'
- && $self->bz_rest_options)
- {
- my $response = $self->response_header(STATUS_OK, "");
- my $options_string = join(', ', @{ $self->bz_rest_options });
- $response->header('Allow' => $options_string,
- 'Access-Control-Allow-Methods' => $options_string);
- return $self->response($response);
- }
-
- ThrowUserError("rest_invalid_resource",
- { path => $self->cgi->path_info,
- method => $self->request->method });
+ my ($self) = @_;
+
+ # Determine how the data should be represented. We do this early so
+ # errors will also be returned with the proper content type.
+ # If no accept header was sent or the content types specified were not
+ # matched, we default to the first type in the whitelist.
+ $self->content_type($self->_best_content_type(REST_CONTENT_TYPE_WHITELIST()));
+
+ # Using current path information, decide which class/method to
+ # use to serve the request. Throw error if no resource was found
+ # unless we were looking for OPTIONS
+ if (!$self->_find_resource($self->cgi->path_info)) {
+ if ($self->request->method eq 'OPTIONS' && $self->bz_rest_options) {
+ my $response = $self->response_header(STATUS_OK, "");
+ my $options_string = join(', ', @{$self->bz_rest_options});
+ $response->header(
+ 'Allow' => $options_string,
+ 'Access-Control-Allow-Methods' => $options_string
+ );
+ return $self->response($response);
}
- # Dispatch to the proper module
- my $class = $self->bz_class_name;
- my ($path) = $class =~ /::([^:]+)$/;
- $self->path_info($path);
- delete $self->{dispatch_path};
- $self->dispatch({ $path => $class });
+ ThrowUserError("rest_invalid_resource",
+ {path => $self->cgi->path_info, method => $self->request->method});
+ }
- my $params = $self->_retrieve_json_params;
+ # Dispatch to the proper module
+ my $class = $self->bz_class_name;
+ my ($path) = $class =~ /::([^:]+)$/;
+ $self->path_info($path);
+ delete $self->{dispatch_path};
+ $self->dispatch({$path => $class});
- fix_credentials($params, $self->cgi);
+ my $params = $self->_retrieve_json_params;
- # Fix includes/excludes for each call
- rest_include_exclude($params);
+ fix_credentials($params, $self->cgi);
- # Set callback name if content-type is 'application/javascript'
- if ($params->{'callback'}
- || $self->content_type eq 'application/javascript')
- {
- $self->_bz_callback($params->{'callback'} || 'callback');
- }
+ # Fix includes/excludes for each call
+ rest_include_exclude($params);
- Bugzilla->input_params($params);
+ # Set callback name if content-type is 'application/javascript'
+ if ($params->{'callback'} || $self->content_type eq 'application/javascript') {
+ $self->_bz_callback($params->{'callback'} || 'callback');
+ }
- # Set the JSON version to 1.1 and the id to the current urlbase
- # also set up the correct handler method
- my $obj = {
- version => '1.1',
- id => Bugzilla->localconfig->{urlbase},
- method => $self->bz_method_name,
- params => $params
- };
+ Bugzilla->input_params($params);
- # Execute the handler
- my $result = $self->_handle($obj);
+ # Set the JSON version to 1.1 and the id to the current urlbase
+ # also set up the correct handler method
+ my $obj = {
+ version => '1.1',
+ id => Bugzilla->localconfig->{urlbase},
+ method => $self->bz_method_name,
+ params => $params
+ };
- if (!$self->error_response_header) {
- return $self->response(
- $self->response_header($self->bz_success_code || STATUS_OK, $result));
- }
+ # Execute the handler
+ my $result = $self->_handle($obj);
- $self->response($self->error_response_header);
+ if (!$self->error_response_header) {
+ return $self->response(
+ $self->response_header($self->bz_success_code || STATUS_OK, $result));
+ }
+
+ $self->response($self->error_response_header);
}
sub response {
- my ($self, $response) = @_;
-
- # If we have thrown an error, the 'error' key will exist
- # otherwise we use 'result'. JSONRPC returns other data
- # along with the result/error such as version and id which
- # we will strip off for REST calls.
- my $content = $response->content;
-
- my $json_data = {};
- if ($content) {
- # Content is in bytes at this point and needs to be converted
- # back to utf8 string.
- enable_utf8();
- utf8::decode($content) if !utf8::is_utf8($content);
- $json_data = $self->json->decode($content);
- }
-
- my $result = {};
- if (exists $json_data->{error}) {
- $result = $json_data->{error};
- $result->{error} = $self->type('boolean', 1);
-
- $result->{documentation} = Bugzilla->params->{docs_urlbase} . "api/";
- delete $result->{'name'}; # Remove JSONRPCError
- }
- elsif (exists $json_data->{result}) {
- $result = $json_data->{result};
- }
-
- Bugzilla::Hook::process('webservice_rest_response',
- { rpc => $self, result => \$result, response => $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));
-
- # ETag support
- my $etag = $self->bz_etag;
- $self->bz_etag($result) if !$etag;
-
- # If accessing through web browser, then display in readable format
- if ($self->content_type eq 'text/html') {
- $result = $self->json->pretty->canonical->allow_nonref->encode($result);
-
- my $template = Bugzilla->template;
- $content = "";
- $result->encode if blessed $result;
- $template->process("rest.html.tmpl", { result => $result }, \$content)
- || ThrowTemplateError($template->error());
-
- $response->content_type('text/html');
- }
- else {
- $content = $self->json->encode($result);
- }
-
- utf8::encode($content) if utf8::is_utf8($content);
- disable_utf8();
-
- $response->content($content);
-
- $self->SUPER::response($response);
+ my ($self, $response) = @_;
+
+ # If we have thrown an error, the 'error' key will exist
+ # otherwise we use 'result'. JSONRPC returns other data
+ # along with the result/error such as version and id which
+ # we will strip off for REST calls.
+ my $content = $response->content;
+
+ my $json_data = {};
+ if ($content) {
+
+ # Content is in bytes at this point and needs to be converted
+ # back to utf8 string.
+ enable_utf8();
+ utf8::decode($content) if !utf8::is_utf8($content);
+ $json_data = $self->json->decode($content);
+ }
+
+ my $result = {};
+ if (exists $json_data->{error}) {
+ $result = $json_data->{error};
+ $result->{error} = $self->type('boolean', 1);
+
+ $result->{documentation} = Bugzilla->params->{docs_urlbase} . "api/";
+ delete $result->{'name'}; # Remove JSONRPCError
+ }
+ elsif (exists $json_data->{result}) {
+ $result = $json_data->{result};
+ }
+
+ Bugzilla::Hook::process('webservice_rest_response',
+ {rpc => $self, result => \$result, response => $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));
+
+ # ETag support
+ my $etag = $self->bz_etag;
+ $self->bz_etag($result) if !$etag;
+
+ # If accessing through web browser, then display in readable format
+ if ($self->content_type eq 'text/html') {
+ $result = $self->json->pretty->canonical->allow_nonref->encode($result);
+
+ my $template = Bugzilla->template;
+ $content = "";
+ $result->encode if blessed $result;
+ $template->process("rest.html.tmpl", {result => $result}, \$content)
+ || ThrowTemplateError($template->error());
+
+ $response->content_type('text/html');
+ }
+ else {
+ $content = $self->json->encode($result);
+ }
+
+ utf8::encode($content) if utf8::is_utf8($content);
+ disable_utf8();
+
+ $response->content($content);
+
+ $self->SUPER::response($response);
}
#######################################
@@ -188,21 +188,21 @@ sub response {
#######################################
sub handle_login {
- my $self = shift;
- my $class = $self->bz_class_name;
- my $method = $self->bz_method_name;
- my $full_method = $class . "." . $method;
- $full_method =~ s/^Bugzilla::WebService:://;
-
- # We never want to create a new session unless the user is calling the
- # login method. Setting dont_persist_session makes
- # Bugzilla::Auth::_handle_login_result() skip calling persist_login().
- if ($full_method ne 'User.login') {
- Bugzilla->request_cache->{dont_persist_session} = 1;
- }
-
- # Bypass JSONRPC::handle_login
- Bugzilla::WebService::Server->handle_login($class, $method, $full_method);
+ my $self = shift;
+ my $class = $self->bz_class_name;
+ my $method = $self->bz_method_name;
+ my $full_method = $class . "." . $method;
+ $full_method =~ s/^Bugzilla::WebService:://;
+
+ # We never want to create a new session unless the user is calling the
+ # login method. Setting dont_persist_session makes
+ # Bugzilla::Auth::_handle_login_result() skip calling persist_login().
+ if ($full_method ne 'User.login') {
+ Bugzilla->request_cache->{dont_persist_session} = 1;
+ }
+
+ # Bypass JSONRPC::handle_login
+ Bugzilla::WebService::Server->handle_login($class, $method, $full_method);
}
############################
@@ -212,79 +212,78 @@ sub handle_login {
# We do not want to run Bugzilla::WebService::Server::JSONRPC->_find_prodedure
# as it determines the method name differently.
sub _find_procedure {
- my $self = shift;
- if ($self->isa('JSON::RPC::Server::CGI')) {
- return JSON::RPC::Server::_find_procedure($self, @_);
- }
- else {
- return JSON::RPC::Legacy::Server::_find_procedure($self, @_);
- }
+ my $self = shift;
+ if ($self->isa('JSON::RPC::Server::CGI')) {
+ return JSON::RPC::Server::_find_procedure($self, @_);
+ }
+ else {
+ return JSON::RPC::Legacy::Server::_find_procedure($self, @_);
+ }
}
sub _argument_type_check {
- my $self = shift;
- my $params;
-
- if ($self->isa('JSON::RPC::Server::CGI')) {
- $params = JSON::RPC::Server::_argument_type_check($self, @_);
- }
- else {
- $params = JSON::RPC::Legacy::Server::_argument_type_check($self, @_);
- }
-
- # JSON-RPC 1.0 requires all parameters to be passed as an array, so
- # we just pull out the first item and assume it's an object.
- my $params_is_array;
- if (ref $params eq 'ARRAY') {
- $params = $params->[0];
- $params_is_array = 1;
- }
-
- taint_data($params);
-
- # Now, convert dateTime fields on input.
- my $method = $self->bz_method_name;
- my $pkg = $self->{dispatch_path}->{$self->path_info};
- my @date_fields = @{ $pkg->DATE_FIELDS->{$method} || [] };
- foreach my $field (@date_fields) {
- if (defined $params->{$field}) {
- my $value = $params->{$field};
- if (ref $value eq 'ARRAY') {
- $params->{$field} =
- [ map { $self->datetime_format_inbound($_) } @$value ];
- }
- else {
- $params->{$field} = $self->datetime_format_inbound($value);
- }
- }
+ my $self = shift;
+ my $params;
+
+ if ($self->isa('JSON::RPC::Server::CGI')) {
+ $params = JSON::RPC::Server::_argument_type_check($self, @_);
+ }
+ else {
+ $params = JSON::RPC::Legacy::Server::_argument_type_check($self, @_);
+ }
+
+ # JSON-RPC 1.0 requires all parameters to be passed as an array, so
+ # we just pull out the first item and assume it's an object.
+ my $params_is_array;
+ if (ref $params eq 'ARRAY') {
+ $params = $params->[0];
+ $params_is_array = 1;
+ }
+
+ taint_data($params);
+
+ # Now, convert dateTime fields on input.
+ my $method = $self->bz_method_name;
+ my $pkg = $self->{dispatch_path}->{$self->path_info};
+ my @date_fields = @{$pkg->DATE_FIELDS->{$method} || []};
+ foreach my $field (@date_fields) {
+ if (defined $params->{$field}) {
+ my $value = $params->{$field};
+ if (ref $value eq 'ARRAY') {
+ $params->{$field} = [map { $self->datetime_format_inbound($_) } @$value];
+ }
+ else {
+ $params->{$field} = $self->datetime_format_inbound($value);
+ }
}
- my @base64_fields = @{ $pkg->BASE64_FIELDS->{$method} || [] };
- foreach my $field (@base64_fields) {
- if (defined $params->{$field}) {
- $params->{$field} = decode_base64($params->{$field});
- }
+ }
+ my @base64_fields = @{$pkg->BASE64_FIELDS->{$method} || []};
+ foreach my $field (@base64_fields) {
+ if (defined $params->{$field}) {
+ $params->{$field} = decode_base64($params->{$field});
}
+ }
- # This is the best time to do login checks.
- $self->handle_login();
+ # 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;
+ # 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;
- # Allow extensions to modify the params post login
- Bugzilla::Hook::process('webservice_rest_request',
- { rpc => $self, params => $params });
+ # Allow extensions to modify the params post login
+ Bugzilla::Hook::process('webservice_rest_request',
+ {rpc => $self, params => $params});
- if ($params_is_array) {
- $params = [$params];
- }
+ if ($params_is_array) {
+ $params = [$params];
+ }
- return $params;
+ return $params;
}
###################
@@ -292,46 +291,46 @@ sub _argument_type_check {
###################
sub bz_method_name {
- my ($self, $method) = @_;
- $self->{_bz_method_name} = $method if $method;
- return $self->{_bz_method_name};
+ my ($self, $method) = @_;
+ $self->{_bz_method_name} = $method if $method;
+ return $self->{_bz_method_name};
}
sub bz_class_name {
- my ($self, $class) = @_;
- $self->{_bz_class_name} = $class if $class;
- return $self->{_bz_class_name};
+ my ($self, $class) = @_;
+ $self->{_bz_class_name} = $class if $class;
+ return $self->{_bz_class_name};
}
sub bz_success_code {
- my ($self, $value) = @_;
- $self->{_bz_success_code} = $value if $value;
- return $self->{_bz_success_code};
+ my ($self, $value) = @_;
+ $self->{_bz_success_code} = $value if $value;
+ return $self->{_bz_success_code};
}
sub bz_rest_params {
- my ($self, $params) = @_;
- $self->{_bz_rest_params} = $params if $params;
- return $self->{_bz_rest_params};
+ my ($self, $params) = @_;
+ $self->{_bz_rest_params} = $params if $params;
+ return $self->{_bz_rest_params};
}
sub bz_rest_options {
- my ($self, $options) = @_;
- $self->{_bz_rest_options} = $options if $options;
- return [ sort { $a cmp $b } @{ $self->{_bz_rest_options} } ];
+ my ($self, $options) = @_;
+ $self->{_bz_rest_options} = $options if $options;
+ return [sort { $a cmp $b } @{$self->{_bz_rest_options}}];
}
sub rest_include_exclude {
- my ($params) = @_;
+ my ($params) = @_;
- if (exists $params->{'include_fields'} && !ref $params->{'include_fields'}) {
- $params->{'include_fields'} = [ split(/[\s+,]/, $params->{'include_fields'}) ];
- }
- if (exists $params->{'exclude_fields'} && !ref $params->{'exclude_fields'}) {
- $params->{'exclude_fields'} = [ split(/[\s+,]/, $params->{'exclude_fields'}) ];
- }
+ if (exists $params->{'include_fields'} && !ref $params->{'include_fields'}) {
+ $params->{'include_fields'} = [split(/[\s+,]/, $params->{'include_fields'})];
+ }
+ if (exists $params->{'exclude_fields'} && !ref $params->{'exclude_fields'}) {
+ $params->{'exclude_fields'} = [split(/[\s+,]/, $params->{'exclude_fields'})];
+ }
- return $params;
+ return $params;
}
##########################
@@ -339,187 +338,195 @@ sub rest_include_exclude {
##########################
sub _retrieve_json_params {
- my $self = shift;
-
- # Make a copy of the current input_params rather than edit directly
- my $params = {};
- %{$params} = %{ Bugzilla->input_params };
-
- # First add any parameters we were able to pull out of the path
- # based on the resource regexp and combine with the normal URL
- # parameters.
- if (my $rest_params = $self->bz_rest_params) {
- foreach my $param (keys %$rest_params) {
- # If the param does not already exist or if the
- # rest param is a single value, add it to the
- # global params.
- if (!exists $params->{$param} || !ref $rest_params->{$param}) {
- $params->{$param} = $rest_params->{$param};
- }
- # If rest_param is a list then add any extra values to the list
- elsif (ref $rest_params->{$param}) {
- my @extra_values = ref $params->{$param}
- ? @{ $params->{$param} }
- : ($params->{$param});
- $params->{$param}
- = [ uniq (@{ $rest_params->{$param} }, @extra_values) ];
- }
- }
+ my $self = shift;
+
+ # Make a copy of the current input_params rather than edit directly
+ my $params = {};
+ %{$params} = %{Bugzilla->input_params};
+
+ # First add any parameters we were able to pull out of the path
+ # based on the resource regexp and combine with the normal URL
+ # parameters.
+ if (my $rest_params = $self->bz_rest_params) {
+ foreach my $param (keys %$rest_params) {
+
+ # If the param does not already exist or if the
+ # rest param is a single value, add it to the
+ # global params.
+ if (!exists $params->{$param} || !ref $rest_params->{$param}) {
+ $params->{$param} = $rest_params->{$param};
+ }
+
+ # If rest_param is a list then add any extra values to the list
+ elsif (ref $rest_params->{$param}) {
+ my @extra_values
+ = ref $params->{$param} ? @{$params->{$param}} : ($params->{$param});
+ $params->{$param} = [uniq(@{$rest_params->{$param}}, @extra_values)];
+ }
+ }
+ }
+
+ # Any parameters passed in in the body of a non-GET request will override
+ # any parameters pull from the url path. Otherwise non-unique keys are
+ # combined.
+ if ($self->request->method ne 'GET') {
+ my $extra_params = {};
+
+ # We do this manually because CGI.pm doesn't understand JSON strings.
+ my $json = delete $params->{'POSTDATA'} || delete $params->{'PUTDATA'};
+ if ($json) {
+ eval { $extra_params = $self->json->utf8(0)->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->utf8(0)->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 preload {
- require_module($_) for values %{ WS_DISPATCH() };
+ require_module($_) for values %{WS_DISPATCH()};
}
sub _find_resource {
- my ($self, $path) = @_;
-
- # Load in the WebService module from the dispatch map and then call
- # $module->rest_resources to get the resources array ref.
- my $resources = {};
- foreach my $module (values %{ $self->{dispatch_path} }) {
- next if !$module->can('rest_resources');
- $resources->{$module} = $module->rest_resources;
- }
-
- Bugzilla::Hook::process('webservice_rest_resources',
- { rpc => $self, resources => $resources }) if Bugzilla::request_cache->{bzapi};
-
- # Use the resources hash from each module loaded earlier to determine
- # which handler to use based on a regex match of the CGI path.
- # Also any matches found in the regex will be passed in later to the
- # handler for possible use.
- my $request_method = $self->request->method;
-
- my (@matches, $handler_found, $handler_method, $handler_class);
- foreach my $class (keys %{ $resources }) {
- # The resource data for each module needs to be
- # an array ref with an even number of elements
- # to work correctly.
- next if (ref $resources->{$class} ne 'ARRAY'
- || scalar @{ $resources->{$class} } % 2 != 0);
-
- while (my $regex = shift @{ $resources->{$class} }) {
- my $options_data = shift @{ $resources->{$class} };
- next if ref $options_data ne 'HASH';
-
- if (@matches = ($path =~ $regex)) {
- # If a specific path is accompanied by a OPTIONS request
- # method, the user is asking for a list of possible request
- # methods for a specific path.
- $self->bz_rest_options([ keys %{ $options_data } ]);
-
- if ($options_data->{$request_method}) {
- my $resource_data = $options_data->{$request_method};
- $self->bz_class_name($class);
-
- # The method key/value can be a simple scalar method name
- # or a anonymous subroutine so we execute it here.
- my $method = ref $resource_data->{method} eq 'CODE'
- ? $resource_data->{method}->($self)
- : $resource_data->{method};
- $self->bz_method_name($method);
-
- # Pull out any parameters parsed from the URL path
- # and store them for use by the method.
- if ($resource_data->{params}) {
- $self->bz_rest_params($resource_data->{params}->(@matches));
- }
-
- # If a special success code is needed for this particular
- # method, then store it for later when generating response.
- if ($resource_data->{success_code}) {
- $self->bz_success_code($resource_data->{success_code});
- }
- $handler_found = 1;
- }
- }
- last if $handler_found;
+ my ($self, $path) = @_;
+
+ # Load in the WebService module from the dispatch map and then call
+ # $module->rest_resources to get the resources array ref.
+ my $resources = {};
+ foreach my $module (values %{$self->{dispatch_path}}) {
+ next if !$module->can('rest_resources');
+ $resources->{$module} = $module->rest_resources;
+ }
+
+ Bugzilla::Hook::process('webservice_rest_resources',
+ {rpc => $self, resources => $resources})
+ if Bugzilla::request_cache->{bzapi};
+
+ # Use the resources hash from each module loaded earlier to determine
+ # which handler to use based on a regex match of the CGI path.
+ # Also any matches found in the regex will be passed in later to the
+ # handler for possible use.
+ my $request_method = $self->request->method;
+
+ my (@matches, $handler_found, $handler_method, $handler_class);
+ foreach my $class (keys %{$resources}) {
+
+ # The resource data for each module needs to be
+ # an array ref with an even number of elements
+ # to work correctly.
+ next
+ if (ref $resources->{$class} ne 'ARRAY'
+ || scalar @{$resources->{$class}} % 2 != 0);
+
+ while (my $regex = shift @{$resources->{$class}}) {
+ my $options_data = shift @{$resources->{$class}};
+ next if ref $options_data ne 'HASH';
+
+ if (@matches = ($path =~ $regex)) {
+
+ # If a specific path is accompanied by a OPTIONS request
+ # method, the user is asking for a list of possible request
+ # methods for a specific path.
+ $self->bz_rest_options([keys %{$options_data}]);
+
+ if ($options_data->{$request_method}) {
+ my $resource_data = $options_data->{$request_method};
+ $self->bz_class_name($class);
+
+ # The method key/value can be a simple scalar method name
+ # or a anonymous subroutine so we execute it here.
+ my $method
+ = ref $resource_data->{method} eq 'CODE'
+ ? $resource_data->{method}->($self)
+ : $resource_data->{method};
+ $self->bz_method_name($method);
+
+ # Pull out any parameters parsed from the URL path
+ # and store them for use by the method.
+ if ($resource_data->{params}) {
+ $self->bz_rest_params($resource_data->{params}->(@matches));
+ }
+
+ # If a special success code is needed for this particular
+ # method, then store it for later when generating response.
+ if ($resource_data->{success_code}) {
+ $self->bz_success_code($resource_data->{success_code});
+ }
+ $handler_found = 1;
}
- last if $handler_found;
+ }
+ last if $handler_found;
}
+ last if $handler_found;
+ }
- return $handler_found;
+ return $handler_found;
}
sub _best_content_type {
- my ($self, @types) = @_;
- return ($self->_simple_content_negotiation(@types))[0] || '*/*';
+ my ($self, @types) = @_;
+ return ($self->_simple_content_negotiation(@types))[0] || '*/*';
}
sub _simple_content_negotiation {
- my ($self, @types) = @_;
- my @accept_types = $self->_get_content_prefs();
- # Return the types as-is if no accept header sent, since sorting will be a no-op.
- if (!@accept_types) {
- return @types;
- }
- my $score = sub { $self->_score_type(shift, @accept_types) };
- return sort {$score->($b) <=> $score->($a)} @types;
+ my ($self, @types) = @_;
+ my @accept_types = $self->_get_content_prefs();
+
+ # Return the types as-is if no accept header sent, since sorting will be a no-op.
+ if (!@accept_types) {
+ return @types;
+ }
+ my $score = sub { $self->_score_type(shift, @accept_types) };
+ return sort { $score->($b) <=> $score->($a) } @types;
}
sub _score_type {
- my ($self, $type, @accept_types) = @_;
- my $score = scalar(@accept_types);
- for my $accept_type (@accept_types) {
- return $score if $type eq $accept_type;
- $score--;
- }
- return 0;
+ my ($self, $type, @accept_types) = @_;
+ my $score = scalar(@accept_types);
+ for my $accept_type (@accept_types) {
+ return $score if $type eq $accept_type;
+ $score--;
+ }
+ return 0;
}
sub _get_content_prefs {
- my $self = shift;
- my $default_weight = 1;
- my @prefs;
-
- # Parse the Accept header, and save type name, score, and position.
- my @accept_types = split /,/, $self->cgi->http('accept') || '';
- my $order = 0;
- for my $accept_type (@accept_types) {
- my ($weight) = ($accept_type =~ /q=(\d\.\d+|\d+)/);
- my ($name) = ($accept_type =~ m#(\S+/[^;]+)#);
- next unless $name;
- push @prefs, { name => $name, order => $order++};
- if (defined $weight) {
- $prefs[-1]->{score} = $weight;
- } else {
- $prefs[-1]->{score} = $default_weight;
- $default_weight -= 0.001;
- }
+ my $self = shift;
+ my $default_weight = 1;
+ my @prefs;
+
+ # Parse the Accept header, and save type name, score, and position.
+ my @accept_types = split /,/, $self->cgi->http('accept') || '';
+ my $order = 0;
+ for my $accept_type (@accept_types) {
+ my ($weight) = ($accept_type =~ /q=(\d\.\d+|\d+)/);
+ my ($name) = ($accept_type =~ m#(\S+/[^;]+)#);
+ next unless $name;
+ push @prefs, {name => $name, order => $order++};
+ if (defined $weight) {
+ $prefs[-1]->{score} = $weight;
+ }
+ else {
+ $prefs[-1]->{score} = $default_weight;
+ $default_weight -= 0.001;
}
+ }
- # Sort the types by score, subscore by order, and pull out just the name
- @prefs = map {$_->{name}} sort {$b->{score} <=> $a->{score} ||
- $a->{order} <=> $b->{order}} @prefs;
- return @prefs;
+ # Sort the types by score, subscore by order, and pull out just the name
+ @prefs = map { $_->{name} }
+ sort { $b->{score} <=> $a->{score} || $a->{order} <=> $b->{order} } @prefs;
+ return @prefs;
}
1;
diff --git a/Bugzilla/WebService/Server/REST/Resources/Bug.pm b/Bugzilla/WebService/Server/REST/Resources/Bug.pm
index 26aec011c..34580368d 100644
--- a/Bugzilla/WebService/Server/REST/Resources/Bug.pm
+++ b/Bugzilla/WebService/Server/REST/Resources/Bug.pm
@@ -15,177 +15,172 @@ use Bugzilla::WebService::Constants;
use Bugzilla::WebService::Bug;
BEGIN {
- *Bugzilla::WebService::Bug::rest_resources = \&_rest_resources;
-};
+ *Bugzilla::WebService::Bug::rest_resources = \&_rest_resources;
+}
sub _rest_resources {
- my $rest_resources = [
- qr{^/bug$}, {
- GET => {
- method => 'search',
- },
- POST => {
- method => 'create',
- status_code => STATUS_CREATED
- }
- },
- qr{^/bug/$}, {
- GET => {
- method => 'get'
- }
- },
- qr{^/bug/possible_duplicates$}, {
- GET => {
- method => 'possible_duplicates'
- }
- },
- 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/(\d+)$}, {
- 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/comment/render$}, {
- POST => {
- method => 'render_comment',
- },
- },
- 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] ] };
- }
- }
- },
- 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] };
- }
- }
- },
- qr{^/flag_types/([^/]+)/([^/]+)$}, {
- GET => {
- method => 'flag_types',
- params => sub {
- return { product => $_[0],
- component => $_[1] };
- }
- }
- },
- qr{^/flag_types/([^/]+)$}, {
- GET => {
- method => 'flag_types',
- params => sub {
- return { product => $_[0] };
- }
- }
+ my $rest_resources = [
+ qr{^/bug$},
+ {
+ GET => {method => 'search',},
+ POST => {method => 'create', status_code => STATUS_CREATED}
+ },
+ qr{^/bug/$},
+ {GET => {method => 'get'}},
+ qr{^/bug/possible_duplicates$},
+ {GET => {method => 'possible_duplicates'}},
+ 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/(\d+)$},
+ {
+ 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/comment/render$},
+ {POST => {method => 'render_comment',},},
+ 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]]};
+ }
+ }
+ },
+ 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]};
+ }
+ }
+ },
+ qr{^/flag_types/([^/]+)/([^/]+)$},
+ {
+ GET => {
+ method => 'flag_types',
+ params => sub {
+ return {product => $_[0], component => $_[1]};
+ }
+ }
+ },
+ qr{^/flag_types/([^/]+)$},
+ {
+ GET => {
+ method => 'flag_types',
+ params => sub {
+ return {product => $_[0]};
}
- ];
- return $rest_resources;
+ }
+ }
+ ];
+ return $rest_resources;
}
1;
diff --git a/Bugzilla/WebService/Server/REST/Resources/BugUserLastVisit.pm b/Bugzilla/WebService/Server/REST/Resources/BugUserLastVisit.pm
index 12290e84e..72aa0d40f 100644
--- a/Bugzilla/WebService/Server/REST/Resources/BugUserLastVisit.pm
+++ b/Bugzilla/WebService/Server/REST/Resources/BugUserLastVisit.pm
@@ -12,36 +12,32 @@ use strict;
use warnings;
BEGIN {
- *Bugzilla::WebService::BugUserLastVisit::rest_resources = \&_rest_resources;
+ *Bugzilla::WebService::BugUserLastVisit::rest_resources = \&_rest_resources;
}
sub _rest_resources {
- return [
- # bug-id
- qr{^/bug_user_last_visit/(\d+)$}, {
- GET => {
- method => 'get',
- params => sub {
- return { ids => $_[0] };
- },
- },
- POST => {
- method => 'update',
- params => sub {
- return { ids => $_[0] };
- },
- },
+ return [
+ # bug-id
+ qr{^/bug_user_last_visit/(\d+)$},
+ {
+ GET => {
+ method => 'get',
+ params => sub {
+ return {ids => $_[0]};
},
- # no bug-id
- qr{^/bug_user_last_visit$}, {
- GET => {
- method => 'get',
- },
- POST => {
- method => 'update',
- },
+ },
+ POST => {
+ method => 'update',
+ params => sub {
+ return {ids => $_[0]};
},
- ];
+ },
+ },
+
+ # no bug-id
+ qr{^/bug_user_last_visit$},
+ {GET => {method => 'get',}, POST => {method => 'update',},},
+ ];
}
1;
diff --git a/Bugzilla/WebService/Server/REST/Resources/Bugzilla.pm b/Bugzilla/WebService/Server/REST/Resources/Bugzilla.pm
index 646355cd3..28872f698 100644
--- a/Bugzilla/WebService/Server/REST/Resources/Bugzilla.pm
+++ b/Bugzilla/WebService/Server/REST/Resources/Bugzilla.pm
@@ -15,48 +15,20 @@ use Bugzilla::WebService::Constants;
use Bugzilla::WebService::Bugzilla;
BEGIN {
- *Bugzilla::WebService::Bugzilla::rest_resources = \&_rest_resources;
-};
+ *Bugzilla::WebService::Bugzilla::rest_resources = \&_rest_resources;
+}
sub _rest_resources {
- my $rest_resources = [
- qr{^/version$}, {
- GET => {
- method => 'version'
- }
- },
- qr{^/extensions$}, {
- GET => {
- method => 'extensions'
- }
- },
- qr{^/timezone$}, {
- GET => {
- method => 'timezone'
- }
- },
- qr{^/time$}, {
- GET => {
- method => 'time'
- }
- },
- qr{^/last_audit_time$}, {
- GET => {
- method => 'last_audit_time'
- }
- },
- qr{^/parameters$}, {
- GET => {
- method => 'parameters'
- }
- },
- qr{^/jobqueue_status$}, {
- GET => {
- method => 'jobqueue_status'
- }
- }
- ];
- 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'}},
+ qr{^/jobqueue_status$}, {GET => {method => 'jobqueue_status'}}
+ ];
+ return $rest_resources;
}
1;
diff --git a/Bugzilla/WebService/Server/REST/Resources/Classification.pm b/Bugzilla/WebService/Server/REST/Resources/Classification.pm
index f20278f55..88ba028ba 100644
--- a/Bugzilla/WebService/Server/REST/Resources/Classification.pm
+++ b/Bugzilla/WebService/Server/REST/Resources/Classification.pm
@@ -15,22 +15,23 @@ use Bugzilla::WebService::Constants;
use Bugzilla::WebService::Classification;
BEGIN {
- *Bugzilla::WebService::Classification::rest_resources = \&_rest_resources;
-};
+ *Bugzilla::WebService::Classification::rest_resources = \&_rest_resources;
+}
sub _rest_resources {
- my $rest_resources = [
- qr{^/classification/([^/]+)$}, {
- GET => {
- method => 'get',
- params => sub {
- my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names';
- return { $param => [ $_[0] ] };
- }
- }
+ my $rest_resources = [
+ qr{^/classification/([^/]+)$},
+ {
+ GET => {
+ method => 'get',
+ params => sub {
+ my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names';
+ return {$param => [$_[0]]};
}
- ];
- return $rest_resources;
+ }
+ }
+ ];
+ return $rest_resources;
}
1;
diff --git a/Bugzilla/WebService/Server/REST/Resources/Elastic.pm b/Bugzilla/WebService/Server/REST/Resources/Elastic.pm
index 2f7c1eaa4..367dd9134 100644
--- a/Bugzilla/WebService/Server/REST/Resources/Elastic.pm
+++ b/Bugzilla/WebService/Server/REST/Resources/Elastic.pm
@@ -15,16 +15,13 @@ use Bugzilla::WebService::Constants;
use Bugzilla::WebService::Elastic;
BEGIN {
- *Bugzilla::WebService::Elastic::rest_resources = \&_rest_resources;
-};
+ *Bugzilla::WebService::Elastic::rest_resources = \&_rest_resources;
+}
sub _rest_resources {
- my $rest_resources = [
- qr{^/elastic/suggest_users$}, {
- GET => { method => 'suggest_users' },
- },
- ];
- return $rest_resources;
+ my $rest_resources
+ = [qr{^/elastic/suggest_users$}, {GET => {method => 'suggest_users'},},];
+ return $rest_resources;
}
1;
diff --git a/Bugzilla/WebService/Server/REST/Resources/Group.pm b/Bugzilla/WebService/Server/REST/Resources/Group.pm
index 6e3d934eb..b6a1b9b34 100644
--- a/Bugzilla/WebService/Server/REST/Resources/Group.pm
+++ b/Bugzilla/WebService/Server/REST/Resources/Group.pm
@@ -15,38 +15,35 @@ use Bugzilla::WebService::Constants;
use Bugzilla::WebService::Group;
BEGIN {
- *Bugzilla::WebService::Group::rest_resources = \&_rest_resources;
-};
+ *Bugzilla::WebService::Group::rest_resources = \&_rest_resources;
+}
sub _rest_resources {
- my $rest_resources = [
- qr{^/group$}, {
- GET => {
- method => 'get'
- },
- POST => {
- method => 'create',
- success_code => STATUS_CREATED
- }
- },
- qr{^/group/([^/]+)$}, {
- GET => {
- method => 'get',
- params => sub {
- my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names';
- return { $param => [ $_[0] ] };
- }
- },
- PUT => {
- method => 'update',
- params => sub {
- my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names';
- return { $param => [ $_[0] ] };
- }
- }
+ my $rest_resources = [
+ qr{^/group$},
+ {
+ GET => {method => 'get'},
+ POST => {method => 'create', success_code => STATUS_CREATED}
+ },
+ qr{^/group/([^/]+)$},
+ {
+ GET => {
+ method => 'get',
+ params => sub {
+ my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names';
+ return {$param => [$_[0]]};
+ }
+ },
+ PUT => {
+ method => 'update',
+ params => sub {
+ my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names';
+ return {$param => [$_[0]]};
}
- ];
- return $rest_resources;
+ }
+ }
+ ];
+ return $rest_resources;
}
1;
diff --git a/Bugzilla/WebService/Server/REST/Resources/Product.pm b/Bugzilla/WebService/Server/REST/Resources/Product.pm
index 9ca6e3074..3222642c8 100644
--- a/Bugzilla/WebService/Server/REST/Resources/Product.pm
+++ b/Bugzilla/WebService/Server/REST/Resources/Product.pm
@@ -17,53 +17,41 @@ use Bugzilla::WebService::Product;
use Bugzilla::Error;
BEGIN {
- *Bugzilla::WebService::Product::rest_resources = \&_rest_resources;
-};
+ *Bugzilla::WebService::Product::rest_resources = \&_rest_resources;
+}
sub _rest_resources {
- my $rest_resources = [
- qr{^/product_accessible$}, {
- GET => {
- method => 'get_accessible_products'
- }
- },
- qr{^/product_enterable$}, {
- GET => {
- method => 'get_enterable_products'
- }
- },
- qr{^/product_selectable$}, {
- GET => {
- method => 'get_selectable_products'
- }
- },
- qr{^/product$}, {
- GET => {
- method => 'get'
- },
- POST => {
- method => 'create',
- success_code => STATUS_CREATED
- }
- },
- qr{^/product/([^/]+)$}, {
- GET => {
- method => 'get',
- params => sub {
- my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names';
- return { $param => [ $_[0] ] };
- }
- },
- PUT => {
- method => 'update',
- params => sub {
- my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names';
- return { $param => [ $_[0] ] };
- }
- }
- },
- ];
- return $rest_resources;
+ my $rest_resources = [
+ qr{^/product_accessible$},
+ {GET => {method => 'get_accessible_products'}},
+ qr{^/product_enterable$},
+ {GET => {method => 'get_enterable_products'}},
+ qr{^/product_selectable$},
+ {GET => {method => 'get_selectable_products'}},
+ qr{^/product$},
+ {
+ GET => {method => 'get'},
+ POST => {method => 'create', success_code => STATUS_CREATED}
+ },
+ qr{^/product/([^/]+)$},
+ {
+ GET => {
+ method => 'get',
+ params => sub {
+ my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names';
+ return {$param => [$_[0]]};
+ }
+ },
+ PUT => {
+ method => 'update',
+ params => sub {
+ my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names';
+ return {$param => [$_[0]]};
+ }
+ }
+ },
+ ];
+ return $rest_resources;
}
1;
diff --git a/Bugzilla/WebService/Server/REST/Resources/User.pm b/Bugzilla/WebService/Server/REST/Resources/User.pm
index 6185237fb..ab5f78bde 100644
--- a/Bugzilla/WebService/Server/REST/Resources/User.pm
+++ b/Bugzilla/WebService/Server/REST/Resources/User.pm
@@ -15,71 +15,54 @@ use Bugzilla::WebService::Constants;
use Bugzilla::WebService::User;
BEGIN {
- *Bugzilla::WebService::User::rest_resources = \&_rest_resources;
-};
+ *Bugzilla::WebService::User::rest_resources = \&_rest_resources;
+}
sub _rest_resources {
- my $rest_resources = [
- qr{^/user/suggest$}, {
- GET => {
- method => 'suggest',
- },
- },
- qr{^/valid_login$}, {
- GET => {
- method => 'valid_login'
- }
- },
- qr{^/login$}, {
- GET => {
- method => 'login'
- }
- },
- qr{^/logout$}, {
- GET => {
- method => 'logout'
- }
- },
- 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{^/user/mfa/([^/]+)/enroll$}, {
- GET => {
- method => 'mfa_enroll',
- params => sub {
- return { provider => $_[0] };
- }
- },
- },
- qr{^/whoami$}, {
- GET => {
- method => 'whoami'
- }
+ my $rest_resources = [
+ qr{^/user/suggest$},
+ {GET => {method => 'suggest',},},
+ qr{^/valid_login$},
+ {GET => {method => 'valid_login'}},
+ qr{^/login$},
+ {GET => {method => 'login'}},
+ qr{^/logout$},
+ {GET => {method => 'logout'}},
+ 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{^/user/mfa/([^/]+)/enroll$},
+ {
+ GET => {
+ method => 'mfa_enroll',
+ params => sub {
+ return {provider => $_[0]};
}
- ];
- return $rest_resources;
+ },
+ },
+ qr{^/whoami$},
+ {GET => {method => 'whoami'}}
+ ];
+ return $rest_resources;
}
1;
diff --git a/Bugzilla/WebService/Server/XMLRPC.pm b/Bugzilla/WebService/Server/XMLRPC.pm
index 5ad50e91c..ca2d119de 100644
--- a/Bugzilla/WebService/Server/XMLRPC.pm
+++ b/Bugzilla/WebService/Server/XMLRPC.pm
@@ -23,99 +23,102 @@ 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.
- if (defined $value) {
- $value = Bugzilla::WebService::Server->datetime_format_outbound($value);
- $value =~ s/-//g;
- }
- else {
- my ($pkg, $file, $line) = caller;
- my $class = ref $self;
- ERROR("$class->type($type, undef) called from $pkg ($file line $line)");
- }
- }
- elsif ($type eq 'email') {
- $type = 'string';
- if (Bugzilla->params->{'webservice_email_filter'}) {
- $value = email_filter($value);
- }
- }
- return XMLRPC::Data->type($type)->value($value);
- };
-
- # Add support for ETags into XMLRPC WebServices
- *Bugzilla::WebService::bz_etag = sub {
- return Bugzilla::WebService::Server->bz_etag($_[1]);
- };
+ # Allow WebService methods to call XMLRPC::Lite's type method directly
+ *Bugzilla::WebService::type = sub {
+ my ($self, $type, $value) = @_;
+ if ($type eq 'dateTime') {
+
+ # This is the XML-RPC implementation, see the README in Bugzilla/WebService/.
+ # Our "base" implementation is in Bugzilla::WebService::Server.
+ if (defined $value) {
+ $value = Bugzilla::WebService::Server->datetime_format_outbound($value);
+ $value =~ s/-//g;
+ }
+ else {
+ my ($pkg, $file, $line) = caller;
+ my $class = ref $self;
+ ERROR("$class->type($type, undef) called from $pkg ($file line $line)");
+ }
+ }
+ elsif ($type eq 'email') {
+ $type = 'string';
+ if (Bugzilla->params->{'webservice_email_filter'}) {
+ $value = email_filter($value);
+ }
+ }
+ return XMLRPC::Data->type($type)->value($value);
+ };
+
+ # Add support for ETags into XMLRPC WebServices
+ *Bugzilla::WebService::bz_etag = sub {
+ return Bugzilla::WebService::Server->bz_etag($_[1]);
+ };
}
sub initialize {
- my $self = shift;
- my %retval = $self->SUPER::initialize(@_);
- $retval{'serializer'} = Bugzilla::XMLRPC::Serializer->new;
- $retval{'deserializer'} = Bugzilla::XMLRPC::Deserializer->new;
- $retval{'dispatch_with'} = WS_DISPATCH;
- return %retval;
+ my $self = shift;
+ my %retval = $self->SUPER::initialize(@_);
+ $retval{'serializer'} = Bugzilla::XMLRPC::Serializer->new;
+ $retval{'deserializer'} = Bugzilla::XMLRPC::Deserializer->new;
+ $retval{'dispatch_with'} = WS_DISPATCH;
+ return %retval;
}
sub make_response {
- my $self = shift;
- my $cgi = Bugzilla->cgi;
-
- $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;
+
+ $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 });
- }
- $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});
+ }
+ $self->SUPER::handle_login($class, $method, $full_method);
+ return;
}
1;
@@ -124,6 +127,7 @@ sub handle_login {
# and also, in some cases, to more-usefully decode them.
package Bugzilla::XMLRPC::Deserializer;
use strict;
+
# We can't use "use base" because XMLRPC::Serializer doesn't return
# a true value.
use XMLRPC::Lite;
@@ -135,100 +139,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'}});
+ }
+
+ 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';
- # Update the params to allow for several convenience key/values
- # use for authentication
- fix_credentials($params);
+ # Update the params to allow for several convenience key/values
+ # use for authentication
+ fix_credentials($params);
- Bugzilla->input_params($params);
+ Bugzilla->input_params($params);
- return $som;
+ 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;
@@ -240,16 +255,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;
@@ -259,43 +274,46 @@ sub paramsin {
package Bugzilla::XMLRPC::Serializer;
use Scalar::Util qw(blessed);
use strict;
+
# We can't use "use base" 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'){
- $data = _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') {
+ $data = _strip_undefs($data);
+ }
+ return $self->SUPER::envelope($type, $method, $data);
}
# In an XMLRPC response we have to handle hashes of arrays, hashes, scalars,
@@ -303,57 +321,57 @@ 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) = @_;
- if (ref $initial eq "HASH" || (blessed $initial && $initial->isa("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 {
- $initial->{$key} = _strip_undefs($value);
- }
- }
+ my ($initial) = @_;
+ if (ref $initial eq "HASH" || (blessed $initial && $initial->isa("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 {
+ $initial->{$key} = _strip_undefs($value);
+ }
}
- if (ref $initial eq "ARRAY" || (blessed $initial && $initial->isa("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 {
- $initial->[$count] = _strip_undefs($value);
- }
- }
+ }
+ if (ref $initial eq "ARRAY" || (blessed $initial && $initial->isa("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 {
+ $initial->[$count] = _strip_undefs($value);
+ }
}
- return $initial;
+ }
+ return $initial;
}
sub BEGIN {
- no strict 'refs';
- for my $type (qw(double i4 int dateTime)) {
- my $method = 'as_' . $type;
- *$method = sub {
- my ($self, $value) = @_;
- if (!defined($value)) {
- return as_nil();
- }
- else {
- my $super_method = "SUPER::$method";
- return $self->$super_method($value);
- }
- }
- }
+ no strict 'refs';
+ for my $type (qw(double i4 int dateTime)) {
+ my $method = 'as_' . $type;
+ *$method = sub {
+ my ($self, $value) = @_;
+ if (!defined($value)) {
+ return as_nil();
+ }
+ else {
+ my $super_method = "SUPER::$method";
+ return $self->$super_method($value);
+ }
+ }
+ }
}
sub as_nil {
- return ['nil', {}];
+ return ['nil', {}];
}
1;
diff --git a/Bugzilla/WebService/User.pm b/Bugzilla/WebService/User.pm
index c569cf9d8..1e5127f8a 100644
--- a/Bugzilla/WebService/User.pm
+++ b/Bugzilla/WebService/User.pm
@@ -20,44 +20,38 @@ 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);
+ translate params_to_objects);
use Bugzilla::Hook;
use List::Util qw(first);
use Taint::Util qw(untaint);
# 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
- suggest
+ get
+ suggest
);
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 => {
- email => 'login',
- full_name => 'name',
- login_denied_text => 'disabledtext',
-};
+use constant MAPPED_FIELDS =>
+ {email => 'login', full_name => 'name', login_denied_text => 'disabledtext',};
use constant MAPPED_RETURNS => {
- login_name => 'email',
- realname => 'full_name',
- disabledtext => 'login_denied_text',
+ login_name => 'email',
+ realname => 'full_name',
+ disabledtext => 'login_denied_text',
};
##############
@@ -65,38 +59,38 @@ use constant MAPPED_RETURNS => {
##############
sub login {
- my ($self, $params) = @_;
+ my ($self, $params) = @_;
- # Check to see if we are already logged in
- my $user = Bugzilla->user;
- if ($user->id) {
- return $self->_login_to_hash($user);
- }
+ # Check to see if we are already logged in
+ my $user = Bugzilla->user;
+ if ($user->id) {
+ return $self->_login_to_hash($user);
+ }
- # Username and password params are required
- foreach my $param ("login", "password") {
- (defined $params->{$param} || defined $params->{'Bugzilla_' . $param})
- || ThrowCodeError('param_required', { param => $param });
- }
+ # Username and password params are required
+ foreach my $param ("login", "password") {
+ (defined $params->{$param} || defined $params->{'Bugzilla_' . $param})
+ || ThrowCodeError('param_required', {param => $param});
+ }
- $user = Bugzilla->login();
- return $self->_login_to_hash($user);
+ $user = Bugzilla->login();
+ return $self->_login_to_hash($user);
}
sub logout {
- my $self = shift;
- Bugzilla->logout;
+ my $self = shift;
+ Bugzilla->logout;
}
sub valid_login {
- my ($self, $params) = @_;
- defined $params->{login}
- || ThrowCodeError('param_required', { param => 'login' });
- Bugzilla->login();
- if (Bugzilla->user->id && Bugzilla->user->login eq $params->{login}) {
- return $self->type('boolean', 1);
- }
- return $self->type('boolean', 0);
+ my ($self, $params) = @_;
+ defined $params->{login}
+ || ThrowCodeError('param_required', {param => 'login'});
+ Bugzilla->login();
+ if (Bugzilla->user->id && Bugzilla->user->login eq $params->{login}) {
+ return $self->type('boolean', 1);
+ }
+ return $self->type('boolean', 0);
}
#################
@@ -104,102 +98,100 @@ sub valid_login {
#################
sub offer_account_by_email {
- my $self = shift;
- my ($params) = @_;
- my $email = trim($params->{email})
- || ThrowCodeError('param_required', { param => 'email' });
-
- Bugzilla->user->check_account_creation_enabled;
- Bugzilla->user->check_and_send_account_creation_confirmation($email);
- return undef;
+ my $self = shift;
+ my ($params) = @_;
+ my $email = trim($params->{email})
+ || ThrowCodeError('param_required', {param => 'email'});
+
+ Bugzilla->user->check_account_creation_enabled;
+ Bugzilla->user->check_and_send_account_creation_confirmation($email);
+ return undef;
}
sub create {
- my $self = shift;
- my ($params) = @_;
-
- Bugzilla->user->in_group('editusers')
- || ThrowUserError("auth_failure", { group => "editusers",
- action => "add",
- object => "users"});
-
- my $email = trim($params->{email})
- || ThrowCodeError('param_required', { param => 'email' });
- my $realname = trim($params->{full_name});
- my $password = trim($params->{password}) || '*';
-
- my $user = Bugzilla::User->create({
- login_name => $email,
- realname => $realname,
- cryptpassword => $password
- });
-
- return { id => $self->type('int', $user->id) };
-}
+ my $self = shift;
+ my ($params) = @_;
-sub suggest {
- my ($self, $params) = @_;
+ Bugzilla->user->in_group('editusers')
+ || ThrowUserError("auth_failure",
+ {group => "editusers", action => "add", object => "users"});
- Bugzilla->switch_to_shadow_db();
+ my $email = trim($params->{email})
+ || ThrowCodeError('param_required', {param => 'email'});
+ my $realname = trim($params->{full_name});
+ my $password = trim($params->{password}) || '*';
- ThrowCodeError('params_required', { function => 'User.suggest', params => ['match'] })
- unless defined $params->{match};
-
- ThrowUserError('user_access_by_match_denied')
- unless Bugzilla->user->id;
-
- untaint($params->{match});
- my $s = $params->{match};
- trim($s);
- return { users => [] } if length($s) < 3;
+ my $user
+ = Bugzilla::User->create({
+ login_name => $email, realname => $realname, cryptpassword => $password
+ });
- my $dbh = Bugzilla->dbh;
- my @select = ('userid AS id', 'realname AS real_name', 'login_name AS name');
- my $order = 'last_seen_date DESC';
- my $where;
- state $have_mysql = $dbh->isa('Bugzilla::DB::Mysql');
+ return {id => $self->type('int', $user->id)};
+}
- if ($s =~ /^[:@](.+)$/s) {
- $where = $dbh->sql_prefix_match(nickname => $1);
+sub suggest {
+ my ($self, $params) = @_;
+
+ Bugzilla->switch_to_shadow_db();
+
+ ThrowCodeError('params_required',
+ {function => 'User.suggest', params => ['match']})
+ unless defined $params->{match};
+
+ ThrowUserError('user_access_by_match_denied') unless Bugzilla->user->id;
+
+ untaint($params->{match});
+ my $s = $params->{match};
+ trim($s);
+ return {users => []} if length($s) < 3;
+
+ my $dbh = Bugzilla->dbh;
+ my @select = ('userid AS id', 'realname AS real_name', 'login_name AS name');
+ my $order = 'last_seen_date DESC';
+ my $where;
+ state $have_mysql = $dbh->isa('Bugzilla::DB::Mysql');
+
+ if ($s =~ /^[:@](.+)$/s) {
+ $where = $dbh->sql_prefix_match(nickname => $1);
+ }
+ elsif ($s =~ /@/) {
+ $where = $dbh->sql_prefix_match(login_name => $s);
+ }
+ else {
+ if ($have_mysql && ($s =~ /[[:space:]]/ || $s =~ /[^[:ascii:]]/)) {
+ my $match = sprintf 'MATCH(realname) AGAINST (%s) ', $dbh->quote($s);
+ push @select, "$match AS relevance";
+ $order = 'relevance DESC';
+ $where = $match;
}
- elsif ($s =~ /@/) {
- $where = $dbh->sql_prefix_match(login_name => $s);
+ elsif ($have_mysql && $s =~ /^[[:upper:]]/) {
+ my $match = sprintf 'MATCH(realname) AGAINST (%s) ', $dbh->quote($s);
+ $where = join ' OR ', $match, $dbh->sql_prefix_match(nickname => $s),
+ $dbh->sql_prefix_match(login_name => $s);
}
else {
- if ($have_mysql && ( $s =~ /[[:space:]]/ || $s =~ /[^[:ascii:]]/ ) ) {
- my $match = sprintf 'MATCH(realname) AGAINST (%s) ', $dbh->quote($s);
- push @select, "$match AS relevance";
- $order = 'relevance DESC';
- $where = $match;
- }
- elsif ($have_mysql && $s =~ /^[[:upper:]]/) {
- my $match = sprintf 'MATCH(realname) AGAINST (%s) ', $dbh->quote($s);
- $where = join ' OR ',
- $match,
- $dbh->sql_prefix_match( nickname => $s ),
- $dbh->sql_prefix_match( login_name => $s );
- }
- else {
- $where = join ' OR ', $dbh->sql_prefix_match( nickname => $s ), $dbh->sql_prefix_match( login_name => $s );
- }
+ $where = join ' OR ', $dbh->sql_prefix_match(nickname => $s),
+ $dbh->sql_prefix_match(login_name => $s);
}
- $where = "($where) AND is_enabled = 1";
+ }
+ $where = "($where) AND is_enabled = 1";
- my $sql = 'SELECT ' . join(', ', @select) . " FROM profiles WHERE $where ORDER BY $order LIMIT 25";
- my $results = $dbh->selectall_arrayref($sql, { Slice => {} });
+ my $sql
+ = 'SELECT '
+ . join(', ', @select)
+ . " FROM profiles WHERE $where ORDER BY $order LIMIT 25";
+ my $results = $dbh->selectall_arrayref($sql, {Slice => {}});
- my @users = map {
- {
- id => $self->type(int => $_->{id}),
- real_name => $self->type(string => $_->{real_name}),
- name => $self->type(email => $_->{name}),
- }
- } @$results;
+ my @users = map { {
+ id => $self->type(int => $_->{id}),
+ real_name => $self->type(string => $_->{real_name}),
+ name => $self->type(email => $_->{name}),
+ } } @$results;
- Bugzilla::Hook::process('webservice_user_suggest',
- { webservice => $self, params => $params, users => \@users });
+ Bugzilla::Hook::process('webservice_user_suggest',
+ {webservice => $self, params => $params, users => \@users});
- return { users => \@users };
+ return {users => \@users};
}
# function to return user information by passing either user ids or
@@ -207,125 +199,132 @@ sub suggest {
# $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 => $self->type('int', $_->id),
- real_name => $self->type('string', $_->name),
- name => $self->type('email', $_->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 he 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 => $self->type('int', $_->id),
+ real_name => $self->type('string', $_->name),
+ name => $self->type('email', $_->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 he 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;
- if ($params->{limit}) {
- detaint_natural($params->{limit})
- || ThrowCodeError('param_must_be_numeric',
- { function => 'User.match', param => 'limit' });
- $limit = $limit ? min($params->{limit}, $limit) : $params->{limit};
+ }
+
+ # User Matching
+ my $limit;
+ if ($params->{limit}) {
+ detaint_natural($params->{limit})
+ || ThrowCodeError('param_must_be_numeric',
+ {function => '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 $exclude_disabled = $params->{'include_disabled'} ? 0 : 1;
- foreach my $match_string (@{ $params->{'match'} || [] }) {
- my $matched = Bugzilla::User::match($match_string, $limit, $exclude_disabled);
- foreach my $user (@$matched) {
- if (!$unique_users{$user->id}) {
- push(@user_objects, $user);
- $unique_users{$user->id} = $user;
- }
- }
+ }
+
+ my $in_group = $self->_filter_users_by_group(\@user_objects, $params);
+ foreach my $user (@$in_group) {
+ my $user_info = filter $params,
+ {
+ id => $self->type('int', $user->id),
+ real_name => $self->type('string', $user->name),
+ name => $self->type('email', $user->login),
+ email => $self->type('email', $user->email),
+ can_login => $self->type('boolean', $user->is_enabled ? 1 : 0),
+ };
+
+ if (Bugzilla->user->in_group('editusers')) {
+ $user_info->{email_enabled} = $self->type('boolean', $user->email_enabled);
+ $user_info->{login_denied_text} = $self->type('string', $user->disabledtext);
}
- my $in_group = $self->_filter_users_by_group(\@user_objects, $params);
- foreach my $user (@$in_group) {
- my $user_info = filter $params, {
- id => $self->type('int', $user->id),
- real_name => $self->type('string', $user->name),
- name => $self->type('email', $user->login),
- email => $self->type('email', $user->email),
- can_login => $self->type('boolean', $user->is_enabled ? 1 : 0),
- };
-
- if (Bugzilla->user->in_group('editusers')) {
- $user_info->{email_enabled} = $self->type('boolean', $user->email_enabled);
- $user_info->{login_denied_text} = $self->type('string', $user->disabledtext);
- }
-
- if (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, '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}];
+ }
+ }
- 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);
+ }
}
- Bugzilla::Hook::process('webservice_user_get',
- { webservice => $self, params => $params, users => \@users });
+ push(@users, $user_info);
+ }
- return { users => \@users };
+ Bugzilla::Hook::process('webservice_user_get',
+ {webservice => $self, params => $params, users => \@users});
+
+ return {users => \@users};
}
###############
@@ -333,145 +332,144 @@ sub get {
###############
sub update {
- my ($self, $params) = @_;
+ my ($self, $params) = @_;
- my $dbh = Bugzilla->dbh;
+ my $dbh = Bugzilla->dbh;
- my $user = Bugzilla->login(LOGIN_REQUIRED);
+ my $user = Bugzilla->login(LOGIN_REQUIRED);
- # Reject access if there is no sense in continuing.
- $user->in_group('editusers')
- || ThrowUserError("auth_failure", {group => "editusers",
- action => "edit",
- object => "users"});
+ # Reject access if there is no sense in continuing.
+ $user->in_group('editusers')
+ || ThrowUserError("auth_failure",
+ {group => "editusers", action => "edit", object => "users"});
- defined($params->{names}) || defined($params->{ids})
- || ThrowCodeError('params_required',
- { function => 'User.update', params => ['ids', 'names'] });
+ defined($params->{names})
+ || defined($params->{ids})
+ || ThrowCodeError('params_required',
+ {function => 'User.update', params => ['ids', 'names']});
- my $user_objects = params_to_objects($params, 'Bugzilla::User');
+ my $user_objects = params_to_objects($params, 'Bugzilla::User');
- my $values = translate($params, MAPPED_FIELDS);
+ my $values = translate($params, MAPPED_FIELDS);
- # We delete names and ids to keep only new values to set.
- delete $values->{names};
- delete $values->{ids};
+ # We delete names and ids to keep only new values to set.
+ delete $values->{names};
+ delete $values->{ids};
- $dbh->bz_start_transaction();
- foreach my $user (@$user_objects){
- $user->set_all($values);
- }
+ $dbh->bz_start_transaction();
+ foreach my $user (@$user_objects) {
+ $user->set_all($values);
+ }
- my %changes;
- foreach my $user (@$user_objects){
- my $returned_changes = $user->update();
- $changes{$user->id} = translate($returned_changes, MAPPED_RETURNS);
- }
- $dbh->bz_commit_transaction();
-
- my @result;
- foreach my $user (@$user_objects) {
- my %hash = (
- id => $user->id,
- changes => {},
- );
-
- foreach my $field (keys %{ $changes{$user->id} }) {
- my $change = $changes{$user->id}->{$field};
- # We normalize undef to an empty string, so that the API
- # stays consistent for things that can become empty.
- $change->[0] = '' if !defined $change->[0];
- $change->[1] = '' if !defined $change->[1];
- # We also flatten arrays (used by groups and blessed_groups)
- $change->[0] = join(',', @{$change->[0]}) if ref $change->[0];
- $change->[1] = join(',', @{$change->[1]}) if ref $change->[1];
-
- $hash{changes}{$field} = {
- removed => $self->type('string', $change->[0]),
- added => $self->type('string', $change->[1])
- };
- }
+ my %changes;
+ foreach my $user (@$user_objects) {
+ my $returned_changes = $user->update();
+ $changes{$user->id} = translate($returned_changes, MAPPED_RETURNS);
+ }
+ $dbh->bz_commit_transaction();
+
+ 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];
- push(@result, \%hash);
+ # 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])
+ };
}
- 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)};
+ 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);
+ # If no groups are specified, we return all users.
+ return $users if (!$group_ids and !$group_names);
- my $user = Bugzilla->user;
+ my $user = Bugzilla->user;
- my @groups = map { Bugzilla::Group->check({ id => $_ }) }
- @{ $group_ids || [] };
+ my @groups = map { Bugzilla::Group->check({id => $_}) } @{$group_ids || []};
- 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 });
- push(@groups, $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});
+ push(@groups, $group);
}
+ }
- 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->in_group('editusers') || $user->can_bless($group->id));
- push(@filtered_groups, $self->_group_to_hash($group));
- }
+ my @filtered_groups;
+ foreach my $group (@$groups) {
+ next unless ($user->in_group('editusers') || $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),
- url => $self->type('string', $query->url),
- };
-
- return $item;
+ my ($self, $query) = @_;
+ my $item = {
+ id => $self->type('int', $query->id),
+ name => $self->type('string', $query->name),
+ url => $self->type('string', $query->url),
+ };
+
+ return $item;
}
sub _login_to_hash {
- my ($self, $user) = @_;
- my $item = { id => $self->type('int', $user->id) };
- if (my $login_token = $user->{_login_token}) {
- $item->{'token'} = $user->id . "-" . $login_token;
- }
- return $item;
+ my ($self, $user) = @_;
+ my $item = {id => $self->type('int', $user->id)};
+ if (my $login_token = $user->{_login_token}) {
+ $item->{'token'} = $user->id . "-" . $login_token;
+ }
+ return $item;
}
#
@@ -479,27 +477,27 @@ sub _login_to_hash {
#
sub mfa_enroll {
- my ($self, $params) = @_;
- my $provider_name = lc($params->{provider});
+ my ($self, $params) = @_;
+ my $provider_name = lc($params->{provider});
- my $user = Bugzilla->login(LOGIN_REQUIRED);
- $user->set_mfa($provider_name);
- my $provider = $user->mfa_provider // die "Unknown MFA provider\n";
- return $provider->enroll_api();
+ my $user = Bugzilla->login(LOGIN_REQUIRED);
+ $user->set_mfa($provider_name);
+ my $provider = $user->mfa_provider // die "Unknown MFA provider\n";
+ return $provider->enroll_api();
}
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 ),
- mfa_status => $self->type( 'boolean', !!$user->mfa ),
- }
- );
+ 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),
+ mfa_status => $self->type('boolean', !!$user->mfa),
+ }
+ );
}
1;
diff --git a/Bugzilla/WebService/Util.pm b/Bugzilla/WebService/Util.pm
index ce5586911..933d1b5f7 100644
--- a/Bugzilla/WebService/Util.pm
+++ b/Bugzilla/WebService/Util.pm
@@ -29,284 +29,292 @@ use base qw(Exporter);
require Test::Taint if ${^TAINT};
our @EXPORT_OK = qw(
- extract_flags
- filter
- filter_wants
- taint_data
- validate
- translate
- params_to_objects
- fix_credentials
+ extract_flags
+ filter
+ filter_wants
+ taint_data
+ validate
+ translate
+ params_to_objects
+ fix_credentials
);
sub extract_flags {
- my ($flags, $bug, $attachment) = @_;
- my (@new_flags, @old_flags);
+ my ($flags, $bug, $attachment) = @_;
+ my (@new_flags, @old_flags);
- my $flag_types = $attachment ? $attachment->flag_types : $bug->flag_types;
- my $current_flags = $attachment ? $attachment->flags : $bug->flags;
+ my $flag_types = $attachment ? $attachment->flag_types : $bug->flag_types;
+ my $current_flags = $attachment ? $attachment->flags : $bug->flags;
- # Copy the user provided $flags as we may call extract_flags more than
- # once when editing multiple bugs or attachments.
- my $flags_copy = dclone($flags);
+ # Copy the user provided $flags as we may call extract_flags more than
+ # once when editing multiple bugs or attachments.
+ my $flags_copy = dclone($flags);
- foreach my $flag (@$flags_copy) {
- my $id = $flag->{id};
- my $type_id = $flag->{type_id};
+ foreach my $flag (@$flags_copy) {
+ my $id = $flag->{id};
+ my $type_id = $flag->{type_id};
- my $new = delete $flag->{new};
- my $name = delete $flag->{name};
+ my $new = delete $flag->{new};
+ my $name = delete $flag->{name};
- if ($id) {
- my $flag_obj = grep($id == $_->id, @$current_flags);
- $flag_obj || ThrowUserError('object_does_not_exist',
- { class => 'Bugzilla::Flag', id => $id });
- }
- elsif ($type_id) {
- my $type_obj = grep($type_id == $_->id, @$flag_types);
- $type_obj || ThrowUserError('object_does_not_exist',
- { class => 'Bugzilla::FlagType', id => $type_id });
- if (!$new) {
- my @flag_matches = grep($type_id == $_->type->id, @$current_flags);
- @flag_matches > 1 && ThrowUserError('flag_not_unique',
- { value => $type_id });
- if (!@flag_matches) {
- delete $flag->{id};
- }
- else {
- delete $flag->{type_id};
- $flag->{id} = $flag_matches[0]->id;
- }
- }
+ if ($id) {
+ my $flag_obj = grep($id == $_->id, @$current_flags);
+ $flag_obj
+ || ThrowUserError('object_does_not_exist',
+ {class => 'Bugzilla::Flag', id => $id});
+ }
+ elsif ($type_id) {
+ my $type_obj = grep($type_id == $_->id, @$flag_types);
+ $type_obj
+ || ThrowUserError('object_does_not_exist',
+ {class => 'Bugzilla::FlagType', id => $type_id});
+ if (!$new) {
+ my @flag_matches = grep($type_id == $_->type->id, @$current_flags);
+ @flag_matches > 1 && ThrowUserError('flag_not_unique', {value => $type_id});
+ if (!@flag_matches) {
+ delete $flag->{id};
}
- elsif ($name) {
- my @type_matches = grep($name eq $_->name, @$flag_types);
- @type_matches > 1 && ThrowUserError('flag_type_not_unique',
- { value => $name });
- @type_matches || ThrowUserError('object_does_not_exist',
- { class => 'Bugzilla::FlagType', name => $name });
- if ($new) {
- delete $flag->{id};
- $flag->{type_id} = $type_matches[0]->id;
- }
- else {
- my @flag_matches = grep($name eq $_->type->name, @$current_flags);
- @flag_matches > 1 && ThrowUserError('flag_not_unique', { value => $name });
- if (@flag_matches) {
- $flag->{id} = $flag_matches[0]->id;
- }
- else {
- delete $flag->{id};
- $flag->{type_id} = $type_matches[0]->id;
- }
- }
+ else {
+ delete $flag->{type_id};
+ $flag->{id} = $flag_matches[0]->id;
}
-
- if ($flag->{id}) {
- push(@old_flags, $flag);
+ }
+ }
+ elsif ($name) {
+ my @type_matches = grep($name eq $_->name, @$flag_types);
+ @type_matches > 1 && ThrowUserError('flag_type_not_unique', {value => $name});
+ @type_matches
+ || ThrowUserError('object_does_not_exist',
+ {class => 'Bugzilla::FlagType', name => $name});
+ if ($new) {
+ delete $flag->{id};
+ $flag->{type_id} = $type_matches[0]->id;
+ }
+ else {
+ my @flag_matches = grep($name eq $_->type->name, @$current_flags);
+ @flag_matches > 1 && ThrowUserError('flag_not_unique', {value => $name});
+ if (@flag_matches) {
+ $flag->{id} = $flag_matches[0]->id;
}
else {
- push(@new_flags, $flag);
+ delete $flag->{id};
+ $flag->{type_id} = $type_matches[0]->id;
}
+ }
}
- return (\@old_flags, \@new_flags);
+ if ($flag->{id}) {
+ push(@old_flags, $flag);
+ }
+ else {
+ push(@new_flags, $flag);
+ }
+ }
+
+ return (\@old_flags, \@new_flags);
}
sub filter($$;$$) {
- my ($params, $hash, $types, $prefix) = @_;
- my %newhash = %$hash;
+ my ($params, $hash, $types, $prefix) = @_;
+ my %newhash = %$hash;
- foreach my $key (keys %$hash) {
- delete $newhash{$key} if !filter_wants($params, $key, $types, $prefix);
- }
+ foreach my $key (keys %$hash) {
+ delete $newhash{$key} if !filter_wants($params, $key, $types, $prefix);
+ }
- return \%newhash;
+ return \%newhash;
}
sub filter_wants($$;$$) {
- my ($params, $field, $types, $prefix) = @_;
-
- # Since this is operation is resource intensive, we will cache the results
- # This assumes that $params->{*_fields} doesn't change between calls
- my $cache = Bugzilla->request_cache->{filter_wants} ||= {};
- $field = "${prefix}.${field}" if $prefix;
-
- if (exists $cache->{$field}) {
- return $cache->{$field};
- }
-
- # Mimic old behavior if no types provided
- my %field_types = map { $_ => 1 } (ref $types ? @$types : ($types || 'default'));
-
- my %include = map { $_ => 1 } @{ $params->{'include_fields'} || [] };
- my %exclude = map { $_ => 1 } @{ $params->{'exclude_fields'} || [] };
-
- my %include_types;
- my %exclude_types;
-
- # Only return default fields if nothing is specified
- $include_types{default} = 1 if !%include;
-
- # Look for any field types requested
- foreach my $key (keys %include) {
- next if $key !~ /^_(.*)$/;
- $include_types{$1} = 1;
- delete $include{$key};
- }
- foreach my $key (keys %exclude) {
- next if $key !~ /^_(.*)$/;
- $exclude_types{$1} = 1;
- delete $exclude{$key};
- }
-
- # Explicit inclusion/exclusion
- return $cache->{$field} = 0 if $exclude{$field};
- return $cache->{$field} = 1 if $include{$field};
-
- # If the user has asked to include all or exclude all
- return $cache->{$field} = 0 if $exclude_types{'all'};
- return $cache->{$field} = 1 if $include_types{'all'};
-
- # If the user has not asked for any fields specifically or if the user has asked
- # for one or more of the field's types (and not excluded them)
- foreach my $type (keys %field_types) {
- return $cache->{$field} = 0 if $exclude_types{$type};
- return $cache->{$field} = 1 if $include_types{$type};
- }
-
- my $wants = 0;
- if ($prefix) {
- # Include the field if the parent is include (and this one is not excluded)
- $wants = 1 if $include{$prefix};
- }
- else {
- # We want to include this if one of the sub keys is included
- my $key = $field . '.';
- my $len = length($key);
- $wants = 1 if grep { substr($_, 0, $len) eq $key } keys %include;
- }
-
- return $cache->{$field} = $wants;
+ my ($params, $field, $types, $prefix) = @_;
+
+ # Since this is operation is resource intensive, we will cache the results
+ # This assumes that $params->{*_fields} doesn't change between calls
+ my $cache = Bugzilla->request_cache->{filter_wants} ||= {};
+ $field = "${prefix}.${field}" if $prefix;
+
+ if (exists $cache->{$field}) {
+ return $cache->{$field};
+ }
+
+ # Mimic old behavior if no types provided
+ my %field_types
+ = map { $_ => 1 } (ref $types ? @$types : ($types || 'default'));
+
+ my %include = map { $_ => 1 } @{$params->{'include_fields'} || []};
+ my %exclude = map { $_ => 1 } @{$params->{'exclude_fields'} || []};
+
+ my %include_types;
+ my %exclude_types;
+
+ # Only return default fields if nothing is specified
+ $include_types{default} = 1 if !%include;
+
+ # Look for any field types requested
+ foreach my $key (keys %include) {
+ next if $key !~ /^_(.*)$/;
+ $include_types{$1} = 1;
+ delete $include{$key};
+ }
+ foreach my $key (keys %exclude) {
+ next if $key !~ /^_(.*)$/;
+ $exclude_types{$1} = 1;
+ delete $exclude{$key};
+ }
+
+ # Explicit inclusion/exclusion
+ return $cache->{$field} = 0 if $exclude{$field};
+ return $cache->{$field} = 1 if $include{$field};
+
+ # If the user has asked to include all or exclude all
+ return $cache->{$field} = 0 if $exclude_types{'all'};
+ return $cache->{$field} = 1 if $include_types{'all'};
+
+ # If the user has not asked for any fields specifically or if the user has asked
+ # for one or more of the field's types (and not excluded them)
+ foreach my $type (keys %field_types) {
+ return $cache->{$field} = 0 if $exclude_types{$type};
+ return $cache->{$field} = 1 if $include_types{$type};
+ }
+
+ my $wants = 0;
+ if ($prefix) {
+
+ # Include the field if the parent is include (and this one is not excluded)
+ $wants = 1 if $include{$prefix};
+ }
+ else {
+ # We want to include this if one of the sub keys is included
+ my $key = $field . '.';
+ my $len = length($key);
+ $wants = 1 if grep { substr($_, 0, $len) eq $key } keys %include;
+ }
+
+ return $cache->{$field} = $wants;
}
sub taint_data {
- my @params = @_;
- return if !@params;
- # Though this is a private function, it hasn't changed since 2004 and
- # should be safe to use, and prevents us from having to write it ourselves
- # or require another module to do it.
- if (${^TAINT}) {
- 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.
+ if (${^TAINT}) {
+ 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+, 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+, so we delete any key that doesn't match that.
+ if ($key !~ /^[\w\.\-]+$/) {
+ delete $item->{$key};
+ }
}
- return @_;
+ }
+ return @_;
}
-sub validate {
- my ($self, $params, @keys) = @_;
- my $cache_key = join('|', (caller(1))[3], sort @keys);
- # Type->of() is the same as Type[], used here because it is easier
- # to chain with plus_coercions.
- state $array_of_nonrefs = ArrayRef->of(Maybe[Value])->plus_coercions(
- Maybe[Value], q{ [ $_ ] },
- );
- state $type_cache = {};
- my $params_type = $type_cache->{$cache_key} //= do {
- my %fields = map { $_ => Optional[$array_of_nonrefs] } @keys;
- Maybe[ Dict[%fields, slurpy Any] ];
- };
-
- # 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);
-
- # If @keys is not empty then we convert any named
- # parameters that have scalar values to arrayrefs
- # that match.
- $params = $params_type->coerce($params);
- if (my $type_error = $params_type->validate($params)) {
- FATAL("validate() found type error: $type_error");
- ThrowUserError('invalid_params', { type_error => $type_error } ) if $type_error;
- }
-
- return ($self, $params);
+sub validate {
+ my ($self, $params, @keys) = @_;
+ my $cache_key = join('|', (caller(1))[3], sort @keys);
+
+ # Type->of() is the same as Type[], used here because it is easier
+ # to chain with plus_coercions.
+ state $array_of_nonrefs
+ = ArrayRef->of(Maybe [Value])->plus_coercions(Maybe [Value], q{ [ $_ ] },);
+ state $type_cache = {};
+ my $params_type = $type_cache->{$cache_key} //= do {
+ my %fields = map { $_ => Optional [$array_of_nonrefs] } @keys;
+ Maybe [Dict [%fields, slurpy Any]];
+ };
+
+ # 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);
+
+ # If @keys is not empty then we convert any named
+ # parameters that have scalar values to arrayrefs
+ # that match.
+ $params = $params_type->coerce($params);
+ if (my $type_error = $params_type->validate($params)) {
+ FATAL("validate() found type error: $type_error");
+ ThrowUserError('invalid_params', {type_error => $type_error}) if $type_error;
+ }
+
+ 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__
diff --git a/Bugzilla/Whine.pm b/Bugzilla/Whine.pm
index c4301b4f6..67fc6cc5d 100644
--- a/Bugzilla/Whine.pm
+++ b/Bugzilla/Whine.pm
@@ -27,11 +27,11 @@ use Bugzilla::Whine::Query;
use constant DB_TABLE => 'whine_events';
use constant DB_COLUMNS => qw(
- id
- owner_userid
- subject
- body
- mailifnobugs
+ id
+ owner_userid
+ subject
+ body
+ mailifnobugs
);
use constant LIST_ORDER => 'id';
@@ -39,15 +39,15 @@ use constant LIST_ORDER => 'id';
####################
# Simple Accessors #
####################
-sub subject { return $_[0]->{'subject'}; }
-sub body { return $_[0]->{'body'}; }
+sub subject { return $_[0]->{'subject'}; }
+sub body { return $_[0]->{'body'}; }
sub mail_if_no_bugs { return $_[0]->{'mailifnobugs'}; }
sub user {
- my ($self) = @_;
- return $self->{user} if defined $self->{user};
- $self->{user} = new Bugzilla::User($self->{'owner_userid'});
- return $self->{user};
+ my ($self) = @_;
+ return $self->{user} if defined $self->{user};
+ $self->{user} = new Bugzilla::User($self->{'owner_userid'});
+ return $self->{user};
}
1;
diff --git a/Bugzilla/Whine/Query.pm b/Bugzilla/Whine/Query.pm
index 6ea91cc51..29e1e0b51 100644
--- a/Bugzilla/Whine/Query.pm
+++ b/Bugzilla/Whine/Query.pm
@@ -23,12 +23,12 @@ use Bugzilla::Search::Saved;
use constant DB_TABLE => 'whine_queries';
use constant DB_COLUMNS => qw(
- id
- eventid
- query_name
- sortkey
- onemailperbug
- title
+ id
+ eventid
+ query_name
+ sortkey
+ onemailperbug
+ title
);
use constant NAME_FIELD => 'id';
@@ -37,11 +37,11 @@ use constant LIST_ORDER => 'sortkey';
####################
# Simple Accessors #
####################
-sub eventid { return $_[0]->{'eventid'}; }
-sub sortkey { return $_[0]->{'sortkey'}; }
+sub eventid { return $_[0]->{'eventid'}; }
+sub sortkey { return $_[0]->{'sortkey'}; }
sub one_email_per_bug { return $_[0]->{'onemailperbug'}; }
-sub title { return $_[0]->{'title'}; }
-sub name { return $_[0]->{'query_name'}; }
+sub title { return $_[0]->{'title'}; }
+sub name { return $_[0]->{'query_name'}; }
1;
diff --git a/Bugzilla/Whine/Schedule.pm b/Bugzilla/Whine/Schedule.pm
index 017b744e5..e6e8e67d9 100644
--- a/Bugzilla/Whine/Schedule.pm
+++ b/Bugzilla/Whine/Schedule.pm
@@ -22,22 +22,22 @@ use Bugzilla::Constants;
use constant DB_TABLE => 'whine_schedules';
use constant DB_COLUMNS => qw(
- id
- eventid
- run_day
- run_time
- run_next
- mailto
- mailto_type
+ id
+ eventid
+ run_day
+ run_time
+ run_next
+ mailto
+ mailto_type
);
use constant UPDATE_COLUMNS => qw(
- eventid
- run_day
- run_time
- run_next
- mailto
- mailto_type
+ eventid
+ run_day
+ run_time
+ run_next
+ mailto
+ mailto_type
);
use constant NAME_FIELD => 'id';
use constant LIST_ORDER => 'id';
@@ -45,36 +45,38 @@ use constant LIST_ORDER => 'id';
####################
# Simple Accessors #
####################
-sub eventid { return $_[0]->{'eventid'}; }
-sub run_day { return $_[0]->{'run_day'}; }
-sub run_time { return $_[0]->{'run_time'}; }
+sub eventid { return $_[0]->{'eventid'}; }
+sub run_day { return $_[0]->{'run_day'}; }
+sub run_time { return $_[0]->{'run_time'}; }
sub mailto_is_group { return $_[0]->{'mailto_type'}; }
sub mailto {
- my $self = shift;
-
- return $self->{mailto_object} if exists $self->{mailto_object};
- my $id = $self->{'mailto'};
-
- if ($self->mailto_is_group) {
- $self->{mailto_object} = Bugzilla::Group->new($id);
- } else {
- $self->{mailto_object} = Bugzilla::User->new($id);
- }
- return $self->{mailto_object};
+ my $self = shift;
+
+ return $self->{mailto_object} if exists $self->{mailto_object};
+ my $id = $self->{'mailto'};
+
+ if ($self->mailto_is_group) {
+ $self->{mailto_object} = Bugzilla::Group->new($id);
+ }
+ else {
+ $self->{mailto_object} = Bugzilla::User->new($id);
+ }
+ return $self->{mailto_object};
}
sub mailto_users {
- my $self = shift;
- return $self->{mailto_users} if exists $self->{mailto_users};
- my $object = $self->mailto;
-
- if ($self->mailto_is_group) {
- $self->{mailto_users} = $object->members_non_inherited if $object->is_active;
- } else {
- $self->{mailto_users} = $object;
- }
- return $self->{mailto_users};
+ 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;