From: Perl Tidy Bugzilla::Chart object:";
- print html_quote(Data::Dumper::Dumper($self));
- print "
";
+ my $self = shift;
+
+ # Make sure we've read in our data
+ my $data = $self->data;
+
+ require Data::Dumper;
+ say "Bugzilla::Chart object:";
+ print html_quote(Data::Dumper::Dumper($self));
+ print "
";
}
1;
diff --git a/Bugzilla/Classification.pm b/Bugzilla/Classification.pm
index 7f62e1ffab..85de92b1a2 100644
--- a/Bugzilla/Classification.pm
+++ b/Bugzilla/Classification.pm
@@ -26,26 +26,26 @@ use parent qw(Bugzilla::Field::ChoiceInterface Bugzilla::Object Exporter);
use constant IS_CONFIG => 1;
-use constant DB_TABLE => 'classifications';
+use constant DB_TABLE => 'classifications';
use constant LIST_ORDER => 'sortkey, name';
use constant DB_COLUMNS => qw(
- id
- name
- description
- sortkey
+ id
+ name
+ description
+ sortkey
);
use constant UPDATE_COLUMNS => qw(
- name
- description
- sortkey
+ name
+ description
+ sortkey
);
use constant VALIDATORS => {
- name => \&_check_name,
- description => \&_check_description,
- sortkey => \&_check_sortkey,
+ name => \&_check_name,
+ description => \&_check_description,
+ sortkey => \&_check_sortkey,
};
###############################
@@ -53,29 +53,31 @@ use constant VALIDATORS => {
###############################
sub remove_from_db {
- my $self = shift;
- my $dbh = Bugzilla->dbh;
+ my $self = shift;
+ my $dbh = Bugzilla->dbh;
- ThrowUserError("classification_not_deletable") if ($self->id == 1);
+ ThrowUserError("classification_not_deletable") if ($self->id == 1);
- $dbh->bz_start_transaction();
+ $dbh->bz_start_transaction();
- # Reclassify products to the default classification, if needed.
- my $product_ids = $dbh->selectcol_arrayref(
- 'SELECT id FROM products WHERE classification_id = ?', undef, $self->id);
-
- if (@$product_ids) {
- $dbh->do('UPDATE products SET classification_id = 1 WHERE '
- . $dbh->sql_in('id', $product_ids));
- foreach my $id (@$product_ids) {
- Bugzilla->memcached->clear({ table => 'products', id => $id });
- }
- Bugzilla->memcached->clear_config();
+ # Reclassify products to the default classification, if needed.
+ my $product_ids
+ = $dbh->selectcol_arrayref(
+ 'SELECT id FROM products WHERE classification_id = ?',
+ undef, $self->id);
+
+ if (@$product_ids) {
+ $dbh->do('UPDATE products SET classification_id = 1 WHERE '
+ . $dbh->sql_in('id', $product_ids));
+ foreach my $id (@$product_ids) {
+ Bugzilla->memcached->clear({table => 'products', id => $id});
}
+ Bugzilla->memcached->clear_config();
+ }
- $self->SUPER::remove_from_db();
+ $self->SUPER::remove_from_db();
- $dbh->bz_commit_transaction();
+ $dbh->bz_commit_transaction();
}
@@ -84,38 +86,41 @@ sub remove_from_db {
###############################
sub _check_name {
- my ($invocant, $name) = @_;
-
- $name = trim($name);
- $name || ThrowUserError('classification_not_specified');
-
- if (length($name) > MAX_CLASSIFICATION_SIZE) {
- ThrowUserError('classification_name_too_long', {'name' => $name});
- }
-
- my $classification = new Bugzilla::Classification({name => $name});
- if ($classification && (!ref $invocant || $classification->id != $invocant->id)) {
- ThrowUserError("classification_already_exists", { name => $classification->name });
- }
- return $name;
+ my ($invocant, $name) = @_;
+
+ $name = trim($name);
+ $name || ThrowUserError('classification_not_specified');
+
+ if (length($name) > MAX_CLASSIFICATION_SIZE) {
+ ThrowUserError('classification_name_too_long', {'name' => $name});
+ }
+
+ my $classification = new Bugzilla::Classification({name => $name});
+ if ($classification && (!ref $invocant || $classification->id != $invocant->id))
+ {
+ ThrowUserError("classification_already_exists",
+ {name => $classification->name});
+ }
+ return $name;
}
sub _check_description {
- my ($invocant, $description) = @_;
+ my ($invocant, $description) = @_;
- $description = trim($description || '');
- return $description;
+ $description = trim($description || '');
+ return $description;
}
sub _check_sortkey {
- my ($invocant, $sortkey) = @_;
-
- $sortkey ||= 0;
- my $stored_sortkey = $sortkey;
- if (!detaint_natural($sortkey) || $sortkey > MAX_SMALLINT) {
- ThrowUserError('classification_invalid_sortkey', { 'sortkey' => $stored_sortkey });
- }
- return $sortkey;
+ my ($invocant, $sortkey) = @_;
+
+ $sortkey ||= 0;
+ my $stored_sortkey = $sortkey;
+ if (!detaint_natural($sortkey) || $sortkey > MAX_SMALLINT) {
+ ThrowUserError('classification_invalid_sortkey',
+ {'sortkey' => $stored_sortkey});
+ }
+ return $sortkey;
}
#####################################
@@ -124,41 +129,45 @@ sub _check_sortkey {
use constant FIELD_NAME => 'classification';
use constant is_default => 0;
-use constant is_active => 1;
+use constant is_active => 1;
###############################
#### Methods ####
###############################
-sub set_name { $_[0]->set('name', $_[1]); }
+sub set_name { $_[0]->set('name', $_[1]); }
sub set_description { $_[0]->set('description', $_[1]); }
-sub set_sortkey { $_[0]->set('sortkey', $_[1]); }
+sub set_sortkey { $_[0]->set('sortkey', $_[1]); }
sub product_count {
- my $self = shift;
- my $dbh = Bugzilla->dbh;
+ my $self = shift;
+ my $dbh = Bugzilla->dbh;
- if (!defined $self->{'product_count'}) {
- $self->{'product_count'} = $dbh->selectrow_array(q{
+ if (!defined $self->{'product_count'}) {
+ $self->{'product_count'} = $dbh->selectrow_array(
+ q{
SELECT COUNT(*) FROM products
- WHERE classification_id = ?}, undef, $self->id) || 0;
- }
- return $self->{'product_count'};
+ WHERE classification_id = ?}, undef, $self->id
+ ) || 0;
+ }
+ return $self->{'product_count'};
}
sub products {
- my $self = shift;
- my $dbh = Bugzilla->dbh;
+ my $self = shift;
+ my $dbh = Bugzilla->dbh;
- if (!$self->{'products'}) {
- my $product_ids = $dbh->selectcol_arrayref(q{
+ if (!$self->{'products'}) {
+ my $product_ids = $dbh->selectcol_arrayref(
+ q{
SELECT id FROM products
WHERE classification_id = ?
- ORDER BY name}, undef, $self->id);
+ ORDER BY name}, undef, $self->id
+ );
- $self->{'products'} = Bugzilla::Product->new_from_list($product_ids);
- }
- return $self->{'products'};
+ $self->{'products'} = Bugzilla::Product->new_from_list($product_ids);
+ }
+ return $self->{'products'};
}
###############################
@@ -166,7 +175,7 @@ sub products {
###############################
sub description { return $_[0]->{'description'}; }
-sub sortkey { return $_[0]->{'sortkey'}; }
+sub sortkey { return $_[0]->{'sortkey'}; }
###############################
@@ -177,27 +186,32 @@ sub sortkey { return $_[0]->{'sortkey'}; }
# in global/choose-product.html.tmpl.
sub sort_products_by_classification {
- my $products = shift;
- my $list;
-
- if (Bugzilla->params->{'useclassification'}) {
- my $class = {};
- # Get all classifications with at least one product.
- foreach my $product (@$products) {
- $class->{$product->classification_id}->{'object'} ||=
- new Bugzilla::Classification($product->classification_id);
- # Nice way to group products per classification, without querying
- # the DB again.
- push(@{$class->{$product->classification_id}->{'products'}}, $product);
- }
- $list = [sort {$a->{'object'}->sortkey <=> $b->{'object'}->sortkey
- || lc($a->{'object'}->name) cmp lc($b->{'object'}->name)}
- (values %$class)];
- }
- else {
- $list = [{object => undef, products => $products}];
+ my $products = shift;
+ my $list;
+
+ if (Bugzilla->params->{'useclassification'}) {
+ my $class = {};
+
+ # Get all classifications with at least one product.
+ foreach my $product (@$products) {
+ $class->{$product->classification_id}->{'object'}
+ ||= new Bugzilla::Classification($product->classification_id);
+
+ # Nice way to group products per classification, without querying
+ # the DB again.
+ push(@{$class->{$product->classification_id}->{'products'}}, $product);
}
- return $list;
+ $list = [
+ sort {
+ $a->{'object'}->sortkey <=> $b->{'object'}->sortkey
+ || lc($a->{'object'}->name) cmp lc($b->{'object'}->name)
+ } (values %$class)
+ ];
+ }
+ else {
+ $list = [{object => undef, products => $products}];
+ }
+ return $list;
}
1;
diff --git a/Bugzilla/Comment.pm b/Bugzilla/Comment.pm
index 2f063934a8..a4c023c881 100644
--- a/Bugzilla/Comment.pm
+++ b/Bugzilla/Comment.pm
@@ -33,49 +33,50 @@ use constant AUDIT_CREATES => 0;
use constant AUDIT_UPDATES => 0;
use constant DB_COLUMNS => qw(
- comment_id
- bug_id
- who
- bug_when
- work_time
- thetext
- isprivate
- already_wrapped
- type
- extra_data
- is_markdown
+ comment_id
+ bug_id
+ who
+ bug_when
+ work_time
+ thetext
+ isprivate
+ already_wrapped
+ type
+ extra_data
+ is_markdown
);
use constant UPDATE_COLUMNS => qw(
- isprivate
- type
- extra_data
+ isprivate
+ type
+ extra_data
);
use constant DB_TABLE => 'longdescs';
use constant ID_FIELD => 'comment_id';
+
# In some rare cases, two comments can have identical timestamps. If
# this happens, we want to be sure that the comment added later shows up
# later in the sequence.
use constant LIST_ORDER => 'bug_when, comment_id';
use constant VALIDATORS => {
- bug_id => \&_check_bug_id,
- who => \&_check_who,
- bug_when => \&_check_bug_when,
- work_time => \&_check_work_time,
- thetext => \&_check_thetext,
- isprivate => \&_check_isprivate,
- is_markdown => \&Bugzilla::Object::check_boolean,
- extra_data => \&_check_extra_data,
- type => \&_check_type,
+ bug_id => \&_check_bug_id,
+ who => \&_check_who,
+ bug_when => \&_check_bug_when,
+ work_time => \&_check_work_time,
+ thetext => \&_check_thetext,
+ isprivate => \&_check_isprivate,
+ is_markdown => \&Bugzilla::Object::check_boolean,
+ extra_data => \&_check_extra_data,
+ type => \&_check_type,
};
use constant VALIDATOR_DEPENDENCIES => {
- extra_data => ['type'],
- bug_id => ['who'],
- work_time => ['who', 'bug_id'],
- isprivate => ['who'],
+ extra_data => ['type'],
+ bug_id => ['who'],
+ work_time => ['who', 'bug_id'],
+ isprivate => ['who'],
};
#########################
@@ -83,95 +84,100 @@ use constant VALIDATOR_DEPENDENCIES => {
#########################
sub update {
- my $self = shift;
- my ($changes, $old_comment) = $self->SUPER::update(@_);
-
- if (exists $changes->{'thetext'} || exists $changes->{'isprivate'}) {
- $self->bug->_sync_fulltext( update_comments => 1);
- }
-
- my @old_tags = @{ $old_comment->tags };
- my @new_tags = @{ $self->tags };
- my ($removed_tags, $added_tags) = diff_arrays(\@old_tags, \@new_tags);
-
- if (@$removed_tags || @$added_tags) {
- my $dbh = Bugzilla->dbh;
- my $when = $dbh->selectrow_array("SELECT LOCALTIMESTAMP(0)");
- my $sth_delete = $dbh->prepare(
- "DELETE FROM longdescs_tags WHERE comment_id = ? AND tag = ?"
- );
- my $sth_insert = $dbh->prepare(
- "INSERT INTO longdescs_tags(comment_id, tag) VALUES (?, ?)"
- );
- my $sth_activity = $dbh->prepare(
- "INSERT INTO longdescs_tags_activity
+ my $self = shift;
+ my ($changes, $old_comment) = $self->SUPER::update(@_);
+
+ if (exists $changes->{'thetext'} || exists $changes->{'isprivate'}) {
+ $self->bug->_sync_fulltext(update_comments => 1);
+ }
+
+ my @old_tags = @{$old_comment->tags};
+ my @new_tags = @{$self->tags};
+ my ($removed_tags, $added_tags) = diff_arrays(\@old_tags, \@new_tags);
+
+ if (@$removed_tags || @$added_tags) {
+ my $dbh = Bugzilla->dbh;
+ my $when = $dbh->selectrow_array("SELECT LOCALTIMESTAMP(0)");
+ my $sth_delete = $dbh->prepare(
+ "DELETE FROM longdescs_tags WHERE comment_id = ? AND tag = ?");
+ my $sth_insert
+ = $dbh->prepare("INSERT INTO longdescs_tags(comment_id, tag) VALUES (?, ?)");
+ my $sth_activity = $dbh->prepare(
+ "INSERT INTO longdescs_tags_activity
(bug_id, comment_id, who, bug_when, added, removed)
VALUES (?, ?, ?, ?, ?, ?)"
- );
-
- foreach my $tag (@$removed_tags) {
- my $weighted = Bugzilla::Comment::TagWeights->new({ name => $tag });
- if ($weighted) {
- if ($weighted->weight == 1) {
- $weighted->remove_from_db();
- } else {
- $weighted->set_weight($weighted->weight - 1);
- $weighted->update();
- }
- }
- trick_taint($tag);
- $sth_delete->execute($self->id, $tag);
- $sth_activity->execute(
- $self->bug_id, $self->id, Bugzilla->user->id, $when, '', $tag);
- }
+ );
- foreach my $tag (@$added_tags) {
- my $weighted = Bugzilla::Comment::TagWeights->new({ name => $tag });
- if ($weighted) {
- $weighted->set_weight($weighted->weight + 1);
- $weighted->update();
- } else {
- Bugzilla::Comment::TagWeights->create({ tag => $tag, weight => 1 });
- }
- trick_taint($tag);
- $sth_insert->execute($self->id, $tag);
- $sth_activity->execute(
- $self->bug_id, $self->id, Bugzilla->user->id, $when, $tag, '');
+ foreach my $tag (@$removed_tags) {
+ my $weighted = Bugzilla::Comment::TagWeights->new({name => $tag});
+ if ($weighted) {
+ if ($weighted->weight == 1) {
+ $weighted->remove_from_db();
}
+ else {
+ $weighted->set_weight($weighted->weight - 1);
+ $weighted->update();
+ }
+ }
+ trick_taint($tag);
+ $sth_delete->execute($self->id, $tag);
+ $sth_activity->execute($self->bug_id, $self->id, Bugzilla->user->id, $when, '',
+ $tag);
}
- return $changes;
+ foreach my $tag (@$added_tags) {
+ my $weighted = Bugzilla::Comment::TagWeights->new({name => $tag});
+ if ($weighted) {
+ $weighted->set_weight($weighted->weight + 1);
+ $weighted->update();
+ }
+ else {
+ Bugzilla::Comment::TagWeights->create({tag => $tag, weight => 1});
+ }
+ trick_taint($tag);
+ $sth_insert->execute($self->id, $tag);
+ $sth_activity->execute($self->bug_id, $self->id, Bugzilla->user->id, $when,
+ $tag, '');
+ }
+ }
+
+ return $changes;
}
# Speeds up displays of comment lists by loading all author objects and tags at
# once for a whole list.
sub preload {
- my ($class, $comments) = @_;
- # Author
- my %user_ids = map { $_->{who} => 1 } @$comments;
- my $users = Bugzilla::User->new_from_list([keys %user_ids]);
- my %user_map = map { $_->id => $_ } @$users;
- foreach my $comment (@$comments) {
- $comment->{author} = $user_map{$comment->{who}};
- }
- # Tags
- if (Bugzilla->params->{'comment_taggers_group'}) {
- my $dbh = Bugzilla->dbh;
- my @comment_ids = map { $_->id } @$comments;
- my %comment_map = map { $_->id => $_ } @$comments;
- my $rows = $dbh->selectall_arrayref(
- "SELECT comment_id, " . $dbh->sql_group_concat('tag', "','") . "
+ my ($class, $comments) = @_;
+
+ # Author
+ my %user_ids = map { $_->{who} => 1 } @$comments;
+ my $users = Bugzilla::User->new_from_list([keys %user_ids]);
+ my %user_map = map { $_->id => $_ } @$users;
+ foreach my $comment (@$comments) {
+ $comment->{author} = $user_map{$comment->{who}};
+ }
+
+ # Tags
+ if (Bugzilla->params->{'comment_taggers_group'}) {
+ my $dbh = Bugzilla->dbh;
+ my @comment_ids = map { $_->id } @$comments;
+ my %comment_map = map { $_->id => $_ } @$comments;
+ my $rows = $dbh->selectall_arrayref(
+ "SELECT comment_id, " . $dbh->sql_group_concat('tag', "','") . "
FROM longdescs_tags
- WHERE " . $dbh->sql_in('comment_id', \@comment_ids) . ' ' .
- $dbh->sql_group_by('comment_id'));
- foreach my $row (@$rows) {
- $comment_map{$row->[0]}->{tags} = [ split(/,/, $row->[1]) ];
- }
- # Also sets the 'tags' attribute for comments which have no entry
- # in the longdescs_tags table, else calling $comment->tags will
- # trigger another SQL query again.
- $comment_map{$_}->{tags} ||= [] foreach @comment_ids;
+ WHERE "
+ . $dbh->sql_in('comment_id', \@comment_ids) . ' '
+ . $dbh->sql_group_by('comment_id')
+ );
+ foreach my $row (@$rows) {
+ $comment_map{$row->[0]}->{tags} = [split(/,/, $row->[1])];
}
+
+ # Also sets the 'tags' attribute for comments which have no entry
+ # in the longdescs_tags table, else calling $comment->tags will
+ # trigger another SQL query again.
+ $comment_map{$_}->{tags} ||= [] foreach @comment_ids;
+ }
}
###############################
@@ -179,138 +185,140 @@ sub preload {
###############################
sub already_wrapped { return $_[0]->{'already_wrapped'}; }
-sub body { return $_[0]->{'thetext'}; }
-sub bug_id { return $_[0]->{'bug_id'}; }
-sub creation_ts { return $_[0]->{'bug_when'}; }
-sub is_private { return $_[0]->{'isprivate'}; }
-sub is_markdown { return $_[0]->{'is_markdown'}; }
-sub work_time {
- # Work time is returned as a string (see bug 607909)
- return 0 if $_[0]->{'work_time'} + 0 == 0;
- return $_[0]->{'work_time'};
+sub body { return $_[0]->{'thetext'}; }
+sub bug_id { return $_[0]->{'bug_id'}; }
+sub creation_ts { return $_[0]->{'bug_when'}; }
+sub is_private { return $_[0]->{'isprivate'}; }
+sub is_markdown { return $_[0]->{'is_markdown'}; }
+
+sub work_time {
+
+ # Work time is returned as a string (see bug 607909)
+ return 0 if $_[0]->{'work_time'} + 0 == 0;
+ return $_[0]->{'work_time'};
}
-sub type { return $_[0]->{'type'}; }
-sub extra_data { return $_[0]->{'extra_data'} }
+sub type { return $_[0]->{'type'}; }
+sub extra_data { return $_[0]->{'extra_data'} }
sub tags {
- my ($self) = @_;
- state $comment_taggers_group = Bugzilla->params->{'comment_taggers_group'};
- return [] unless $comment_taggers_group;
- $self->{'tags'} ||= Bugzilla->dbh->selectcol_arrayref(
- "SELECT tag
+ my ($self) = @_;
+ state $comment_taggers_group = Bugzilla->params->{'comment_taggers_group'};
+ return [] unless $comment_taggers_group;
+ $self->{'tags'} ||= Bugzilla->dbh->selectcol_arrayref(
+ "SELECT tag
FROM longdescs_tags
WHERE comment_id = ?
- ORDER BY tag",
- undef, $self->id);
- return $self->{'tags'};
+ ORDER BY tag", undef, $self->id
+ );
+ return $self->{'tags'};
}
sub collapsed {
- my ($self) = @_;
- state $comment_taggers_group = Bugzilla->params->{'comment_taggers_group'};
- return 0 unless $comment_taggers_group;
- return $self->{collapsed} if exists $self->{collapsed};
-
- state $collapsed_comment_tags = Bugzilla->params->{'collapsed_comment_tags'};
- $self->{collapsed} = 0;
- Bugzilla->request_cache->{comment_tags_collapsed}
- ||= [ split(/\s*,\s*/, $collapsed_comment_tags) ];
- my @collapsed_tags = @{ Bugzilla->request_cache->{comment_tags_collapsed} };
- foreach my $my_tag (@{ $self->tags }) {
- $my_tag = lc($my_tag);
- foreach my $collapsed_tag (@collapsed_tags) {
- if ($my_tag eq lc($collapsed_tag)) {
- $self->{collapsed} = 1;
- last;
- }
- }
- last if $self->{collapsed};
+ my ($self) = @_;
+ state $comment_taggers_group = Bugzilla->params->{'comment_taggers_group'};
+ return 0 unless $comment_taggers_group;
+ return $self->{collapsed} if exists $self->{collapsed};
+
+ state $collapsed_comment_tags = Bugzilla->params->{'collapsed_comment_tags'};
+ $self->{collapsed} = 0;
+ Bugzilla->request_cache->{comment_tags_collapsed}
+ ||= [split(/\s*,\s*/, $collapsed_comment_tags)];
+ my @collapsed_tags = @{Bugzilla->request_cache->{comment_tags_collapsed}};
+ foreach my $my_tag (@{$self->tags}) {
+ $my_tag = lc($my_tag);
+ foreach my $collapsed_tag (@collapsed_tags) {
+ if ($my_tag eq lc($collapsed_tag)) {
+ $self->{collapsed} = 1;
+ last;
+ }
}
- return $self->{collapsed};
+ last if $self->{collapsed};
+ }
+ return $self->{collapsed};
}
sub bug {
- my $self = shift;
- require Bugzilla::Bug;
+ my $self = shift;
+ require Bugzilla::Bug;
- # note $bug exists as a strong reference to keep $self->{bug} defined until the end of this method
- my $bug = $self->{bug} ||= new Bugzilla::Bug($self->bug_id);
- weaken($self->{bug}) unless isweak($self->{bug});
- return $bug;
+# note $bug exists as a strong reference to keep $self->{bug} defined until the end of this method
+ my $bug = $self->{bug} ||= new Bugzilla::Bug($self->bug_id);
+ weaken($self->{bug}) unless isweak($self->{bug});
+ return $bug;
}
sub is_about_attachment {
- my ($self) = @_;
- return 1 if ($self->type == CMT_ATTACHMENT_CREATED
- or $self->type == CMT_ATTACHMENT_UPDATED);
- return 0;
+ my ($self) = @_;
+ return 1
+ if ($self->type == CMT_ATTACHMENT_CREATED
+ or $self->type == CMT_ATTACHMENT_UPDATED);
+ return 0;
}
sub attachment {
- my ($self) = @_;
- return undef if not $self->is_about_attachment;
- $self->{attachment} ||=
- new Bugzilla::Attachment({ id => $self->extra_data, cache => 1 });
- return $self->{attachment};
+ my ($self) = @_;
+ return undef if not $self->is_about_attachment;
+ $self->{attachment}
+ ||= new Bugzilla::Attachment({id => $self->extra_data, cache => 1});
+ return $self->{attachment};
}
-sub author {
- my $self = shift;
- $self->{'author'}
- ||= new Bugzilla::User({ id => $self->{'who'}, cache => 1 });
- return $self->{'author'};
+sub author {
+ my $self = shift;
+ $self->{'author'} ||= new Bugzilla::User({id => $self->{'who'}, cache => 1});
+ return $self->{'author'};
}
sub body_full {
- my ($self, $params) = @_;
- $params ||= {};
- my $template = Bugzilla->template_inner;
- my $body;
- if ($self->type) {
- $template->process("bug/format_comment.txt.tmpl",
- { comment => $self, %$params }, \$body)
- || ThrowTemplateError($template->error());
- $body =~ s/^X//;
- }
- else {
- $body = $self->body;
- }
- if (!$self->is_markdown and !$self->already_wrapped) {
- $body = wrap_cite($body);
- }
- if ($params->{wrap} and !$self->already_wrapped) {
- $body = wrap_comment($body);
- }
- return $body;
+ my ($self, $params) = @_;
+ $params ||= {};
+ my $template = Bugzilla->template_inner;
+ my $body;
+ if ($self->type) {
+ $template->process("bug/format_comment.txt.tmpl", {comment => $self, %$params},
+ \$body)
+ || ThrowTemplateError($template->error());
+ $body =~ s/^X//;
+ }
+ else {
+ $body = $self->body;
+ }
+ if (!$self->is_markdown and !$self->already_wrapped) {
+ $body = wrap_cite($body);
+ }
+ if ($params->{wrap} and !$self->already_wrapped) {
+ $body = wrap_comment($body);
+ }
+ return $body;
}
############
# Mutators #
############
-sub set_is_private { $_[0]->set('isprivate', $_[1]); }
-sub set_type { $_[0]->set('type', $_[1]); }
-sub set_extra_data { $_[0]->set('extra_data', $_[1]); }
+sub set_is_private { $_[0]->set('isprivate', $_[1]); }
+sub set_type { $_[0]->set('type', $_[1]); }
+sub set_extra_data { $_[0]->set('extra_data', $_[1]); }
sub set_is_markdown { $_[0]->set('is_markdown', $_[1]); }
sub add_tag {
- my ($self, $tag) = @_;
- $tag = $self->_check_tag($tag);
+ my ($self, $tag) = @_;
+ $tag = $self->_check_tag($tag);
- my $tags = $self->tags;
- return if grep { lc($tag) eq lc($_) } @$tags;
- push @$tags, $tag;
- $self->{'tags'} = [ sort @$tags ];
+ my $tags = $self->tags;
+ return if grep { lc($tag) eq lc($_) } @$tags;
+ push @$tags, $tag;
+ $self->{'tags'} = [sort @$tags];
}
sub remove_tag {
- my ($self, $tag) = @_;
- $tag = $self->_check_tag($tag);
+ my ($self, $tag) = @_;
+ $tag = $self->_check_tag($tag);
- my $tags = $self->tags;
- my $index = first { lc($tags->[$_]) eq lc($tag) } 0..scalar(@$tags) - 1;
- return unless defined $index;
- splice(@$tags, $index, 1);
+ my $tags = $self->tags;
+ my $index = first { lc($tags->[$_]) eq lc($tag) } 0 .. scalar(@$tags) - 1;
+ return unless defined $index;
+ splice(@$tags, $index, 1);
}
##############
@@ -318,178 +326,177 @@ sub remove_tag {
##############
sub run_create_validators {
- my $self = shift;
- my $params = $self->SUPER::run_create_validators(@_);
- # Sometimes this run_create_validators is called with parameters that
- # skip bug_id validation, so it might not exist in the resulting hash.
- if (defined $params->{bug_id}) {
- $params->{bug_id} = $params->{bug_id}->id;
- }
- return $params;
+ my $self = shift;
+ my $params = $self->SUPER::run_create_validators(@_);
+
+ # Sometimes this run_create_validators is called with parameters that
+ # skip bug_id validation, so it might not exist in the resulting hash.
+ if (defined $params->{bug_id}) {
+ $params->{bug_id} = $params->{bug_id}->id;
+ }
+ return $params;
}
sub _check_extra_data {
- my ($invocant, $extra_data, undef, $params) = @_;
- my $type = blessed($invocant) ? $invocant->type : $params->{type};
+ my ($invocant, $extra_data, undef, $params) = @_;
+ my $type = blessed($invocant) ? $invocant->type : $params->{type};
- if ($type == CMT_NORMAL) {
- if (defined $extra_data) {
- ThrowCodeError('comment_extra_data_not_allowed',
- { type => $type, extra_data => $extra_data });
- }
+ if ($type == CMT_NORMAL) {
+ if (defined $extra_data) {
+ ThrowCodeError('comment_extra_data_not_allowed',
+ {type => $type, extra_data => $extra_data});
+ }
+ }
+ else {
+ if (!defined $extra_data) {
+ ThrowCodeError('comment_extra_data_required', {type => $type});
+ }
+ elsif ($type == CMT_ATTACHMENT_CREATED or $type == CMT_ATTACHMENT_UPDATED) {
+ my $attachment = Bugzilla::Attachment->check({id => $extra_data});
+ $extra_data = $attachment->id;
}
else {
- if (!defined $extra_data) {
- ThrowCodeError('comment_extra_data_required', { type => $type });
- }
- elsif ($type == CMT_ATTACHMENT_CREATED
- or $type == CMT_ATTACHMENT_UPDATED)
- {
- my $attachment = Bugzilla::Attachment->check({
- id => $extra_data });
- $extra_data = $attachment->id;
- }
- else {
- my $original = $extra_data;
- detaint_natural($extra_data)
- or ThrowCodeError('comment_extra_data_not_numeric',
- { type => $type, extra_data => $original });
- }
+ my $original = $extra_data;
+ detaint_natural($extra_data)
+ or ThrowCodeError('comment_extra_data_not_numeric',
+ {type => $type, extra_data => $original});
}
+ }
- return $extra_data;
+ return $extra_data;
}
sub _check_type {
- my ($invocant, $type) = @_;
- $type ||= CMT_NORMAL;
- my $original = $type;
- detaint_natural($type)
- or ThrowCodeError('comment_type_invalid', { type => $original });
- return $type;
+ my ($invocant, $type) = @_;
+ $type ||= CMT_NORMAL;
+ my $original = $type;
+ detaint_natural($type)
+ or ThrowCodeError('comment_type_invalid', {type => $original});
+ return $type;
}
sub _check_bug_id {
- my ($invocant, $bug_id) = @_;
-
- ThrowCodeError('param_required', {function => 'Bugzilla::Comment->create',
- param => 'bug_id'}) unless $bug_id;
-
- my $bug;
- if (blessed $bug_id) {
- # We got a bug object passed in, use it
- $bug = $bug_id;
- $bug->check_is_visible;
- }
- else {
- # We got a bug id passed in, check it and get the bug object
- $bug = Bugzilla::Bug->check({ id => $bug_id });
- }
-
- # Make sure the user can edit the product
- Bugzilla->user->can_edit_product($bug->{product_id});
-
- # Make sure the user can comment
- my $privs;
- $bug->check_can_change_field('longdesc', 0, 1, \$privs)
- || ThrowUserError('illegal_change',
- { field => 'longdesc', privs => $privs });
- return $bug;
+ my ($invocant, $bug_id) = @_;
+
+ ThrowCodeError('param_required',
+ {function => 'Bugzilla::Comment->create', param => 'bug_id'})
+ unless $bug_id;
+
+ my $bug;
+ if (blessed $bug_id) {
+
+ # We got a bug object passed in, use it
+ $bug = $bug_id;
+ $bug->check_is_visible;
+ }
+ else {
+ # We got a bug id passed in, check it and get the bug object
+ $bug = Bugzilla::Bug->check({id => $bug_id});
+ }
+
+ # Make sure the user can edit the product
+ Bugzilla->user->can_edit_product($bug->{product_id});
+
+ # Make sure the user can comment
+ my $privs;
+ $bug->check_can_change_field('longdesc', 0, 1, \$privs)
+ || ThrowUserError('illegal_change', {field => 'longdesc', privs => $privs});
+ return $bug;
}
sub _check_who {
- my ($invocant, $who) = @_;
- Bugzilla->login(LOGIN_REQUIRED);
- return Bugzilla->user->id;
+ my ($invocant, $who) = @_;
+ Bugzilla->login(LOGIN_REQUIRED);
+ return Bugzilla->user->id;
}
sub _check_bug_when {
- my ($invocant, $when) = @_;
+ my ($invocant, $when) = @_;
- # Make sure the timestamp is defined, default to a timestamp from the db
- if (!defined $when) {
- $when = Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
- }
+ # Make sure the timestamp is defined, default to a timestamp from the db
+ if (!defined $when) {
+ $when = Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
+ }
- # Make sure the timestamp parses
- if (!datetime_from($when)) {
- ThrowCodeError('invalid_timestamp', { timestamp => $when });
- }
+ # Make sure the timestamp parses
+ if (!datetime_from($when)) {
+ ThrowCodeError('invalid_timestamp', {timestamp => $when});
+ }
- return $when;
+ return $when;
}
sub _check_work_time {
- my ($invocant, $value_in, $field, $params) = @_;
-
- # Call down to Bugzilla::Object, letting it know negative
- # values are ok
- my $time = $invocant->check_time($value_in, $field, $params, 1);
- my $privs;
- $params->{bug_id}->check_can_change_field('work_time', 0, $time, \$privs)
- || ThrowUserError('illegal_change',
- { field => 'work_time', privs => $privs });
- return $time;
+ my ($invocant, $value_in, $field, $params) = @_;
+
+ # Call down to Bugzilla::Object, letting it know negative
+ # values are ok
+ my $time = $invocant->check_time($value_in, $field, $params, 1);
+ my $privs;
+ $params->{bug_id}->check_can_change_field('work_time', 0, $time, \$privs)
+ || ThrowUserError('illegal_change', {field => 'work_time', privs => $privs});
+ return $time;
}
sub _check_thetext {
- my ($invocant, $thetext) = @_;
-
- ThrowCodeError('param_required',{function => 'Bugzilla::Comment->create',
- param => 'thetext'}) unless defined $thetext;
-
- # Remove any trailing whitespace. Leading whitespace could be
- # a valid part of the comment.
- $thetext =~ s/\s*$//s;
- $thetext =~ s/\r\n?/\n/g; # Get rid of \r.
-
- # Characters above U+FFFF cannot be stored by MySQL older than 5.5.3 as they
- # require the new utf8mb4 character set. Other DB servers are handling them
- # without any problem. So we need to replace these characters if we use MySQL,
- # else the comment is truncated.
- # XXX - Once we use utf8mb4 for comments, this hack for MySQL can go away.
- state $is_mysql = Bugzilla->dbh->isa('Bugzilla::DB::Mysql') ? 1 : 0;
- if ($is_mysql) {
- $thetext =~ s/([\x{10000}-\x{10FFFF}])/"\x{FDD0}[" . uc(sprintf('U+%04x', ord($1))) . "]\x{FDD1}"/eg;
- }
-
- ThrowUserError('comment_too_long') if length($thetext) > MAX_COMMENT_LENGTH;
- return $thetext;
+ my ($invocant, $thetext) = @_;
+
+ ThrowCodeError('param_required',
+ {function => 'Bugzilla::Comment->create', param => 'thetext'})
+ unless defined $thetext;
+
+ # Remove any trailing whitespace. Leading whitespace could be
+ # a valid part of the comment.
+ $thetext =~ s/\s*$//s;
+ $thetext =~ s/\r\n?/\n/g; # Get rid of \r.
+
+ # Characters above U+FFFF cannot be stored by MySQL older than 5.5.3 as they
+ # require the new utf8mb4 character set. Other DB servers are handling them
+ # without any problem. So we need to replace these characters if we use MySQL,
+ # else the comment is truncated.
+ # XXX - Once we use utf8mb4 for comments, this hack for MySQL can go away.
+ state $is_mysql = Bugzilla->dbh->isa('Bugzilla::DB::Mysql') ? 1 : 0;
+ if ($is_mysql) {
+ $thetext
+ =~ s/([\x{10000}-\x{10FFFF}])/"\x{FDD0}[" . uc(sprintf('U+%04x', ord($1))) . "]\x{FDD1}"/eg;
+ }
+
+ ThrowUserError('comment_too_long') if length($thetext) > MAX_COMMENT_LENGTH;
+ return $thetext;
}
sub _check_isprivate {
- my ($invocant, $isprivate) = @_;
- if ($isprivate && !Bugzilla->user->is_insider) {
- ThrowUserError('user_not_insider');
- }
- return $isprivate ? 1 : 0;
+ my ($invocant, $isprivate) = @_;
+ if ($isprivate && !Bugzilla->user->is_insider) {
+ ThrowUserError('user_not_insider');
+ }
+ return $isprivate ? 1 : 0;
}
sub _check_tag {
- my ($invocant, $tag) = @_;
- length($tag) < MIN_COMMENT_TAG_LENGTH
- and ThrowUserError('comment_tag_too_short', { tag => $tag });
- length($tag) > MAX_COMMENT_TAG_LENGTH
- and ThrowUserError('comment_tag_too_long', { tag => $tag });
- $tag =~ /^[\w\d\._-]+$/
- or ThrowUserError('comment_tag_invalid', { tag => $tag });
- return $tag;
+ my ($invocant, $tag) = @_;
+ length($tag) < MIN_COMMENT_TAG_LENGTH
+ and ThrowUserError('comment_tag_too_short', {tag => $tag});
+ length($tag) > MAX_COMMENT_TAG_LENGTH
+ and ThrowUserError('comment_tag_too_long', {tag => $tag});
+ $tag =~ /^[\w\d\._-]+$/ or ThrowUserError('comment_tag_invalid', {tag => $tag});
+ return $tag;
}
sub count {
- my ($self) = @_;
+ my ($self) = @_;
- return $self->{'count'} if defined $self->{'count'};
+ return $self->{'count'} if defined $self->{'count'};
- my $dbh = Bugzilla->dbh;
- ($self->{'count'}) = $dbh->selectrow_array(
- "SELECT COUNT(*)
+ my $dbh = Bugzilla->dbh;
+ ($self->{'count'}) = $dbh->selectrow_array(
+ "SELECT COUNT(*)
FROM longdescs
WHERE bug_id = ?
- AND bug_when <= ?",
- undef, $self->bug_id, $self->creation_ts);
+ AND bug_when <= ?", undef, $self->bug_id, $self->creation_ts
+ );
- return --$self->{'count'};
+ return --$self->{'count'};
}
1;
diff --git a/Bugzilla/Comment/TagWeights.pm b/Bugzilla/Comment/TagWeights.pm
index f4aa519a30..42200f663c 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 77d07e4c27..b9688d009f 100644
--- a/Bugzilla/Component.pm
+++ b/Bugzilla/Component.pm
@@ -27,150 +27,147 @@ use Scalar::Util qw(blessed);
###############################
use constant DB_TABLE => 'components';
+
# This is mostly for the editfields.cgi case where ->get_all is called.
use constant LIST_ORDER => 'product_id, name';
use constant DB_COLUMNS => qw(
- id
- name
- product_id
- initialowner
- initialqacontact
- description
- isactive
+ id
+ name
+ product_id
+ initialowner
+ initialqacontact
+ description
+ isactive
);
use constant UPDATE_COLUMNS => qw(
- name
- initialowner
- initialqacontact
- description
- isactive
+ name
+ initialowner
+ initialqacontact
+ description
+ isactive
);
-use constant REQUIRED_FIELD_MAP => {
- product_id => 'product',
-};
+use constant REQUIRED_FIELD_MAP => {product_id => 'product',};
use constant VALIDATORS => {
- create_series => \&Bugzilla::Object::check_boolean,
- product => \&_check_product,
- initialowner => \&_check_initialowner,
- initialqacontact => \&_check_initialqacontact,
- description => \&_check_description,
- initial_cc => \&_check_cc_list,
- name => \&_check_name,
- isactive => \&Bugzilla::Object::check_boolean,
+ create_series => \&Bugzilla::Object::check_boolean,
+ product => \&_check_product,
+ initialowner => \&_check_initialowner,
+ initialqacontact => \&_check_initialqacontact,
+ description => \&_check_description,
+ initial_cc => \&_check_cc_list,
+ name => \&_check_name,
+ isactive => \&Bugzilla::Object::check_boolean,
};
-use constant VALIDATOR_DEPENDENCIES => {
- name => ['product'],
-};
+use constant VALIDATOR_DEPENDENCIES => {name => ['product'],};
###############################
sub new {
- my $class = shift;
- my $param = shift;
- my $dbh = Bugzilla->dbh;
-
- my $product;
- if (ref $param and !defined $param->{id}) {
- $product = $param->{product};
- my $name = $param->{name};
- if (!defined $product) {
- ThrowCodeError('bad_arg',
- {argument => 'product',
- function => "${class}::new"});
- }
- if (!defined $name) {
- ThrowCodeError('bad_arg',
- {argument => 'name',
- function => "${class}::new"});
- }
-
- my $condition = 'product_id = ? AND name = ?';
- my @values = ($product->id, $name);
- $param = { condition => $condition, values => \@values };
+ my $class = shift;
+ my $param = shift;
+ my $dbh = Bugzilla->dbh;
+
+ my $product;
+ if (ref $param and !defined $param->{id}) {
+ $product = $param->{product};
+ my $name = $param->{name};
+ if (!defined $product) {
+ ThrowCodeError('bad_arg', {argument => 'product', function => "${class}::new"});
}
+ if (!defined $name) {
+ ThrowCodeError('bad_arg', {argument => 'name', function => "${class}::new"});
+ }
+
+ my $condition = 'product_id = ? AND name = ?';
+ my @values = ($product->id, $name);
+ $param = {condition => $condition, values => \@values};
+ }
- unshift @_, $param;
- my $component = $class->SUPER::new(@_);
- # Add the product object as attribute only if the component exists.
- $component->{product} = $product if ($component && $product);
- return $component;
+ unshift @_, $param;
+ my $component = $class->SUPER::new(@_);
+
+ # Add the product object as attribute only if the component exists.
+ $component->{product} = $product if ($component && $product);
+ return $component;
}
sub create {
- my $class = shift;
- my $dbh = Bugzilla->dbh;
+ my $class = shift;
+ my $dbh = Bugzilla->dbh;
- $dbh->bz_start_transaction();
+ $dbh->bz_start_transaction();
- $class->check_required_create_fields(@_);
- my $params = $class->run_create_validators(@_);
- my $cc_list = delete $params->{initial_cc};
- my $create_series = delete $params->{create_series};
- my $product = delete $params->{product};
- $params->{product_id} = $product->id;
+ $class->check_required_create_fields(@_);
+ my $params = $class->run_create_validators(@_);
+ my $cc_list = delete $params->{initial_cc};
+ my $create_series = delete $params->{create_series};
+ my $product = delete $params->{product};
+ $params->{product_id} = $product->id;
- my $component = $class->insert_create_data($params);
- $component->{product} = $product;
+ my $component = $class->insert_create_data($params);
+ $component->{product} = $product;
- # We still have to fill the component_cc table.
- $component->_update_cc_list($cc_list) if $cc_list;
+ # We still have to fill the component_cc table.
+ $component->_update_cc_list($cc_list) if $cc_list;
- # Create series for the new component.
- $component->_create_series() if $create_series;
+ # Create series for the new component.
+ $component->_create_series() if $create_series;
- $dbh->bz_commit_transaction();
- return $component;
+ $dbh->bz_commit_transaction();
+ return $component;
}
sub update {
- my $self = shift;
- my $changes = $self->SUPER::update(@_);
-
- # Update the component_cc table if necessary.
- if (defined $self->{cc_ids}) {
- my $diff = $self->_update_cc_list($self->{cc_ids});
- $changes->{cc_list} = $diff if defined $diff;
- }
- return $changes;
+ my $self = shift;
+ my $changes = $self->SUPER::update(@_);
+
+ # Update the component_cc table if necessary.
+ if (defined $self->{cc_ids}) {
+ my $diff = $self->_update_cc_list($self->{cc_ids});
+ $changes->{cc_list} = $diff if defined $diff;
+ }
+ return $changes;
}
sub remove_from_db {
- my $self = shift;
- my $dbh = Bugzilla->dbh;
+ my $self = shift;
+ my $dbh = Bugzilla->dbh;
- $self->_check_if_controller(); # From ChoiceInterface
+ $self->_check_if_controller(); # From ChoiceInterface
- $dbh->bz_start_transaction();
+ $dbh->bz_start_transaction();
- # Products must have at least one component.
- my @components = @{ $self->product->components };
- if (scalar(@components) == 1) {
- ThrowUserError('component_is_last', { comp => $self });
- }
+ # Products must have at least one component.
+ my @components = @{$self->product->components};
+ if (scalar(@components) == 1) {
+ ThrowUserError('component_is_last', {comp => $self});
+ }
- if ($self->bug_count) {
- if (Bugzilla->params->{'allowbugdeletion'}) {
- require Bugzilla::Bug;
- foreach my $bug_id (@{$self->bug_ids}) {
- # Note: We allow admins to delete bugs even if they can't
- # see them, as long as they can see the product.
- my $bug = new Bugzilla::Bug($bug_id);
- $bug->remove_from_db();
- }
- } else {
- ThrowUserError('component_has_bugs', {nb => $self->bug_count});
- }
+ if ($self->bug_count) {
+ if (Bugzilla->params->{'allowbugdeletion'}) {
+ require Bugzilla::Bug;
+ foreach my $bug_id (@{$self->bug_ids}) {
+
+ # Note: We allow admins to delete bugs even if they can't
+ # see them, as long as they can see the product.
+ my $bug = new Bugzilla::Bug($bug_id);
+ $bug->remove_from_db();
+ }
+ }
+ else {
+ ThrowUserError('component_has_bugs', {nb => $self->bug_count});
}
- # Update the list of components in the product object.
- $self->product->{components} = [grep { $_->id != $self->id } @components];
- $self->SUPER::remove_from_db();
+ }
+
+ # Update the list of components in the product object.
+ $self->product->{components} = [grep { $_->id != $self->id } @components];
+ $self->SUPER::remove_from_db();
- $dbh->bz_commit_transaction();
+ $dbh->bz_commit_transaction();
}
################################
@@ -178,69 +175,70 @@ sub remove_from_db {
################################
sub _check_name {
- my ($invocant, $name, undef, $params) = @_;
- my $product = blessed($invocant) ? $invocant->product : $params->{product};
-
- $name = trim($name);
- $name || ThrowUserError('component_blank_name');
-
- if (length($name) > MAX_COMPONENT_SIZE) {
- ThrowUserError('component_name_too_long', {'name' => $name});
- }
-
- my $component = new Bugzilla::Component({product => $product, name => $name});
- if ($component && (!ref $invocant || $component->id != $invocant->id)) {
- ThrowUserError('component_already_exists', { name => $component->name,
- product => $product });
- }
- return $name;
+ my ($invocant, $name, undef, $params) = @_;
+ my $product = blessed($invocant) ? $invocant->product : $params->{product};
+
+ $name = trim($name);
+ $name || ThrowUserError('component_blank_name');
+
+ if (length($name) > MAX_COMPONENT_SIZE) {
+ ThrowUserError('component_name_too_long', {'name' => $name});
+ }
+
+ my $component = new Bugzilla::Component({product => $product, name => $name});
+ if ($component && (!ref $invocant || $component->id != $invocant->id)) {
+ ThrowUserError('component_already_exists',
+ {name => $component->name, product => $product});
+ }
+ return $name;
}
sub _check_description {
- my ($invocant, $description) = @_;
+ my ($invocant, $description) = @_;
- $description = trim($description);
- $description || ThrowUserError('component_blank_description');
- return $description;
+ $description = trim($description);
+ $description || ThrowUserError('component_blank_description');
+ return $description;
}
sub _check_initialowner {
- my ($invocant, $owner) = @_;
+ my ($invocant, $owner) = @_;
- $owner || ThrowUserError('component_need_initialowner');
- my $owner_id = Bugzilla::User->check($owner)->id;
- return $owner_id;
+ $owner || ThrowUserError('component_need_initialowner');
+ my $owner_id = Bugzilla::User->check($owner)->id;
+ return $owner_id;
}
sub _check_initialqacontact {
- my ($invocant, $qa_contact) = @_;
-
- my $qa_contact_id;
- if (Bugzilla->params->{'useqacontact'}) {
- $qa_contact_id = Bugzilla::User->check($qa_contact)->id if $qa_contact;
- }
- elsif (ref $invocant) {
- $qa_contact_id = $invocant->{initialqacontact};
- }
- return $qa_contact_id;
+ my ($invocant, $qa_contact) = @_;
+
+ my $qa_contact_id;
+ if (Bugzilla->params->{'useqacontact'}) {
+ $qa_contact_id = Bugzilla::User->check($qa_contact)->id if $qa_contact;
+ }
+ elsif (ref $invocant) {
+ $qa_contact_id = $invocant->{initialqacontact};
+ }
+ return $qa_contact_id;
}
sub _check_product {
- my ($invocant, $product) = @_;
- $product || ThrowCodeError('param_required',
- { function => "$invocant->create", param => 'product' });
- return Bugzilla->user->check_can_admin_product($product->name);
+ my ($invocant, $product) = @_;
+ $product
+ || ThrowCodeError('param_required',
+ {function => "$invocant->create", param => 'product'});
+ return Bugzilla->user->check_can_admin_product($product->name);
}
sub _check_cc_list {
- my ($invocant, $cc_list) = @_;
-
- my %cc_ids;
- foreach my $cc (@$cc_list) {
- my $id = login_to_id($cc, THROW_ERROR);
- $cc_ids{$id} = 1;
- }
- return [keys %cc_ids];
+ my ($invocant, $cc_list) = @_;
+
+ my %cc_ids;
+ foreach my $cc (@$cc_list) {
+ my $id = login_to_id($cc, THROW_ERROR);
+ $cc_ids{$id} = 1;
+ }
+ return [keys %cc_ids];
}
###############################
@@ -248,155 +246,176 @@ sub _check_cc_list {
###############################
sub _update_cc_list {
- my ($self, $cc_list) = @_;
- my $dbh = Bugzilla->dbh;
+ my ($self, $cc_list) = @_;
+ my $dbh = Bugzilla->dbh;
- my $old_cc_list =
- $dbh->selectcol_arrayref('SELECT user_id FROM component_cc
- WHERE component_id = ?', undef, $self->id);
+ my $old_cc_list = $dbh->selectcol_arrayref(
+ 'SELECT user_id FROM component_cc
+ WHERE component_id = ?', undef, $self->id
+ );
- my ($removed, $added) = diff_arrays($old_cc_list, $cc_list);
- my $diff;
- if (scalar @$removed || scalar @$added) {
- $diff = [join(', ', @$removed), join(', ', @$added)];
- }
+ my ($removed, $added) = diff_arrays($old_cc_list, $cc_list);
+ my $diff;
+ if (scalar @$removed || scalar @$added) {
+ $diff = [join(', ', @$removed), join(', ', @$added)];
+ }
- $dbh->do('DELETE FROM component_cc WHERE component_id = ?', undef, $self->id);
+ $dbh->do('DELETE FROM component_cc WHERE component_id = ?', undef, $self->id);
- my $sth = $dbh->prepare('INSERT INTO component_cc
- (user_id, component_id) VALUES (?, ?)');
- $sth->execute($_, $self->id) foreach (@$cc_list);
+ my $sth = $dbh->prepare(
+ 'INSERT INTO component_cc
+ (user_id, component_id) VALUES (?, ?)'
+ );
+ $sth->execute($_, $self->id) foreach (@$cc_list);
- return $diff;
+ return $diff;
}
sub _create_series {
- my $self = shift;
-
- # Insert default charting queries for this product.
- # If they aren't using charting, this won't do any harm.
- my $prodcomp = "&product=" . url_quote($self->product->name) .
- "&component=" . url_quote($self->name);
-
- my $open_query = 'field0-0-0=resolution&type0-0-0=notregexp&value0-0-0=.' .
- $prodcomp;
- my $nonopen_query = 'field0-0-0=resolution&type0-0-0=regexp&value0-0-0=.' .
- $prodcomp;
-
- my @series = ([get_text('series_all_open'), $open_query],
- [get_text('series_all_closed'), $nonopen_query]);
-
- foreach my $sdata (@series) {
- my $series = new Bugzilla::Series(undef, $self->product->name,
- $self->name, $sdata->[0],
- Bugzilla->user->id, 1, $sdata->[1], 1);
- $series->writeToDatabase();
- }
+ my $self = shift;
+
+ # Insert default charting queries for this product.
+ # If they aren't using charting, this won't do any harm.
+ my $prodcomp
+ = "&product="
+ . url_quote($self->product->name)
+ . "&component="
+ . url_quote($self->name);
+
+ my $open_query
+ = 'field0-0-0=resolution&type0-0-0=notregexp&value0-0-0=.' . $prodcomp;
+ my $nonopen_query
+ = 'field0-0-0=resolution&type0-0-0=regexp&value0-0-0=.' . $prodcomp;
+
+ my @series = (
+ [get_text('series_all_open'), $open_query],
+ [get_text('series_all_closed'), $nonopen_query]
+ );
+
+ foreach my $sdata (@series) {
+ my $series
+ = new Bugzilla::Series(undef, $self->product->name, $self->name, $sdata->[0],
+ Bugzilla->user->id, 1, $sdata->[1], 1);
+ $series->writeToDatabase();
+ }
}
-sub set_name { $_[0]->set('name', $_[1]); }
+sub set_name { $_[0]->set('name', $_[1]); }
sub set_description { $_[0]->set('description', $_[1]); }
-sub set_is_active { $_[0]->set('isactive', $_[1]); }
+sub set_is_active { $_[0]->set('isactive', $_[1]); }
+
sub set_default_assignee {
- my ($self, $owner) = @_;
+ my ($self, $owner) = @_;
- $self->set('initialowner', $owner);
- # Reset the default owner object.
- delete $self->{default_assignee};
+ $self->set('initialowner', $owner);
+
+ # Reset the default owner object.
+ delete $self->{default_assignee};
}
+
sub set_default_qa_contact {
- my ($self, $qa_contact) = @_;
+ my ($self, $qa_contact) = @_;
- $self->set('initialqacontact', $qa_contact);
- # Reset the default QA contact object.
- delete $self->{default_qa_contact};
+ $self->set('initialqacontact', $qa_contact);
+
+ # Reset the default QA contact object.
+ delete $self->{default_qa_contact};
}
+
sub set_cc_list {
- my ($self, $cc_list) = @_;
+ my ($self, $cc_list) = @_;
+
+ $self->{cc_ids} = $self->_check_cc_list($cc_list);
- $self->{cc_ids} = $self->_check_cc_list($cc_list);
- # Reset the list of CC user objects.
- delete $self->{initial_cc};
+ # Reset the list of CC user objects.
+ delete $self->{initial_cc};
}
sub bug_count {
- my $self = shift;
- my $dbh = Bugzilla->dbh;
+ my $self = shift;
+ my $dbh = Bugzilla->dbh;
- if (!defined $self->{'bug_count'}) {
- $self->{'bug_count'} = $dbh->selectrow_array(q{
+ if (!defined $self->{'bug_count'}) {
+ $self->{'bug_count'} = $dbh->selectrow_array(
+ q{
SELECT COUNT(*) FROM bugs
- WHERE component_id = ?}, undef, $self->id) || 0;
- }
- return $self->{'bug_count'};
+ WHERE component_id = ?}, undef, $self->id
+ ) || 0;
+ }
+ return $self->{'bug_count'};
}
sub bug_ids {
- my $self = shift;
- my $dbh = Bugzilla->dbh;
+ my $self = shift;
+ my $dbh = Bugzilla->dbh;
- if (!defined $self->{'bugs_ids'}) {
- $self->{'bugs_ids'} = $dbh->selectcol_arrayref(q{
+ if (!defined $self->{'bugs_ids'}) {
+ $self->{'bugs_ids'} = $dbh->selectcol_arrayref(
+ q{
SELECT bug_id FROM bugs
- WHERE component_id = ?}, undef, $self->id);
- }
- return $self->{'bugs_ids'};
+ WHERE component_id = ?}, undef, $self->id
+ );
+ }
+ return $self->{'bugs_ids'};
}
sub default_assignee {
- my $self = shift;
+ my $self = shift;
- return $self->{'default_assignee'}
- ||= new Bugzilla::User({ id => $self->{'initialowner'}, cache => 1 });
+ return $self->{'default_assignee'}
+ ||= new Bugzilla::User({id => $self->{'initialowner'}, cache => 1});
}
sub default_qa_contact {
- my $self = shift;
+ my $self = shift;
- return unless $self->{'initialqacontact'};
- return $self->{'default_qa_contact'}
- ||= new Bugzilla::User({id => $self->{'initialqacontact'}, cache => 1 });
+ return unless $self->{'initialqacontact'};
+ return $self->{'default_qa_contact'}
+ ||= new Bugzilla::User({id => $self->{'initialqacontact'}, cache => 1});
}
sub flag_types {
- my $self = shift;
-
- if (!defined $self->{'flag_types'}) {
- my $flagtypes = Bugzilla::FlagType::match({ product_id => $self->product_id,
- component_id => $self->id });
-
- $self->{'flag_types'} = {};
- $self->{'flag_types'}->{'bug'} =
- [grep { $_->target_type eq 'bug' } @$flagtypes];
- $self->{'flag_types'}->{'attachment'} =
- [grep { $_->target_type eq 'attachment' } @$flagtypes];
- }
- return $self->{'flag_types'};
+ my $self = shift;
+
+ if (!defined $self->{'flag_types'}) {
+ my $flagtypes = Bugzilla::FlagType::match(
+ {product_id => $self->product_id, component_id => $self->id});
+
+ $self->{'flag_types'} = {};
+ $self->{'flag_types'}->{'bug'}
+ = [grep { $_->target_type eq 'bug' } @$flagtypes];
+ $self->{'flag_types'}->{'attachment'}
+ = [grep { $_->target_type eq 'attachment' } @$flagtypes];
+ }
+ return $self->{'flag_types'};
}
sub initial_cc {
- my $self = shift;
- my $dbh = Bugzilla->dbh;
-
- if (!defined $self->{'initial_cc'}) {
- # If set_cc_list() has been called but data are not yet written
- # into the DB, we want the new values defined by it.
- my $cc_ids = $self->{cc_ids}
- || $dbh->selectcol_arrayref('SELECT user_id FROM component_cc
- WHERE component_id = ?',
- undef, $self->id);
-
- $self->{'initial_cc'} = Bugzilla::User->new_from_list($cc_ids);
- }
- return $self->{'initial_cc'};
+ my $self = shift;
+ my $dbh = Bugzilla->dbh;
+
+ if (!defined $self->{'initial_cc'}) {
+
+ # If set_cc_list() has been called but data are not yet written
+ # into the DB, we want the new values defined by it.
+ my $cc_ids = $self->{cc_ids} || $dbh->selectcol_arrayref(
+ 'SELECT user_id FROM component_cc
+ WHERE component_id = ?', undef,
+ $self->id
+ );
+
+ $self->{'initial_cc'} = Bugzilla::User->new_from_list($cc_ids);
+ }
+ return $self->{'initial_cc'};
}
sub product {
- my $self = shift;
+ my $self = shift;
- require Bugzilla::Product;
- $self->{'product'} ||= Bugzilla::Product->new({ id => $self->product_id, cache => 1 });
- return $self->{'product'};
+ require Bugzilla::Product;
+ $self->{'product'}
+ ||= Bugzilla::Product->new({id => $self->product_id, cache => 1});
+ return $self->{'product'};
}
###############################
@@ -404,8 +423,8 @@ sub product {
###############################
sub description { return $_[0]->{'description'}; }
-sub product_id { return $_[0]->{'product_id'}; }
-sub is_active { return $_[0]->{'isactive'}; }
+sub product_id { return $_[0]->{'product_id'}; }
+sub is_active { return $_[0]->{'isactive'}; }
##############################################
# Implement Bugzilla::Field::ChoiceInterface #
@@ -415,11 +434,11 @@ use constant FIELD_NAME => 'component';
use constant is_default => 0;
sub is_set_on_bug {
- my ($self, $bug) = @_;
- my $value = blessed($bug) ? $bug->component_id : $bug->{component};
- $value = $value->id if blessed($value);
- return 0 unless $value;
- return $value == $self->id ? 1 : 0;
+ my ($self, $bug) = @_;
+ my $value = blessed($bug) ? $bug->component_id : $bug->{component};
+ $value = $value->id if blessed($value);
+ return 0 unless $value;
+ return $value == $self->id ? 1 : 0;
}
###############################
diff --git a/Bugzilla/Config.pm b/Bugzilla/Config.pm
index 0f13240805..378b7bd9f3 100644
--- a/Bugzilla/Config.pm
+++ b/Bugzilla/Config.pm
@@ -26,324 +26,332 @@ use File::Basename;
# Don't export localvars by default - people should have to explicitly
# ask for it, as a (probably futile) attempt to stop code using it
# when it shouldn't
-%Bugzilla::Config::EXPORT_TAGS =
- (
- admin => [qw(update_params
- SetParam
- call_param_onchange_handlers
- write_params)],
- );
+%Bugzilla::Config::EXPORT_TAGS = (
+ admin => [
+ qw(update_params
+ SetParam
+ call_param_onchange_handlers
+ write_params)
+ ],
+);
Exporter::export_ok_tags('admin');
# new installs get these set of defaults (unless overridden by the answers file)
-my %NEW_INSTALL_DEFAULT = (
- or_groups => 1,
- use_email_as_login => 0,
-);
+my %NEW_INSTALL_DEFAULT = (or_groups => 1, use_email_as_login => 0,);
# INITIALISATION CODE
# Perl throws a warning if we use bz_locations() directly after do.
our %params;
+
# Load in the param definitions
sub _load_params {
- my $panels = param_panels();
- my %hook_panels;
- foreach my $panel (keys %$panels) {
- my $module = $panels->{$panel};
- eval("require $module") || die $@;
- my @new_param_list = $module->get_param_list();
- $hook_panels{lc($panel)} = { params => \@new_param_list };
- }
- # This hook is also called in editparams.cgi. This call here is required
- # to make SetParam work.
- Bugzilla::Hook::process('config_modify_panels',
- { panels => \%hook_panels });
-
- foreach my $panel (keys %hook_panels) {
- foreach my $item (@{$hook_panels{$panel}->{params}}) {
- $params{$item->{'name'}} = $item;
- }
+ my $panels = param_panels();
+ my %hook_panels;
+ foreach my $panel (keys %$panels) {
+ my $module = $panels->{$panel};
+ eval("require $module") || die $@;
+ my @new_param_list = $module->get_param_list();
+ $hook_panels{lc($panel)} = {params => \@new_param_list};
+ }
+
+ # This hook is also called in editparams.cgi. This call here is required
+ # to make SetParam work.
+ Bugzilla::Hook::process('config_modify_panels', {panels => \%hook_panels});
+
+ foreach my $panel (keys %hook_panels) {
+ foreach my $item (@{$hook_panels{$panel}->{params}}) {
+ $params{$item->{'name'}} = $item;
}
+ }
}
+
# END INIT CODE
# Subroutines go here
sub param_panels {
- my $param_panels = {};
- my $libpath = bz_locations()->{'libpath'};
- foreach my $item ((glob "$libpath/Bugzilla/Config/*.pm")) {
- $item =~ m#/([^/]+)\.pm$#;
- my $module = $1;
- $param_panels->{$module} = "Bugzilla::Config::$module" unless $module eq 'Common';
- }
- # Now check for any hooked params
- Bugzilla::Hook::process('config_add_panels',
- { panel_modules => $param_panels });
- return $param_panels;
+ my $param_panels = {};
+ my $libpath = bz_locations()->{'libpath'};
+ foreach my $item ((glob "$libpath/Bugzilla/Config/*.pm")) {
+ $item =~ m#/([^/]+)\.pm$#;
+ my $module = $1;
+ $param_panels->{$module} = "Bugzilla::Config::$module"
+ unless $module eq 'Common';
+ }
+
+ # Now check for any hooked params
+ Bugzilla::Hook::process('config_add_panels', {panel_modules => $param_panels});
+ return $param_panels;
}
sub SetParam {
- my ($name, $value) = @_;
+ my ($name, $value) = @_;
- _load_params() unless %params;
- die "Unknown param $name" unless (exists $params{$name});
+ _load_params() unless %params;
+ die "Unknown param $name" unless (exists $params{$name});
- my $entry = $params{$name};
+ my $entry = $params{$name};
- # sanity check the value
+ # sanity check the value
- # XXX - This runs the checks. Which would be good, except that
- # check_shadowdb creates the database as a side effect, and so the
- # checker fails the second time around...
- if ($name ne 'shadowdb' && exists $entry->{'checker'}) {
- my $err = $entry->{'checker'}->($value, $entry);
- die "Param $name is not valid: $err" unless $err eq '';
- }
+ # XXX - This runs the checks. Which would be good, except that
+ # check_shadowdb creates the database as a side effect, and so the
+ # checker fails the second time around...
+ if ($name ne 'shadowdb' && exists $entry->{'checker'}) {
+ my $err = $entry->{'checker'}->($value, $entry);
+ die "Param $name is not valid: $err" unless $err eq '';
+ }
- Bugzilla->params->{$name} = $value;
+ Bugzilla->params->{$name} = $value;
}
sub call_param_onchange_handlers {
- my ($changes) = @_;
+ my ($changes) = @_;
- _load_params() unless %params;
+ _load_params() unless %params;
- foreach my $name (@$changes) {
- my $param = $params{$name};
- if (exists $param->{'onchange'}) {
- $param->{'onchange'}->(Bugzilla->params->{$name});
- }
+ foreach my $name (@$changes) {
+ my $param = $params{$name};
+ if (exists $param->{'onchange'}) {
+ $param->{'onchange'}->(Bugzilla->params->{$name});
}
+ }
}
sub update_params {
- my ($params) = @_;
- my $answer = Bugzilla->installation_answers;
- my $datadir = bz_locations()->{'datadir'};
- my $param;
-
- # If the old data/params file using Data::Dumper output still exists,
- # read it. It will be deleted once the parameters are stored in the new
- # data/params.json file.
- my $old_file = "$datadir/params";
-
- if (-e $old_file) {
- require Safe;
- my $s = new Safe;
-
- $s->rdo($old_file);
- die "Error reading $old_file: $!" if $!;
- die "Error evaluating $old_file: $@" if $@;
-
- # Now read the param back out from the sandbox.
- $param = \%{ $s->varglob('param') };
+ my ($params) = @_;
+ my $answer = Bugzilla->installation_answers;
+ my $datadir = bz_locations()->{'datadir'};
+ my $param;
+
+ # If the old data/params file using Data::Dumper output still exists,
+ # read it. It will be deleted once the parameters are stored in the new
+ # data/params.json file.
+ my $old_file = "$datadir/params";
+
+ if (-e $old_file) {
+ require Safe;
+ my $s = new Safe;
+
+ $s->rdo($old_file);
+ die "Error reading $old_file: $!" if $!;
+ die "Error evaluating $old_file: $@" if $@;
+
+ # Now read the param back out from the sandbox.
+ $param = \%{$s->varglob('param')};
+ }
+ else {
+ # Rename params.js to params.json if checksetup.pl
+ # was executed with an earlier version of this change
+ rename "$old_file.js", "$old_file.json"
+ if -e "$old_file.js" && !-e "$old_file.json";
+
+ # Read the new data/params.json file.
+ $param = read_param_file();
+ }
+
+ my %new_params;
+
+ # If we didn't return any param values, then this is a new installation.
+ my $new_install = !(keys %$param);
+
+ # --- UPDATE OLD PARAMS ---
+
+ # Change from usebrowserinfo to defaultplatform/defaultopsys combo
+ if (exists $param->{'usebrowserinfo'}) {
+ if (!$param->{'usebrowserinfo'}) {
+ if (!exists $param->{'defaultplatform'}) {
+ $new_params{'defaultplatform'} = 'Other';
+ }
+ if (!exists $param->{'defaultopsys'}) {
+ $new_params{'defaultopsys'} = 'Other';
+ }
}
- else {
- # Rename params.js to params.json if checksetup.pl
- # was executed with an earlier version of this change
- rename "$old_file.js", "$old_file.json"
- if -e "$old_file.js" && !-e "$old_file.json";
-
- # Read the new data/params.json file.
- $param = read_param_file();
+ }
+
+ # Change from a boolean for quips to multi-state
+ if (exists $param->{'usequip'} && !exists $param->{'enablequips'}) {
+ $new_params{'enablequips'} = $param->{'usequip'} ? 'on' : 'off';
+ }
+
+ # Modularise auth code
+ if (exists $param->{'useLDAP'} && !exists $param->{'loginmethod'}) {
+ $new_params{'loginmethod'} = $param->{'useLDAP'} ? "LDAP" : "DB";
+ }
+
+ # set verify method to whatever loginmethod was
+ if (exists $param->{'loginmethod'} && !exists $param->{'user_verify_class'}) {
+ $new_params{'user_verify_class'} = $param->{'loginmethod'};
+ }
+
+ # Remove quip-display control from parameters
+ # and give it to users via User Settings (Bug 41972)
+ if (exists $param->{'enablequips'}
+ && !exists $param->{'quip_list_entry_control'})
+ {
+ my $new_value;
+ ($param->{'enablequips'} eq 'on') && do { $new_value = 'open'; };
+ ($param->{'enablequips'} eq 'approved') && do { $new_value = 'moderated'; };
+ ($param->{'enablequips'} eq 'frozen') && do { $new_value = 'closed'; };
+ ($param->{'enablequips'} eq 'off') && do { $new_value = 'closed'; };
+ $new_params{'quip_list_entry_control'} = $new_value;
+ }
+
+ # Old mail_delivery_method choices contained no uppercase characters
+ my $mta = $param->{'mail_delivery_method'};
+ if ($mta) {
+ if ($mta !~ /[A-Z]/) {
+ my %translation = (
+ 'sendmail' => 'Sendmail',
+ 'smtp' => 'SMTP',
+ 'qmail' => 'Qmail',
+ 'testfile' => 'Test',
+ 'none' => 'None'
+ );
+ $param->{'mail_delivery_method'} = $translation{$mta};
}
- my %new_params;
-
- # If we didn't return any param values, then this is a new installation.
- my $new_install = !(keys %$param);
-
- # --- UPDATE OLD PARAMS ---
-
- # Change from usebrowserinfo to defaultplatform/defaultopsys combo
- if (exists $param->{'usebrowserinfo'}) {
- if (!$param->{'usebrowserinfo'}) {
- if (!exists $param->{'defaultplatform'}) {
- $new_params{'defaultplatform'} = 'Other';
- }
- if (!exists $param->{'defaultopsys'}) {
- $new_params{'defaultopsys'} = 'Other';
- }
- }
+ # This will force the parameter to be reset to its default value.
+ delete $param->{'mail_delivery_method'}
+ if $param->{'mail_delivery_method'} eq 'Qmail';
+ }
+
+ # Convert the old "ssl" parameter to the new "ssl_redirect" parameter.
+ # Both "authenticated sessions" and "always" turn on "ssl_redirect"
+ # when upgrading.
+ if (exists $param->{'ssl'} and $param->{'ssl'} ne 'never') {
+ $new_params{'ssl_redirect'} = 1;
+ }
+
+# "specific_search_allow_empty_words" has been renamed to "search_allow_no_criteria".
+ if (exists $param->{'specific_search_allow_empty_words'}) {
+ $new_params{'search_allow_no_criteria'}
+ = $param->{'specific_search_allow_empty_words'};
+ }
+
+ if (exists $param->{'noresolveonopenblockers'}) {
+ $new_params{'resolution_forbidden_with_open_blockers'}
+ = $param->{'noresolveonopenblockers'} ? 'FIXED' : "";
+ }
+
+ # --- DEFAULTS FOR NEW PARAMS ---
+
+ _load_params() unless %params;
+ foreach my $name (keys %params) {
+ my $item = $params{$name};
+ unless (exists $param->{$name}) {
+ print "New parameter: $name\n" unless $new_install;
+ if (exists $new_params{$name}) {
+ $param->{$name} = $new_params{$name};
+ }
+ elsif (exists $answer->{$name}) {
+ $param->{$name} = $answer->{$name};
+ }
+ elsif ($new_install and exists $NEW_INSTALL_DEFAULT{$name}) {
+ $param->{$name} = $NEW_INSTALL_DEFAULT{$name};
+ }
+ else {
+ $param->{$name} = $item->{'default'};
+ }
}
+ }
- # Change from a boolean for quips to multi-state
- if (exists $param->{'usequip'} && !exists $param->{'enablequips'}) {
- $new_params{'enablequips'} = $param->{'usequip'} ? 'on' : 'off';
- }
+ # --- REMOVE OLD PARAMS ---
- # Modularise auth code
- if (exists $param->{'useLDAP'} && !exists $param->{'loginmethod'}) {
- $new_params{'loginmethod'} = $param->{'useLDAP'} ? "LDAP" : "DB";
- }
+ my %oldparams;
- # set verify method to whatever loginmethod was
- if (exists $param->{'loginmethod'}
- && !exists $param->{'user_verify_class'})
- {
- $new_params{'user_verify_class'} = $param->{'loginmethod'};
+ # Remove any old params
+ foreach my $item (keys %$param) {
+ if (!exists $params{$item}) {
+ $oldparams{$item} = delete $param->{$item};
}
-
- # Remove quip-display control from parameters
- # and give it to users via User Settings (Bug 41972)
- if ( exists $param->{'enablequips'}
- && !exists $param->{'quip_list_entry_control'})
- {
- my $new_value;
- ($param->{'enablequips'} eq 'on') && do {$new_value = 'open';};
- ($param->{'enablequips'} eq 'approved') && do {$new_value = 'moderated';};
- ($param->{'enablequips'} eq 'frozen') && do {$new_value = 'closed';};
- ($param->{'enablequips'} eq 'off') && do {$new_value = 'closed';};
- $new_params{'quip_list_entry_control'} = $new_value;
+ }
+
+ # Write any old parameters to old-params.txt
+ my $old_param_file = "$datadir/old-params.txt";
+ if (scalar(keys %oldparams)) {
+ my $op_file = new IO::File($old_param_file, '>>', 0600)
+ || die "Couldn't create $old_param_file: $!";
+
+ print "The following parameters are no longer used in Bugzilla,",
+ " and so have been\nmoved from your parameters file into",
+ " $old_param_file:\n";
+
+ my $comma = "";
+ foreach my $item (keys %oldparams) {
+ print $op_file "\n\n$item:\n" . $oldparams{$item} . "\n";
+ print "${comma}$item";
+ $comma = ", ";
}
+ print "\n";
+ $op_file->close;
+ }
- # Old mail_delivery_method choices contained no uppercase characters
- my $mta = $param->{'mail_delivery_method'};
- if ($mta) {
- if ($mta !~ /[A-Z]/) {
- my %translation = (
- 'sendmail' => 'Sendmail',
- 'smtp' => 'SMTP',
- 'qmail' => 'Qmail',
- 'testfile' => 'Test',
- 'none' => 'None');
- $param->{'mail_delivery_method'} = $translation{$mta};
- }
- # This will force the parameter to be reset to its default value.
- delete $param->{'mail_delivery_method'} if $param->{'mail_delivery_method'} eq 'Qmail';
- }
+ write_params($param);
- # Convert the old "ssl" parameter to the new "ssl_redirect" parameter.
- # Both "authenticated sessions" and "always" turn on "ssl_redirect"
- # when upgrading.
- if (exists $param->{'ssl'} and $param->{'ssl'} ne 'never') {
- $new_params{'ssl_redirect'} = 1;
- }
-
- # "specific_search_allow_empty_words" has been renamed to "search_allow_no_criteria".
- if (exists $param->{'specific_search_allow_empty_words'}) {
- $new_params{'search_allow_no_criteria'} = $param->{'specific_search_allow_empty_words'};
- }
-
- if (exists $param->{'noresolveonopenblockers'}) {
- $new_params{'resolution_forbidden_with_open_blockers'} = $param->{'noresolveonopenblockers'} ? 'FIXED' : "";
- }
-
- # --- DEFAULTS FOR NEW PARAMS ---
-
- _load_params() unless %params;
- foreach my $name (keys %params) {
- my $item = $params{$name};
- unless (exists $param->{$name}) {
- print "New parameter: $name\n" unless $new_install;
- if (exists $new_params{$name}) {
- $param->{$name} = $new_params{$name};
- }
- elsif (exists $answer->{$name}) {
- $param->{$name} = $answer->{$name};
- }
- elsif ($new_install and exists $NEW_INSTALL_DEFAULT{$name}) {
- $param->{$name} = $NEW_INSTALL_DEFAULT{$name};
- }
- else {
- $param->{$name} = $item->{'default'};
- }
- }
- }
-
- # --- REMOVE OLD PARAMS ---
-
- my %oldparams;
- # Remove any old params
- foreach my $item (keys %$param) {
- if (!exists $params{$item}) {
- $oldparams{$item} = delete $param->{$item};
- }
- }
-
- # Write any old parameters to old-params.txt
- my $old_param_file = "$datadir/old-params.txt";
- if (scalar(keys %oldparams)) {
- my $op_file = new IO::File($old_param_file, '>>', 0600)
- || die "Couldn't create $old_param_file: $!";
-
- print "The following parameters are no longer used in Bugzilla,",
- " and so have been\nmoved from your parameters file into",
- " $old_param_file:\n";
-
- my $comma = "";
- foreach my $item (keys %oldparams) {
- print $op_file "\n\n$item:\n" . $oldparams{$item} . "\n";
- print "${comma}$item";
- $comma = ", ";
- }
- print "\n";
- $op_file->close;
- }
-
- write_params($param);
-
- if (-e $old_file) {
- unlink $old_file;
- say "$old_file has been converted into $old_file.json, using the JSON format.";
- }
+ if (-e $old_file) {
+ unlink $old_file;
+ say "$old_file has been converted into $old_file.json, using the JSON format.";
+ }
- # Return deleted params and values so that checksetup.pl has a chance
- # to convert old params to new data.
- return %oldparams;
+ # Return deleted params and values so that checksetup.pl has a chance
+ # to convert old params to new data.
+ return %oldparams;
}
sub write_params {
- my ($param_data) = @_;
- $param_data ||= Bugzilla->params;
- my $param_file = bz_locations()->{'datadir'} . '/params.json';
+ my ($param_data) = @_;
+ $param_data ||= Bugzilla->params;
+ my $param_file = bz_locations()->{'datadir'} . '/params.json';
- my $json_data = JSON::XS->new->canonical->pretty->encode($param_data);
- write_text($param_file, $json_data);
+ my $json_data = JSON::XS->new->canonical->pretty->encode($param_data);
+ write_text($param_file, $json_data);
- # It's not common to edit parameters and loading
- # Bugzilla::Install::Filesystem is slow.
- require Bugzilla::Install::Filesystem;
- Bugzilla::Install::Filesystem::fix_file_permissions($param_file);
+ # It's not common to edit parameters and loading
+ # Bugzilla::Install::Filesystem is slow.
+ require Bugzilla::Install::Filesystem;
+ Bugzilla::Install::Filesystem::fix_file_permissions($param_file);
- # And now we have to reset the params cache so that Bugzilla will re-read
- # them.
- delete Bugzilla->request_cache->{params};
+ # And now we have to reset the params cache so that Bugzilla will re-read
+ # them.
+ delete Bugzilla->request_cache->{params};
}
sub read_param_file {
- my $params;
- my $file = bz_locations()->{'datadir'} . '/params.json';
-
- if (-e $file) {
- my $data = read_text($file);
- trick_taint($data);
-
- # If params.json has been manually edited and e.g. some quotes are
- # missing, we don't want JSON::XS to leak the content of the file
- # to all users in its error message, so we have to eval'uate it.
- $params = eval { JSON::XS->new->decode($data) };
- if ($@) {
- my $error_msg = (basename($0) eq 'checksetup.pl') ?
- $@ : 'run checksetup.pl to see the details.';
- die "Error parsing $file: $error_msg";
- }
+ my $params;
+ my $file = bz_locations()->{'datadir'} . '/params.json';
+
+ if (-e $file) {
+ my $data = read_text($file);
+ trick_taint($data);
+
+ # If params.json has been manually edited and e.g. some quotes are
+ # missing, we don't want JSON::XS to leak the content of the file
+ # to all users in its error message, so we have to eval'uate it.
+ $params = eval { JSON::XS->new->decode($data) };
+ if ($@) {
+ my $error_msg
+ = (basename($0) eq 'checksetup.pl')
+ ? $@
+ : 'run checksetup.pl to see the details.';
+ die "Error parsing $file: $error_msg";
}
- elsif ($ENV{'SERVER_SOFTWARE'}) {
- # We're in a CGI, but the params file doesn't exist. We can't
- # Template Toolkit, or even install_string, since checksetup
- # might not have thrown an error. Bugzilla::CGI->new
- # hasn't even been called yet, so we manually use CGI::Carp here
- # so that the user sees the error.
- unless (i_am_persistent()) {
- require CGI::Carp;
- CGI::Carp->import('fatalsToBrowser');
- }
- die "The $file file does not exist."
- . ' You probably need to run checksetup.pl.',
+ }
+ elsif ($ENV{'SERVER_SOFTWARE'}) {
+
+ # We're in a CGI, but the params file doesn't exist. We can't
+ # Template Toolkit, or even install_string, since checksetup
+ # might not have thrown an error. Bugzilla::CGI->new
+ # hasn't even been called yet, so we manually use CGI::Carp here
+ # so that the user sees the error.
+ unless (i_am_persistent()) {
+ require CGI::Carp;
+ CGI::Carp->import('fatalsToBrowser');
}
- return $params // {};
+ die "The $file file does not exist."
+ . ' You probably need to run checksetup.pl.',;
+ }
+ return $params // {};
}
1;
diff --git a/Bugzilla/Config/Admin.pm b/Bugzilla/Config/Admin.pm
index c21ae5310b..44173366f5 100644
--- a/Bugzilla/Config/Admin.pm
+++ b/Bugzilla/Config/Admin.pm
@@ -16,32 +16,21 @@ use Bugzilla::Config::Common;
our $sortkey = 200;
sub get_param_list {
- my $class = shift;
+ my $class = shift;
my @param_list = (
- {
- name => 'allowbugdeletion',
- type => 'b',
- default => 0
- },
-
- {
- name => 'allowemailchange',
- type => 'b',
- default => 1
- },
-
- {
- name => 'allowuserdeletion',
- type => 'b',
- default => 0
- },
-
- {
- name => 'last_visit_keep_days',
- type => 't',
- default => 10,
- checker => \&check_numeric
- });
+ {name => 'allowbugdeletion', type => 'b', default => 0},
+
+ {name => 'allowemailchange', type => 'b', default => 1},
+
+ {name => 'allowuserdeletion', type => 'b', default => 0},
+
+ {
+ name => 'last_visit_keep_days',
+ type => 't',
+ default => 10,
+ checker => \&check_numeric
+ }
+ );
return @param_list;
}
diff --git a/Bugzilla/Config/Advanced.pm b/Bugzilla/Config/Advanced.pm
index 7a61bf7c47..5ef16568f4 100644
--- a/Bugzilla/Config/Advanced.pm
+++ b/Bugzilla/Config/Advanced.pm
@@ -17,36 +17,32 @@ our $sortkey = 1700;
use constant get_param_list => (
{
- name => 'inbound_proxies',
- type => 't',
- default => '',
- checker => \&check_inbound_proxies
+ name => 'inbound_proxies',
+ type => 't',
+ default => '',
+ checker => \&check_inbound_proxies
},
- {
- name => 'proxy_url',
- type => 't',
- default => ''
- },
+ {name => 'proxy_url', type => 't', default => ''},
{
- name => 'strict_transport_security',
- type => 's',
- choices => ['off', 'this_domain_only', 'include_subdomains'],
- default => 'off',
- checker => \&check_multi
+ name => 'strict_transport_security',
+ type => 's',
+ choices => ['off', 'this_domain_only', 'include_subdomains'],
+ default => 'off',
+ checker => \&check_multi
},
);
sub check_inbound_proxies {
- my $inbound_proxies = shift;
-
- return "" if $inbound_proxies eq "*";
- my @proxies = split(/[\s,]+/, $inbound_proxies);
- foreach my $proxy (@proxies) {
- validate_ip($proxy) || return "$proxy is not a valid IPv4 or IPv6 address";
- }
- return "";
+ my $inbound_proxies = shift;
+
+ return "" if $inbound_proxies eq "*";
+ my @proxies = split(/[\s,]+/, $inbound_proxies);
+ foreach my $proxy (@proxies) {
+ validate_ip($proxy) || return "$proxy is not a valid IPv4 or IPv6 address";
+ }
+ return "";
}
1;
diff --git a/Bugzilla/Config/Attachment.pm b/Bugzilla/Config/Attachment.pm
index daa844ba59..a37b2b1519 100644
--- a/Bugzilla/Config/Attachment.pm
+++ b/Bugzilla/Config/Attachment.pm
@@ -16,56 +16,49 @@ use Bugzilla::Config::Common;
our $sortkey = 400;
sub get_param_list {
- my $class = shift;
+ my $class = shift;
my @param_list = (
- {
- name => 'allow_attachment_display',
- type => 'b',
- default => 0
- },
+ {name => 'allow_attachment_display', type => 'b', default => 0},
- {
- name => 'attachment_base',
- type => 't',
- default => '',
- checker => \&check_urlbase
- },
+ {
+ name => 'attachment_base',
+ type => 't',
+ default => '',
+ checker => \&check_urlbase
+ },
- {
- name => 'allow_attachment_deletion',
- type => 'b',
- default => 0
- },
+ {name => 'allow_attachment_deletion', type => 'b', default => 0},
- {
- name => 'xsendfile_header',
- type => 's',
- choices => ['off', 'X-Sendfile', 'X-Accel-Redirect', 'X-LIGHTTPD-send-file'],
- default => 'off',
- checker => \&check_multi
- },
+ {
+ name => 'xsendfile_header',
+ type => 's',
+ choices => ['off', 'X-Sendfile', 'X-Accel-Redirect', 'X-LIGHTTPD-send-file'],
+ default => 'off',
+ checker => \&check_multi
+ },
- {
- name => 'maxattachmentsize',
- type => 't',
- default => '1000',
- checker => \&check_maxattachmentsize
- },
+ {
+ name => 'maxattachmentsize',
+ type => 't',
+ default => '1000',
+ checker => \&check_maxattachmentsize
+ },
- # The maximum size (in bytes) for patches and non-patch attachments.
- # The default limit is 1000KB, which is 24KB less than mysql's default
- # maximum packet size (which determines how much data can be sent in a
- # single mysql packet and thus how much data can be inserted into the
- # database) to provide breathing space for the data in other fields of
- # the attachment record as well as any mysql packet overhead (I don't
- # know of any, but I suspect there may be some.)
+ # The maximum size (in bytes) for patches and non-patch attachments.
+ # The default limit is 1000KB, which is 24KB less than mysql's default
+ # maximum packet size (which determines how much data can be sent in a
+ # single mysql packet and thus how much data can be inserted into the
+ # database) to provide breathing space for the data in other fields of
+ # the attachment record as well as any mysql packet overhead (I don't
+ # know of any, but I suspect there may be some.)
- {
- name => 'maxlocalattachment',
- type => 't',
- default => '0',
- checker => \&check_numeric
- } );
+ {
+ name => 'maxlocalattachment',
+ type => 't',
+ default => '0',
+ checker => \&check_numeric
+ }
+ );
return @param_list;
}
diff --git a/Bugzilla/Config/Auth.pm b/Bugzilla/Config/Auth.pm
index 326c4cd3f6..d559e3b247 100644
--- a/Bugzilla/Config/Auth.pm
+++ b/Bugzilla/Config/Auth.pm
@@ -16,111 +16,89 @@ use Bugzilla::Config::Common;
our $sortkey = 300;
sub get_param_list {
- my $class = shift;
+ my $class = shift;
my @param_list = (
- {
- name => 'auth_env_id',
- type => 't',
- default => '',
- },
-
- {
- name => 'auth_env_email',
- type => 't',
- default => '',
- },
-
- {
- name => 'auth_env_realname',
- type => 't',
- default => '',
- },
-
- # XXX in the future:
- #
- # user_verify_class and user_info_class should have choices gathered from
- # whatever sits in their respective directories
- #
- # rather than comma-separated lists, these two should eventually become
- # arrays, but that requires alterations to editparams first
-
- {
- name => 'user_info_class',
- type => 's',
- choices => [ 'CGI', 'Env', 'Env,CGI' ],
- default => 'CGI',
- checker => \&check_multi
- },
-
- {
- name => 'user_verify_class',
- type => 'o',
- choices => [ 'DB', 'RADIUS', 'LDAP' ],
- default => 'DB',
- checker => \&check_user_verify_class
- },
-
- {
- name => 'rememberlogin',
- type => 's',
- choices => ['on', 'defaulton', 'defaultoff', 'off'],
- default => 'on',
- checker => \&check_multi
- },
-
- {
- name => 'requirelogin',
- type => 'b',
- default => '0'
- },
-
- {
- name => 'emailregexp',
- type => 't',
- default => q:^[\\w\\.\\+\\-=']+@[\\w\\.\\-]+\\.[\\w\\-]+$:,
- checker => \&check_regexp
- },
-
- {
- name => 'emailregexpdesc',
- type => 'l',
- default => 'A legal address must contain exactly one \'@\', and at least ' .
- 'one \'.\' after the @.'
- },
-
- {
- name => 'use_email_as_login',
- type => 'b',
- default => '1',
- onchange => \&change_use_email_as_login
- },
-
- {
- name => 'createemailregexp',
- type => 't',
- default => q:.*:,
- checker => \&check_regexp
- },
-
- {
- name => 'password_complexity',
- type => 's',
- choices => [ 'no_constraints', 'mixed_letters', 'letters_numbers',
- 'letters_numbers_specialchars' ],
- default => 'no_constraints',
- checker => \&check_multi
- },
-
- {
- name => 'password_check_on_login',
- type => 'b',
- default => '1'
- },
- {
- name => 'auth_delegation',
- type => 'b',
- default => 0,
- },
+ {name => 'auth_env_id', type => 't', default => '',},
+
+ {name => 'auth_env_email', type => 't', default => '',},
+
+ {name => 'auth_env_realname', type => 't', default => '',},
+
+ # XXX in the future:
+ #
+ # user_verify_class and user_info_class should have choices gathered from
+ # whatever sits in their respective directories
+ #
+ # rather than comma-separated lists, these two should eventually become
+ # arrays, but that requires alterations to editparams first
+
+ {
+ name => 'user_info_class',
+ type => 's',
+ choices => ['CGI', 'Env', 'Env,CGI'],
+ default => 'CGI',
+ checker => \&check_multi
+ },
+
+ {
+ name => 'user_verify_class',
+ type => 'o',
+ choices => ['DB', 'RADIUS', 'LDAP'],
+ default => 'DB',
+ checker => \&check_user_verify_class
+ },
+
+ {
+ name => 'rememberlogin',
+ type => 's',
+ choices => ['on', 'defaulton', 'defaultoff', 'off'],
+ default => 'on',
+ checker => \&check_multi
+ },
+
+ {name => 'requirelogin', type => 'b', default => '0'},
+
+ {
+ name => 'emailregexp',
+ type => 't',
+ default => q:^[\\w\\.\\+\\-=']+@[\\w\\.\\-]+\\.[\\w\\-]+$:,
+ checker => \&check_regexp
+ },
+
+ {
+ name => 'emailregexpdesc',
+ type => 'l',
+ default => 'A legal address must contain exactly one \'@\', and at least '
+ . 'one \'.\' after the @.'
+ },
+
+ {
+ name => 'use_email_as_login',
+ type => 'b',
+ default => '1',
+ onchange => \&change_use_email_as_login
+ },
+
+ {
+ name => 'createemailregexp',
+ type => 't',
+ default => q:.*:,
+ checker => \&check_regexp
+ },
+
+ {
+ name => 'password_complexity',
+ type => 's',
+ choices => [
+ 'no_constraints', 'mixed_letters',
+ 'letters_numbers', 'letters_numbers_specialchars'
+ ],
+ default => 'no_constraints',
+ checker => \&check_multi
+ },
+
+ {name => 'password_check_on_login', type => 'b', default => '1'},
+ {name => 'auth_delegation', type => 'b', default => 0,},
);
return @param_list;
}
diff --git a/Bugzilla/Config/BugChange.pm b/Bugzilla/Config/BugChange.pm
index b6cd7ae436..0028cf95f6 100644
--- a/Bugzilla/Config/BugChange.pm
+++ b/Bugzilla/Config/BugChange.pm
@@ -27,60 +27,47 @@ sub get_param_list {
# and bug_status.is_open is not yet defined (hence the eval), so we use
# the bug statuses above as they are still hardcoded.
eval {
- my @current_closed_states = map {$_->name} closed_bug_statuses();
- # If no closed state was found, use the default list above.
- @closed_bug_statuses = @current_closed_states if scalar(@current_closed_states);
+ my @current_closed_states = map { $_->name } closed_bug_statuses();
+
+ # If no closed state was found, use the default list above.
+ @closed_bug_statuses = @current_closed_states if scalar(@current_closed_states);
};
my @param_list = (
- {
- name => 'duplicate_or_move_bug_status',
- type => 's',
- choices => \@closed_bug_statuses,
- default => $closed_bug_statuses[0],
- checker => \&check_bug_status,
- onchange => \&change_duplicate_or_move_bug_status
- },
-
- {
- name => 'letsubmitterchoosepriority',
- type => 'b',
- default => 1
- },
-
- {
- name => 'letsubmitterchoosemilestone',
- type => 'b',
- default => 1
- },
-
- {
- name => 'commentonchange_resolution',
- type => 'b',
- default => 0
- },
-
- {
- name => 'commentonduplicate',
- type => 'b',
- default => 0
- },
-
- {
- name => 'resolution_forbidden_with_open_blockers',
- type => 's',
- choices => \&_get_resolutions,
- default => '',
- checker => \&check_resolution,
- } );
+ {
+ name => 'duplicate_or_move_bug_status',
+ type => 's',
+ choices => \@closed_bug_statuses,
+ default => $closed_bug_statuses[0],
+ checker => \&check_bug_status,
+ onchange => \&change_duplicate_or_move_bug_status
+ },
+
+ {name => 'letsubmitterchoosepriority', type => 'b', default => 1},
+
+ {name => 'letsubmitterchoosemilestone', type => 'b', default => 1},
+
+ {name => 'commentonchange_resolution', type => 'b', default => 0},
+
+ {name => 'commentonduplicate', type => 'b', default => 0},
+
+ {
+ name => 'resolution_forbidden_with_open_blockers',
+ type => 's',
+ choices => \&_get_resolutions,
+ default => '',
+ checker => \&check_resolution,
+ }
+ );
return @param_list;
}
sub _get_resolutions {
- my $resolution_field = Bugzilla::Field->new({ name => 'resolution', cache => 1 });
- # The empty resolution is included - it represents "no value".
- return [ map { $_->name } @{ $resolution_field->legal_values } ];
+ my $resolution_field = Bugzilla::Field->new({name => 'resolution', cache => 1});
+
+ # The empty resolution is included - it represents "no value".
+ return [map { $_->name } @{$resolution_field->legal_values}];
}
1;
diff --git a/Bugzilla/Config/BugFields.pm b/Bugzilla/Config/BugFields.pm
index f6c20cd121..adc6fe7ccb 100644
--- a/Bugzilla/Config/BugFields.pm
+++ b/Bugzilla/Config/BugFields.pm
@@ -25,73 +25,50 @@ sub get_param_list {
my @legal_OS = @{get_legal_field_values('op_sys')};
my @param_list = (
- {
- name => 'useclassification',
- type => 'b',
- default => 0
- },
-
- {
- name => 'usetargetmilestone',
- type => 'b',
- default => 0
- },
-
- {
- name => 'useqacontact',
- type => 'b',
- default => 0
- },
-
- {
- name => 'usestatuswhiteboard',
- type => 'b',
- default => 0
- },
-
- {
- name => 'use_see_also',
- type => 'b',
- default => 1
- },
-
- {
- name => 'defaultpriority',
- type => 's',
- choices => \@legal_priorities,
- default => $legal_priorities[-1],
- checker => \&check_priority
- },
-
- {
- name => 'defaultseverity',
- type => 's',
- choices => \@legal_severities,
- default => $legal_severities[-1],
- checker => \&check_severity
- },
-
- {
- name => 'defaultplatform',
- type => 's',
- choices => ['', @legal_platforms],
- default => '',
- checker => \&check_platform
- },
-
- {
- name => 'defaultopsys',
- type => 's',
- choices => ['', @legal_OS],
- default => '',
- checker => \&check_opsys
- },
-
- {
- name => 'collapsed_comment_tags',
- type => 't',
- default => 'obsolete, spam',
- });
+ {name => 'useclassification', type => 'b', default => 0},
+
+ {name => 'usetargetmilestone', type => 'b', default => 0},
+
+ {name => 'useqacontact', type => 'b', default => 0},
+
+ {name => 'usestatuswhiteboard', type => 'b', default => 0},
+
+ {name => 'use_see_also', type => 'b', default => 1},
+
+ {
+ name => 'defaultpriority',
+ type => 's',
+ choices => \@legal_priorities,
+ default => $legal_priorities[-1],
+ checker => \&check_priority
+ },
+
+ {
+ name => 'defaultseverity',
+ type => 's',
+ choices => \@legal_severities,
+ default => $legal_severities[-1],
+ checker => \&check_severity
+ },
+
+ {
+ name => 'defaultplatform',
+ type => 's',
+ choices => ['', @legal_platforms],
+ default => '',
+ checker => \&check_platform
+ },
+
+ {
+ name => 'defaultopsys',
+ type => 's',
+ choices => ['', @legal_OS],
+ default => '',
+ checker => \&check_opsys
+ },
+
+ {name => 'collapsed_comment_tags', type => 't', default => 'obsolete, spam',}
+ );
return @param_list;
}
diff --git a/Bugzilla/Config/Common.pm b/Bugzilla/Config/Common.pm
index 633325291a..3f7e3a0461 100644
--- a/Bugzilla/Config/Common.pm
+++ b/Bugzilla/Config/Common.pm
@@ -21,347 +21,357 @@ use Bugzilla::Group;
use Bugzilla::Status;
use parent qw(Exporter);
-@Bugzilla::Config::Common::EXPORT =
- qw(check_multi check_numeric check_regexp check_group
- check_sslbase check_priority check_severity check_platform
- check_opsys check_shadowdb check_urlbase check_user_verify_class
- check_ip check_mail_delivery_method check_notification
- check_bug_status check_smtp_auth check_theschwartz_available
- check_maxattachmentsize check_email check_smtp_ssl
- check_comment_taggers_group check_smtp_server check_resolution
-
- change_use_email_as_login change_duplicate_or_move_bug_status
+@Bugzilla::Config::Common::EXPORT
+ = qw(check_multi check_numeric check_regexp check_group
+ check_sslbase check_priority check_severity check_platform
+ check_opsys check_shadowdb check_urlbase check_user_verify_class
+ check_ip check_mail_delivery_method check_notification
+ check_bug_status check_smtp_auth check_theschwartz_available
+ check_maxattachmentsize check_email check_smtp_ssl
+ check_comment_taggers_group check_smtp_server check_resolution
+
+ change_use_email_as_login change_duplicate_or_move_bug_status
);
# Checking functions for the various values
sub check_multi {
- my ($value, $param) = (@_);
+ my ($value, $param) = (@_);
- if ($param->{'type'} eq "s") {
- unless (scalar(grep {$_ eq $value} (@{$param->{'choices'}}))) {
- return "Invalid choice '$value' for single-select list param '$param->{'name'}'";
- }
-
- return "";
+ if ($param->{'type'} eq "s") {
+ unless (scalar(grep { $_ eq $value } (@{$param->{'choices'}}))) {
+ return
+ "Invalid choice '$value' for single-select list param '$param->{'name'}'";
}
- elsif ($param->{'type'} eq 'm' || $param->{'type'} eq 'o') {
- if (ref($value) ne "ARRAY") {
- $value = [split(',', $value)]
- }
- foreach my $chkParam (@$value) {
- unless (scalar(grep {$_ eq $chkParam} (@{$param->{'choices'}}))) {
- return "Invalid choice '$chkParam' for multi-select list param '$param->{'name'}'";
- }
- }
-
- return "";
+
+ return "";
+ }
+ elsif ($param->{'type'} eq 'm' || $param->{'type'} eq 'o') {
+ if (ref($value) ne "ARRAY") {
+ $value = [split(',', $value)];
}
- else {
- return "Invalid param type '$param->{'type'}' for check_multi(); " .
- "contact your Bugzilla administrator";
+ foreach my $chkParam (@$value) {
+ unless (scalar(grep { $_ eq $chkParam } (@{$param->{'choices'}}))) {
+ return
+ "Invalid choice '$chkParam' for multi-select list param '$param->{'name'}'";
+ }
}
+
+ return "";
+ }
+ else {
+ return "Invalid param type '$param->{'type'}' for check_multi(); "
+ . "contact your Bugzilla administrator";
+ }
}
sub check_numeric {
- my ($value) = (@_);
- if ($value !~ /^[0-9]+$/) {
- return "must be a numeric value";
- }
- return "";
+ my ($value) = (@_);
+ if ($value !~ /^[0-9]+$/) {
+ return "must be a numeric value";
+ }
+ return "";
}
sub check_regexp {
- my ($value) = (@_);
- eval { qr/$value/ };
- return $@;
+ my ($value) = (@_);
+ eval {qr/$value/};
+ return $@;
}
sub check_email {
- my ($value) = @_;
- if ($value !~ $Email::Address::mailbox) {
- return "must be a valid email address.";
- }
- return "";
+ my ($value) = @_;
+ if ($value !~ $Email::Address::mailbox) {
+ return "must be a valid email address.";
+ }
+ return "";
}
sub check_sslbase {
- my $url = shift;
- if ($url ne '') {
- if ($url !~ m#^https://([^/]+).*/$#) {
- return "must be a legal URL, that starts with https and ends with a slash";
- }
- my $host = $1;
- # Fall back to port 443 if for some reason getservbyname() fails.
- my $port = getservbyname('https', 'tcp') || 443;
- if ($host =~ /^(.+):(\d+)$/) {
- $host = $1;
- $port = $2;
- }
- local *SOCK;
- my $proto = getprotobyname('tcp');
- socket(SOCK, PF_INET, SOCK_STREAM, $proto);
- my $iaddr = inet_aton($host) || return "The host $host cannot be resolved";
- my $sin = sockaddr_in($port, $iaddr);
- if (!connect(SOCK, $sin)) {
- return "Failed to connect to $host:$port ($!); unable to enable SSL";
- }
- close(SOCK);
+ my $url = shift;
+ if ($url ne '') {
+ if ($url !~ m#^https://([^/]+).*/$#) {
+ return "must be a legal URL, that starts with https and ends with a slash";
}
- return "";
+ my $host = $1;
+
+ # Fall back to port 443 if for some reason getservbyname() fails.
+ my $port = getservbyname('https', 'tcp') || 443;
+ if ($host =~ /^(.+):(\d+)$/) {
+ $host = $1;
+ $port = $2;
+ }
+ local *SOCK;
+ my $proto = getprotobyname('tcp');
+ socket(SOCK, PF_INET, SOCK_STREAM, $proto);
+ my $iaddr = inet_aton($host) || return "The host $host cannot be resolved";
+ my $sin = sockaddr_in($port, $iaddr);
+ if (!connect(SOCK, $sin)) {
+ return "Failed to connect to $host:$port ($!); unable to enable SSL";
+ }
+ close(SOCK);
+ }
+ return "";
}
sub check_ip {
- my $inbound_proxies = shift;
- my @proxies = split(/[\s,]+/, $inbound_proxies);
- foreach my $proxy (@proxies) {
- validate_ip($proxy) || return "$proxy is not a valid IPv4 or IPv6 address";
- }
- return "";
+ my $inbound_proxies = shift;
+ my @proxies = split(/[\s,]+/, $inbound_proxies);
+ foreach my $proxy (@proxies) {
+ validate_ip($proxy) || return "$proxy is not a valid IPv4 or IPv6 address";
+ }
+ return "";
}
sub check_priority {
- my ($value) = (@_);
- my $legal_priorities = get_legal_field_values('priority');
- if (!grep($_ eq $value, @$legal_priorities)) {
- return "Must be a legal priority value: one of " .
- join(", ", @$legal_priorities);
- }
- return "";
+ my ($value) = (@_);
+ my $legal_priorities = get_legal_field_values('priority');
+ if (!grep($_ eq $value, @$legal_priorities)) {
+ return "Must be a legal priority value: one of "
+ . join(", ", @$legal_priorities);
+ }
+ return "";
}
sub check_severity {
- my ($value) = (@_);
- my $legal_severities = get_legal_field_values('bug_severity');
- if (!grep($_ eq $value, @$legal_severities)) {
- return "Must be a legal severity value: one of " .
- join(", ", @$legal_severities);
- }
- return "";
+ my ($value) = (@_);
+ my $legal_severities = get_legal_field_values('bug_severity');
+ if (!grep($_ eq $value, @$legal_severities)) {
+ return "Must be a legal severity value: one of "
+ . join(", ", @$legal_severities);
+ }
+ return "";
}
sub check_platform {
- my ($value) = (@_);
- my $legal_platforms = get_legal_field_values('rep_platform');
- if (!grep($_ eq $value, '', @$legal_platforms)) {
- return "Must be empty or a legal platform value: one of " .
- join(", ", @$legal_platforms);
- }
- return "";
+ my ($value) = (@_);
+ my $legal_platforms = get_legal_field_values('rep_platform');
+ if (!grep($_ eq $value, '', @$legal_platforms)) {
+ return "Must be empty or a legal platform value: one of "
+ . join(", ", @$legal_platforms);
+ }
+ return "";
}
sub check_opsys {
- my ($value) = (@_);
- my $legal_OS = get_legal_field_values('op_sys');
- if (!grep($_ eq $value, '', @$legal_OS)) {
- return "Must be empty or a legal operating system value: one of " .
- join(", ", @$legal_OS);
- }
- return "";
+ my ($value) = (@_);
+ my $legal_OS = get_legal_field_values('op_sys');
+ if (!grep($_ eq $value, '', @$legal_OS)) {
+ return "Must be empty or a legal operating system value: one of "
+ . join(", ", @$legal_OS);
+ }
+ return "";
}
sub check_bug_status {
- my $bug_status = shift;
- my @closed_bug_statuses = map {$_->name} closed_bug_statuses();
- if (!grep($_ eq $bug_status, @closed_bug_statuses)) {
- return "Must be a valid closed status: one of " . join(', ', @closed_bug_statuses);
- }
- return "";
+ my $bug_status = shift;
+ my @closed_bug_statuses = map { $_->name } closed_bug_statuses();
+ if (!grep($_ eq $bug_status, @closed_bug_statuses)) {
+ return "Must be a valid closed status: one of "
+ . join(', ', @closed_bug_statuses);
+ }
+ return "";
}
sub check_resolution {
- my $resolution = shift;
- my $resolution_field = Bugzilla::Field->new({ name => 'resolution', cache => 1 });
- # The empty resolution is included - it represents "no value"
- my @resolutions = map {$_->name} @{ $resolution_field->legal_values };
+ my $resolution = shift;
+ my $resolution_field = Bugzilla::Field->new({name => 'resolution', cache => 1});
- if (!grep($_ eq $resolution, @resolutions)) {
- return "Must be blank or a valid resolution: one of " . join(', ', @resolutions);
- }
- return "";
+ # The empty resolution is included - it represents "no value"
+ my @resolutions = map { $_->name } @{$resolution_field->legal_values};
+
+ if (!grep($_ eq $resolution, @resolutions)) {
+ return "Must be blank or a valid resolution: one of "
+ . join(', ', @resolutions);
+ }
+ return "";
}
sub check_group {
- my $group_name = shift;
- return "" unless $group_name;
- my $group = new Bugzilla::Group({'name' => $group_name});
- unless (defined $group) {
- return "Must be an existing group name";
- }
- return "";
+ my $group_name = shift;
+ return "" unless $group_name;
+ my $group = new Bugzilla::Group({'name' => $group_name});
+ unless (defined $group) {
+ return "Must be an existing group name";
+ }
+ return "";
}
sub check_shadowdb {
- my ($value) = (@_);
- $value = trim($value);
- if ($value eq "") {
- return "";
- }
+ my ($value) = (@_);
+ $value = trim($value);
+ if ($value eq "") {
+ return "";
+ }
- if (!Bugzilla->params->{'shadowdbhost'}) {
- return "You need to specify a host when using a shadow database";
- }
+ if (!Bugzilla->params->{'shadowdbhost'}) {
+ return "You need to specify a host when using a shadow database";
+ }
- # Can't test existence of this because ConnectToDatabase uses the param,
- # but we can't set this before testing....
- # This can really only be fixed after we can use the DBI more openly
- return "";
+ # Can't test existence of this because ConnectToDatabase uses the param,
+ # but we can't set this before testing....
+ # This can really only be fixed after we can use the DBI more openly
+ return "";
}
sub check_urlbase {
- my ($url) = (@_);
- if ($url && $url !~ m:^http.*/$:) {
- return "must be a legal URL, that starts with http and ends with a slash.";
- }
- return "";
+ my ($url) = (@_);
+ if ($url && $url !~ m:^http.*/$:) {
+ return "must be a legal URL, that starts with http and ends with a slash.";
+ }
+ return "";
}
sub check_user_verify_class {
- # doeditparams traverses the list of params, and for each one it checks,
- # then updates. This means that if one param checker wants to look at
- # other params, it must be below that other one. So you can't have two
- # params mutually dependent on each other.
- # This means that if someone clears the LDAP config params after setting
- # the login method as LDAP, we won't notice, but all logins will fail.
- # So don't do that.
-
- my $params = Bugzilla->params;
- my ($list, $entry) = @_;
- $list || return 'You need to specify at least one authentication mechanism';
- for my $class (split /,\s*/, $list) {
- my $res = check_multi($class, $entry);
- return $res if $res;
- if ($class eq 'RADIUS') {
- if (!Bugzilla->feature('auth_radius')) {
- return "RADIUS support is not available. Run checksetup.pl"
- . " for more details";
- }
- return "RADIUS servername (RADIUS_server) is missing"
- if !$params->{"RADIUS_server"};
- return "RADIUS_secret is empty" if !$params->{"RADIUS_secret"};
- }
- elsif ($class eq 'LDAP') {
- if (!Bugzilla->feature('auth_ldap')) {
- return "LDAP support is not available. Run checksetup.pl"
- . " for more details";
- }
- return "LDAP servername (LDAPserver) is missing"
- if !$params->{"LDAPserver"};
- return "LDAPBaseDN is empty" if !$params->{"LDAPBaseDN"};
- }
+
+ # doeditparams traverses the list of params, and for each one it checks,
+ # then updates. This means that if one param checker wants to look at
+ # other params, it must be below that other one. So you can't have two
+ # params mutually dependent on each other.
+ # This means that if someone clears the LDAP config params after setting
+ # the login method as LDAP, we won't notice, but all logins will fail.
+ # So don't do that.
+
+ my $params = Bugzilla->params;
+ my ($list, $entry) = @_;
+ $list || return 'You need to specify at least one authentication mechanism';
+ for my $class (split /,\s*/, $list) {
+ my $res = check_multi($class, $entry);
+ return $res if $res;
+ if ($class eq 'RADIUS') {
+ if (!Bugzilla->feature('auth_radius')) {
+ return "RADIUS support is not available. Run checksetup.pl"
+ . " for more details";
+ }
+ return "RADIUS servername (RADIUS_server) is missing"
+ if !$params->{"RADIUS_server"};
+ return "RADIUS_secret is empty" if !$params->{"RADIUS_secret"};
}
- return "";
+ elsif ($class eq 'LDAP') {
+ if (!Bugzilla->feature('auth_ldap')) {
+ return "LDAP support is not available. Run checksetup.pl" . " for more details";
+ }
+ return "LDAP servername (LDAPserver) is missing" if !$params->{"LDAPserver"};
+ return "LDAPBaseDN is empty" if !$params->{"LDAPBaseDN"};
+ }
+ }
+ return "";
}
sub check_mail_delivery_method {
- my $check = check_multi(@_);
- return $check if $check;
- my $mailer = shift;
- if ($mailer eq 'Sendmail' and ON_WINDOWS) {
- # look for sendmail.exe
- return "Failed to locate " . SENDMAIL_EXE
- unless -e SENDMAIL_EXE;
- }
- return "";
+ my $check = check_multi(@_);
+ return $check if $check;
+ my $mailer = shift;
+ if ($mailer eq 'Sendmail' and ON_WINDOWS) {
+
+ # look for sendmail.exe
+ return "Failed to locate " . SENDMAIL_EXE unless -e SENDMAIL_EXE;
+ }
+ return "";
}
sub check_maxattachmentsize {
- my $check = check_numeric(@_);
- return $check if $check;
- my $size = shift;
- my $dbh = Bugzilla->dbh;
- if ($dbh->isa('Bugzilla::DB::Mysql')) {
- my (undef, $max_packet) = $dbh->selectrow_array(
- q{SHOW VARIABLES LIKE 'max\_allowed\_packet'});
- my $byte_size = $size * 1024;
- if ($max_packet < $byte_size) {
- return "You asked for a maxattachmentsize of $byte_size bytes,"
- . " but the max_allowed_packet setting in MySQL currently"
- . " only allows packets up to $max_packet bytes";
- }
+ my $check = check_numeric(@_);
+ return $check if $check;
+ my $size = shift;
+ my $dbh = Bugzilla->dbh;
+ if ($dbh->isa('Bugzilla::DB::Mysql')) {
+ my (undef, $max_packet)
+ = $dbh->selectrow_array(q{SHOW VARIABLES LIKE 'max\_allowed\_packet'});
+ my $byte_size = $size * 1024;
+ if ($max_packet < $byte_size) {
+ return
+ "You asked for a maxattachmentsize of $byte_size bytes,"
+ . " but the max_allowed_packet setting in MySQL currently"
+ . " only allows packets up to $max_packet bytes";
}
- return "";
+ }
+ return "";
}
sub check_notification {
- my $option = shift;
- my @current_version =
- (BUGZILLA_VERSION =~ m/^(\d+)\.(\d+)(?:(rc|\.)(\d+))?\+?$/);
- if ($current_version[1] % 2 && $option eq 'stable_branch_release') {
- return "You are currently running a development snapshot, and so your " .
- "installation is not based on a branch. If you want to be notified " .
- "about the next stable release, you should select " .
- "'latest_stable_release' instead";
- }
- if ($option ne 'disabled' && !Bugzilla->feature('updates')) {
- return "Some Perl modules are missing to get notifications about " .
- "new releases. See the output of checksetup.pl for more information";
- }
- return "";
+ my $option = shift;
+ my @current_version
+ = (BUGZILLA_VERSION =~ m/^(\d+)\.(\d+)(?:(rc|\.)(\d+))?\+?$/);
+ if ($current_version[1] % 2 && $option eq 'stable_branch_release') {
+ return
+ "You are currently running a development snapshot, and so your "
+ . "installation is not based on a branch. If you want to be notified "
+ . "about the next stable release, you should select "
+ . "'latest_stable_release' instead";
+ }
+ if ($option ne 'disabled' && !Bugzilla->feature('updates')) {
+ return "Some Perl modules are missing to get notifications about "
+ . "new releases. See the output of checksetup.pl for more information";
+ }
+ return "";
}
sub check_smtp_server {
- my $host = shift;
- my $port;
+ my $host = shift;
+ my $port;
- return '' unless $host;
+ return '' unless $host;
- if ($host =~ /:/) {
- ($host, $port) = split(/:/, $host, 2);
- unless ($port && detaint_natural($port)) {
- return "Invalid port. It must be an integer (typically 25, 465 or 587)";
- }
+ if ($host =~ /:/) {
+ ($host, $port) = split(/:/, $host, 2);
+ unless ($port && detaint_natural($port)) {
+ return "Invalid port. It must be an integer (typically 25, 465 or 587)";
}
- trick_taint($host);
- # Let's first try to connect using SSL. If this fails, we fall back to
- # an unencrypted connection.
- foreach my $method (['Net::SMTP::SSL', 465], ['Net::SMTP', 25]) {
- my ($class, $default_port) = @$method;
- next if $class eq 'Net::SMTP::SSL' && !Bugzilla->feature('smtp_ssl');
- eval "require $class";
- my $smtp = $class->new($host, Port => $port || $default_port, Timeout => 5);
- if ($smtp) {
- # The connection works!
- $smtp->quit;
- return '';
- }
+ }
+ trick_taint($host);
+
+ # Let's first try to connect using SSL. If this fails, we fall back to
+ # an unencrypted connection.
+ foreach my $method (['Net::SMTP::SSL', 465], ['Net::SMTP', 25]) {
+ my ($class, $default_port) = @$method;
+ next if $class eq 'Net::SMTP::SSL' && !Bugzilla->feature('smtp_ssl');
+ eval "require $class";
+ my $smtp = $class->new($host, Port => $port || $default_port, Timeout => 5);
+ if ($smtp) {
+
+ # The connection works!
+ $smtp->quit;
+ return '';
}
- return "Cannot connect to $host" . ($port ? " using port $port" : "");
+ }
+ return "Cannot connect to $host" . ($port ? " using port $port" : "");
}
sub check_smtp_auth {
- my $username = shift;
- if ($username and !Bugzilla->feature('smtp_auth')) {
- return "SMTP Authentication is not available. Run checksetup.pl for"
- . " more details";
- }
- return "";
+ my $username = shift;
+ if ($username and !Bugzilla->feature('smtp_auth')) {
+ return "SMTP Authentication is not available. Run checksetup.pl for"
+ . " more details";
+ }
+ return "";
}
sub check_smtp_ssl {
- my $use_ssl = shift;
- if ($use_ssl && !Bugzilla->feature('smtp_ssl')) {
- return "SSL support is not available. Run checksetup.pl for more details";
- }
- return "";
+ my $use_ssl = shift;
+ if ($use_ssl && !Bugzilla->feature('smtp_ssl')) {
+ return "SSL support is not available. Run checksetup.pl for more details";
+ }
+ return "";
}
sub check_theschwartz_available {
- my $use_queue = shift;
- if ($use_queue && !Bugzilla->feature('jobqueue')) {
- return "Using the job queue requires that you have certain Perl"
- . " modules installed. See the output of checksetup.pl"
- . " for more information";
- }
- return "";
+ my $use_queue = shift;
+ if ($use_queue && !Bugzilla->feature('jobqueue')) {
+ return
+ "Using the job queue requires that you have certain Perl"
+ . " modules installed. See the output of checksetup.pl"
+ . " for more information";
+ }
+ return "";
}
sub check_comment_taggers_group {
- my $group_name = shift;
- if ($group_name && !Bugzilla->feature('jsonrpc')) {
- return "Comment tagging requires installation of the JSONRPC feature";
- }
- return check_group($group_name);
+ my $group_name = shift;
+ if ($group_name && !Bugzilla->feature('jsonrpc')) {
+ return "Comment tagging requires installation of the JSONRPC feature";
+ }
+ return check_group($group_name);
}
# Change handler functions for various parameters
@@ -369,15 +379,15 @@ sub check_comment_taggers_group {
# If use_email_as_login is turned on, update all login names to be email
# addresses.
sub change_use_email_as_login {
- my $newvalue = shift;
- if ($newvalue) {
- Bugzilla->dbh->do('UPDATE profiles SET login_name = email');
- }
+ my $newvalue = shift;
+ if ($newvalue) {
+ Bugzilla->dbh->do('UPDATE profiles SET login_name = email');
+ }
}
sub change_duplicate_or_move_bug_status {
- my $newvalue = shift;
- Bugzilla::Status::add_missing_bug_status_transitions($newvalue);
+ my $newvalue = shift;
+ Bugzilla::Status::add_missing_bug_status_transitions($newvalue);
}
# OK, here are the parameter definitions themselves.
@@ -435,7 +445,7 @@ sub change_duplicate_or_move_bug_status {
# }
#
# Here, 'b' is the default option, and 'a' and 'c' are other possible
-# options, but only one at a time!
+# options, but only one at a time!
#
# &check_multi should always be used as the param verification function
# for list (single and multiple) parameter types.
@@ -448,7 +458,7 @@ sub change_duplicate_or_move_bug_status {
# For instance: default => 'a,c'.
sub get_param_list {
- return;
+ return;
}
1;
diff --git a/Bugzilla/Config/Core.pm b/Bugzilla/Config/Core.pm
index c9a291d4c4..96d763c5db 100644
--- a/Bugzilla/Config/Core.pm
+++ b/Bugzilla/Config/Core.pm
@@ -16,25 +16,11 @@ use Bugzilla::Config::Common;
our $sortkey = 100;
use constant get_param_list => (
- {
- name => 'urlbase',
- type => 't',
- default => '',
- checker => \&check_urlbase
- },
-
- {
- name => 'ssl_redirect',
- type => 'b',
- default => 0
- },
-
- {
- name => 'sslbase',
- type => 't',
- default => '',
- checker => \&check_sslbase
- },
+ {name => 'urlbase', type => 't', default => '', checker => \&check_urlbase},
+
+ {name => 'ssl_redirect', type => 'b', default => 0},
+
+ {name => 'sslbase', type => 't', default => '', checker => \&check_sslbase},
);
1;
diff --git a/Bugzilla/Config/General.pm b/Bugzilla/Config/General.pm
index 1a03995f33..673806b331 100644
--- a/Bugzilla/Config/General.pm
+++ b/Bugzilla/Config/General.pm
@@ -17,32 +17,26 @@ our $sortkey = 150;
use constant get_param_list => (
{
- name => 'maintainer',
- type => 't',
- no_reset => '1',
- default => '',
- checker => \&check_email
+ name => 'maintainer',
+ type => 't',
+ no_reset => '1',
+ default => '',
+ checker => \&check_email
},
- {
- name => 'shutdownhtml',
- type => 'l',
- default => ''
- },
+ {name => 'shutdownhtml', type => 'l', default => ''},
- {
- name => 'announcehtml',
- type => 'l',
- default => ''
- },
+ {name => 'announcehtml', type => 'l', default => ''},
{
- name => 'upgrade_notification',
- type => 's',
- choices => ['development_snapshot', 'latest_stable_release',
- 'stable_branch_release', 'disabled'],
- default => 'latest_stable_release',
- checker => \&check_notification
+ name => 'upgrade_notification',
+ type => 's',
+ choices => [
+ 'development_snapshot', 'latest_stable_release',
+ 'stable_branch_release', 'disabled'
+ ],
+ default => 'latest_stable_release',
+ checker => \&check_notification
},
);
diff --git a/Bugzilla/Config/GroupSecurity.pm b/Bugzilla/Config/GroupSecurity.pm
index ba81a468e3..0b05d23b25 100644
--- a/Bugzilla/Config/GroupSecurity.pm
+++ b/Bugzilla/Config/GroupSecurity.pm
@@ -20,86 +20,75 @@ sub get_param_list {
my $class = shift;
my @param_list = (
- {
- name => 'chartgroup',
- type => 's',
- choices => \&_get_all_group_names,
- default => 'editbugs',
- checker => \&check_group
- },
-
- {
- name => 'insidergroup',
- type => 's',
- choices => \&_get_all_group_names,
- default => '',
- checker => \&check_group
- },
-
- {
- name => 'timetrackinggroup',
- type => 's',
- choices => \&_get_all_group_names,
- default => 'editbugs',
- checker => \&check_group
- },
-
- {
- name => 'querysharegroup',
- type => 's',
- choices => \&_get_all_group_names,
- default => 'editbugs',
- checker => \&check_group
- },
-
- {
- name => 'comment_taggers_group',
- type => 's',
- choices => \&_get_all_group_names,
- default => 'editbugs',
- checker => \&check_comment_taggers_group
- },
-
- {
- name => 'minor_update_group',
- type => 's',
- choices => \&_get_all_group_names,
- default => '',
- checker => \&check_group
- },
-
- {
- name => 'debug_group',
- type => 's',
- choices => \&_get_all_group_names,
- default => 'admin',
- checker => \&check_group
- },
-
- {
- name => 'usevisibilitygroups',
- type => 'b',
- default => 0
- },
-
- {
- name => 'strict_isolation',
- type => 'b',
- default => 0
- },
-
- {
- name => 'or_groups',
- type => 'b',
- default => 0
- } );
+ {
+ name => 'chartgroup',
+ type => 's',
+ choices => \&_get_all_group_names,
+ default => 'editbugs',
+ checker => \&check_group
+ },
+
+ {
+ name => 'insidergroup',
+ type => 's',
+ choices => \&_get_all_group_names,
+ default => '',
+ checker => \&check_group
+ },
+
+ {
+ name => 'timetrackinggroup',
+ type => 's',
+ choices => \&_get_all_group_names,
+ default => 'editbugs',
+ checker => \&check_group
+ },
+
+ {
+ name => 'querysharegroup',
+ type => 's',
+ choices => \&_get_all_group_names,
+ default => 'editbugs',
+ checker => \&check_group
+ },
+
+ {
+ name => 'comment_taggers_group',
+ type => 's',
+ choices => \&_get_all_group_names,
+ default => 'editbugs',
+ checker => \&check_comment_taggers_group
+ },
+
+ {
+ name => 'minor_update_group',
+ type => 's',
+ choices => \&_get_all_group_names,
+ default => '',
+ checker => \&check_group
+ },
+
+ {
+ name => 'debug_group',
+ type => 's',
+ choices => \&_get_all_group_names,
+ default => 'admin',
+ checker => \&check_group
+ },
+
+ {name => 'usevisibilitygroups', type => 'b', default => 0},
+
+ {name => 'strict_isolation', type => 'b', default => 0},
+
+ {name => 'or_groups', type => 'b', default => 0}
+ );
return @param_list;
}
sub _get_all_group_names {
- my @group_names = map {$_->name} Bugzilla::Group->get_all;
- unshift(@group_names, '');
- return \@group_names;
+ my @group_names = map { $_->name } Bugzilla::Group->get_all;
+ unshift(@group_names, '');
+ return \@group_names;
}
1;
diff --git a/Bugzilla/Config/LDAP.pm b/Bugzilla/Config/LDAP.pm
index 1970b5791e..1aac80dc84 100644
--- a/Bugzilla/Config/LDAP.pm
+++ b/Bugzilla/Config/LDAP.pm
@@ -16,49 +16,22 @@ use Bugzilla::Config::Common;
our $sortkey = 1000;
sub get_param_list {
- my $class = shift;
+ my $class = shift;
my @param_list = (
- {
- name => 'LDAPserver',
- type => 't',
- default => ''
- },
+ {name => 'LDAPserver', type => 't', default => ''},
- {
- name => 'LDAPstarttls',
- type => 'b',
- default => 0
- },
+ {name => 'LDAPstarttls', type => 'b', default => 0},
- {
- name => 'LDAPbinddn',
- type => 't',
- default => ''
- },
+ {name => 'LDAPbinddn', type => 't', default => ''},
- {
- name => 'LDAPBaseDN',
- type => 't',
- default => ''
- },
+ {name => 'LDAPBaseDN', type => 't', default => ''},
- {
- name => 'LDAPuidattribute',
- type => 't',
- default => 'uid'
- },
+ {name => 'LDAPuidattribute', type => 't', default => 'uid'},
- {
- name => 'LDAPmailattribute',
- type => 't',
- default => 'mail'
- },
+ {name => 'LDAPmailattribute', type => 't', default => 'mail'},
- {
- name => 'LDAPfilter',
- type => 't',
- default => '',
- } );
+ {name => 'LDAPfilter', type => 't', default => '',}
+ );
return @param_list;
}
diff --git a/Bugzilla/Config/MTA.pm b/Bugzilla/Config/MTA.pm
index 37f61aa20d..bb85fdc966 100644
--- a/Bugzilla/Config/MTA.pm
+++ b/Bugzilla/Config/MTA.pm
@@ -16,68 +16,43 @@ use Bugzilla::Config::Common;
our $sortkey = 1200;
sub get_param_list {
- my $class = shift;
+ my $class = shift;
my @param_list = (
- {
- name => 'mail_delivery_method',
- type => 's',
- choices => ['Sendmail', 'SMTP', 'Test', 'None'],
- default => 'Sendmail',
- checker => \&check_mail_delivery_method
- },
-
- {
- name => 'mailfrom',
- type => 't',
- default => 'bugzilla-daemon'
- },
-
- {
- name => 'use_mailer_queue',
- type => 'b',
- default => 0,
- checker => \&check_theschwartz_available,
- },
-
- {
- name => 'smtpserver',
- type => 't',
- default => 'localhost',
- checker => \&check_smtp_server
- },
- {
- name => 'smtp_username',
- type => 't',
- default => '',
- checker => \&check_smtp_auth
- },
- {
- name => 'smtp_password',
- type => 'p',
- default => ''
- },
- {
- name => 'smtp_ssl',
- type => 'b',
- default => 0,
- checker => \&check_smtp_ssl
- },
- {
- name => 'smtp_debug',
- type => 'b',
- default => 0
- },
- {
- name => 'whinedays',
- type => 't',
- default => 7,
- checker => \&check_numeric
- },
- {
- name => 'globalwatchers',
- type => 't',
- default => '',
- }, );
+ {
+ name => 'mail_delivery_method',
+ type => 's',
+ choices => ['Sendmail', 'SMTP', 'Test', 'None'],
+ default => 'Sendmail',
+ checker => \&check_mail_delivery_method
+ },
+
+ {name => 'mailfrom', type => 't', default => 'bugzilla-daemon'},
+
+ {
+ name => 'use_mailer_queue',
+ type => 'b',
+ default => 0,
+ checker => \&check_theschwartz_available,
+ },
+
+ {
+ name => 'smtpserver',
+ type => 't',
+ default => 'localhost',
+ checker => \&check_smtp_server
+ },
+ {
+ name => 'smtp_username',
+ type => 't',
+ default => '',
+ checker => \&check_smtp_auth
+ },
+ {name => 'smtp_password', type => 'p', default => ''},
+ {name => 'smtp_ssl', type => 'b', default => 0, checker => \&check_smtp_ssl},
+ {name => 'smtp_debug', type => 'b', default => 0},
+ {name => 'whinedays', type => 't', default => 7, checker => \&check_numeric},
+ {name => 'globalwatchers', type => 't', default => '',},
+ );
return @param_list;
}
diff --git a/Bugzilla/Config/Memcached.pm b/Bugzilla/Config/Memcached.pm
index e45c33da19..e3f8003304 100644
--- a/Bugzilla/Config/Memcached.pm
+++ b/Bugzilla/Config/Memcached.pm
@@ -17,16 +17,8 @@ our $sortkey = 1550;
sub get_param_list {
return (
- {
- name => 'memcached_servers',
- type => 't',
- default => ''
- },
- {
- name => 'memcached_namespace',
- type => 't',
- default => 'bugzilla:',
- },
+ {name => 'memcached_servers', type => 't', default => ''},
+ {name => 'memcached_namespace', type => 't', default => 'bugzilla:',},
);
}
diff --git a/Bugzilla/Config/Query.pm b/Bugzilla/Config/Query.pm
index 5e0d72e338..346551df11 100644
--- a/Bugzilla/Config/Query.pm
+++ b/Bugzilla/Config/Query.pm
@@ -16,47 +16,45 @@ use Bugzilla::Config::Common;
our $sortkey = 1400;
sub get_param_list {
- my $class = shift;
+ my $class = shift;
my @param_list = (
- {
- name => 'quip_list_entry_control',
- type => 's',
- choices => ['open', 'moderated', 'closed'],
- default => 'open',
- checker => \&check_multi
- },
-
- {
- name => 'mybugstemplate',
- type => 't',
- default => 'buglist.cgi?resolution=---&emailassigned_to1=1&emailreporter1=1&emailtype1=exact&email1=%userid%'
- },
-
- {
- name => 'defaultquery',
- type => 't',
- default => 'resolution=---&emailassigned_to1=1&emailassigned_to2=1&emailreporter2=1&emailcc2=1&emailqa_contact2=1&emaillongdesc3=1&order=Importance&long_desc_type=substring'
- },
-
- {
- name => 'search_allow_no_criteria',
- type => 'b',
- default => 1
- },
-
- {
- name => 'default_search_limit',
- type => 't',
- default => '500',
- checker => \&check_numeric
- },
-
- {
- name => 'max_search_results',
- type => 't',
- default => '10000',
- checker => \&check_numeric
- },
+ {
+ name => 'quip_list_entry_control',
+ type => 's',
+ choices => ['open', 'moderated', 'closed'],
+ default => 'open',
+ checker => \&check_multi
+ },
+
+ {
+ name => 'mybugstemplate',
+ type => 't',
+ default =>
+ 'buglist.cgi?resolution=---&emailassigned_to1=1&emailreporter1=1&emailtype1=exact&email1=%userid%'
+ },
+
+ {
+ name => 'defaultquery',
+ type => 't',
+ default =>
+ 'resolution=---&emailassigned_to1=1&emailassigned_to2=1&emailreporter2=1&emailcc2=1&emailqa_contact2=1&emaillongdesc3=1&order=Importance&long_desc_type=substring'
+ },
+
+ {name => 'search_allow_no_criteria', type => 'b', default => 1},
+
+ {
+ name => 'default_search_limit',
+ type => 't',
+ default => '500',
+ checker => \&check_numeric
+ },
+
+ {
+ name => 'max_search_results',
+ type => 't',
+ default => '10000',
+ checker => \&check_numeric
+ },
);
return @param_list;
}
diff --git a/Bugzilla/Config/RADIUS.pm b/Bugzilla/Config/RADIUS.pm
index ddc8d95242..f952785e94 100644
--- a/Bugzilla/Config/RADIUS.pm
+++ b/Bugzilla/Config/RADIUS.pm
@@ -16,31 +16,15 @@ use Bugzilla::Config::Common;
our $sortkey = 1100;
sub get_param_list {
- my $class = shift;
+ my $class = shift;
my @param_list = (
- {
- name => 'RADIUS_server',
- type => 't',
- default => ''
- },
-
- {
- name => 'RADIUS_secret',
- type => 't',
- default => ''
- },
-
- {
- name => 'RADIUS_NAS_IP',
- type => 't',
- default => ''
- },
-
- {
- name => 'RADIUS_email_suffix',
- type => 't',
- default => ''
- },
+ {name => 'RADIUS_server', type => 't', default => ''},
+
+ {name => 'RADIUS_secret', type => 't', default => ''},
+
+ {name => 'RADIUS_NAS_IP', type => 't', default => ''},
+
+ {name => 'RADIUS_email_suffix', type => 't', default => ''},
);
return @param_list;
}
diff --git a/Bugzilla/Config/ShadowDB.pm b/Bugzilla/Config/ShadowDB.pm
index 20212629cf..3f0e0e0219 100644
--- a/Bugzilla/Config/ShadowDB.pm
+++ b/Bugzilla/Config/ShadowDB.pm
@@ -16,35 +16,23 @@ use Bugzilla::Config::Common;
our $sortkey = 1500;
sub get_param_list {
- my $class = shift;
+ my $class = shift;
my @param_list = (
- {
- name => 'shadowdbhost',
- type => 't',
- default => '',
- },
-
- {
- name => 'shadowdbport',
- type => 't',
- default => '3306',
- checker => \&check_numeric,
- },
-
- {
- name => 'shadowdbsock',
- type => 't',
- default => '',
- },
-
- # This entry must be _after_ the shadowdb{host,port,sock} settings so that
- # they can be used in the validation here
- {
- name => 'shadowdb',
- type => 't',
- default => '',
- checker => \&check_shadowdb
- } );
+ {name => 'shadowdbhost', type => 't', default => '',},
+
+ {
+ name => 'shadowdbport',
+ type => 't',
+ default => '3306',
+ checker => \&check_numeric,
+ },
+
+ {name => 'shadowdbsock', type => 't', default => '',},
+
+ # This entry must be _after_ the shadowdb{host,port,sock} settings so that
+ # they can be used in the validation here
+ {name => 'shadowdb', type => 't', default => '', checker => \&check_shadowdb}
+ );
return @param_list;
}
diff --git a/Bugzilla/Config/UserMatch.pm b/Bugzilla/Config/UserMatch.pm
index c8b6f8dc38..cbd8515fa6 100644
--- a/Bugzilla/Config/UserMatch.pm
+++ b/Bugzilla/Config/UserMatch.pm
@@ -16,32 +16,21 @@ use Bugzilla::Config::Common;
our $sortkey = 1600;
sub get_param_list {
- my $class = shift;
+ my $class = shift;
my @param_list = (
- {
- name => 'usemenuforusers',
- type => 'b',
- default => '0'
- },
-
- {
- name => 'ajax_user_autocompletion',
- type => 'b',
- default => '1',
- },
-
- {
- name => 'maxusermatches',
- type => 't',
- default => '1000',
- checker => \&check_numeric
- },
-
- {
- name => 'confirmuniqueusermatch',
- type => 'b',
- default => 1,
- } );
+ {name => 'usemenuforusers', type => 'b', default => '0'},
+
+ {name => 'ajax_user_autocompletion', type => 'b', default => '1',},
+
+ {
+ name => 'maxusermatches',
+ type => 't',
+ default => '1000',
+ checker => \&check_numeric
+ },
+
+ {name => 'confirmuniqueusermatch', type => 'b', default => 1,}
+ );
return @param_list;
}
diff --git a/Bugzilla/Constants.pm b/Bugzilla/Constants.pm
index d7562f5081..7d0a4c7eb2 100644
--- a/Bugzilla/Constants.pm
+++ b/Bugzilla/Constants.pm
@@ -17,191 +17,191 @@ use parent qw(Exporter);
use File::Basename;
@Bugzilla::Constants::EXPORT = qw(
- BUGZILLA_VERSION
- REST_DOC
-
- REMOTE_FILE
- LOCAL_FILE
+ BUGZILLA_VERSION
+ REST_DOC
- bz_locations
+ REMOTE_FILE
+ LOCAL_FILE
- CAN_HAS_FEATURE
+ bz_locations
- CONCATENATE_ASSETS
-
- IS_NULL
- NOT_NULL
-
- CONTROLMAPNA
- CONTROLMAPSHOWN
- CONTROLMAPDEFAULT
- CONTROLMAPMANDATORY
-
- AUTH_OK
- AUTH_NODATA
- AUTH_ERROR
- AUTH_LOGINFAILED
- AUTH_DISABLED
- AUTH_NO_SUCH_USER
- AUTH_LOCKOUT
-
- USER_PASSWORD_MIN_LENGTH
-
- LOGIN_OPTIONAL
- LOGIN_NORMAL
- LOGIN_REQUIRED
-
- LOGOUT_ALL
- LOGOUT_CURRENT
- LOGOUT_KEEP_CURRENT
-
- GRANT_DIRECT
- GRANT_REGEXP
-
- GROUP_MEMBERSHIP
- GROUP_BLESS
- GROUP_VISIBLE
-
- MAILTO_USER
- MAILTO_GROUP
-
- DEFAULT_COLUMN_LIST
- DEFAULT_QUERY_NAME
- DEFAULT_MILESTONE
-
- SAVE_NUM_SEARCHES
-
- COMMENT_COLS
- MAX_COMMENT_LENGTH
-
- MIN_COMMENT_TAG_LENGTH
- MAX_COMMENT_TAG_LENGTH
-
- CMT_NORMAL
- CMT_DUPE_OF
- CMT_HAS_DUPE
- CMT_ATTACHMENT_CREATED
- CMT_ATTACHMENT_UPDATED
-
- THROW_ERROR
-
- RELATIONSHIPS
- REL_ASSIGNEE REL_QA REL_REPORTER REL_CC REL_GLOBAL_WATCHER
- REL_ANY
-
- POS_EVENTS
- EVT_OTHER EVT_ADDED_REMOVED EVT_COMMENT EVT_ATTACHMENT EVT_ATTACHMENT_DATA
- EVT_PROJ_MANAGEMENT EVT_OPENED_CLOSED EVT_KEYWORD EVT_CC EVT_DEPEND_BLOCK
- EVT_BUG_CREATED EVT_COMPONENT
-
- NEG_EVENTS
- EVT_UNCONFIRMED EVT_CHANGED_BY_ME EVT_MINOR_UPDATE
-
- GLOBAL_EVENTS
- EVT_FLAG_REQUESTED EVT_REQUESTED_FLAG
-
- ADMIN_GROUP_NAME
- PER_PRODUCT_PRIVILEGES
-
- SENDMAIL_EXE
- SENDMAIL_PATH
-
- FIELD_TYPE_UNKNOWN
- FIELD_TYPE_FREETEXT
- FIELD_TYPE_SINGLE_SELECT
- FIELD_TYPE_MULTI_SELECT
- FIELD_TYPE_TEXTAREA
- FIELD_TYPE_DATETIME
- FIELD_TYPE_DATE
- FIELD_TYPE_BUG_ID
- FIELD_TYPE_BUG_URLS
- FIELD_TYPE_KEYWORDS
- FIELD_TYPE_INTEGER
- FIELD_TYPE_HIGHEST_PLUS_ONE
-
- EMPTY_DATETIME_REGEX
-
- ABNORMAL_SELECTS
-
- TIMETRACKING_FIELDS
-
- USAGE_MODE_BROWSER
- USAGE_MODE_CMDLINE
- USAGE_MODE_XMLRPC
- USAGE_MODE_EMAIL
- USAGE_MODE_JSON
- USAGE_MODE_TEST
- USAGE_MODE_REST
-
- ERROR_MODE_WEBPAGE
- ERROR_MODE_DIE
- ERROR_MODE_DIE_SOAP_FAULT
- ERROR_MODE_JSON_RPC
- ERROR_MODE_TEST
- ERROR_MODE_REST
-
- COLOR_ERROR
- COLOR_SUCCESS
-
- INSTALLATION_MODE_INTERACTIVE
- INSTALLATION_MODE_NON_INTERACTIVE
-
- DB_MODULE
- ROOT_USER
- ON_WINDOWS
- ON_ACTIVESTATE
-
- MAX_TOKEN_AGE
- MAX_LOGINCOOKIE_AGE
- MAX_SUDO_TOKEN_AGE
- MAX_LOGIN_ATTEMPTS
- LOGIN_LOCKOUT_INTERVAL
- ACCOUNT_CHANGE_INTERVAL
- MAX_STS_AGE
-
- SAFE_PROTOCOLS
- LEGAL_CONTENT_TYPES
-
- MIN_SMALLINT
- MAX_SMALLINT
- MAX_INT_32
-
- MAX_LEN_QUERY_NAME
- MAX_CLASSIFICATION_SIZE
- MAX_PRODUCT_SIZE
- MAX_MILESTONE_SIZE
- MAX_COMPONENT_SIZE
- MAX_FIELD_VALUE_SIZE
- MAX_FIELD_LONG_DESC_LENGTH
- MAX_FREETEXT_LENGTH
- MAX_BUG_URL_LENGTH
- MAX_POSSIBLE_DUPLICATES
- MAX_ATTACH_FILENAME_LENGTH
- MAX_QUIP_LENGTH
- MAX_WEBDOT_BUGS
-
- PASSWORD_DIGEST_ALGORITHM
- PASSWORD_SALT_LENGTH
-
- CGI_URI_LIMIT
-
- PRIVILEGES_REQUIRED_NONE
- PRIVILEGES_REQUIRED_REPORTER
- PRIVILEGES_REQUIRED_ASSIGNEE
- PRIVILEGES_REQUIRED_EMPOWERED
-
- AUDIT_CREATE
- AUDIT_REMOVE
-
- MOST_FREQUENT_THRESHOLD
-
- MARKDOWN_TAB_WIDTH
-
- EMAIL_LIMIT_PER_MINUTE
- EMAIL_LIMIT_PER_HOUR
- EMAIL_LIMIT_EXCEPTION
-
- JOB_QUEUE_VIEW_MAX_JOBS
+ CAN_HAS_FEATURE
+
+ CONCATENATE_ASSETS
+
+ IS_NULL
+ NOT_NULL
+
+ CONTROLMAPNA
+ CONTROLMAPSHOWN
+ CONTROLMAPDEFAULT
+ CONTROLMAPMANDATORY
+
+ AUTH_OK
+ AUTH_NODATA
+ AUTH_ERROR
+ AUTH_LOGINFAILED
+ AUTH_DISABLED
+ AUTH_NO_SUCH_USER
+ AUTH_LOCKOUT
+
+ USER_PASSWORD_MIN_LENGTH
+
+ LOGIN_OPTIONAL
+ LOGIN_NORMAL
+ LOGIN_REQUIRED
+
+ LOGOUT_ALL
+ LOGOUT_CURRENT
+ LOGOUT_KEEP_CURRENT
+
+ GRANT_DIRECT
+ GRANT_REGEXP
+
+ GROUP_MEMBERSHIP
+ GROUP_BLESS
+ GROUP_VISIBLE
+
+ MAILTO_USER
+ MAILTO_GROUP
+
+ DEFAULT_COLUMN_LIST
+ DEFAULT_QUERY_NAME
+ DEFAULT_MILESTONE
+
+ SAVE_NUM_SEARCHES
+
+ COMMENT_COLS
+ MAX_COMMENT_LENGTH
+
+ MIN_COMMENT_TAG_LENGTH
+ MAX_COMMENT_TAG_LENGTH
+
+ CMT_NORMAL
+ CMT_DUPE_OF
+ CMT_HAS_DUPE
+ CMT_ATTACHMENT_CREATED
+ CMT_ATTACHMENT_UPDATED
+
+ THROW_ERROR
+
+ RELATIONSHIPS
+ REL_ASSIGNEE REL_QA REL_REPORTER REL_CC REL_GLOBAL_WATCHER
+ REL_ANY
+
+ POS_EVENTS
+ EVT_OTHER EVT_ADDED_REMOVED EVT_COMMENT EVT_ATTACHMENT EVT_ATTACHMENT_DATA
+ EVT_PROJ_MANAGEMENT EVT_OPENED_CLOSED EVT_KEYWORD EVT_CC EVT_DEPEND_BLOCK
+ EVT_BUG_CREATED EVT_COMPONENT
+
+ NEG_EVENTS
+ EVT_UNCONFIRMED EVT_CHANGED_BY_ME EVT_MINOR_UPDATE
+
+ GLOBAL_EVENTS
+ EVT_FLAG_REQUESTED EVT_REQUESTED_FLAG
+
+ ADMIN_GROUP_NAME
+ PER_PRODUCT_PRIVILEGES
+
+ SENDMAIL_EXE
+ SENDMAIL_PATH
+
+ FIELD_TYPE_UNKNOWN
+ FIELD_TYPE_FREETEXT
+ FIELD_TYPE_SINGLE_SELECT
+ FIELD_TYPE_MULTI_SELECT
+ FIELD_TYPE_TEXTAREA
+ FIELD_TYPE_DATETIME
+ FIELD_TYPE_DATE
+ FIELD_TYPE_BUG_ID
+ FIELD_TYPE_BUG_URLS
+ FIELD_TYPE_KEYWORDS
+ FIELD_TYPE_INTEGER
+ FIELD_TYPE_HIGHEST_PLUS_ONE
+
+ EMPTY_DATETIME_REGEX
+
+ ABNORMAL_SELECTS
+
+ TIMETRACKING_FIELDS
+
+ USAGE_MODE_BROWSER
+ USAGE_MODE_CMDLINE
+ USAGE_MODE_XMLRPC
+ USAGE_MODE_EMAIL
+ USAGE_MODE_JSON
+ USAGE_MODE_TEST
+ USAGE_MODE_REST
+
+ ERROR_MODE_WEBPAGE
+ ERROR_MODE_DIE
+ ERROR_MODE_DIE_SOAP_FAULT
+ ERROR_MODE_JSON_RPC
+ ERROR_MODE_TEST
+ ERROR_MODE_REST
+
+ COLOR_ERROR
+ COLOR_SUCCESS
+
+ INSTALLATION_MODE_INTERACTIVE
+ INSTALLATION_MODE_NON_INTERACTIVE
+
+ DB_MODULE
+ ROOT_USER
+ ON_WINDOWS
+ ON_ACTIVESTATE
+
+ MAX_TOKEN_AGE
+ MAX_LOGINCOOKIE_AGE
+ MAX_SUDO_TOKEN_AGE
+ MAX_LOGIN_ATTEMPTS
+ LOGIN_LOCKOUT_INTERVAL
+ ACCOUNT_CHANGE_INTERVAL
+ MAX_STS_AGE
+
+ SAFE_PROTOCOLS
+ LEGAL_CONTENT_TYPES
+
+ MIN_SMALLINT
+ MAX_SMALLINT
+ MAX_INT_32
+
+ MAX_LEN_QUERY_NAME
+ MAX_CLASSIFICATION_SIZE
+ MAX_PRODUCT_SIZE
+ MAX_MILESTONE_SIZE
+ MAX_COMPONENT_SIZE
+ MAX_FIELD_VALUE_SIZE
+ MAX_FIELD_LONG_DESC_LENGTH
+ MAX_FREETEXT_LENGTH
+ MAX_BUG_URL_LENGTH
+ MAX_POSSIBLE_DUPLICATES
+ MAX_ATTACH_FILENAME_LENGTH
+ MAX_QUIP_LENGTH
+ MAX_WEBDOT_BUGS
+
+ PASSWORD_DIGEST_ALGORITHM
+ PASSWORD_SALT_LENGTH
+
+ CGI_URI_LIMIT
+
+ PRIVILEGES_REQUIRED_NONE
+ PRIVILEGES_REQUIRED_REPORTER
+ PRIVILEGES_REQUIRED_ASSIGNEE
+ PRIVILEGES_REQUIRED_EMPOWERED
+
+ AUDIT_CREATE
+ AUDIT_REMOVE
+
+ MOST_FREQUENT_THRESHOLD
+
+ MARKDOWN_TAB_WIDTH
+
+ EMAIL_LIMIT_PER_MINUTE
+ EMAIL_LIMIT_PER_HOUR
+ EMAIL_LIMIT_EXCEPTION
+
+ JOB_QUEUE_VIEW_MAX_JOBS
);
@Bugzilla::Constants::EXPORT_OK = qw(contenttypes);
@@ -217,23 +217,25 @@ use constant REST_DOC => 'https://bugzilla.readthedocs.org/en/latest/api/';
# Location of the remote and local XML files to track new releases.
use constant REMOTE_FILE => 'http://updates.bugzilla.org/bugzilla-update.xml';
-use constant LOCAL_FILE => 'bugzilla-update.xml'; # Relative to datadir.
+use constant LOCAL_FILE => 'bugzilla-update.xml'; # Relative to datadir.
use constant CAN_HAS_FEATURE => eval {
- require CPAN::Meta::Prereqs;
- require CPAN::Meta::Requirements;
- require Module::Metadata;
- require Module::Runtime;
- CPAN::Meta::Prereqs->VERSION('2.132830');
- CPAN::Meta::Requirements->VERSION('2.121');
- Module::Metadata->VERSION('1.000019');
- 1;
+ require CPAN::Meta::Prereqs;
+ require CPAN::Meta::Requirements;
+ require Module::Metadata;
+ require Module::Runtime;
+ CPAN::Meta::Prereqs->VERSION('2.132830');
+ CPAN::Meta::Requirements->VERSION('2.121');
+ Module::Metadata->VERSION('1.000019');
+ 1;
};
# When true CSS and JavaScript assets will be concatanted and minified at
# run-time, to reduce the number of requests required to render a page.
# Setting this to a false value can help debugging.
-use constant CONCATENATE_ASSETS => $ENV{PLACK_ENV} ? $ENV{PLACK_ENV} ne "development" : 1;
+use constant CONCATENATE_ASSETS => $ENV{PLACK_ENV}
+ ? $ENV{PLACK_ENV} ne "development"
+ : 1;
# These are unique values that are unlikely to match a string or a number,
# to be used in criteria for match() functions and other things. They start
@@ -249,9 +251,9 @@ use constant NOT_NULL => ' __NOT_NULL__ ';
#
# ControlMap constants for group_control_map.
# membercontol:othercontrol => meaning
-# Na:Na => Bugs in this product may not be restricted to this
+# Na:Na => Bugs in this product may not be restricted to this
# group.
-# Shown:Na => Members of the group may restrict bugs
+# Shown:Na => Members of the group may restrict bugs
# in this product to this group.
# Shown:Shown => Members of the group may restrict bugs
# in this product to this group.
@@ -273,46 +275,46 @@ use constant NOT_NULL => ' __NOT_NULL__ ';
# Mandatory:Mandatory => Bug will be forced into this group regardless.
# All other combinations are illegal.
-use constant CONTROLMAPNA => 0;
-use constant CONTROLMAPSHOWN => 1;
-use constant CONTROLMAPDEFAULT => 2;
+use constant CONTROLMAPNA => 0;
+use constant CONTROLMAPSHOWN => 1;
+use constant CONTROLMAPDEFAULT => 2;
use constant CONTROLMAPMANDATORY => 3;
# See Bugzilla::Auth for docs on AUTH_*, LOGIN_* and LOGOUT_*
-use constant AUTH_OK => 0;
-use constant AUTH_NODATA => 1;
-use constant AUTH_ERROR => 2;
-use constant AUTH_LOGINFAILED => 3;
-use constant AUTH_DISABLED => 4;
-use constant AUTH_NO_SUCH_USER => 5;
-use constant AUTH_LOCKOUT => 6;
+use constant AUTH_OK => 0;
+use constant AUTH_NODATA => 1;
+use constant AUTH_ERROR => 2;
+use constant AUTH_LOGINFAILED => 3;
+use constant AUTH_DISABLED => 4;
+use constant AUTH_NO_SUCH_USER => 5;
+use constant AUTH_LOCKOUT => 6;
# The minimum length a password must have.
use constant USER_PASSWORD_MIN_LENGTH => 6;
use constant LOGIN_OPTIONAL => 0;
-use constant LOGIN_NORMAL => 1;
+use constant LOGIN_NORMAL => 1;
use constant LOGIN_REQUIRED => 2;
-use constant LOGOUT_ALL => 0;
-use constant LOGOUT_CURRENT => 1;
+use constant LOGOUT_ALL => 0;
+use constant LOGOUT_CURRENT => 1;
use constant LOGOUT_KEEP_CURRENT => 2;
use constant GRANT_DIRECT => 0;
use constant GRANT_REGEXP => 2;
use constant GROUP_MEMBERSHIP => 0;
-use constant GROUP_BLESS => 1;
-use constant GROUP_VISIBLE => 2;
+use constant GROUP_BLESS => 1;
+use constant GROUP_VISIBLE => 2;
-use constant MAILTO_USER => 0;
+use constant MAILTO_USER => 0;
use constant MAILTO_GROUP => 1;
# The default list of columns for buglist.cgi
use constant DEFAULT_COLUMN_LIST => (
- "product", "component", "assigned_to",
- "bug_status", "resolution", "short_desc", "changeddate"
+ "product", "component", "assigned_to", "bug_status",
+ "resolution", "short_desc", "changeddate"
);
# Used by query.cgi and buglist.cgi as the named-query name
@@ -327,6 +329,7 @@ use constant SAVE_NUM_SEARCHES => 10;
# The column width for comment textareas and comments in bugmails.
use constant COMMENT_COLS => 80;
+
# Used in _check_comment(). Gives the max length allowed for a comment.
use constant MAX_COMMENT_LENGTH => 65535;
@@ -335,9 +338,10 @@ use constant MIN_COMMENT_TAG_LENGTH => 3;
use constant MAX_COMMENT_TAG_LENGTH => 24;
# The type of bug comments.
-use constant CMT_NORMAL => 0;
-use constant CMT_DUPE_OF => 1;
+use constant CMT_NORMAL => 0;
+use constant CMT_DUPE_OF => 1;
use constant CMT_HAS_DUPE => 2;
+
# Type 3 was CMT_POPULAR_VOTES, which moved to the Voting extension.
# Type 4 was CMT_MOVED_TO, which moved to the OldBugMove extension.
use constant CMT_ATTACHMENT_CREATED => 5;
@@ -347,27 +351,26 @@ use constant CMT_ATTACHMENT_UPDATED => 6;
# an error when the validation fails.
use constant THROW_ERROR => 1;
-use constant REL_ASSIGNEE => 0;
-use constant REL_QA => 1;
-use constant REL_REPORTER => 2;
-use constant REL_CC => 3;
+use constant REL_ASSIGNEE => 0;
+use constant REL_QA => 1;
+use constant REL_REPORTER => 2;
+use constant REL_CC => 3;
+
# REL 4 was REL_VOTER, before it was moved ino an extension.
-use constant REL_GLOBAL_WATCHER => 5;
+use constant REL_GLOBAL_WATCHER => 5;
# We need these strings for the X-Bugzilla-Reasons header
# Note: this hash uses "," rather than "=>" to avoid auto-quoting of the LHS.
# This should be accessed through Bugzilla::BugMail::relationships() instead
# of being accessed directly.
use constant RELATIONSHIPS => {
- REL_ASSIGNEE , "AssignedTo",
- REL_REPORTER , "Reporter",
- REL_QA , "QAcontact",
- REL_CC , "CC",
- REL_GLOBAL_WATCHER, "GlobalWatcher"
+ REL_ASSIGNEE, "AssignedTo", REL_REPORTER, "Reporter",
+ REL_QA, "QAcontact", REL_CC, "CC",
+ REL_GLOBAL_WATCHER, "GlobalWatcher"
};
-
+
# Used for global events like EVT_FLAG_REQUESTED
-use constant REL_ANY => 100;
+use constant REL_ANY => 100;
# There are two sorts of event - positive and negative. Positive events are
# those for which the user says "I want mail if this happens." Negative events
@@ -375,35 +378,35 @@ use constant REL_ANY => 100;
#
# Exactly when each event fires is defined in wants_bug_mail() in User.pm; I'm
# not commenting them here in case the comments and the code get out of sync.
-use constant EVT_OTHER => 0;
-use constant EVT_ADDED_REMOVED => 1;
-use constant EVT_COMMENT => 2;
-use constant EVT_ATTACHMENT => 3;
-use constant EVT_ATTACHMENT_DATA => 4;
-use constant EVT_PROJ_MANAGEMENT => 5;
-use constant EVT_OPENED_CLOSED => 6;
-use constant EVT_KEYWORD => 7;
-use constant EVT_CC => 8;
-use constant EVT_DEPEND_BLOCK => 9;
-use constant EVT_BUG_CREATED => 10;
-use constant EVT_COMPONENT => 11;
-
-use constant POS_EVENTS => EVT_OTHER, EVT_ADDED_REMOVED, EVT_COMMENT,
- EVT_ATTACHMENT, EVT_ATTACHMENT_DATA,
- EVT_PROJ_MANAGEMENT, EVT_OPENED_CLOSED, EVT_KEYWORD,
- EVT_CC, EVT_DEPEND_BLOCK, EVT_BUG_CREATED,
- EVT_COMPONENT;
-
-use constant EVT_UNCONFIRMED => 50;
-use constant EVT_CHANGED_BY_ME => 51;
-use constant EVT_MINOR_UPDATE => 52;
+use constant EVT_OTHER => 0;
+use constant EVT_ADDED_REMOVED => 1;
+use constant EVT_COMMENT => 2;
+use constant EVT_ATTACHMENT => 3;
+use constant EVT_ATTACHMENT_DATA => 4;
+use constant EVT_PROJ_MANAGEMENT => 5;
+use constant EVT_OPENED_CLOSED => 6;
+use constant EVT_KEYWORD => 7;
+use constant EVT_CC => 8;
+use constant EVT_DEPEND_BLOCK => 9;
+use constant EVT_BUG_CREATED => 10;
+use constant EVT_COMPONENT => 11;
+
+use constant
+ POS_EVENTS => EVT_OTHER,
+ EVT_ADDED_REMOVED, EVT_COMMENT, EVT_ATTACHMENT, EVT_ATTACHMENT_DATA,
+ EVT_PROJ_MANAGEMENT, EVT_OPENED_CLOSED, EVT_KEYWORD, EVT_CC,
+ EVT_DEPEND_BLOCK, EVT_BUG_CREATED, EVT_COMPONENT;
+
+use constant EVT_UNCONFIRMED => 50;
+use constant EVT_CHANGED_BY_ME => 51;
+use constant EVT_MINOR_UPDATE => 52;
use constant NEG_EVENTS => EVT_UNCONFIRMED, EVT_CHANGED_BY_ME, EVT_MINOR_UPDATE;
# These are the "global" flags, which aren't tied to a particular relationship.
# and so use REL_ANY.
-use constant EVT_FLAG_REQUESTED => 100; # Flag has been requested of me
-use constant EVT_REQUESTED_FLAG => 101; # I have requested a flag
+use constant EVT_FLAG_REQUESTED => 100; # Flag has been requested of me
+use constant EVT_REQUESTED_FLAG => 101; # I have requested a flag
use constant GLOBAL_EVENTS => EVT_FLAG_REQUESTED, EVT_REQUESTED_FLAG;
@@ -411,10 +414,12 @@ use constant GLOBAL_EVENTS => EVT_FLAG_REQUESTED, EVT_REQUESTED_FLAG;
use constant ADMIN_GROUP_NAME => 'admin';
# Privileges which can be per-product.
-use constant PER_PRODUCT_PRIVILEGES => ('editcomponents', 'editbugs', 'canconfirm');
+use constant PER_PRODUCT_PRIVILEGES =>
+ ('editcomponents', 'editbugs', 'canconfirm');
# Path to sendmail.exe (Windows only)
use constant SENDMAIL_EXE => '/usr/lib/sendmail.exe';
+
# Paths to search for the sendmail binary (non-Windows)
use constant SENDMAIL_PATH => '/usr/lib:/usr/sbin:/usr/ucblib';
@@ -425,45 +430,46 @@ use constant SENDMAIL_PATH => '/usr/lib:/usr/sbin:/usr/ucblib';
# we do more than we would do for a standard integer type (f.e. we might
# display a user picker).
-use constant FIELD_TYPE_UNKNOWN => 0;
-use constant FIELD_TYPE_FREETEXT => 1;
+use constant FIELD_TYPE_UNKNOWN => 0;
+use constant FIELD_TYPE_FREETEXT => 1;
use constant FIELD_TYPE_SINGLE_SELECT => 2;
-use constant FIELD_TYPE_MULTI_SELECT => 3;
-use constant FIELD_TYPE_TEXTAREA => 4;
-use constant FIELD_TYPE_DATETIME => 5;
-use constant FIELD_TYPE_BUG_ID => 6;
-use constant FIELD_TYPE_BUG_URLS => 7;
-use constant FIELD_TYPE_KEYWORDS => 8;
-use constant FIELD_TYPE_DATE => 9;
-use constant FIELD_TYPE_INTEGER => 10;
+use constant FIELD_TYPE_MULTI_SELECT => 3;
+use constant FIELD_TYPE_TEXTAREA => 4;
+use constant FIELD_TYPE_DATETIME => 5;
+use constant FIELD_TYPE_BUG_ID => 6;
+use constant FIELD_TYPE_BUG_URLS => 7;
+use constant FIELD_TYPE_KEYWORDS => 8;
+use constant FIELD_TYPE_DATE => 9;
+use constant FIELD_TYPE_INTEGER => 10;
+
# Add new field types above this line, and change the below value in the
# obvious fashion
use constant FIELD_TYPE_HIGHEST_PLUS_ONE => 11;
-use constant EMPTY_DATETIME_REGEX => qr/^[0\-:\sA-Za-z]+$/;
+use constant EMPTY_DATETIME_REGEX => qr/^[0\-:\sA-Za-z]+$/;
# See the POD for Bugzilla::Field/is_abnormal to see why these are listed
# here.
-use constant ABNORMAL_SELECTS => {
- classification => 1,
- component => 1,
- product => 1,
-};
+use constant ABNORMAL_SELECTS =>
+ {classification => 1, component => 1, product => 1,};
# The fields from fielddefs that are blocked from non-timetracking users.
# work_time is sometimes called actual_time.
use constant TIMETRACKING_FIELDS =>
- qw(estimated_time remaining_time work_time actual_time percentage_complete);
+ qw(estimated_time remaining_time work_time actual_time percentage_complete);
# The maximum number of days a token will remain valid.
use constant MAX_TOKEN_AGE => 3;
+
# How many days a logincookie will remain valid if not used.
use constant MAX_LOGINCOOKIE_AGE => 30;
+
# How many seconds (default is 6 hours) a sudo cookie remains valid.
use constant MAX_SUDO_TOKEN_AGE => 21600;
# Maximum failed logins to lock account for this IP
use constant MAX_LOGIN_ATTEMPTS => 5;
+
# If the maximum login attempts occur during this many minutes, the
# account is locked.
use constant LOGIN_LOCKOUT_INTERVAL => 30;
@@ -477,36 +483,39 @@ use constant ACCOUNT_CHANGE_INTERVAL => 10;
use constant MAX_STS_AGE => 604800;
# Protocols which are considered as safe.
-use constant SAFE_PROTOCOLS => ('afs', 'cid', 'ftp', 'gopher', 'http', 'https',
- 'irc', 'ircs', 'mid', 'news', 'nntp', 'prospero',
- 'telnet', 'view-source', 'wais');
+use constant SAFE_PROTOCOLS => (
+ 'afs', 'cid', 'ftp', 'gopher', 'http', 'https',
+ 'irc', 'ircs', 'mid', 'news', 'nntp', 'prospero',
+ 'telnet', 'view-source', 'wais'
+);
# Valid MIME types for attachments.
-use constant LEGAL_CONTENT_TYPES => ('application', 'audio', 'image', 'message',
- 'model', 'multipart', 'text', 'video');
-
-use constant contenttypes =>
- {
- "html" => "text/html" ,
- "rdf" => "application/rdf+xml" ,
- "atom" => "application/atom+xml" ,
- "xml" => "application/xml" ,
- "dtd" => "application/xml-dtd" ,
- "js" => "application/x-javascript" ,
- "json" => "application/json" ,
- "csv" => "text/csv" ,
- "png" => "image/png" ,
- "ics" => "text/calendar" ,
- };
+use constant LEGAL_CONTENT_TYPES => (
+ 'application', 'audio', 'image', 'message',
+ 'model', 'multipart', 'text', 'video'
+);
+
+use constant contenttypes => {
+ "html" => "text/html",
+ "rdf" => "application/rdf+xml",
+ "atom" => "application/atom+xml",
+ "xml" => "application/xml",
+ "dtd" => "application/xml-dtd",
+ "js" => "application/x-javascript",
+ "json" => "application/json",
+ "csv" => "text/csv",
+ "png" => "image/png",
+ "ics" => "text/calendar",
+};
# Usage modes. Default USAGE_MODE_BROWSER. Use with Bugzilla->usage_mode.
-use constant USAGE_MODE_BROWSER => 0;
-use constant USAGE_MODE_CMDLINE => 1;
-use constant USAGE_MODE_XMLRPC => 2;
-use constant USAGE_MODE_EMAIL => 3;
-use constant USAGE_MODE_JSON => 4;
-use constant USAGE_MODE_TEST => 5;
-use constant USAGE_MODE_REST => 6;
+use constant USAGE_MODE_BROWSER => 0;
+use constant USAGE_MODE_CMDLINE => 1;
+use constant USAGE_MODE_XMLRPC => 2;
+use constant USAGE_MODE_EMAIL => 3;
+use constant USAGE_MODE_JSON => 4;
+use constant USAGE_MODE_TEST => 5;
+use constant USAGE_MODE_REST => 6;
# Error modes. Default set by Bugzilla->usage_mode (so ERROR_MODE_WEBPAGE
# usually). Use with Bugzilla->error_mode.
@@ -518,27 +527,33 @@ use constant ERROR_MODE_TEST => 4;
use constant ERROR_MODE_REST => 5;
# The ANSI colors of messages that command-line scripts use
-use constant COLOR_ERROR => 'red';
+use constant COLOR_ERROR => 'red';
use constant COLOR_SUCCESS => 'green';
# The various modes that checksetup.pl can run in.
-use constant INSTALLATION_MODE_INTERACTIVE => 0;
+use constant INSTALLATION_MODE_INTERACTIVE => 0;
use constant INSTALLATION_MODE_NON_INTERACTIVE => 1;
# Data about what we require for different databases.
use constant DB_MODULE => {
- # MySQL 5.0.15 was the first production 5.0.x release.
- mysql => { db => 'Bugzilla::DB::Mysql', db_version => '5.0.15', name => 'MySQL'},
- pg => { db => 'Bugzilla::DB::Pg', db_version => '9.00.0000', name => 'PostgreSQL'},
- oracle => { db => 'Bugzilla::DB::Oracle', db_version => '10.02.0', name => 'Oracle'},
- # SQLite 3.6.22 fixes a WHERE clause problem that may affect us.
- sqlite => { db => 'Bugzilla::DB::Sqlite', db_version => '3.6.22', name => 'SQLite'},
+
+ # MySQL 5.0.15 was the first production 5.0.x release.
+ mysql => {db => 'Bugzilla::DB::Mysql', db_version => '5.0.15', name => 'MySQL'},
+ pg =>
+ {db => 'Bugzilla::DB::Pg', db_version => '9.00.0000', name => 'PostgreSQL'},
+ oracle =>
+ {db => 'Bugzilla::DB::Oracle', db_version => '10.02.0', name => 'Oracle'},
+
+ # SQLite 3.6.22 fixes a WHERE clause problem that may affect us.
+ sqlite =>
+ {db => 'Bugzilla::DB::Sqlite', db_version => '3.6.22', name => 'SQLite'},
};
# True if we're on Win32.
use constant ON_WINDOWS => $^O eq 'MSWin32' ? 1 : 0;
+
# True if we're using ActiveState Perl (as opposed to Strawberry) on Windows.
-use constant ON_ACTIVESTATE => eval { &Win32::BuildNumber };
+use constant ON_ACTIVESTATE => eval {&Win32::BuildNumber};
# The user who should be considered "root" when we're giving
# instructions to Bugzilla administrators.
@@ -546,7 +561,7 @@ use constant ROOT_USER => ON_WINDOWS ? 'Administrator' : 'root';
use constant MIN_SMALLINT => -32768;
use constant MAX_SMALLINT => 32767;
-use constant MAX_INT_32 => 2147483647;
+use constant MAX_INT_32 => 2147483647;
# The longest that a saved search name can be.
use constant MAX_LEN_QUERY_NAME => 64;
@@ -595,6 +610,7 @@ use constant MAX_WEBDOT_BUGS => 2000;
# Perl's "Digest" module. Note that if you change this, it won't take
# effect until a user logs in or changes their password.
use constant PASSWORD_DIGEST_ALGORITHM => 'SHA-256';
+
# How long of a salt should we use? Note that if you change this, it
# won't take effect until a user logs in or changes their password.
use constant PASSWORD_SALT_LENGTH => 8;
@@ -603,7 +619,9 @@ use constant PASSWORD_SALT_LENGTH => 8;
# via POST such as buglist.cgi. This value determines whether the redirect
# can be safely done or not based on the web server's URI length setting.
# See http://support.microsoft.com/kb/208427 for why MSIE is different
-use constant CGI_URI_LIMIT => ($ENV{'HTTP_USER_AGENT'} || '') =~ /MSIE/ ? 2083 : 8000;
+use constant CGI_URI_LIMIT => ($ENV{'HTTP_USER_AGENT'} || '') =~ /MSIE/
+ ? 2083
+ : 8000;
# If the user isn't allowed to change a field, we must tell them who can.
# We store the required permission set into the $PrivilegesRequired
@@ -632,79 +650,87 @@ use constant MARKDOWN_TAB_WIDTH => 2;
# Setting a limit to 0 will disable this feature.
use constant EMAIL_LIMIT_PER_MINUTE => 1000;
use constant EMAIL_LIMIT_PER_HOUR => 2500;
+
# Don't change this exception message.
-use constant EMAIL_LIMIT_EXCEPTION => "email_limit_exceeded\n";
+use constant EMAIL_LIMIT_EXCEPTION => "email_limit_exceeded\n";
# The maximum number of jobs to show when viewing the job queue
# (view_job_queue.cgi).
use constant JOB_QUEUE_VIEW_MAX_JOBS => 500;
sub bz_locations {
- # Force $memoize to re-compute data per project, to avoid
- # sharing the same data across different installations.
- state $memoize = {};
- my $project = $ENV{PROJECT};
- my $key = $project // '__DEFAULT';
- return $memoize->{$key} //= _bz_locations($project);
+ # Force $memoize to re-compute data per project, to avoid
+ # sharing the same data across different installations.
+ state $memoize = {};
+ my $project = $ENV{PROJECT};
+ my $key = $project // '__DEFAULT';
+
+ return $memoize->{$key} //= _bz_locations($project);
}
sub _bz_locations {
- my $project = shift;
- # We know that Bugzilla/Constants.pm must be in %INC at this point.
- # So the only question is, what's the name of the directory
- # above it? This is the most reliable way to get our current working
- # directory under both mod_cgi and mod_perl. We call dirname twice
- # to get the name of the directory above the "Bugzilla/" directory.
- #
- # Calling dirname twice like that won't work on VMS or AmigaOS
- # but I doubt anybody runs Bugzilla on those.
- #
- # On mod_cgi this will be a relative path. On mod_perl it will be an
- # absolute path.
- my $libpath = dirname(dirname($INC{'Bugzilla/Constants.pm'}));
- # We have to detaint $libpath, but we can't use Bugzilla::Util here.
- $libpath =~ /(.*)/;
- $libpath = $1;
-
- my ($localconfig, $datadir);
- if ($project && $project =~ /^([\w-]+)$/) {
- $project = $1;
- $localconfig = "localconfig.$project";
- $datadir = "data/$project";
- } else {
- $project = undef;
- $localconfig = "localconfig";
- $datadir = "data";
- }
-
- $datadir = "$libpath/$datadir";
- # We have to return absolute paths for mod_perl.
- # That means that if you modify these paths, they must be absolute paths.
- return {
- 'libpath' => $libpath,
- 'ext_libpath' => "$libpath/lib",
- # If you put the libraries in a different location than the CGIs,
- # make sure this still points to the CGIs.
- 'cgi_path' => $libpath,
- 'templatedir' => "$libpath/template",
- 'template_cache' => "$datadir/template",
- 'project' => $project,
- 'localconfig' => "$libpath/$localconfig",
- 'datadir' => $datadir,
- 'attachdir' => "$datadir/attachments",
- 'skinsdir' => "$libpath/skins",
- 'graphsdir' => "$libpath/graphs",
- # $webdotdir must be in the web server's tree somewhere. Even if you use a
- # local dot, we output images to there. Also, if $webdotdir is
- # not relative to the bugzilla root directory, you'll need to
- # change showdependencygraph.cgi to set image_url to the correct
- # location.
- # The script should really generate these graphs directly...
- 'webdotdir' => "$datadir/webdot",
- 'extensionsdir' => "$libpath/extensions",
- 'assetsdir' => "$datadir/assets",
- };
+ my $project = shift;
+
+ # We know that Bugzilla/Constants.pm must be in %INC at this point.
+ # So the only question is, what's the name of the directory
+ # above it? This is the most reliable way to get our current working
+ # directory under both mod_cgi and mod_perl. We call dirname twice
+ # to get the name of the directory above the "Bugzilla/" directory.
+ #
+ # Calling dirname twice like that won't work on VMS or AmigaOS
+ # but I doubt anybody runs Bugzilla on those.
+ #
+ # On mod_cgi this will be a relative path. On mod_perl it will be an
+ # absolute path.
+ my $libpath = dirname(dirname($INC{'Bugzilla/Constants.pm'}));
+
+ # We have to detaint $libpath, but we can't use Bugzilla::Util here.
+ $libpath =~ /(.*)/;
+ $libpath = $1;
+
+ my ($localconfig, $datadir);
+ if ($project && $project =~ /^([\w-]+)$/) {
+ $project = $1;
+ $localconfig = "localconfig.$project";
+ $datadir = "data/$project";
+ }
+ else {
+ $project = undef;
+ $localconfig = "localconfig";
+ $datadir = "data";
+ }
+
+ $datadir = "$libpath/$datadir";
+
+ # We have to return absolute paths for mod_perl.
+ # That means that if you modify these paths, they must be absolute paths.
+ return {
+ 'libpath' => $libpath,
+ 'ext_libpath' => "$libpath/lib",
+
+ # If you put the libraries in a different location than the CGIs,
+ # make sure this still points to the CGIs.
+ 'cgi_path' => $libpath,
+ 'templatedir' => "$libpath/template",
+ 'template_cache' => "$datadir/template",
+ 'project' => $project,
+ 'localconfig' => "$libpath/$localconfig",
+ 'datadir' => $datadir,
+ 'attachdir' => "$datadir/attachments",
+ 'skinsdir' => "$libpath/skins",
+ 'graphsdir' => "$libpath/graphs",
+
+ # $webdotdir must be in the web server's tree somewhere. Even if you use a
+ # local dot, we output images to there. Also, if $webdotdir is
+ # not relative to the bugzilla root directory, you'll need to
+ # change showdependencygraph.cgi to set image_url to the correct
+ # location.
+ # The script should really generate these graphs directly...
+ 'webdotdir' => "$datadir/webdot",
+ 'extensionsdir' => "$libpath/extensions",
+ 'assetsdir' => "$datadir/assets",
+ };
}
1;
diff --git a/Bugzilla/DB.pm b/Bugzilla/DB.pm
index ec5d1ed96d..be692cc8fc 100644
--- a/Bugzilla/DB.pm
+++ b/Bugzilla/DB.pm
@@ -33,7 +33,7 @@ use Storable qw(dclone);
# Constants
#####################################################################
-use constant BLOB_TYPE => DBI::SQL_BLOB;
+use constant BLOB_TYPE => DBI::SQL_BLOB;
use constant ISOLATION_LEVEL => 'REPEATABLE READ';
# Set default values for what used to be the enum types. These values
@@ -46,14 +46,14 @@ use constant ISOLATION_LEVEL => 'REPEATABLE READ';
# Bugzilla with enums. After that, they are either controlled through
# the Bugzilla UI or through the DB.
use constant ENUM_DEFAULTS => {
- bug_severity => ['blocker', 'critical', 'major', 'normal',
- 'minor', 'trivial', 'enhancement'],
- priority => ["Highest", "High", "Normal", "Low", "Lowest", "---"],
- op_sys => ["All","Windows","Mac OS","Linux","Other"],
- rep_platform => ["All","PC","Macintosh","Other"],
- bug_status => ["UNCONFIRMED","CONFIRMED","IN_PROGRESS","RESOLVED",
- "VERIFIED"],
- resolution => ["","FIXED","INVALID","WONTFIX", "DUPLICATE","WORKSFORME"],
+ bug_severity =>
+ ['blocker', 'critical', 'major', 'normal', 'minor', 'trivial', 'enhancement'],
+ priority => ["Highest", "High", "Normal", "Low", "Lowest", "---"],
+ op_sys => ["All", "Windows", "Mac OS", "Linux", "Other"],
+ rep_platform => ["All", "PC", "Macintosh", "Other"],
+ bug_status =>
+ ["UNCONFIRMED", "CONFIRMED", "IN_PROGRESS", "RESOLVED", "VERIFIED"],
+ resolution => ["", "FIXED", "INVALID", "WONTFIX", "DUPLICATE", "WORKSFORME"],
};
# The character that means "OR" in a boolean fulltext search. If empty,
@@ -83,14 +83,14 @@ use constant WORD_END => '($|[^[:alnum:]])';
use constant INDEX_DROPS_REQUIRE_FK_DROPS => 1;
#####################################################################
-# Overridden Superclass Methods
+# Overridden Superclass Methods
#####################################################################
sub quote {
- my $self = shift;
- my $retval = $self->SUPER::quote(@_);
- trick_taint($retval) if defined $retval;
- return $retval;
+ my $self = shift;
+ my $retval = $self->SUPER::quote(@_);
+ trick_taint($retval) if defined $retval;
+ return $retval;
}
#####################################################################
@@ -98,180 +98,185 @@ sub quote {
#####################################################################
sub connect_shadow {
- my $params = Bugzilla->params;
- die "Tried to connect to non-existent shadowdb"
- unless $params->{'shadowdb'};
+ my $params = Bugzilla->params;
+ die "Tried to connect to non-existent shadowdb" unless $params->{'shadowdb'};
- # Instead of just passing in a new hashref, we locally modify the
- # values of "localconfig", because some drivers access it while
- # connecting.
- my %connect_params = %{ Bugzilla->localconfig };
- $connect_params{db_host} = $params->{'shadowdbhost'};
- $connect_params{db_name} = $params->{'shadowdb'};
- $connect_params{db_port} = $params->{'shadowdbport'};
- $connect_params{db_sock} = $params->{'shadowdbsock'};
+ # Instead of just passing in a new hashref, we locally modify the
+ # values of "localconfig", because some drivers access it while
+ # connecting.
+ my %connect_params = %{Bugzilla->localconfig};
+ $connect_params{db_host} = $params->{'shadowdbhost'};
+ $connect_params{db_name} = $params->{'shadowdb'};
+ $connect_params{db_port} = $params->{'shadowdbport'};
+ $connect_params{db_sock} = $params->{'shadowdbsock'};
- return _connect(\%connect_params);
+ return _connect(\%connect_params);
}
sub connect_main {
- return _connect(Bugzilla->localconfig);
+ return _connect(Bugzilla->localconfig);
}
sub _connect {
- my ($params) = @_;
+ my ($params) = @_;
- my $driver = $params->{db_driver};
- my $pkg_module = DB_MODULE->{lc($driver)}->{db};
+ my $driver = $params->{db_driver};
+ my $pkg_module = DB_MODULE->{lc($driver)}->{db};
- # do the actual import
- eval ("require $pkg_module")
- || die ("'$driver' is not a valid choice for \$db_driver in "
- . " localconfig: " . $@);
+ # do the actual import
+ eval("require $pkg_module")
+ || die(
+ "'$driver' is not a valid choice for \$db_driver in " . " localconfig: " . $@);
- # instantiate the correct DB specific module
- my $dbh = $pkg_module->new($params);
+ # instantiate the correct DB specific module
+ my $dbh = $pkg_module->new($params);
- return $dbh;
+ return $dbh;
}
sub _handle_error {
- require Carp;
+ require Carp;
- # Cut down the error string to a reasonable size
- $_[0] = substr($_[0], 0, 2000) . ' ... ' . substr($_[0], -2000)
- if length($_[0]) > 4000;
- $_[0] = Carp::longmess($_[0]);
+ # Cut down the error string to a reasonable size
+ $_[0] = substr($_[0], 0, 2000) . ' ... ' . substr($_[0], -2000)
+ if length($_[0]) > 4000;
+ $_[0] = Carp::longmess($_[0]);
- if (!Bugzilla->request_cache->{in_error} && Bugzilla->usage_mode == USAGE_MODE_BROWSER) {
- Bugzilla->request_cache->{in_error} = 1;
- ThrowCodeError("db_error", {err_message => $_[0]});
- }
+ if (!Bugzilla->request_cache->{in_error}
+ && Bugzilla->usage_mode == USAGE_MODE_BROWSER)
+ {
+ Bugzilla->request_cache->{in_error} = 1;
+ ThrowCodeError("db_error", {err_message => $_[0]});
+ }
- Bugzilla->request_cache->{in_error} = undef;
+ Bugzilla->request_cache->{in_error} = undef;
- return 0; # Now let DBI handle raising the error
+ return 0; # Now let DBI handle raising the error
}
sub bz_check_requirements {
- my ($output) = @_;
+ my ($output) = @_;
- my $lc = Bugzilla->localconfig;
- my $db = DB_MODULE->{lc($lc->{db_driver})};
+ my $lc = Bugzilla->localconfig;
+ my $db = DB_MODULE->{lc($lc->{db_driver})};
- # Only certain values are allowed for $db_driver.
- if (!defined $db) {
- die "$lc->{db_driver} is not a valid choice for \$db_driver in"
- . bz_locations()->{'localconfig'};
- }
+ # Only certain values are allowed for $db_driver.
+ if (!defined $db) {
+ die "$lc->{db_driver} is not a valid choice for \$db_driver in"
+ . bz_locations()->{'localconfig'};
+ }
- # We don't try to connect to the actual database if $db_check is
- # disabled.
- unless ($lc->{db_check}) {
- print "\n" if $output;
- return;
- }
+ # We don't try to connect to the actual database if $db_check is
+ # disabled.
+ unless ($lc->{db_check}) {
+ print "\n" if $output;
+ return;
+ }
- # And now check the version of the database server itself.
- my $dbh = _get_no_db_connection();
- $dbh->bz_check_server_version($db, $output);
+ # And now check the version of the database server itself.
+ my $dbh = _get_no_db_connection();
+ $dbh->bz_check_server_version($db, $output);
- print "\n" if $output;
+ print "\n" if $output;
}
sub bz_check_server_version {
- my ($self, $db, $output) = @_;
+ my ($self, $db, $output) = @_;
- my $sql_vers = $self->bz_server_version;
- $self->disconnect;
+ my $sql_vers = $self->bz_server_version;
+ $self->disconnect;
- my $sql_want = $db->{db_version};
- my $version_ok = vers_cmp($sql_vers, $sql_want) > -1 ? 1 : 0;
+ my $sql_want = $db->{db_version};
+ my $version_ok = vers_cmp($sql_vers, $sql_want) > -1 ? 1 : 0;
- my $sql_server = $db->{name};
- if ($output) {
- Bugzilla::Install::Requirements::_checking_for({
- package => $sql_server, wanted => $sql_want,
- found => $sql_vers, ok => $version_ok });
- }
+ my $sql_server = $db->{name};
+ if ($output) {
+ Bugzilla::Install::Requirements::_checking_for({
+ package => $sql_server,
+ wanted => $sql_want,
+ found => $sql_vers,
+ ok => $version_ok
+ });
+ }
- # Check what version of the database server is installed and let
- # the user know if the version is too old to be used with Bugzilla.
- if (!$version_ok) {
- die <
Second error: $error2
\n$bq\n\n\n"; }egmx; - $text = $self->_DoDelayCodeBlocks($text); - return $text; + $text = $self->_DoDelayCodeBlocks($text); + return $text; } sub _DoLists { - my ($self, $text) = @_; + my ($self, $text) = @_; - $text = $self->SUPER::_DoLists($text); + $text = $self->SUPER::_DoLists($text); - # strip trailing newlines created by DoLists - $text =~ s/\n/g; - $text =~ s/"/"/g; - $text =~ s/@/@/g; - # '&' substitution must be the last one, otherwise a literal like '>' - # will turn to '>' because '&' is already changed to '&' in Bugzilla::Util::html_quote(). - # In other words, html_quote() will change '>' to '>' and then we will - # change '>' -> '>' -> '>' if we write this substitution as the first one. - $text =~ s/&/&/g; - $text =~ s{ \1 }{$1}xmgi; - $text = $self->SUPER::_EncodeCode($text); - $text =~ s/~/$g_escape_table{'~'}/go; - # Encode '<' to prevent URLs from getting linkified in code spans - $text =~ s/</$g_escape_table{'<'}/go; - - return $text; + my ($self, $text) = @_; + + # We need to unescape the escaped HTML characters in code blocks. + # These are the reverse of the escapings done in Bugzilla::Util::html_quote() + $text =~ s/<//g; + $text =~ s/"/"/g; + $text =~ s/@/@/g; + +# '&' substitution must be the last one, otherwise a literal like '>' +# will turn to '>' because '&' is already changed to '&' in Bugzilla::Util::html_quote(). +# In other words, html_quote() will change '>' to '>' and then we will +# change '>' -> '>' -> '>' if we write this substitution as the first one. + $text =~ s/&/&/g; + $text =~ s{ \1 }{$1}xmgi; + $text = $self->SUPER::_EncodeCode($text); + $text =~ s/~/$g_escape_table{'~'}/go; + + # Encode '<' to prevent URLs from getting linkified in code spans + $text =~ s/</$g_escape_table{'<'}/go; + + return $text; } sub _EncodeBackslashEscapes { - my ($self, $text) = @_; + my ($self, $text) = @_; - $text = $self->SUPER::_EncodeBackslashEscapes($text); - $text =~ s/\\~/$g_escape_table{'~'}/go; + $text = $self->SUPER::_EncodeBackslashEscapes($text); + $text =~ s/\\~/$g_escape_table{'~'}/go; - return $text; + return $text; } sub _UnescapeSpecialChars { - my ($self, $text) = @_; + my ($self, $text) = @_; - $text = $self->SUPER::_UnescapeSpecialChars($text); - $text =~ s/$g_escape_table{'~'}/~/go; - $text =~ s/$g_escape_table{'<'}/</go; + $text = $self->SUPER::_UnescapeSpecialChars($text); + $text =~ s/$g_escape_table{'~'}/~/go; + $text =~ s/$g_escape_table{'<'}/</go; - return $text; + return $text; } # Check if the passed string is of the form multiple_underscores_in_a_word. @@ -538,11 +552,11 @@ sub _UnescapeSpecialChars { # any white-space. Then, if the string is composed of non-space chunks which # are bound together with underscores, the string has the desired form. sub _has_multiple_underscores { - my $string = shift; - return 0 unless $string; - return 0 if $string =~ /\s/; - return 1 if $string =~ /_/; - return 0; + my $string = shift; + return 0 unless $string; + return 0 if $string =~ /\s/; + return 1 if $string =~ /_/; + return 0; } 1; diff --git a/Bugzilla/Memcached.pm b/Bugzilla/Memcached.pm index ed32fa27b0..f71ff2572b 100644 --- a/Bugzilla/Memcached.pm +++ b/Bugzilla/Memcached.pm @@ -20,238 +20,234 @@ use URI::Escape; use constant MAX_KEY_LENGTH => 250; sub _new { - my $invocant = shift; - my $class = ref($invocant) || $invocant; - my $self = {}; - - # always return an object to simplify calling code when memcached is - # disabled. - if (Bugzilla->feature('memcached') - && Bugzilla->params->{memcached_servers}) - { - require Cache::Memcached::Fast; - $self->{namespace} = Bugzilla->params->{memcached_namespace} || ''; - $self->{memcached} = - Cache::Memcached::Fast->new({ - servers => [ split(/[, ]+/, Bugzilla->params->{memcached_servers}) ], - namespace => $self->{namespace}, - }); - } - return bless($self, $class); + my $invocant = shift; + my $class = ref($invocant) || $invocant; + my $self = {}; + + # always return an object to simplify calling code when memcached is + # disabled. + if (Bugzilla->feature('memcached') && Bugzilla->params->{memcached_servers}) { + require Cache::Memcached::Fast; + $self->{namespace} = Bugzilla->params->{memcached_namespace} || ''; + $self->{memcached} = Cache::Memcached::Fast->new({ + servers => [split(/[, ]+/, Bugzilla->params->{memcached_servers})], + namespace => $self->{namespace}, + }); + } + return bless($self, $class); } sub enabled { - return $_[0]->{memcached} ? 1 : 0; + return $_[0]->{memcached} ? 1 : 0; } sub set { - my ($self, $args) = @_; - return unless $self->{memcached}; - - # { key => $key, value => $value } - if (exists $args->{key}) { - $self->_set($args->{key}, $args->{value}); + my ($self, $args) = @_; + return unless $self->{memcached}; + + # { key => $key, value => $value } + if (exists $args->{key}) { + $self->_set($args->{key}, $args->{value}); + } + + # { table => $table, id => $id, name => $name, data => $data } + elsif (exists $args->{table} && exists $args->{id} && exists $args->{name}) { + + # For caching of Bugzilla::Object, we have to be able to clear the + # cached values when given either the object's id or name. + my ($table, $id, $name, $data) = @$args{qw(table id name data)}; + $self->_set("$table.id.$id", $data); + if (defined $name) { + $self->_set("$table.name_id.$name", $id); + $self->_set("$table.id_name.$id", $name); } + } - # { table => $table, id => $id, name => $name, data => $data } - elsif (exists $args->{table} && exists $args->{id} && exists $args->{name}) { - # For caching of Bugzilla::Object, we have to be able to clear the - # cached values when given either the object's id or name. - my ($table, $id, $name, $data) = @$args{qw(table id name data)}; - $self->_set("$table.id.$id", $data); - if (defined $name) { - $self->_set("$table.name_id.$name", $id); - $self->_set("$table.id_name.$id", $name); - } - } - - else { - ThrowCodeError('params_required', { function => "Bugzilla::Memcached::set", - params => [ 'key', 'table' ] }); - } + else { + ThrowCodeError('params_required', + {function => "Bugzilla::Memcached::set", params => ['key', 'table']}); + } } sub get { - my ($self, $args) = @_; - return unless $self->{memcached}; - - # { key => $key } - if (exists $args->{key}) { - return $self->_get($args->{key}); - } - - # { table => $table, id => $id } - elsif (exists $args->{table} && exists $args->{id}) { - my ($table, $id) = @$args{qw(table id)}; - return $self->_get("$table.id.$id"); - } - - # { table => $table, name => $name } - elsif (exists $args->{table} && exists $args->{name}) { - my ($table, $name) = @$args{qw(table name)}; - return unless my $id = $self->_get("$table.name_id.$name"); - return $self->_get("$table.id.$id"); - } - - else { - ThrowCodeError('params_required', { function => "Bugzilla::Memcached::get", - params => [ 'key', 'table' ] }); - } + my ($self, $args) = @_; + return unless $self->{memcached}; + + # { key => $key } + if (exists $args->{key}) { + return $self->_get($args->{key}); + } + + # { table => $table, id => $id } + elsif (exists $args->{table} && exists $args->{id}) { + my ($table, $id) = @$args{qw(table id)}; + return $self->_get("$table.id.$id"); + } + + # { table => $table, name => $name } + elsif (exists $args->{table} && exists $args->{name}) { + my ($table, $name) = @$args{qw(table name)}; + return unless my $id = $self->_get("$table.name_id.$name"); + return $self->_get("$table.id.$id"); + } + + else { + ThrowCodeError('params_required', + {function => "Bugzilla::Memcached::get", params => ['key', 'table']}); + } } sub set_config { - my ($self, $args) = @_; - return unless $self->{memcached}; - - if (exists $args->{key}) { - return $self->_set($self->_config_prefix . '.' . $args->{key}, $args->{data}); - } - else { - ThrowCodeError('params_required', { function => "Bugzilla::Memcached::set_config", - params => [ 'key' ] }); - } + my ($self, $args) = @_; + return unless $self->{memcached}; + + if (exists $args->{key}) { + return $self->_set($self->_config_prefix . '.' . $args->{key}, $args->{data}); + } + else { + ThrowCodeError('params_required', + {function => "Bugzilla::Memcached::set_config", params => ['key']}); + } } sub get_config { - my ($self, $args) = @_; - return unless $self->{memcached}; - - if (exists $args->{key}) { - return $self->_get($self->_config_prefix . '.' . $args->{key}); - } - else { - ThrowCodeError('params_required', { function => "Bugzilla::Memcached::get_config", - params => [ 'key' ] }); - } + my ($self, $args) = @_; + return unless $self->{memcached}; + + if (exists $args->{key}) { + return $self->_get($self->_config_prefix . '.' . $args->{key}); + } + else { + ThrowCodeError('params_required', + {function => "Bugzilla::Memcached::get_config", params => ['key']}); + } } sub clear { - my ($self, $args) = @_; - return unless $self->{memcached}; - - # { key => $key } - if (exists $args->{key}) { - $self->_delete($args->{key}); - } - - # { table => $table, id => $id } - elsif (exists $args->{table} && exists $args->{id}) { - my ($table, $id) = @$args{qw(table id)}; - my $name = $self->_get("$table.id_name.$id"); - $self->_delete("$table.id.$id"); - $self->_delete("$table.name_id.$name") if defined $name; - $self->_delete("$table.id_name.$id"); - } - - # { table => $table, name => $name } - elsif (exists $args->{table} && exists $args->{name}) { - my ($table, $name) = @$args{qw(table name)}; - return unless my $id = $self->_get("$table.name_id.$name"); - $self->_delete("$table.id.$id"); - $self->_delete("$table.name_id.$name"); - $self->_delete("$table.id_name.$id"); - } - - else { - ThrowCodeError('params_required', { function => "Bugzilla::Memcached::clear", - params => [ 'key', 'table' ] }); - } + my ($self, $args) = @_; + return unless $self->{memcached}; + + # { key => $key } + if (exists $args->{key}) { + $self->_delete($args->{key}); + } + + # { table => $table, id => $id } + elsif (exists $args->{table} && exists $args->{id}) { + my ($table, $id) = @$args{qw(table id)}; + my $name = $self->_get("$table.id_name.$id"); + $self->_delete("$table.id.$id"); + $self->_delete("$table.name_id.$name") if defined $name; + $self->_delete("$table.id_name.$id"); + } + + # { table => $table, name => $name } + elsif (exists $args->{table} && exists $args->{name}) { + my ($table, $name) = @$args{qw(table name)}; + return unless my $id = $self->_get("$table.name_id.$name"); + $self->_delete("$table.id.$id"); + $self->_delete("$table.name_id.$name"); + $self->_delete("$table.id_name.$id"); + } + + else { + ThrowCodeError('params_required', + {function => "Bugzilla::Memcached::clear", params => ['key', 'table']}); + } } sub clear_all { - my ($self) = @_; - return unless $self->{memcached}; - $self->_inc_prefix("global"); + my ($self) = @_; + return unless $self->{memcached}; + $self->_inc_prefix("global"); } sub clear_config { - my ($self, $args) = @_; - return unless $self->{memcached}; - if ($args && exists $args->{key}) { - $self->_delete($self->_config_prefix . '.' . $args->{key}); - } - else { - $self->_inc_prefix("config"); - } + my ($self, $args) = @_; + return unless $self->{memcached}; + if ($args && exists $args->{key}) { + $self->_delete($self->_config_prefix . '.' . $args->{key}); + } + else { + $self->_inc_prefix("config"); + } } # in order to clear all our keys, we add a prefix to all our keys. when we # need to "clear" all current keys, we increment the prefix. sub _prefix { - my ($self, $name) = @_; - # we don't want to change prefixes in the middle of a request - my $request_cache = Bugzilla->request_cache; - my $request_cache_key = "memcached_prefix_$name"; - if (!$request_cache->{$request_cache_key}) { - my $memcached = $self->{memcached}; - my $prefix = $memcached->get($name); - if (!$prefix) { - $prefix = time(); - if (!$memcached->add($name, $prefix)) { - # if this failed, either another process set the prefix, or - # memcached is down. assume we lost the race, and get the new - # value. if that fails, memcached is down so use a dummy - # prefix for this request. - $prefix = $memcached->get($name) || 0; - } - } - $request_cache->{$request_cache_key} = $prefix; + my ($self, $name) = @_; + + # we don't want to change prefixes in the middle of a request + my $request_cache = Bugzilla->request_cache; + my $request_cache_key = "memcached_prefix_$name"; + if (!$request_cache->{$request_cache_key}) { + my $memcached = $self->{memcached}; + my $prefix = $memcached->get($name); + if (!$prefix) { + $prefix = time(); + if (!$memcached->add($name, $prefix)) { + + # if this failed, either another process set the prefix, or + # memcached is down. assume we lost the race, and get the new + # value. if that fails, memcached is down so use a dummy + # prefix for this request. + $prefix = $memcached->get($name) || 0; + } } - return $request_cache->{$request_cache_key}; + $request_cache->{$request_cache_key} = $prefix; + } + return $request_cache->{$request_cache_key}; } sub _inc_prefix { - my ($self, $name) = @_; - my $memcached = $self->{memcached}; - if (!$memcached->incr($name, 1)) { - $memcached->add($name, time()); - } - delete Bugzilla->request_cache->{"memcached_prefix_$name"}; + my ($self, $name) = @_; + my $memcached = $self->{memcached}; + if (!$memcached->incr($name, 1)) { + $memcached->add($name, time()); + } + delete Bugzilla->request_cache->{"memcached_prefix_$name"}; } sub _global_prefix { - return $_[0]->_prefix("global"); + return $_[0]->_prefix("global"); } sub _config_prefix { - return $_[0]->_prefix("config"); + return $_[0]->_prefix("config"); } sub _encode_key { - my ($self, $key) = @_; - $key = $self->_global_prefix . '.' . uri_escape_utf8($key); - trick_taint($key) if defined $key; - return length($self->{namespace} . $key) > MAX_KEY_LENGTH - ? undef - : $key; + my ($self, $key) = @_; + $key = $self->_global_prefix . '.' . uri_escape_utf8($key); + trick_taint($key) if defined $key; + return length($self->{namespace} . $key) > MAX_KEY_LENGTH ? undef : $key; } sub _set { - my ($self, $key, $value) = @_; - if (blessed($value)) { - # we don't support blessed objects - ThrowCodeError('param_invalid', { function => "Bugzilla::Memcached::set", - param => "value" }); - } + my ($self, $key, $value) = @_; + if (blessed($value)) { + + # we don't support blessed objects + ThrowCodeError('param_invalid', + {function => "Bugzilla::Memcached::set", param => "value"}); + } - $key = $self->_encode_key($key) - or return; - return $self->{memcached}->set($key, $value); + $key = $self->_encode_key($key) or return; + return $self->{memcached}->set($key, $value); } sub _get { - my ($self, $key) = @_; + my ($self, $key) = @_; - $key = $self->_encode_key($key) - or return; - return $self->{memcached}->get($key); + $key = $self->_encode_key($key) or return; + return $self->{memcached}->get($key); } sub _delete { - my ($self, $key) = @_; - $key = $self->_encode_key($key) - or return; - return $self->{memcached}->delete($key); + my ($self, $key) = @_; + $key = $self->_encode_key($key) or return; + return $self->{memcached}->delete($key); } 1; diff --git a/Bugzilla/Migrate.pm b/Bugzilla/Migrate.pm index df30e34cb8..cf558c90c4 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; @@ -30,6 +30,7 @@ use DateTime; use Fcntl qw(SEEK_SET); use File::Basename; use List::Util qw(first); + # Bug 1270550 - Tie::Hash::NamedCapture must be loaded before Safe. use Tie::Hash::NamedCapture; use Safe; @@ -39,10 +40,10 @@ use constant REQUIRED_MODULES => []; use constant NON_COMMENT_FIELDS => (); use constant CONFIG_VARS => ( - { - name => 'translate_fields', - default => {}, - desc => <<'END', + { + name => 'translate_fields', + default => {}, + desc => <<'END', # This maps field names in your bug-tracker to Bugzilla field names. If a field # has the same name in your bug-tracker and Bugzilla (case-insensitively), it # doesn't need a mapping here. If a field isn't listed here and doesn't have @@ -66,11 +67,11 @@ use constant CONFIG_VARS => ( # variable by default, then that field will be automatically created by # the migrator and you don't have to worry about it. END - }, - { - name => 'translate_values', - default => {}, - desc => <<'END', + }, + { + name => 'translate_values', + default => {}, + desc => <<'END', # This configuration variable allows you to say that a particular field # value in your current bug-tracker should be translated to a different # value when it's imported into Bugzilla. @@ -110,22 +111,22 @@ END # # Values that don't get translated will be imported as-is. END - }, - { - name => 'starting_bug_id', - default => 0, - desc => <<'END', + }, + { + name => 'starting_bug_id', + default => 0, + desc => <<'END', # What bug ID do you want the first imported bug to get? If you set this to # 0, then the imported bug ids will just start right after the current # bug ids. If you use this configuration variable, you must make sure that # nobody else is using your Bugzilla while you run the migration, or a new # bug filed by a user might take this ID instead. END - }, - { - name => 'timezone', - default => 'local', - desc => <<'END', + }, + { + name => 'timezone', + default => 'local', + desc => <<'END', # If migrate.pl comes across any dates without timezones, while doing the # migration, what timezone should we assume those dates are in? # The best format for this variable is something like "America/Los Angeles". @@ -135,7 +136,7 @@ END # The special value "local" means "use the same timezone as the system I # am running this script on now". END - }, + }, ); use constant USER_FIELDS => qw(user assigned_to qa_contact reporter cc); @@ -145,43 +146,46 @@ use constant USER_FIELDS => qw(user assigned_to qa_contact reporter cc); ######################### sub do_migration { - my $self = shift; - my $dbh = Bugzilla->dbh; - # On MySQL, setting serial values implicitly commits a transaction, - # so we want to do it up here, outside of any transaction. This also - # has the advantage of loading the config before anything else is done. - if ($self->config('starting_bug_id')) { - $dbh->bz_set_next_serial_value('bugs', 'bug_id', - $self->config('starting_bug_id')); - } - $dbh->bz_start_transaction(); - - $self->before_read(); - # Read Other Database - my $users = $self->users; - my $products = $self->products; - my $bugs = $self->bugs; - $self->after_read(); - - $self->translate_all_bugs($bugs); - - Bugzilla->set_user(Bugzilla::User->super_user); - - # Insert into Bugzilla - $self->before_insert(); - $self->insert_users($users); - $self->insert_products($products); - $self->create_custom_fields(); - $self->create_legal_values($bugs); - $self->insert_bugs($bugs); - $self->after_insert(); - if ($self->dry_run) { - $dbh->bz_rollback_transaction(); - $self->reset_serial_values(); - } - else { - $dbh->bz_commit_transaction(); - } + my $self = shift; + my $dbh = Bugzilla->dbh; + + # On MySQL, setting serial values implicitly commits a transaction, + # so we want to do it up here, outside of any transaction. This also + # has the advantage of loading the config before anything else is done. + if ($self->config('starting_bug_id')) { + $dbh->bz_set_next_serial_value('bugs', 'bug_id', + $self->config('starting_bug_id')); + } + $dbh->bz_start_transaction(); + + $self->before_read(); + + # Read Other Database + my $users = $self->users; + my $products = $self->products; + my $bugs = $self->bugs; + $self->after_read(); + + $self->translate_all_bugs($bugs); + + Bugzilla->set_user(Bugzilla::User->super_user); + + # Insert into Bugzilla + $self->before_insert(); + $self->insert_users($users); + $self->insert_products($products); + $self->create_custom_fields(); + $self->create_legal_values($bugs); + $self->insert_bugs($bugs); + $self->after_insert(); + + if ($self->dry_run) { + $dbh->bz_rollback_transaction(); + $self->reset_serial_values(); + } + else { + $dbh->bz_commit_transaction(); + } } ################ @@ -189,36 +193,34 @@ sub do_migration { ################ sub new { - my ($class) = @_; - my $self = { }; - bless $self, $class; - return $self; + my ($class) = @_; + my $self = {}; + bless $self, $class; + return $self; } sub load { - my ($class, $from) = @_; - my $libdir = bz_locations()->{libpath}; - my @migration_modules = glob("$libdir/Bugzilla/Migrate/*"); - my ($module) = grep { basename($_) =~ /^\Q$from\E\.pm$/i } - @migration_modules; - if ($module) { - require $module; - my $canonical_name = _canonical_name($module); - return "Bugzilla::Migrate::$canonical_name"->new; - } - else { - my $migrate_module = {}; - Bugzilla::Hook::process('migrate_modules', $migrate_module); - @migration_modules = glob($migrate_module->{path}."/*"); - ($module) = grep { basename($_) =~ /^\Q$from\E\.pm$/i } - @migration_modules; - if (!$module) { - ThrowUserError('migrate_from_invalid', { from => $from }); - } - require $module; - my $canonical_name = _canonical_name($module); - return "$migrate_module->{prefix}::$canonical_name"->new; + my ($class, $from) = @_; + my $libdir = bz_locations()->{libpath}; + my @migration_modules = glob("$libdir/Bugzilla/Migrate/*"); + my ($module) = grep { basename($_) =~ /^\Q$from\E\.pm$/i } @migration_modules; + if ($module) { + require $module; + my $canonical_name = _canonical_name($module); + return "Bugzilla::Migrate::$canonical_name"->new; + } + else { + my $migrate_module = {}; + Bugzilla::Hook::process('migrate_modules', $migrate_module); + @migration_modules = glob($migrate_module->{path} . "/*"); + ($module) = grep { basename($_) =~ /^\Q$from\E\.pm$/i } @migration_modules; + if (!$module) { + ThrowUserError('migrate_from_invalid', {from => $from}); } + require $module; + my $canonical_name = _canonical_name($module); + return "$migrate_module->{prefix}::$canonical_name"->new; + } } ############# @@ -226,67 +228,67 @@ sub load { ############# sub name { - my $self = shift; - return _canonical_name(ref $self); + my $self = shift; + return _canonical_name(ref $self); } sub dry_run { - my ($self, $value) = @_; - if (scalar(@_) > 1) { - $self->{dry_run} = $value; - } - return $self->{dry_run} || 0; + my ($self, $value) = @_; + if (scalar(@_) > 1) { + $self->{dry_run} = $value; + } + return $self->{dry_run} || 0; } sub verbose { - my ($self, $value) = @_; - if (scalar(@_) > 1) { - $self->{verbose} = $value; - } - return $self->{verbose} || 0; + my ($self, $value) = @_; + if (scalar(@_) > 1) { + $self->{verbose} = $value; + } + return $self->{verbose} || 0; } sub debug { - my ($self, $value, $level) = @_; - $level ||= 1; - if ($self->verbose >= $level) { - $value = Dumper($value) if ref $value; - print STDERR $value, "\n"; - } + my ($self, $value, $level) = @_; + $level ||= 1; + if ($self->verbose >= $level) { + $value = Dumper($value) if ref $value; + print STDERR $value, "\n"; + } } sub bug_fields { - my $self = shift; - $self->{bug_fields} ||= Bugzilla->fields({ by_name => 1 }); - return $self->{bug_fields}; + my $self = shift; + $self->{bug_fields} ||= Bugzilla->fields({by_name => 1}); + return $self->{bug_fields}; } sub users { - my $self = shift; - if (!exists $self->{users}) { - say get_text('migrate_reading_users'); - $self->{users} = $self->_read_users(); - } - return $self->{users}; + my $self = shift; + if (!exists $self->{users}) { + say get_text('migrate_reading_users'); + $self->{users} = $self->_read_users(); + } + return $self->{users}; } sub products { - my $self = shift; - if (!exists $self->{products}) { - say get_text('migrate_reading_products'); - $self->{products} = $self->_read_products(); - } - return $self->{products}; + my $self = shift; + if (!exists $self->{products}) { + say get_text('migrate_reading_products'); + $self->{products} = $self->_read_products(); + } + return $self->{products}; } sub bugs { - my $self = shift; - if (!exists $self->{bugs}) { - say get_text('migrate_reading_bugs'); - $self->{bugs} = $self->_read_bugs(); - } - return $self->{bugs}; + my $self = shift; + if (!exists $self->{bugs}) { + say get_text('migrate_reading_bugs'); + $self->{bugs} = $self->_read_bugs(); + } + return $self->{bugs}; } ########### @@ -294,49 +296,49 @@ sub bugs { ########### sub check_requirements { - my $self = shift; - my $missing = Bugzilla::Install::Requirements::_check_missing( - $self->REQUIRED_MODULES, 1); - my %results = ( - apache => [], - pass => @$missing ? 0 : 1, - missing => $missing, - any_missing => @$missing ? 1 : 0, - hide_all => 1, - # These are just for compatibility with print_module_instructions - one_dbd => 1, - optional => [], - ); - Bugzilla::Install::Requirements::print_module_instructions( - \%results, 1); - exit(1) if @$missing; + my $self = shift; + my $missing + = Bugzilla::Install::Requirements::_check_missing($self->REQUIRED_MODULES, 1); + my %results = ( + apache => [], + pass => @$missing ? 0 : 1, + missing => $missing, + any_missing => @$missing ? 1 : 0, + hide_all => 1, + + # These are just for compatibility with print_module_instructions + one_dbd => 1, + optional => [], + ); + Bugzilla::Install::Requirements::print_module_instructions(\%results, 1); + exit(1) if @$missing; } sub reset_serial_values { - my $self = shift; - return if $self->{serial_values_reset}; - my $dbh = Bugzilla->dbh; - my %reset = ( - 'bugs' => 'bug_id', - 'attachments' => 'attach_id', - 'profiles' => 'userid', - 'longdescs' => 'comment_id', - 'products' => 'id', - 'components' => 'id', - 'versions' => 'id', - 'milestones' => 'id', - ); - my @select_fields = grep { $_->is_select } (values %{ $self->bug_fields }); - foreach my $field (@select_fields) { - next if $field->is_abnormal; - $reset{$field->name} = 'id'; - } - - while (my ($table, $column) = each %reset) { - $dbh->bz_set_next_serial_value($table, $column); - } - - $self->{serial_values_reset} = 1; + my $self = shift; + return if $self->{serial_values_reset}; + my $dbh = Bugzilla->dbh; + my %reset = ( + 'bugs' => 'bug_id', + 'attachments' => 'attach_id', + 'profiles' => 'userid', + 'longdescs' => 'comment_id', + 'products' => 'id', + 'components' => 'id', + 'versions' => 'id', + 'milestones' => 'id', + ); + my @select_fields = grep { $_->is_select } (values %{$self->bug_fields}); + foreach my $field (@select_fields) { + next if $field->is_abnormal; + $reset{$field->name} = 'id'; + } + + while (my ($table, $column) = each %reset) { + $dbh->bz_set_next_serial_value($table, $column); + } + + $self->{serial_values_reset} = 1; } ################### @@ -344,160 +346,167 @@ sub reset_serial_values { ################### sub translate_all_bugs { - my ($self, $bugs) = @_; - say get_text('migrate_translating_bugs'); - # We modify the array in place so that $self->bugs will return the - # modified bugs, in case $self->before_insert wants them. - my $num_bugs = scalar(@$bugs); - for (my $i = 0; $i < $num_bugs; $i++) { - $bugs->[$i] = $self->translate_bug($bugs->[$i]); - } + my ($self, $bugs) = @_; + say get_text('migrate_translating_bugs'); + + # We modify the array in place so that $self->bugs will return the + # modified bugs, in case $self->before_insert wants them. + my $num_bugs = scalar(@$bugs); + for (my $i = 0; $i < $num_bugs; $i++) { + $bugs->[$i] = $self->translate_bug($bugs->[$i]); + } } sub translate_bug { - my ($self, $fields) = @_; - my (%bug, %other_fields); - my $original_status; - foreach my $field (keys %$fields) { - my $value = delete $fields->{$field}; - my $bz_field = $self->translate_field($field); - if ($bz_field) { - $bug{$bz_field} = $self->translate_value($bz_field, $value); - if ($bz_field eq 'bug_status') { - $original_status = $value; - } - } - else { - $other_fields{$field} = $value; - } + my ($self, $fields) = @_; + my (%bug, %other_fields); + my $original_status; + foreach my $field (keys %$fields) { + my $value = delete $fields->{$field}; + my $bz_field = $self->translate_field($field); + if ($bz_field) { + $bug{$bz_field} = $self->translate_value($bz_field, $value); + if ($bz_field eq 'bug_status') { + $original_status = $value; + } } - - if (defined $original_status and !defined $bug{resolution} - and $self->map_value('bug_status_resolution', $original_status)) - { - $bug{resolution} = $self->map_value('bug_status_resolution', - $original_status); + else { + $other_fields{$field} = $value; } - - $bug{comment} = $self->_generate_description(\%bug, \%other_fields); - - return wantarray ? (\%bug, \%other_fields) : \%bug; + } + + if ( defined $original_status + and !defined $bug{resolution} + and $self->map_value('bug_status_resolution', $original_status)) + { + $bug{resolution} = $self->map_value('bug_status_resolution', $original_status); + } + + $bug{comment} = $self->_generate_description(\%bug, \%other_fields); + + return wantarray ? (\%bug, \%other_fields) : \%bug; } sub _generate_description { - my ($self, $bug, $fields) = @_; - - my $description = ""; - foreach my $field (sort keys %$fields) { - next if grep($_ eq $field, $self->NON_COMMENT_FIELDS); - my $value = delete $fields->{$field}; - next if $value eq ''; - $description .= "$field: $value\n"; - } - $description .= "\n" if $description; - - return $description . $bug->{comment}; + my ($self, $bug, $fields) = @_; + + my $description = ""; + foreach my $field (sort keys %$fields) { + next if grep($_ eq $field, $self->NON_COMMENT_FIELDS); + my $value = delete $fields->{$field}; + next if $value eq ''; + $description .= "$field: $value\n"; + } + $description .= "\n" if $description; + + return $description . $bug->{comment}; } sub translate_field { - my ($self, $field) = @_; - my $mapped = $self->config('translate_fields')->{$field}; - return $mapped if defined $mapped; - ($mapped) = grep { lc($_) eq lc($field) } (keys %{ $self->bug_fields }); - return $mapped; + my ($self, $field) = @_; + my $mapped = $self->config('translate_fields')->{$field}; + return $mapped if defined $mapped; + ($mapped) = grep { lc($_) eq lc($field) } (keys %{$self->bug_fields}); + return $mapped; } sub parse_date { - my ($self, $date) = @_; - my @time = strptime($date); - # Handle times with timezones that strptime doesn't know about. - if (!scalar @time) { - $date =~ s/\s+\S+$//; - @time = strptime($date); + my ($self, $date) = @_; + my @time = strptime($date); + + # Handle times with timezones that strptime doesn't know about. + if (!scalar @time) { + $date =~ s/\s+\S+$//; + @time = strptime($date); + } + my $tz; + if ($time[6]) { + $tz = DateTime::TimeZone->offset_as_string($time[6]); + } + else { + $tz = $self->config('timezone'); + $tz =~ s/\s/_/g; + if ($tz eq 'local') { + $tz = Bugzilla->local_timezone; } - my $tz; - if ($time[6]) { - $tz = DateTime::TimeZone->offset_as_string($time[6]); - } - else { - $tz = $self->config('timezone'); - $tz =~ s/\s/_/g; - if ($tz eq 'local') { - $tz = Bugzilla->local_timezone; - } - } - my $dt = DateTime->new({ - year => $time[5] + 1900, - month => $time[4] + 1, - day => $time[3], - hour => $time[2], - minute => $time[1], - second => int($time[0]), - time_zone => $tz, - }); - $dt->set_time_zone(Bugzilla->local_timezone); - return $dt->iso8601; + } + my $dt = DateTime->new({ + year => $time[5] + 1900, + month => $time[4] + 1, + day => $time[3], + hour => $time[2], + minute => $time[1], + second => int($time[0]), + time_zone => $tz, + }); + $dt->set_time_zone(Bugzilla->local_timezone); + return $dt->iso8601; } sub translate_value { - my ($self, $field, $value) = @_; - - if (!defined $value) { - warn("Got undefined value for $field\n"); - $value = ''; - } - - if (ref($value) eq 'ARRAY') { - return [ map($self->translate_value($field, $_), @$value) ]; - } + my ($self, $field, $value) = @_; + + if (!defined $value) { + warn("Got undefined value for $field\n"); + $value = ''; + } + + if (ref($value) eq 'ARRAY') { + return [map($self->translate_value($field, $_), @$value)]; + } - - if (defined $self->map_value($field, $value)) { - return $self->map_value($field, $value); - } - - if (grep($_ eq $field, USER_FIELDS)) { - if (defined $self->map_value('user', $value)) { - return $self->map_value('user', $value); - } - } - my $field_obj = $self->bug_fields->{$field}; - if ($field eq 'creation_ts' - or $field eq 'delta_ts' - or ($field_obj and - ($field_obj->type == FIELD_TYPE_DATETIME - or $field_obj->type == FIELD_TYPE_DATE))) - { - $value = trim($value); - return undef if !$value; - return $self->parse_date($value); + if (defined $self->map_value($field, $value)) { + return $self->map_value($field, $value); + } + + if (grep($_ eq $field, USER_FIELDS)) { + if (defined $self->map_value('user', $value)) { + return $self->map_value('user', $value); } - - return $value; + } + + my $field_obj = $self->bug_fields->{$field}; + if ( + $field eq 'creation_ts' + or $field eq 'delta_ts' + or ( + $field_obj + and + ($field_obj->type == FIELD_TYPE_DATETIME or $field_obj->type == FIELD_TYPE_DATE) + ) + ) + { + $value = trim($value); + return undef if !$value; + return $self->parse_date($value); + } + + return $value; } sub map_value { - my ($self, $field, $value) = @_; - return $self->_value_map->{$field}->{lc($value)}; + my ($self, $field, $value) = @_; + return $self->_value_map->{$field}->{lc($value)}; } sub _value_map { - my $self = shift; - if (!defined $self->{_value_map}) { - # Lowercase all values to make them case-insensitive. - my %map; - my $translation = $self->config('translate_values'); - foreach my $field (keys %$translation) { - my $value_mapping = $translation->{$field}; - foreach my $value (keys %$value_mapping) { - $map{$field}->{lc($value)} = $value_mapping->{$value}; - } - } - $self->{_value_map} = \%map; + my $self = shift; + if (!defined $self->{_value_map}) { + + # Lowercase all values to make them case-insensitive. + my %map; + my $translation = $self->config('translate_values'); + foreach my $field (keys %$translation) { + my $value_mapping = $translation->{$field}; + foreach my $value (keys %$value_mapping) { + $map{$field}->{lc($value)} = $value_mapping->{$value}; + } } - return $self->{_value_map}; + $self->{_value_map} = \%map; + } + return $self->{_value_map}; } ################# @@ -505,387 +514,402 @@ sub _value_map { ################# sub config { - my ($self, $var) = @_; - if (!exists $self->{config}) { - $self->{config} = $self->read_config; - } - return $self->{config}->{$var}; + my ($self, $var) = @_; + if (!exists $self->{config}) { + $self->{config} = $self->read_config; + } + return $self->{config}->{$var}; } sub config_file_name { - my $self = shift; - my $name = $self->name; - my $dir = bz_locations()->{datadir}; - return "$dir/migrate-$name.cfg" + my $self = shift; + my $name = $self->name; + my $dir = bz_locations()->{datadir}; + return "$dir/migrate-$name.cfg"; } sub read_config { - my ($self) = @_; - my $file = $self->config_file_name; - if (!-e $file) { - $self->write_config(); - ThrowUserError('migrate_config_created', { file => $file }); - } - open(my $fh, "<", $file) || die "$file: $!"; - my $safe = new Safe; - $safe->rdo($file); - my @read_symbols = map($_->{name}, $self->CONFIG_VARS); - my %config; - foreach my $var (@read_symbols) { - my $glob = $safe->varglob($var); - $config{$var} = $$glob; - } - return \%config; + my ($self) = @_; + my $file = $self->config_file_name; + if (!-e $file) { + $self->write_config(); + ThrowUserError('migrate_config_created', {file => $file}); + } + open(my $fh, "<", $file) || die "$file: $!"; + my $safe = new Safe; + $safe->rdo($file); + my @read_symbols = map($_->{name}, $self->CONFIG_VARS); + my %config; + foreach my $var (@read_symbols) { + my $glob = $safe->varglob($var); + $config{$var} = $$glob; + } + return \%config; } sub write_config { - my ($self) = @_; - my $file = $self->config_file_name; - open(my $fh, ">", $file) || die "$file: $!"; - # Fixed indentation - local $Data::Dumper::Indent = 1; - local $Data::Dumper::Quotekeys = 0; - local $Data::Dumper::Sortkeys = 1; - foreach my $var ($self->CONFIG_VARS) { - print $fh "\n", $var->{desc}, - Data::Dumper->Dump([$var->{default}], [$var->{name}]); - } - close($fh); + my ($self) = @_; + my $file = $self->config_file_name; + open(my $fh, ">", $file) || die "$file: $!"; + + # Fixed indentation + local $Data::Dumper::Indent = 1; + local $Data::Dumper::Quotekeys = 0; + local $Data::Dumper::Sortkeys = 1; + foreach my $var ($self->CONFIG_VARS) { + print $fh "\n", $var->{desc}, + Data::Dumper->Dump([$var->{default}], [$var->{name}]); + } + close($fh); } #################################### # Default Implementations of Hooks # #################################### -sub after_insert {} -sub before_insert {} -sub after_read {} -sub before_read {} +sub after_insert { } +sub before_insert { } +sub after_read { } +sub before_read { } ############# # Inserters # ############# sub insert_users { - my ($self, $users) = @_; - foreach my $user (@$users) { - next if new Bugzilla::User({ name => $user->{login_name} }); - my $generated_password; - if (!defined $user->{cryptpassword}) { - $generated_password = lc(generate_random_password()); - $user->{cryptpassword} = $generated_password; - } - my $created = Bugzilla::User->create($user); - print get_text('migrate_user_created', - { created => $created, - password => $generated_password }), "\n"; + my ($self, $users) = @_; + foreach my $user (@$users) { + next if new Bugzilla::User({name => $user->{login_name}}); + my $generated_password; + if (!defined $user->{cryptpassword}) { + $generated_password = lc(generate_random_password()); + $user->{cryptpassword} = $generated_password; } + my $created = Bugzilla::User->create($user); + print get_text('migrate_user_created', + {created => $created, password => $generated_password}), + "\n"; + } } # XXX This should also insert Classifications. sub insert_products { - my ($self, $products) = @_; - foreach my $product (@$products) { - my $components = delete $product->{components}; - - my $created_prod = new Bugzilla::Product({ name => $product->{name} }); - if (!$created_prod) { - $created_prod = Bugzilla::Product->create($product); - print get_text('migrate_product_created', - { created => $created_prod }), "\n"; - } - - foreach my $component (@$components) { - next if new Bugzilla::Component({ product => $created_prod, - name => $component->{name} }); - my $created_comp = Bugzilla::Component->create( - { %$component, product => $created_prod }); - print ' ', get_text('migrate_component_created', - { comp => $created_comp, - product => $created_prod }), "\n"; - } + my ($self, $products) = @_; + foreach my $product (@$products) { + my $components = delete $product->{components}; + + my $created_prod = new Bugzilla::Product({name => $product->{name}}); + if (!$created_prod) { + $created_prod = Bugzilla::Product->create($product); + print get_text('migrate_product_created', {created => $created_prod}), "\n"; + } + + foreach my $component (@$components) { + next + if new Bugzilla::Component({ + product => $created_prod, name => $component->{name} + }); + my $created_comp + = Bugzilla::Component->create({%$component, product => $created_prod}); + print ' ', + get_text('migrate_component_created', + {comp => $created_comp, product => $created_prod}), + "\n"; } + } } sub create_custom_fields { - my $self = shift; - foreach my $field (keys %{ $self->CUSTOM_FIELDS }) { - next if new Bugzilla::Field({ name => $field }); - my %values = %{ $self->CUSTOM_FIELDS->{$field} }; - # We set these all here for the dry-run case. - my $created = { %values, name => $field, custom => 1 }; - if (!$self->dry_run) { - $created = Bugzilla::Field->create($created); - } - say get_text('migrate_field_created', { field => $created }); + my $self = shift; + foreach my $field (keys %{$self->CUSTOM_FIELDS}) { + next if new Bugzilla::Field({name => $field}); + my %values = %{$self->CUSTOM_FIELDS->{$field}}; + + # We set these all here for the dry-run case. + my $created = {%values, name => $field, custom => 1}; + if (!$self->dry_run) { + $created = Bugzilla::Field->create($created); } - delete $self->{bug_fields}; + say get_text('migrate_field_created', {field => $created}); + } + delete $self->{bug_fields}; } sub create_legal_values { - my ($self, $bugs) = @_; - my @select_fields = grep($_->is_select, values %{ $self->bug_fields }); - - # Get all the values in use on all the bugs we're importing. - my (%values, %product_values); - foreach my $bug (@$bugs) { - foreach my $field (@select_fields) { - my $name = $field->name; - next if !defined $bug->{$name}; - $values{$name}->{$bug->{$name}} = 1; - } - foreach my $field (qw(version target_milestone)) { - # Fix per-product bug values here, because it's easier than - # doing it during _insert_bugs. - if (!defined $bug->{$field} or trim($bug->{$field}) eq '') { - my $accessor = $field; - $accessor =~ s/^target_//; $accessor .= "s"; - my $product = Bugzilla::Product->check($bug->{product}); - $bug->{$field} = $product->$accessor->[0]->name; - next; - } - $product_values{$bug->{product}}->{$field}->{$bug->{$field}} = 1; - } - } - + my ($self, $bugs) = @_; + my @select_fields = grep($_->is_select, values %{$self->bug_fields}); + + # Get all the values in use on all the bugs we're importing. + my (%values, %product_values); + foreach my $bug (@$bugs) { foreach my $field (@select_fields) { - next if $field->is_abnormal; - my $name = $field->name; - foreach my $value (keys %{ $values{$name} }) { - next if Bugzilla::Field::Choice->type($field)->new({ name => $value }); - Bugzilla::Field::Choice->type($field)->create({ value => $value }); - print get_text('migrate_value_created', - { field => $field, value => $value }), "\n"; - } + my $name = $field->name; + next if !defined $bug->{$name}; + $values{$name}->{$bug->{$name}} = 1; } - - foreach my $product (keys %product_values) { - my $prod_obj = Bugzilla::Product->check($product); - foreach my $version (keys %{ $product_values{$product}->{version} }) { - next if new Bugzilla::Version({ product => $prod_obj, - name => $version }); - my $created = Bugzilla::Version->create({ product => $prod_obj, - value => $version }); - my $field = $self->bug_fields->{version}; - print get_text('migrate_value_created', { product => $prod_obj, - field => $field, - value => $created->name }), "\n"; - } - foreach my $milestone (keys %{ $product_values{$product}->{target_milestone} }) { - next if new Bugzilla::Milestone({ product => $prod_obj, - name => $milestone }); - my $created = Bugzilla::Milestone->create( - { product => $prod_obj, value => $milestone }); - my $field = $self->bug_fields->{target_milestone}; - print get_text('migrate_value_created', { product => $prod_obj, - field => $field, - value => $created->name }), "\n"; - - } + foreach my $field (qw(version target_milestone)) { + + # Fix per-product bug values here, because it's easier than + # doing it during _insert_bugs. + if (!defined $bug->{$field} or trim($bug->{$field}) eq '') { + my $accessor = $field; + $accessor =~ s/^target_//; + $accessor .= "s"; + my $product = Bugzilla::Product->check($bug->{product}); + $bug->{$field} = $product->$accessor->[0]->name; + next; + } + $product_values{$bug->{product}}->{$field}->{$bug->{$field}} = 1; + } + } + + foreach my $field (@select_fields) { + next if $field->is_abnormal; + my $name = $field->name; + foreach my $value (keys %{$values{$name}}) { + next if Bugzilla::Field::Choice->type($field)->new({name => $value}); + Bugzilla::Field::Choice->type($field)->create({value => $value}); + print get_text('migrate_value_created', {field => $field, value => $value}), + "\n"; + } + } + + foreach my $product (keys %product_values) { + my $prod_obj = Bugzilla::Product->check($product); + foreach my $version (keys %{$product_values{$product}->{version}}) { + next if new Bugzilla::Version({product => $prod_obj, name => $version}); + my $created + = Bugzilla::Version->create({product => $prod_obj, value => $version}); + my $field = $self->bug_fields->{version}; + print get_text('migrate_value_created', + {product => $prod_obj, field => $field, value => $created->name}), + "\n"; + } + foreach my $milestone (keys %{$product_values{$product}->{target_milestone}}) { + next if new Bugzilla::Milestone({product => $prod_obj, name => $milestone}); + my $created + = Bugzilla::Milestone->create({product => $prod_obj, value => $milestone}); + my $field = $self->bug_fields->{target_milestone}; + print get_text('migrate_value_created', + {product => $prod_obj, field => $field, value => $created->name}), + "\n"; + } - + } + } sub insert_bugs { - my ($self, $bugs) = @_; - my $dbh = Bugzilla->dbh; - say get_text('migrate_creating_bugs'); - - my $init_statuses = Bugzilla::Status->can_change_to(); - my %allowed_statuses = map { lc($_->name) => 1 } @$init_statuses; - # Bypass the question of whether or not we can file UNCONFIRMED - # in any product by simply picking a non-UNCONFIRMED status as our - # default for bugs that don't have a status specified. - my $default_status = first { $_->name ne 'UNCONFIRMED' } @$init_statuses; - # Use the first resolution that's not blank. - my $default_resolution = - first { $_->name ne '' } - @{ $self->bug_fields->{resolution}->legal_values }; - - # Set the values of any required drop-down fields that aren't set. - my @standard_drop_downs = grep { !$_->custom and $_->is_select } - (values %{ $self->bug_fields }); - # Make bug_status get set before resolution. - @standard_drop_downs = sort { $a->name cmp $b->name } @standard_drop_downs; - # Cache all statuses for setting the resolution. - my %statuses = map { lc($_->name) => $_ } Bugzilla::Status->get_all; - - my $total = scalar @$bugs; - my $count = 1; - foreach my $bug (@$bugs) { - my $comments = delete $bug->{comments}; - my $history = delete $bug->{history}; - my $attachments = delete $bug->{attachments}; - - $self->debug($bug, 3); - - foreach my $field (@standard_drop_downs) { - next if $field->is_abnormal; - my $field_name = $field->name; - if (!defined $bug->{$field_name}) { - # If there's a default value for this, then just let create() - # pick it. - next if grep($_->is_default, @{ $field->legal_values }); - # Otherwise, pick the first valid value if this is a required - # field. - if ($field_name eq 'bug_status') { - $bug->{bug_status} = $default_status; - } - elsif ($field_name eq 'resolution') { - my $status = $statuses{lc($bug->{bug_status})}; - if (!$status->is_open) { - $bug->{resolution} = $default_resolution; - } - } - else { - $bug->{$field_name} = $field->legal_values->[0]->name; - } - } - } - - my $product = Bugzilla::Product->check($bug->{product}); - - # If this isn't a legal starting status, or if the bug has a - # resolution, then those will have to be set after creating the bug. - # We make them into objects so that we can normalize their names. - my ($set_status, $set_resolution); - if (defined $bug->{resolution}) { - $set_resolution = Bugzilla::Field::Choice->type('resolution') - ->new({ name => delete $bug->{resolution} }); + my ($self, $bugs) = @_; + my $dbh = Bugzilla->dbh; + say get_text('migrate_creating_bugs'); + + my $init_statuses = Bugzilla::Status->can_change_to(); + my %allowed_statuses = map { lc($_->name) => 1 } @$init_statuses; + + # Bypass the question of whether or not we can file UNCONFIRMED + # in any product by simply picking a non-UNCONFIRMED status as our + # default for bugs that don't have a status specified. + my $default_status = first { $_->name ne 'UNCONFIRMED' } @$init_statuses; + + # Use the first resolution that's not blank. + my $default_resolution = first { $_->name ne '' } + @{$self->bug_fields->{resolution}->legal_values}; + + # Set the values of any required drop-down fields that aren't set. + my @standard_drop_downs + = grep { !$_->custom and $_->is_select } (values %{$self->bug_fields}); + + # Make bug_status get set before resolution. + @standard_drop_downs = sort { $a->name cmp $b->name } @standard_drop_downs; + + # Cache all statuses for setting the resolution. + my %statuses = map { lc($_->name) => $_ } Bugzilla::Status->get_all; + + my $total = scalar @$bugs; + my $count = 1; + foreach my $bug (@$bugs) { + my $comments = delete $bug->{comments}; + my $history = delete $bug->{history}; + my $attachments = delete $bug->{attachments}; + + $self->debug($bug, 3); + + foreach my $field (@standard_drop_downs) { + next if $field->is_abnormal; + my $field_name = $field->name; + if (!defined $bug->{$field_name}) { + + # If there's a default value for this, then just let create() + # pick it. + next if grep($_->is_default, @{$field->legal_values}); + + # Otherwise, pick the first valid value if this is a required + # field. + if ($field_name eq 'bug_status') { + $bug->{bug_status} = $default_status; } - if (!$allowed_statuses{lc($bug->{bug_status})}) { - $set_status = new Bugzilla::Status({ name => $bug->{bug_status} }); - # Set the starting status to some status that Bugzilla will - # accept. We're going to overwrite it immediately afterward. - $bug->{bug_status} = $default_status; + elsif ($field_name eq 'resolution') { + my $status = $statuses{lc($bug->{bug_status})}; + if (!$status->is_open) { + $bug->{resolution} = $default_resolution; + } } - - # If we're in dry-run mode, our custom fields haven't been created - # yet, so we shouldn't try to set them on creation. - if ($self->dry_run) { - foreach my $field (keys %{ $self->CUSTOM_FIELDS }) { - delete $bug->{$field}; - } + else { + $bug->{$field_name} = $field->legal_values->[0]->name; } - - # File the bug as the reporter. - my $super_user = Bugzilla->user; - my $reporter = Bugzilla::User->check($bug->{reporter}); - # Allow the user to file a bug in any product, no matter their current - # permissions. - $reporter->{groups} = $super_user->groups; - Bugzilla->set_user($reporter); - my $created = Bugzilla::Bug->create($bug); - $self->debug('Created bug ' . $created->id); - Bugzilla->set_user($super_user); - - if (defined $bug->{creation_ts}) { - $dbh->do('UPDATE bugs SET creation_ts = ?, delta_ts = ? + } + } + + my $product = Bugzilla::Product->check($bug->{product}); + + # If this isn't a legal starting status, or if the bug has a + # resolution, then those will have to be set after creating the bug. + # We make them into objects so that we can normalize their names. + my ($set_status, $set_resolution); + if (defined $bug->{resolution}) { + $set_resolution = Bugzilla::Field::Choice->type('resolution') + ->new({name => delete $bug->{resolution}}); + } + if (!$allowed_statuses{lc($bug->{bug_status})}) { + $set_status = new Bugzilla::Status({name => $bug->{bug_status}}); + + # Set the starting status to some status that Bugzilla will + # accept. We're going to overwrite it immediately afterward. + $bug->{bug_status} = $default_status; + } + + # If we're in dry-run mode, our custom fields haven't been created + # yet, so we shouldn't try to set them on creation. + if ($self->dry_run) { + foreach my $field (keys %{$self->CUSTOM_FIELDS}) { + delete $bug->{$field}; + } + } + + # File the bug as the reporter. + my $super_user = Bugzilla->user; + my $reporter = Bugzilla::User->check($bug->{reporter}); + + # Allow the user to file a bug in any product, no matter their current + # permissions. + $reporter->{groups} = $super_user->groups; + Bugzilla->set_user($reporter); + my $created = Bugzilla::Bug->create($bug); + $self->debug('Created bug ' . $created->id); + Bugzilla->set_user($super_user); + + if (defined $bug->{creation_ts}) { + $dbh->do( + 'UPDATE bugs SET creation_ts = ?, delta_ts = ? WHERE bug_id = ?', undef, $bug->{creation_ts}, - $bug->{creation_ts}, $created->id); - } - if (defined $bug->{delta_ts}) { - $dbh->do('UPDATE bugs SET delta_ts = ? WHERE bug_id = ?', - undef, $bug->{delta_ts}, $created->id); - } - # We don't need to send email for imported bugs. - $dbh->do('UPDATE bugs SET lastdiffed = delta_ts WHERE bug_id = ?', - undef, $created->id); - - # We don't use set_ and update() because that would create - # a bugs_activity entry that we don't want. - if ($set_status) { - $dbh->do('UPDATE bugs SET bug_status = ? WHERE bug_id = ?', - undef, $set_status->name, $created->id); - } - if ($set_resolution) { - $dbh->do('UPDATE bugs SET resolution = ? WHERE bug_id = ?', - undef, $set_resolution->name, $created->id); - } - - $self->_insert_comments($created, $comments); - $self->_insert_history($created, $history); - $self->_insert_attachments($created, $attachments); - - # bugs_fulltext isn't transactional, so if we're in a dry-run we - # need to delete anything that we put in there. - if ($self->dry_run) { - $dbh->do('DELETE FROM bugs_fulltext WHERE bug_id = ?', - undef, $created->id); - } + $bug->{creation_ts}, $created->id + ); + } + if (defined $bug->{delta_ts}) { + $dbh->do('UPDATE bugs SET delta_ts = ? WHERE bug_id = ?', + undef, $bug->{delta_ts}, $created->id); + } - if (!$self->verbose) { - indicate_progress({ current => $count++, every => 5, total => $total }); - } + # We don't need to send email for imported bugs. + $dbh->do('UPDATE bugs SET lastdiffed = delta_ts WHERE bug_id = ?', + undef, $created->id); + + # We don't use set_ and update() because that would create + # a bugs_activity entry that we don't want. + if ($set_status) { + $dbh->do('UPDATE bugs SET bug_status = ? WHERE bug_id = ?', + undef, $set_status->name, $created->id); } + if ($set_resolution) { + $dbh->do('UPDATE bugs SET resolution = ? WHERE bug_id = ?', + undef, $set_resolution->name, $created->id); + } + + $self->_insert_comments($created, $comments); + $self->_insert_history($created, $history); + $self->_insert_attachments($created, $attachments); + + # bugs_fulltext isn't transactional, so if we're in a dry-run we + # need to delete anything that we put in there. + if ($self->dry_run) { + $dbh->do('DELETE FROM bugs_fulltext WHERE bug_id = ?', undef, $created->id); + } + + if (!$self->verbose) { + indicate_progress({current => $count++, every => 5, total => $total}); + } + } } sub _insert_comments { - my ($self, $bug, $comments) = @_; - return if !$comments; - $self->debug(' Inserting comments:', 2); - foreach my $comment (@$comments) { - $self->debug($comment, 3); - my %copy = %$comment; - # XXX In the future, if we have a Bugzilla::Comment->create, this - # should use it. - my $who = Bugzilla::User->check(delete $copy{who}); - $copy{who} = $who->id; - $copy{bug_id} = $bug->id; - $self->_do_table_insert('longdescs', \%copy); - $self->debug(" Inserted comment from " . $who->login, 2); - } - $bug->_sync_fulltext( update_comments => 1 ); + my ($self, $bug, $comments) = @_; + return if !$comments; + $self->debug(' Inserting comments:', 2); + foreach my $comment (@$comments) { + $self->debug($comment, 3); + my %copy = %$comment; + + # XXX In the future, if we have a Bugzilla::Comment->create, this + # should use it. + my $who = Bugzilla::User->check(delete $copy{who}); + $copy{who} = $who->id; + $copy{bug_id} = $bug->id; + $self->_do_table_insert('longdescs', \%copy); + $self->debug(" Inserted comment from " . $who->login, 2); + } + $bug->_sync_fulltext(update_comments => 1); } sub _insert_history { - my ($self, $bug, $history) = @_; - return if !$history; - $self->debug(' Inserting history:', 2); - foreach my $item (@$history) { - $self->debug($item, 3); - my $who = Bugzilla::User->check($item->{who}); - LogActivityEntry($bug->id, $item->{field}, $item->{removed}, - $item->{added}, $who->id, $item->{bug_when}); - $self->debug(" $item->{field} change from " . $who->login, 2); - } + my ($self, $bug, $history) = @_; + return if !$history; + $self->debug(' Inserting history:', 2); + foreach my $item (@$history) { + $self->debug($item, 3); + my $who = Bugzilla::User->check($item->{who}); + LogActivityEntry($bug->id, $item->{field}, $item->{removed}, $item->{added}, + $who->id, $item->{bug_when}); + $self->debug(" $item->{field} change from " . $who->login, 2); + } } sub _insert_attachments { - my ($self, $bug, $attachments) = @_; - return if !$attachments; - $self->debug(' Inserting attachments:', 2); - foreach my $attachment (@$attachments) { - $self->debug($attachment, 3); - # Make sure that our pointer is at the beginning of the file, - # because usually it will be at the end, having just been fully - # written to. - if (ref $attachment->{data}) { - $attachment->{data}->seek(0, SEEK_SET); - } - - my $submitter = Bugzilla::User->check(delete $attachment->{submitter}); - my $super_user = Bugzilla->user; - # Make sure the submitter can attach this attachment no matter what. - $submitter->{groups} = $super_user->groups; - Bugzilla->set_user($submitter); - my $created = - Bugzilla::Attachment->create({ %$attachment, bug => $bug }); - $self->debug(' Attachment ' . $created->description . ' from ' - . $submitter->login, 2); - Bugzilla->set_user($super_user); + my ($self, $bug, $attachments) = @_; + return if !$attachments; + $self->debug(' Inserting attachments:', 2); + foreach my $attachment (@$attachments) { + $self->debug($attachment, 3); + + # Make sure that our pointer is at the beginning of the file, + # because usually it will be at the end, having just been fully + # written to. + if (ref $attachment->{data}) { + $attachment->{data}->seek(0, SEEK_SET); } + + my $submitter = Bugzilla::User->check(delete $attachment->{submitter}); + my $super_user = Bugzilla->user; + + # Make sure the submitter can attach this attachment no matter what. + $submitter->{groups} = $super_user->groups; + Bugzilla->set_user($submitter); + my $created = Bugzilla::Attachment->create({%$attachment, bug => $bug}); + $self->debug( + ' Attachment ' . $created->description . ' from ' . $submitter->login, 2); + Bugzilla->set_user($super_user); + } } sub _do_table_insert { - my ($self, $table, $hash) = @_; - my @fields = keys %$hash; - my @questions = ('?') x @fields; - my @values = map { $hash->{$_} } @fields; - my $field_sql = join(',', @fields); - my $question_sql = join(',', @questions); - Bugzilla->dbh->do("INSERT INTO $table ($field_sql) VALUES ($question_sql)", - undef, @values); + my ($self, $table, $hash) = @_; + my @fields = keys %$hash; + my @questions = ('?') x @fields; + my @values = map { $hash->{$_} } @fields; + my $field_sql = join(',', @fields); + my $question_sql = join(',', @questions); + Bugzilla->dbh->do("INSERT INTO $table ($field_sql) VALUES ($question_sql)", + undef, @values); } ###################### @@ -893,11 +917,11 @@ sub _do_table_insert { ###################### sub _canonical_name { - my ($module) = @_; - $module =~ s{::}{/}g; - $module = basename($module); - $module =~ s/\.pm$//g; - return $module; + my ($module) = @_; + $module =~ s{::}{/}g; + $module = basename($module); + $module =~ s/\.pm$//g; + return $module; } 1; diff --git a/Bugzilla/Migrate/Gnats.pm b/Bugzilla/Migrate/Gnats.pm index f16faf78cf..34e6cad2d4 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 => <
tags. - return qq|| - . qq|$link_text| - . qq| [details]| - . qq||; + if ($attachment) { + my $title = ""; + my $className = ""; + if ($user->can_see_bug($attachment->bug_id) + && (!$attachment->isprivate || $user->is_insider)) + { + $title = $attachment->description; } - else { - return qq{$link_text}; + if ($attachment->isobsolete) { + $className = "bz_obsolete"; } + + # Prevent code injection in the title. + $title = html_quote(clean_text($title)); + + $link_text =~ s/ \[details\]$//; + my $linkval = "attachment.cgi?id=$attachid"; + + # If the attachment is a patch, try to link to the diff rather + # than the text, by default. + my $patchlink = ""; + if ($attachment->ispatch and Bugzilla->feature('patch_viewer')) { + $patchlink = '&action=diff'; + } + + # Whitespace matters here because these links are intags. + return + qq|| + . qq|$link_text| + . qq| [details]| + . qq||; + } + else { + return qq{$link_text}; + } } # Creates a link to a bug, including its title. @@ -346,55 +360,61 @@ sub get_attachment_link { # comment in the bug sub get_bug_link { - my ($bug, $link_text, $options) = @_; - $options ||= {}; - $options->{user} ||= Bugzilla->user; - - if (defined $bug && $bug ne '') { - if (!blessed($bug)) { - require Bugzilla::Bug; - $bug = new Bugzilla::Bug({ id => $bug, cache => 1 }); - } - return $link_text if $bug->{error}; + my ($bug, $link_text, $options) = @_; + $options ||= {}; + $options->{user} ||= Bugzilla->user; + + if (defined $bug && $bug ne '') { + if (!blessed($bug)) { + require Bugzilla::Bug; + $bug = new Bugzilla::Bug({id => $bug, cache => 1}); } - - my $template = Bugzilla->template_inner; - my $linkified; - $template->process('bug/link.html.tmpl', - { bug => $bug, link_text => $link_text, %$options }, \$linkified); - $linkified =~ s/\n//g; # strip newlines to prevent markdown conflicts - $linkified =~ s/\|/|/g; # escape '|', it confuses markdown tables - return $linkified; + return $link_text if $bug->{error}; + } + + my $template = Bugzilla->template_inner; + my $linkified; + $template->process('bug/link.html.tmpl', + {bug => $bug, link_text => $link_text, %$options}, + \$linkified); + $linkified =~ s/\n//g; # strip newlines to prevent markdown conflicts + $linkified =~ s/\|/|/g; # escape '|', it confuses markdown tables + return $linkified; } # We use this instead of format because format doesn't deal well with # multi-byte languages. sub multiline_sprintf { - my ($format, $args, $sizes) = @_; - my @parts; - my @my_sizes = @$sizes; # Copy this so we don't modify the input array. - foreach my $string (@$args) { - my $size = shift @my_sizes; - my @pieces = split("\n", wrap_hard($string, $size)); - push(@parts, \@pieces); - } - - my $formatted; - while (1) { - # Get the first item of each part. - my @line = map { shift @$_ } @parts; - # If they're all undef, we're done. - last if !grep { defined $_ } @line; - # Make any single undef item into '' - @line = map { defined $_ ? $_ : '' } @line; - # And append a formatted line - $formatted .= sprintf($format, @line); - # Remove trailing spaces, or they become lots of =20's in - # quoted-printable emails. - $formatted =~ s/\s+$//; - $formatted .= "\n"; - } - return $formatted; + my ($format, $args, $sizes) = @_; + my @parts; + my @my_sizes = @$sizes; # Copy this so we don't modify the input array. + foreach my $string (@$args) { + my $size = shift @my_sizes; + my @pieces = split("\n", wrap_hard($string, $size)); + push(@parts, \@pieces); + } + + my $formatted; + while (1) { + + # Get the first item of each part. + my @line = map { shift @$_ } @parts; + + # If they're all undef, we're done. + last if !grep { defined $_ } @line; + + # Make any single undef item into '' + @line = map { defined $_ ? $_ : '' } @line; + + # And append a formatted line + $formatted .= sprintf($format, @line); + + # Remove trailing spaces, or they become lots of =20's in + # quoted-printable emails. + $formatted =~ s/\s+$//; + $formatted .= "\n"; + } + return $formatted; } ##################### @@ -406,17 +426,18 @@ sub multiline_sprintf { sub _mtime { return (stat($_[0]))[9] } sub mtime_filter { - my ($file_url, $mtime) = @_; - # This environment var is set in the .htaccess if we have mod_headers - # and mod_expires installed, to make sure that JS and CSS with "?" - # after them will still be cached by clients. - return $file_url if !$ENV{BZ_CACHE_CONTROL}; - if (!$mtime) { - my $cgi_path = bz_locations()->{'cgi_path'}; - my $file_path = "$cgi_path/$file_url"; - $mtime = _mtime($file_path); - } - return "$file_url?$mtime"; + my ($file_url, $mtime) = @_; + + # This environment var is set in the .htaccess if we have mod_headers + # and mod_expires installed, to make sure that JS and CSS with "?" + # after them will still be cached by clients. + return $file_url if !$ENV{BZ_CACHE_CONTROL}; + if (!$mtime) { + my $cgi_path = bz_locations()->{'cgi_path'}; + my $file_path = "$cgi_path/$file_url"; + $mtime = _mtime($file_path); + } + return "$file_url?$mtime"; } # Set up the skin CSS cascade: @@ -429,185 +450,188 @@ sub mtime_filter { # 6. Custom Bugzilla stylesheet set sub css_files { - my ($style_urls, $yui, $yui_css) = @_; + my ($style_urls, $yui, $yui_css) = @_; - # global.css goes on every page. - my @requested_css = ('skins/standard/global.css', @$style_urls); + # global.css goes on every page. + my @requested_css = ('skins/standard/global.css', @$style_urls); - my @yui_required_css; - foreach my $yui_name (@$yui) { - next if !$yui_css->{$yui_name}; - push(@yui_required_css, "js/yui/assets/skins/sam/$yui_name.css"); - } - unshift(@requested_css, @yui_required_css); - - my @css_sets = map { _css_link_set($_) } @requested_css; - - my %by_type = (standard => [], skin => [], custom => []); - foreach my $set (@css_sets) { - foreach my $key (keys %$set) { - push(@{ $by_type{$key} }, $set->{$key}); - } - } + my @yui_required_css; + foreach my $yui_name (@$yui) { + next if !$yui_css->{$yui_name}; + push(@yui_required_css, "js/yui/assets/skins/sam/$yui_name.css"); + } + unshift(@requested_css, @yui_required_css); + + my @css_sets = map { _css_link_set($_) } @requested_css; - # build concatenated - if (CONCATENATE_ASSETS) { - $by_type{concatenated_standard_skin} = _concatenate_css($by_type{standard}, - $by_type{skin}); - $by_type{concatenated_custom} = _concatenate_css($by_type{custom}); + my %by_type = (standard => [], skin => [], custom => []); + foreach my $set (@css_sets) { + foreach my $key (keys %$set) { + push(@{$by_type{$key}}, $set->{$key}); } + } + + # build concatenated + if (CONCATENATE_ASSETS) { + $by_type{concatenated_standard_skin} + = _concatenate_css($by_type{standard}, $by_type{skin}); + $by_type{concatenated_custom} = _concatenate_css($by_type{custom}); + } - return \%by_type; + return \%by_type; } sub _css_link_set { - my ($file_name) = @_; - - my %set = (standard => mtime_filter($file_name)); - - # We use (?:^|/) to allow Extensions to use the skins system if they want. - if ($file_name !~ m{(?:^|/)skins/standard/}) { - return \%set; - } + my ($file_name) = @_; - my $skin = Bugzilla->user->settings->{skin}->{value}; - my $cgi_path = bz_locations()->{'cgi_path'}; - my $skin_file_name = $file_name; - $skin_file_name =~ s{(?:^|/)skins/standard/}{skins/contrib/$skin/}; - if (my $mtime = _mtime("$cgi_path/$skin_file_name")) { - $set{skin} = mtime_filter($skin_file_name, $mtime); - } - - my $custom_file_name = $file_name; - $custom_file_name =~ s{(?:^|/)skins/standard/}{skins/custom/}; - if (my $custom_mtime = _mtime("$cgi_path/$custom_file_name")) { - $set{custom} = mtime_filter($custom_file_name, $custom_mtime); - } + my %set = (standard => mtime_filter($file_name)); + # We use (?:^|/) to allow Extensions to use the skins system if they want. + if ($file_name !~ m{(?:^|/)skins/standard/}) { return \%set; + } + + my $skin = Bugzilla->user->settings->{skin}->{value}; + my $cgi_path = bz_locations()->{'cgi_path'}; + my $skin_file_name = $file_name; + $skin_file_name =~ s{(?:^|/)skins/standard/}{skins/contrib/$skin/}; + if (my $mtime = _mtime("$cgi_path/$skin_file_name")) { + $set{skin} = mtime_filter($skin_file_name, $mtime); + } + + my $custom_file_name = $file_name; + $custom_file_name =~ s{(?:^|/)skins/standard/}{skins/custom/}; + if (my $custom_mtime = _mtime("$cgi_path/$custom_file_name")) { + $set{custom} = mtime_filter($custom_file_name, $custom_mtime); + } + + return \%set; } sub _concatenate_css { - my @sources = map { @$_ } @_; - return unless @sources; - - my %files = - map { - (my $file = $_) =~ s/(^[^\?]+)\?.+/$1/; - $_ => $file; - } @sources; - - my $cgi_path = bz_locations()->{cgi_path}; - my $skins_path = bz_locations()->{assetsdir}; - - # build minified files - my @minified; - foreach my $source (@sources) { - next unless -e "$cgi_path/$files{$source}"; - my $file = $skins_path . '/' . md5_hex($source) . '.css'; - if (!-e $file) { - my $content = read_text("$cgi_path/$files{$source}"); - - # minify - $content =~ s{/\*.*?\*/}{}sg; # comments - $content =~ s{(^\s+|\s+$)}{}mg; # leading/trailing whitespace - $content =~ s{\n}{}g; # single line - - # rewrite urls - $content =~ s{url\(([^\)]+)\)}{_css_url_rewrite($source, $1)}eig; - - write_text($file, "/* $files{$source} */\n" . $content . "\n"); - } - push @minified, $file; - } - - # concat files - my $file = $skins_path . '/' . md5_hex(join(' ', @sources)) . '.css'; + my @sources = map {@$_} @_; + return unless @sources; + + my %files = map { + (my $file = $_) =~ s/(^[^\?]+)\?.+/$1/; + $_ => $file; + } @sources; + + my $cgi_path = bz_locations()->{cgi_path}; + my $skins_path = bz_locations()->{assetsdir}; + + # build minified files + my @minified; + foreach my $source (@sources) { + next unless -e "$cgi_path/$files{$source}"; + my $file = $skins_path . '/' . md5_hex($source) . '.css'; if (!-e $file) { - my $content = ''; - foreach my $source (@minified) { - $content .= read_text($source); - } - write_text($file, $content); + my $content = read_text("$cgi_path/$files{$source}"); + + # minify + $content =~ s{/\*.*?\*/}{}sg; # comments + $content =~ s{(^\s+|\s+$)}{}mg; # leading/trailing whitespace + $content =~ s{\n}{}g; # single line + + # rewrite urls + $content =~ s{url\(([^\)]+)\)}{_css_url_rewrite($source, $1)}eig; + + write_text($file, "/* $files{$source} */\n" . $content . "\n"); } + push @minified, $file; + } + + # concat files + my $file = $skins_path . '/' . md5_hex(join(' ', @sources)) . '.css'; + if (!-e $file) { + my $content = ''; + foreach my $source (@minified) { + $content .= read_text($source); + } + write_text($file, $content); + } - $file =~ s/^\Q$cgi_path\E\///o; - return mtime_filter($file); + $file =~ s/^\Q$cgi_path\E\///o; + return mtime_filter($file); } sub _css_url_rewrite { - my ($source, $url) = @_; - # rewrite relative urls as the concatenated stylesheet lives in a different - # directory from the source - $url =~ s/(^['"]|['"]$)//g; - if (substr($url, 0, 1) eq '/' || substr($url, 0, 5) eq 'data:') { - return 'url(' . $url . ')'; - } - return 'url(../../' . ($ENV{'PROJECT'} ? '../' : '') . dirname($source) . '/' . $url . ')'; + my ($source, $url) = @_; + + # rewrite relative urls as the concatenated stylesheet lives in a different + # directory from the source + $url =~ s/(^['"]|['"]$)//g; + if (substr($url, 0, 1) eq '/' || substr($url, 0, 5) eq 'data:') { + return 'url(' . $url . ')'; + } + return + 'url(../../' + . ($ENV{'PROJECT'} ? '../' : '') + . dirname($source) . '/' + . $url . ')'; } sub _concatenate_js { - return @_ unless CONCATENATE_ASSETS; - my ($sources) = @_; - return [] unless $sources; - $sources = ref($sources) ? $sources : [ $sources ]; - - my %files = - map { - (my $file = $_) =~ s/(^[^\?]+)\?.+/$1/; - $_ => $file; - } @$sources; - - my $cgi_path = bz_locations()->{cgi_path}; - my $skins_path = bz_locations()->{assetsdir}; - - # build minified files - my @minified; - foreach my $source (@$sources) { - next unless -e "$cgi_path/$files{$source}"; - my $file = $skins_path . '/' . md5_hex($source) . '.js'; - if (!-e $file) { - my $content = read_text("$cgi_path/$files{$source}"); - - # minimal minification - $content =~ s#/\*.*?\*/##sg; # block comments - $content =~ s#(^ +| +$)##gm; # leading/trailing spaces - $content =~ s#^//.+$##gm; # single line comments - $content =~ s#\n{2,}#\n#g; # blank lines - $content =~ s#(^\s+|\s+$)##g; # whitespace at the start/end of file - - write_text($file, ";/* $files{$source} */\n" . $content . "\n"); - } - push @minified, $file; - } - - # concat files - my $file = $skins_path . '/' . md5_hex(join(' ', @$sources)) . '.js'; + return @_ unless CONCATENATE_ASSETS; + my ($sources) = @_; + return [] unless $sources; + $sources = ref($sources) ? $sources : [$sources]; + + my %files = map { + (my $file = $_) =~ s/(^[^\?]+)\?.+/$1/; + $_ => $file; + } @$sources; + + my $cgi_path = bz_locations()->{cgi_path}; + my $skins_path = bz_locations()->{assetsdir}; + + # build minified files + my @minified; + foreach my $source (@$sources) { + next unless -e "$cgi_path/$files{$source}"; + my $file = $skins_path . '/' . md5_hex($source) . '.js'; if (!-e $file) { - my $content = ''; - foreach my $source (@minified) { - $content .= read_text($source); - } - write_text($file, $content); + my $content = read_text("$cgi_path/$files{$source}"); + + # minimal minification + $content =~ s#/\*.*?\*/##sg; # block comments + $content =~ s#(^ +| +$)##gm; # leading/trailing spaces + $content =~ s#^//.+$##gm; # single line comments + $content =~ s#\n{2,}#\n#g; # blank lines + $content =~ s#(^\s+|\s+$)##g; # whitespace at the start/end of file + + write_text($file, ";/* $files{$source} */\n" . $content . "\n"); } + push @minified, $file; + } + + # concat files + my $file = $skins_path . '/' . md5_hex(join(' ', @$sources)) . '.js'; + if (!-e $file) { + my $content = ''; + foreach my $source (@minified) { + $content .= read_text($source); + } + write_text($file, $content); + } - $file =~ s/^\Q$cgi_path\E\///o; - return [ $file ]; + $file =~ s/^\Q$cgi_path\E\///o; + return [$file]; } # YUI dependency resolution sub yui_resolve_deps { - my ($yui, $yui_deps) = @_; - - my @yui_resolved; - foreach my $yui_name (@$yui) { - my $deps = $yui_deps->{$yui_name} || []; - foreach my $dep (reverse @$deps) { - push(@yui_resolved, $dep) if !grep { $_ eq $dep } @yui_resolved; - } - push(@yui_resolved, $yui_name) if !grep { $_ eq $yui_name } @yui_resolved; + my ($yui, $yui_deps) = @_; + + my @yui_resolved; + foreach my $yui_name (@$yui) { + my $deps = $yui_deps->{$yui_name} || []; + foreach my $dep (reverse @$deps) { + push(@yui_resolved, $dep) if !grep { $_ eq $dep } @yui_resolved; } - return \@yui_resolved; + push(@yui_resolved, $yui_name) if !grep { $_ eq $yui_name } @yui_resolved; + } + return \@yui_resolved; } ############################################################################### @@ -626,73 +650,75 @@ use Template::Stash; # Allow keys to start with an underscore or a dot. $Template::Stash::PRIVATE = undef; -# Add "contains***" methods to list variables that search for one or more -# items in a list and return boolean values representing whether or not +# Add "contains***" methods to list variables that search for one or more +# items in a list and return boolean values representing whether or not # one/all/any item(s) were found. -$Template::Stash::LIST_OPS->{ contains } = - sub { - my ($list, $item) = @_; - if (ref $item && $item->isa('Bugzilla::Object')) { - return grep($_->id == $item->id, @$list); - } else { - return grep($_ eq $item, @$list); - } - }; - -$Template::Stash::LIST_OPS->{ containsany } = - sub { - my ($list, $items) = @_; - foreach my $item (@$items) { - if (ref $item && $item->isa('Bugzilla::Object')) { - return 1 if grep($_->id == $item->id, @$list); - } else { - return 1 if grep($_ eq $item, @$list); - } - } - return 0; - }; +$Template::Stash::LIST_OPS->{contains} = sub { + my ($list, $item) = @_; + if (ref $item && $item->isa('Bugzilla::Object')) { + return grep($_->id == $item->id, @$list); + } + else { + return grep($_ eq $item, @$list); + } +}; + +$Template::Stash::LIST_OPS->{containsany} = sub { + my ($list, $items) = @_; + foreach my $item (@$items) { + if (ref $item && $item->isa('Bugzilla::Object')) { + return 1 if grep($_->id == $item->id, @$list); + } + else { + return 1 if grep($_ eq $item, @$list); + } + } + return 0; +}; # Clone the array reference to leave the original one unaltered. -$Template::Stash::LIST_OPS->{ clone } = - sub { - my $list = shift; - return [@$list]; - }; +$Template::Stash::LIST_OPS->{clone} = sub { + my $list = shift; + return [@$list]; +}; # Allow us to sort the list of fields correctly -$Template::Stash::LIST_OPS->{ sort_by_field_name } = - sub { - sub field_name { - if ($_[0] eq 'noop') { - # Sort --- first - return ''; - } - # Otherwise sort by field_desc or description - return $_[1]{$_[0]} || $_[0]; - } - my ($list, $field_desc) = @_; - return [ sort { lc field_name($a, $field_desc) cmp lc field_name($b, $field_desc) } @$list ]; - }; +$Template::Stash::LIST_OPS->{sort_by_field_name} = sub { + + sub field_name { + if ($_[0] eq 'noop') { + + # Sort --- first + return ''; + } + + # Otherwise sort by field_desc or description + return $_[1]{$_[0]} || $_[0]; + } + my ($list, $field_desc) = @_; + return [ + sort { lc field_name($a, $field_desc) cmp lc field_name($b, $field_desc) } + @$list + ]; +}; # Allow us to still get the scalar if we use the list operation ".0" on it, # as we often do for defaults in query.cgi and other places. -$Template::Stash::SCALAR_OPS->{ 0 } = - sub { - return $_[0]; - }; +$Template::Stash::SCALAR_OPS->{0} = sub { + return $_[0]; +}; # Add a "truncate" method to the Template Toolkit's "scalar" object # that truncates a string to a certain length. -$Template::Stash::SCALAR_OPS->{ truncate } = - sub { - my ($string, $length, $ellipsis) = @_; - return $string if !$length || length($string) <= $length; - - $ellipsis ||= ''; - my $strlen = $length - length($ellipsis); - my $newstr = substr($string, 0, $strlen) . $ellipsis; - return $newstr; - }; +$Template::Stash::SCALAR_OPS->{truncate} = sub { + my ($string, $length, $ellipsis) = @_; + return $string if !$length || length($string) <= $length; + + $ellipsis ||= ''; + my $strlen = $length - length($ellipsis); + my $newstr = substr($string, 0, $strlen) . $ellipsis; + return $newstr; +}; # Create the template object that processes templates and specify # configuration parameters that apply to all templates. @@ -702,15 +728,16 @@ $Template::Stash::SCALAR_OPS->{ truncate } = our $is_processing = 0; sub process { - my $self = shift; - # All of this current_langs stuff allows template_inner to correctly - # determine what-language Template object it should instantiate. - my $current_langs = Bugzilla->request_cache->{template_current_lang} ||= []; - unshift(@$current_langs, $self->context->{bz_language}); - local $is_processing = 1; - my $retval = $self->SUPER::process(@_); - shift @$current_langs; - return $retval; + my $self = shift; + + # All of this current_langs stuff allows template_inner to correctly + # determine what-language Template object it should instantiate. + my $current_langs = Bugzilla->request_cache->{template_current_lang} ||= []; + unshift(@$current_langs, $self->context->{bz_language}); + local $is_processing = 1; + my $retval = $self->SUPER::process(@_); + shift @$current_langs; + return $retval; } # Construct the Template object @@ -719,616 +746,645 @@ sub process { # since we won't have a template to use... sub create { - my $class = shift; - my %opts = @_; - - # IMPORTANT - If you make any FILTER changes here, make sure to - # make them in t/004.template.t also, if required. - - my $config = { - # Colon-separated list of directories containing templates. - INCLUDE_PATH => $opts{'include_path'} - || _include_path($opts{'language'}), - - # Remove white-space before template directives (PRE_CHOMP) and at the - # beginning and end of templates and template blocks (TRIM) for better - # looking, more compact content. Use the plus sign at the beginning - # of directives to maintain white space (i.e. [%+ DIRECTIVE %]). - PRE_CHOMP => 1, - TRIM => 1, - - # Bugzilla::Template::Plugin::Hook uses the absolute (in mod_perl) - # or relative (in mod_cgi) paths of hook files to explicitly compile - # a specific file. Also, these paths may be absolute at any time - # if a packager has modified bz_locations() to contain absolute - # paths. - ABSOLUTE => 1, - RELATIVE => i_am_persistent() ? 0 : 1, - - COMPILE_DIR => bz_locations()->{'template_cache'}, - - # Don't check for a template update until 1 hour has passed since the - # last check. - STAT_TTL => 60 * 60, - - # Initialize templates (f.e. by loading plugins like Hook). - PRE_PROCESS => ["global/variables.none.tmpl"], - - ENCODING => 'UTF-8', - - # Functions for processing text within templates in various ways. - # IMPORTANT! When adding a filter here that does not override a - # built-in filter, please also add a stub filter to t/004template.t. - FILTERS => { - - # Returns the text with backslashes, single/double quotes, - # and newlines/carriage returns escaped for use in JS strings. - js => sub { - my ($var) = @_; - $var =~ s/([\\\'\"\/])/\\$1/g; - $var =~ s/\n/\\n/g; - $var =~ s/\r/\\r/g; - $var =~ s/\x{2028}/\\u2028/g; # unicode line separator - $var =~ s/\x{2029}/\\u2029/g; # unicode paragraph separator - $var =~ s/\@/\\x40/g; # anti-spam for email addresses - $var =~ s/\\x3c/g; - $var =~ s/>/\\x3e/g; - return $var; - }, - - # Converts data to base64 - base64 => sub { - my ($data) = @_; - return encode_base64($data); - }, - - # Strips out control characters excepting whitespace - strip_control_chars => sub { - my ($data) = @_; - $data =~ s/(?![\t\r\n])[[:cntrl:]]//g; - return $data; - }, - - # HTML collapses newlines in element attributes to a single space, - # so form elements which may have whitespace (ie comments) need - # to be encoded using - # See bugs 4928, 22983 and 32000 for more details - html_linebreak => sub { - my ($var) = @_; - $var = html_quote($var); - $var =~ s/\r\n/\ /g; - $var =~ s/\n\r/\ /g; - $var =~ s/\r/\ /g; - $var =~ s/\n/\ /g; - return $var; - }, - - xml => \&Bugzilla::Util::xml_quote , - - # This filter is similar to url_quote but used a \ instead of a % - # as prefix. In addition it replaces a ' ' by a '_'. - css_class_quote => \&Bugzilla::Util::css_class_quote , - - # Removes control characters and trims extra whitespace. - clean_text => \&Bugzilla::Util::clean_text , - - quoteUrls => [ sub { - my ($context, $bug, $comment, $user) = @_; - return sub { - my $text = shift; - return quoteUrls($text, $bug, $comment, $user); - }; - }, - 1 - ], - - markdown => [ sub { - my ($context, $bug, $comment, $user) = @_; - return sub { - my $text = shift; - return unless $text; - - if (Bugzilla->feature('markdown') - && ((ref($comment) eq 'HASH' && $comment->{is_markdown}) - || (ref($comment) eq 'Bugzilla::Comment' && $comment->is_markdown))) - { - return Bugzilla->markdown->markdown($text, $bug, $comment); - } - return quoteUrls($text, $bug, $comment, $user); - }; - }, - 1 - ], - - bug_link => [ sub { - my ($context, $bug, $options) = @_; - return sub { - my $text = shift; - return get_bug_link($bug, $text, $options); - }; - }, - 1 - ], - - bug_list_link => sub { - my ($buglist, $options) = @_; - return join(", ", map(get_bug_link($_, $_, $options), split(/ *, */, $buglist))); - }, - - # In CSV, quotes are doubled, and any value containing a quote or a - # comma is enclosed in quotes. - # If a field starts with either "=", "+", "-" or "@", it is preceded - # by a space to prevent stupid formula execution from Excel & co. - csv => sub - { - my ($var) = @_; - $var = ' ' . $var if $var =~ /^[+=@-]/; - # backslash is not special to CSV, but it can be used to confuse some browsers... - # so we do not allow it to happen. We only do this for logged-in users. - $var =~ s/\\/\x{FF3C}/g if Bugzilla->user->id; - $var =~ s/\"/\"\"/g; - if ($var !~ /^-?(\d+\.)?\d*$/) { - $var = "\"$var\""; - } - return $var; - } , - - # Format a filesize in bytes to a human readable value - unitconvert => sub + my $class = shift; + my %opts = @_; + + # IMPORTANT - If you make any FILTER changes here, make sure to + # make them in t/004.template.t also, if required. + + my $config = { + + # Colon-separated list of directories containing templates. + INCLUDE_PATH => $opts{'include_path'} || _include_path($opts{'language'}), + + # Remove white-space before template directives (PRE_CHOMP) and at the + # beginning and end of templates and template blocks (TRIM) for better + # looking, more compact content. Use the plus sign at the beginning + # of directives to maintain white space (i.e. [%+ DIRECTIVE %]). + PRE_CHOMP => 1, + TRIM => 1, + + # Bugzilla::Template::Plugin::Hook uses the absolute (in mod_perl) + # or relative (in mod_cgi) paths of hook files to explicitly compile + # a specific file. Also, these paths may be absolute at any time + # if a packager has modified bz_locations() to contain absolute + # paths. + ABSOLUTE => 1, + RELATIVE => i_am_persistent() ? 0 : 1, + + COMPILE_DIR => bz_locations()->{'template_cache'}, + + # Don't check for a template update until 1 hour has passed since the + # last check. + STAT_TTL => 60 * 60, + + # Initialize templates (f.e. by loading plugins like Hook). + PRE_PROCESS => ["global/variables.none.tmpl"], + + ENCODING => 'UTF-8', + + # Functions for processing text within templates in various ways. + # IMPORTANT! When adding a filter here that does not override a + # built-in filter, please also add a stub filter to t/004template.t. + FILTERS => { + + # Returns the text with backslashes, single/double quotes, + # and newlines/carriage returns escaped for use in JS strings. + js => sub { + my ($var) = @_; + $var =~ s/([\\\'\"\/])/\\$1/g; + $var =~ s/\n/\\n/g; + $var =~ s/\r/\\r/g; + $var =~ s/\x{2028}/\\u2028/g; # unicode line separator + $var =~ s/\x{2029}/\\u2029/g; # unicode paragraph separator + $var =~ s/\@/\\x40/g; # anti-spam for email addresses + $var =~ s/\\x3c/g; + $var =~ s/>/\\x3e/g; + return $var; + }, + + # Converts data to base64 + base64 => sub { + my ($data) = @_; + return encode_base64($data); + }, + + # Strips out control characters excepting whitespace + strip_control_chars => sub { + my ($data) = @_; + $data =~ s/(?![\t\r\n])[[:cntrl:]]//g; + return $data; + }, + + # HTML collapses newlines in element attributes to a single space, + # so form elements which may have whitespace (ie comments) need + # to be encoded using + # See bugs 4928, 22983 and 32000 for more details + html_linebreak => sub { + my ($var) = @_; + $var = html_quote($var); + $var =~ s/\r\n/\ /g; + $var =~ s/\n\r/\ /g; + $var =~ s/\r/\ /g; + $var =~ s/\n/\ /g; + return $var; + }, + + xml => \&Bugzilla::Util::xml_quote, + + # This filter is similar to url_quote but used a \ instead of a % + # as prefix. In addition it replaces a ' ' by a '_'. + css_class_quote => \&Bugzilla::Util::css_class_quote, + + # Removes control characters and trims extra whitespace. + clean_text => \&Bugzilla::Util::clean_text, + + quoteUrls => [ + sub { + my ($context, $bug, $comment, $user) = @_; + return sub { + my $text = shift; + return quoteUrls($text, $bug, $comment, $user); + }; + }, + 1 + ], + + markdown => [ + sub { + my ($context, $bug, $comment, $user) = @_; + return sub { + my $text = shift; + return unless $text; + + if ( + Bugzilla->feature('markdown') + && ( (ref($comment) eq 'HASH' && $comment->{is_markdown}) + || (ref($comment) eq 'Bugzilla::Comment' && $comment->is_markdown)) + ) { - my ($data) = @_; - my $retval = ""; - my %units = ( - 'KB' => 1024, - 'MB' => 1024 * 1024, - 'GB' => 1024 * 1024 * 1024, - ); - - if ($data < 1024) { - return "$data bytes"; - } - else { - my $u; - foreach $u ('GB', 'MB', 'KB') { - if ($data >= $units{$u}) { - return sprintf("%.2f %s", $data/$units{$u}, $u); - } - } - } - }, - - # Format a time for display (more info in Bugzilla::Util) - time => [ sub { - my ($context, $format, $timezone) = @_; - return sub { - my $time = shift; - return format_time($time, $format, $timezone); - }; - }, - 1 - ], - - html => \&Bugzilla::Util::html_quote, - - html_light => \&Bugzilla::Util::html_light_quote, - - email => \&Bugzilla::Util::email_filter, - - mtime => \&mtime_filter, - - # iCalendar contentline filter - ics => [ sub { - my ($context, @args) = @_; - return sub { - my ($var) = shift; - my ($par) = shift @args; - my ($output) = ""; - - $var =~ s/[\r\n]/ /g; - $var =~ s/([;\\\",])/\\$1/g; - - if ($par) { - $output = sprintf("%s:%s", $par, $var); - } else { - $output = $var; - } - - $output =~ s/(.{75,75})/$1\n /g; - - return $output; - }; - }, - 1 - ], - - # Note that using this filter is even more dangerous than - # using "none," and you should only use it when you're SURE - # the output won't be displayed directly to a web browser. - txt => sub { - my ($var) = @_; - # Trivial HTML tag remover - $var =~ s/<[^>]*>//g; - # And this basically reverses the html filter. - $var =~ s/\@/@/g; - $var =~ s/\<//g; - $var =~ s/\"/\"/g; - $var =~ s/\&/\&/g; - # Now remove extra whitespace... - my $collapse_filter = $Template::Filters::FILTERS->{collapse}; - $var = $collapse_filter->($var); - # And if we're not in the WebService, wrap the message. - # (Wrapping the message in the WebService is unnecessary - # and causes awkward things like \n's appearing in error - # messages in JSON-RPC.) - unless (i_am_webservice()) { - $var = wrap_comment($var, 72); - } - $var =~ s/\ / /g; - - return $var; - }, - - # Wrap a displayed comment to the appropriate length - wrap_comment => [ - sub { - my ($context, $cols) = @_; - return sub { wrap_comment($_[0], $cols) } - }, 1], - - # Wrap cited text - wrap_cite => [ - sub { - my ($context, $cols) = @_; - return sub { wrap_cite($_[0], $cols) } - }, 1], - - # We force filtering of every variable in key security-critical - # places; we have a none filter for people to use when they - # really, really don't want a variable to be changed. - none => sub { return $_[0]; } , + return Bugzilla->markdown->markdown($text, $bug, $comment); + } + return quoteUrls($text, $bug, $comment, $user); + }; + }, + 1 + ], + + bug_link => [ + sub { + my ($context, $bug, $options) = @_; + return sub { + my $text = shift; + return get_bug_link($bug, $text, $options); + }; + }, + 1 + ], + + bug_list_link => sub { + my ($buglist, $options) = @_; + return + join(", ", map(get_bug_link($_, $_, $options), split(/ *, */, $buglist))); + }, + + # In CSV, quotes are doubled, and any value containing a quote or a + # comma is enclosed in quotes. + # If a field starts with either "=", "+", "-" or "@", it is preceded + # by a space to prevent stupid formula execution from Excel & co. + csv => sub { + my ($var) = @_; + $var = ' ' . $var if $var =~ /^[+=@-]/; + + # backslash is not special to CSV, but it can be used to confuse some browsers... + # so we do not allow it to happen. We only do this for logged-in users. + $var =~ s/\\/\x{FF3C}/g if Bugzilla->user->id; + $var =~ s/\"/\"\"/g; + if ($var !~ /^-?(\d+\.)?\d*$/) { + $var = "\"$var\""; + } + return $var; + }, + + # Format a filesize in bytes to a human readable value + unitconvert => sub { + my ($data) = @_; + my $retval = ""; + my %units = ('KB' => 1024, 'MB' => 1024 * 1024, 'GB' => 1024 * 1024 * 1024,); + + if ($data < 1024) { + return "$data bytes"; + } + else { + my $u; + foreach $u ('GB', 'MB', 'KB') { + if ($data >= $units{$u}) { + return sprintf("%.2f %s", $data / $units{$u}, $u); + } + } + } + }, + + # Format a time for display (more info in Bugzilla::Util) + time => [ + sub { + my ($context, $format, $timezone) = @_; + return sub { + my $time = shift; + return format_time($time, $format, $timezone); + }; }, + 1 + ], + + html => \&Bugzilla::Util::html_quote, + + html_light => \&Bugzilla::Util::html_light_quote, + + email => \&Bugzilla::Util::email_filter, + + mtime => \&mtime_filter, + + # iCalendar contentline filter + ics => [ + sub { + my ($context, @args) = @_; + return sub { + my ($var) = shift; + my ($par) = shift @args; + my ($output) = ""; + + $var =~ s/[\r\n]/ /g; + $var =~ s/([;\\\",])/\\$1/g; - PLUGIN_BASE => 'Bugzilla::Template::Plugin', - - CONSTANTS => _load_constants(), - - # Default variables for all templates - VARIABLES => { - # Function for retrieving global parameters. - 'Param' => sub { return Bugzilla->params->{$_[0]}; }, - - # Function to create date strings - 'time2str' => \&Date::Format::time2str, - - # Fixed size column formatting for bugmail. - 'format_columns' => sub { - my $cols = shift; - my $format = ($cols == 3) ? FORMAT_TRIPLE : FORMAT_DOUBLE; - my $col_size = ($cols == 3) ? FORMAT_3_SIZE : FORMAT_2_SIZE; - return multiline_sprintf($format, \@_, $col_size); - }, - - # Generic linear search function - 'lsearch' => sub { - my ($array, $item) = @_; - return firstidx { $_ eq $item } @$array; - }, - - # Currently logged in user, if any - # If an sudo session is in progress, this is the user we're faking - 'user' => sub { return Bugzilla->user; }, - - # TT directives are evaluated in list context, conflicting - # with CGI checks about using $cgi->param() in list context. - 'cgi_param' => sub { return scalar Bugzilla->cgi->param($_[0]) }, - - # Currenly active language - 'current_language' => sub { return Bugzilla->current_language; }, - - # If an sudo session is in progress, this is the user who - # started the session. - 'sudoer' => sub { return Bugzilla->sudoer; }, - - # Allow templates to access the "correct" URLBase value - 'urlbase' => sub { return Bugzilla::Util::correct_urlbase(); }, - - # Allow templates to access docs url with users' preferred language - # We fall back to English if documentation in the preferred - # language is not available - 'docs_urlbase' => sub { - my $docs_urlbase; - my $lang = Bugzilla->current_language; - # Translations currently available on readthedocs.org - my @rtd_translations = ('en', 'fr'); - - if ($lang ne 'en' && -f "docs/$lang/html/index.html") { - $docs_urlbase = "docs/$lang/html/"; - } - elsif (-f "docs/en/html/index.html") { - $docs_urlbase = "docs/en/html/"; - } - else { - if (!grep { $_ eq $lang } @rtd_translations) { - $lang = "en"; - } - - my $version = BUGZILLA_VERSION; - $version =~ /^(\d+)\.(\d+)/; - if ($2 % 2 == 1) { - # second number is odd; development version - $version = 'latest'; - } - else { - $version = "$1.$2"; - } - - $docs_urlbase = "https://bugzilla.readthedocs.org/$lang/$version/"; - } - - return $docs_urlbase; - }, - - # Check whether the URL is safe. - 'is_safe_url' => sub { - my $url = shift; - return 0 unless $url; - - my $safe_url_regexp = SAFE_URL_REGEXP(); - return 1 if $url =~ /^$safe_url_regexp$/; - # Pointing to a local file with no colon in its name is fine. - return 1 if $url =~ /^[^\s<>\":]+[\w\/]$/i; - # If we come here, then we cannot guarantee it's safe. - return 0; - }, - - # Allow templates to generate a token themselves. - 'issue_hash_token' => \&Bugzilla::Token::issue_hash_token, - - 'get_login_request_token' => sub { - my $cookie = Bugzilla->cgi->cookie('Bugzilla_login_request_cookie'); - return $cookie ? issue_hash_token(['login_request', $cookie]) : ''; - }, - - 'get_api_token' => sub { - return '' unless Bugzilla->user->id; - my $cache = Bugzilla->request_cache; - return $cache->{api_token} //= issue_api_token(); - }, - - # A way for all templates to get at Field data, cached. - 'bug_fields' => sub { - my $cache = Bugzilla->request_cache; - $cache->{template_bug_fields} ||= - Bugzilla->fields({ by_name => 1 }); - return $cache->{template_bug_fields}; - }, - - # A general purpose cache to store rendered templates for reuse. - # Make sure to not mix language-specific data. - 'template_cache' => sub { - my $cache = Bugzilla->request_cache->{template_cache} ||= {}; - $cache->{users} ||= {}; - return $cache; - }, - - 'css_files' => \&css_files, - yui_resolve_deps => \&yui_resolve_deps, - concatenate_js => \&_concatenate_js, - - # All classifications (sorted by sortkey, name) - 'all_classifications' => sub { - return [map { $_->name } Bugzilla::Classification->get_all()]; - }, - - # Whether or not keywords are enabled, in this Bugzilla. - 'use_keywords' => sub { return Bugzilla::Keyword->any_exist; }, - - # All the keywords - 'all_keywords' => sub { - return [map { $_->name } Bugzilla::Keyword->get_all()]; - }, - - # All the active keywords - 'active_keywords' => sub { - return [map { $_->name } grep { $_->is_active } Bugzilla::Keyword->get_all()]; - }, - - 'feature_enabled' => sub { return Bugzilla->feature(@_); }, - - # field_descs can be somewhat slow to generate, so we generate - # it only once per-language no matter how many times - # $template->process() is called. - 'field_descs' => sub { return template_var('field_descs') }, - - # Calling bug/field-help.none.tmpl once per label is very - # expensive, so we generate it once per-language. - 'help_html' => sub { return template_var('help_html') }, - - # This way we don't have to load field-descs.none.tmpl in - # many templates. - 'display_value' => \&Bugzilla::Util::display_value, - - 'install_string' => \&Bugzilla::Install::Util::install_string, - - 'feature_description' => \&Bugzilla::Install::Util::feature_description, - - 'report_columns' => \&Bugzilla::Search::REPORT_COLUMNS, - - # These don't work as normal constants. - DB_MODULE => \&Bugzilla::Constants::DB_MODULE, - REQUIRED_MODULES => sub { - my %required_modules; - my $cache = Bugzilla->request_cache; - my $meta = $cache->{cpan_meta} ||= Bugzilla::Install::Requirements::load_cpan_meta(); - my $reqs = $meta->effective_prereqs->merged_requirements(['configure', 'runtime'], ['requires']); - foreach my $module (sort $reqs->required_modules) { - next if $module eq 'perl'; - $required_modules{$module} = { version => $reqs->requirements_for_module($module) }; - } - return \%required_modules; - }, - OPTIONAL_MODULES => sub { - my %optional_modules; - my $cache = Bugzilla->request_cache; - my $meta = $cache->{cpan_meta} ||= Bugzilla::Install::Requirements::load_cpan_meta(); - foreach my $feature ($meta->features) { - my $reqs = $feature->prereqs->merged_requirements(['configure', 'runtime'], ['requires']); - foreach my $module ($reqs->required_modules) { - my $version = $reqs->requirements_for_module($module); - $optional_modules{$module} ||= {}; - $optional_modules{$module}{version} = $version; - $optional_modules{$module}{features} ||= []; - push(@{$optional_modules{$module}{features}}, $feature->description); - } - } - return \%optional_modules; - }, - 'default_authorizer' => sub { return Bugzilla::Auth->new() }, - - 'login_not_email' => sub { - my $params = Bugzilla->params; - my $cache = Bugzilla->request_cache; - - return $cache->{login_not_email} //= - (!$params->{use_email_as_login} - || ($params->{user_verify_class} =~ /LDAP/ && $params->{LDAPmailattribute}) - || ($params->{user_verify_class} =~ /RADIUS/ && $params->{RADIUS_email_suffix})) - ? 1 : 0; - }, + if ($par) { + $output = sprintf("%s:%s", $par, $var); + } + else { + $output = $var; + } + + $output =~ s/(.{75,75})/$1\n /g; + + return $output; + }; + }, + 1 + ], + + # Note that using this filter is even more dangerous than + # using "none," and you should only use it when you're SURE + # the output won't be displayed directly to a web browser. + txt => sub { + my ($var) = @_; + + # Trivial HTML tag remover + $var =~ s/<[^>]*>//g; + + # And this basically reverses the html filter. + $var =~ s/\@/@/g; + $var =~ s/\<//g; + $var =~ s/\"/\"/g; + $var =~ s/\&/\&/g; + + # Now remove extra whitespace... + my $collapse_filter = $Template::Filters::FILTERS->{collapse}; + $var = $collapse_filter->($var); + + # And if we're not in the WebService, wrap the message. + # (Wrapping the message in the WebService is unnecessary + # and causes awkward things like \n's appearing in error + # messages in JSON-RPC.) + unless (i_am_webservice()) { + $var = wrap_comment($var, 72); + } + $var =~ s/\ / /g; + + return $var; + }, + + # Wrap a displayed comment to the appropriate length + wrap_comment => [ + sub { + my ($context, $cols) = @_; + return sub { wrap_comment($_[0], $cols) } }, - }; - # Use a per-process provider to cache compiled templates in memory across - # requests. - my $provider_key = join(':', @{ $config->{INCLUDE_PATH} }); - my $shared_providers = Bugzilla->process_cache->{shared_providers} ||= {}; - $shared_providers->{$provider_key} ||= Template::Provider->new($config); - $config->{LOAD_TEMPLATES} = [ $shared_providers->{$provider_key} ]; + 1 + ], + + # Wrap cited text + wrap_cite => [ + sub { + my ($context, $cols) = @_; + return sub { wrap_cite($_[0], $cols) } + }, + 1 + ], + + # We force filtering of every variable in key security-critical + # places; we have a none filter for people to use when they + # really, really don't want a variable to be changed. + none => sub { return $_[0]; }, + }, - local $Template::Config::CONTEXT = 'Bugzilla::Template::Context'; + PLUGIN_BASE => 'Bugzilla::Template::Plugin', - Bugzilla::Hook::process('template_before_create', { config => $config }); - my $template = $class->new($config) - || die("Template creation failed: " . $class->error()); + CONSTANTS => _load_constants(), - # Pass on our current language to any template hooks or inner templates - # called by this Template object. - $template->context->{bz_language} = $opts{language} || ''; + # Default variables for all templates + VARIABLES => { - return $template; + # Function for retrieving global parameters. + 'Param' => sub { return Bugzilla->params->{$_[0]}; }, + + # Function to create date strings + 'time2str' => \&Date::Format::time2str, + + # Fixed size column formatting for bugmail. + 'format_columns' => sub { + my $cols = shift; + my $format = ($cols == 3) ? FORMAT_TRIPLE : FORMAT_DOUBLE; + my $col_size = ($cols == 3) ? FORMAT_3_SIZE : FORMAT_2_SIZE; + return multiline_sprintf($format, \@_, $col_size); + }, + + # Generic linear search function + 'lsearch' => sub { + my ($array, $item) = @_; + return firstidx { $_ eq $item } @$array; + }, + + # Currently logged in user, if any + # If an sudo session is in progress, this is the user we're faking + 'user' => sub { return Bugzilla->user; }, + + # TT directives are evaluated in list context, conflicting + # with CGI checks about using $cgi->param() in list context. + 'cgi_param' => sub { return scalar Bugzilla->cgi->param($_[0]) }, + + # Currenly active language + 'current_language' => sub { return Bugzilla->current_language; }, + + # If an sudo session is in progress, this is the user who + # started the session. + 'sudoer' => sub { return Bugzilla->sudoer; }, + + # Allow templates to access the "correct" URLBase value + 'urlbase' => sub { return Bugzilla::Util::correct_urlbase(); }, + + # Allow templates to access docs url with users' preferred language + # We fall back to English if documentation in the preferred + # language is not available + 'docs_urlbase' => sub { + my $docs_urlbase; + my $lang = Bugzilla->current_language; + + # Translations currently available on readthedocs.org + my @rtd_translations = ('en', 'fr'); + + if ($lang ne 'en' && -f "docs/$lang/html/index.html") { + $docs_urlbase = "docs/$lang/html/"; + } + elsif (-f "docs/en/html/index.html") { + $docs_urlbase = "docs/en/html/"; + } + else { + if (!grep { $_ eq $lang } @rtd_translations) { + $lang = "en"; + } + + my $version = BUGZILLA_VERSION; + $version =~ /^(\d+)\.(\d+)/; + if ($2 % 2 == 1) { + + # second number is odd; development version + $version = 'latest'; + } + else { + $version = "$1.$2"; + } + + $docs_urlbase = "https://bugzilla.readthedocs.org/$lang/$version/"; + } + + return $docs_urlbase; + }, + + # Check whether the URL is safe. + 'is_safe_url' => sub { + my $url = shift; + return 0 unless $url; + + my $safe_url_regexp = SAFE_URL_REGEXP(); + return 1 if $url =~ /^$safe_url_regexp$/; + + # Pointing to a local file with no colon in its name is fine. + return 1 if $url =~ /^[^\s<>\":]+[\w\/]$/i; + + # If we come here, then we cannot guarantee it's safe. + return 0; + }, + + # Allow templates to generate a token themselves. + 'issue_hash_token' => \&Bugzilla::Token::issue_hash_token, + + 'get_login_request_token' => sub { + my $cookie = Bugzilla->cgi->cookie('Bugzilla_login_request_cookie'); + return $cookie ? issue_hash_token(['login_request', $cookie]) : ''; + }, + + 'get_api_token' => sub { + return '' unless Bugzilla->user->id; + my $cache = Bugzilla->request_cache; + return $cache->{api_token} //= issue_api_token(); + }, + + # A way for all templates to get at Field data, cached. + 'bug_fields' => sub { + my $cache = Bugzilla->request_cache; + $cache->{template_bug_fields} ||= Bugzilla->fields({by_name => 1}); + return $cache->{template_bug_fields}; + }, + + # A general purpose cache to store rendered templates for reuse. + # Make sure to not mix language-specific data. + 'template_cache' => sub { + my $cache = Bugzilla->request_cache->{template_cache} ||= {}; + $cache->{users} ||= {}; + return $cache; + }, + + 'css_files' => \&css_files, + yui_resolve_deps => \&yui_resolve_deps, + concatenate_js => \&_concatenate_js, + + # All classifications (sorted by sortkey, name) + 'all_classifications' => sub { + return [map { $_->name } Bugzilla::Classification->get_all()]; + }, + + # Whether or not keywords are enabled, in this Bugzilla. + 'use_keywords' => sub { return Bugzilla::Keyword->any_exist; }, + + # All the keywords + 'all_keywords' => sub { + return [map { $_->name } Bugzilla::Keyword->get_all()]; + }, + + # All the active keywords + 'active_keywords' => sub { + return [map { $_->name } grep { $_->is_active } Bugzilla::Keyword->get_all()]; + }, + + 'feature_enabled' => sub { return Bugzilla->feature(@_); }, + + # field_descs can be somewhat slow to generate, so we generate + # it only once per-language no matter how many times + # $template->process() is called. + 'field_descs' => sub { return template_var('field_descs') }, + + # Calling bug/field-help.none.tmpl once per label is very + # expensive, so we generate it once per-language. + 'help_html' => sub { return template_var('help_html') }, + + # This way we don't have to load field-descs.none.tmpl in + # many templates. + 'display_value' => \&Bugzilla::Util::display_value, + + 'install_string' => \&Bugzilla::Install::Util::install_string, + + 'feature_description' => \&Bugzilla::Install::Util::feature_description, + + 'report_columns' => \&Bugzilla::Search::REPORT_COLUMNS, + + # These don't work as normal constants. + DB_MODULE => \&Bugzilla::Constants::DB_MODULE, + REQUIRED_MODULES => sub { + my %required_modules; + my $cache = Bugzilla->request_cache; + my $meta = $cache->{cpan_meta} + ||= Bugzilla::Install::Requirements::load_cpan_meta(); + my $reqs + = $meta->effective_prereqs->merged_requirements(['configure', 'runtime'], + ['requires']); + foreach my $module (sort $reqs->required_modules) { + next if $module eq 'perl'; + $required_modules{$module} + = {version => $reqs->requirements_for_module($module)}; + } + return \%required_modules; + }, + OPTIONAL_MODULES => sub { + my %optional_modules; + my $cache = Bugzilla->request_cache; + my $meta = $cache->{cpan_meta} + ||= Bugzilla::Install::Requirements::load_cpan_meta(); + foreach my $feature ($meta->features) { + my $reqs = $feature->prereqs->merged_requirements(['configure', 'runtime'], + ['requires']); + foreach my $module ($reqs->required_modules) { + my $version = $reqs->requirements_for_module($module); + $optional_modules{$module} ||= {}; + $optional_modules{$module}{version} = $version; + $optional_modules{$module}{features} ||= []; + push(@{$optional_modules{$module}{features}}, $feature->description); + } + } + return \%optional_modules; + }, + 'default_authorizer' => sub { return Bugzilla::Auth->new() }, + + 'login_not_email' => sub { + my $params = Bugzilla->params; + my $cache = Bugzilla->request_cache; + + return $cache->{login_not_email} + //= (!$params->{use_email_as_login} + || ($params->{user_verify_class} =~ /LDAP/ && $params->{LDAPmailattribute}) + || ($params->{user_verify_class} =~ /RADIUS/ + && $params->{RADIUS_email_suffix})) ? 1 : 0; + }, + }, + }; + + # Use a per-process provider to cache compiled templates in memory across + # requests. + my $provider_key = join(':', @{$config->{INCLUDE_PATH}}); + my $shared_providers = Bugzilla->process_cache->{shared_providers} ||= {}; + $shared_providers->{$provider_key} ||= Template::Provider->new($config); + $config->{LOAD_TEMPLATES} = [$shared_providers->{$provider_key}]; + + local $Template::Config::CONTEXT = 'Bugzilla::Template::Context'; + + Bugzilla::Hook::process('template_before_create', {config => $config}); + my $template = $class->new($config) + || die("Template creation failed: " . $class->error()); + + # Pass on our current language to any template hooks or inner templates + # called by this Template object. + $template->context->{bz_language} = $opts{language} || ''; + + return $template; } # Used as part of the two subroutines below. our %_templates_to_precompile; + sub precompile_templates { - my ($output) = @_; + my ($output) = @_; - # Remove the compiled templates. - my $cache_dir = bz_locations()->{'template_cache'}; - my $datadir = bz_locations()->{'datadir'}; + # Remove the compiled templates. + my $cache_dir = bz_locations()->{'template_cache'}; + my $datadir = bz_locations()->{'datadir'}; + if (-e $cache_dir) { + print install_string('template_removing_dir') . "\n" if $output; + + # This frequently fails if the webserver made the files, because + # then the webserver owns the directories. + rmtree($cache_dir); + + # Check that the directory was really removed, and if not, move it + # into data/deleteme/. if (-e $cache_dir) { - print install_string('template_removing_dir') . "\n" if $output; - - # This frequently fails if the webserver made the files, because - # then the webserver owns the directories. - rmtree($cache_dir); - - # Check that the directory was really removed, and if not, move it - # into data/deleteme/. - if (-e $cache_dir) { - my $deleteme = "$datadir/deleteme"; - - print STDERR "\n\n", - install_string('template_removal_failed', - { deleteme => $deleteme, - template_cache => $cache_dir }), "\n\n"; - mkpath($deleteme); - my $random = generate_random_password(); - rename($cache_dir, "$deleteme/$random") - or die "move failed: $!"; - } + my $deleteme = "$datadir/deleteme"; + + print STDERR "\n\n", + install_string('template_removal_failed', + {deleteme => $deleteme, template_cache => $cache_dir}), + "\n\n"; + mkpath($deleteme); + my $random = generate_random_password(); + rename($cache_dir, "$deleteme/$random") or die "move failed: $!"; } + } - print install_string('template_precompile') if $output; + print install_string('template_precompile') if $output; - # Pre-compile all available languages. - my $paths = template_include_path({ language => Bugzilla->languages }); + # Pre-compile all available languages. + my $paths = template_include_path({language => Bugzilla->languages}); - foreach my $dir (@$paths) { - my $template = Bugzilla::Template->create(include_path => [$dir]); + foreach my $dir (@$paths) { + my $template = Bugzilla::Template->create(include_path => [$dir]); - %_templates_to_precompile = (); - # Traverse the template hierarchy. - find({ wanted => \&_precompile_push, no_chdir => 1 }, $dir); - # The sort isn't totally necessary, but it makes debugging easier - # by making the templates always be compiled in the same order. - foreach my $file (sort keys %_templates_to_precompile) { - $file =~ s{^\Q$dir\E/}{}; - # Compile the template but throw away the result. This has the side- - # effect of writing the compiled version to disk. - $template->context->template($file); - } + %_templates_to_precompile = (); - # Clear out the cached Provider object - Bugzilla->process_cache->{shared_providers} = undef; - } + # Traverse the template hierarchy. + find({wanted => \&_precompile_push, no_chdir => 1}, $dir); - # Under mod_perl, we look for templates using the absolute path of the - # template directory, which causes Template Toolkit to look for their - # *compiled* versions using the full absolute path under the data/template - # directory. (Like data/template/var/www/html/bugzilla/.) To avoid - # re-compiling templates under mod_perl, we symlink to the - # already-compiled templates. This doesn't work on Windows. - if (!ON_WINDOWS) { - # We do these separately in case they're in different locations. - _do_template_symlink(bz_locations()->{'templatedir'}); - _do_template_symlink(bz_locations()->{'extensionsdir'}); + # The sort isn't totally necessary, but it makes debugging easier + # by making the templates always be compiled in the same order. + foreach my $file (sort keys %_templates_to_precompile) { + $file =~ s{^\Q$dir\E/}{}; + + # Compile the template but throw away the result. This has the side- + # effect of writing the compiled version to disk. + $template->context->template($file); } - # If anything created a Template object before now, clear it out. - delete Bugzilla->request_cache->{template}; + # Clear out the cached Provider object + Bugzilla->process_cache->{shared_providers} = undef; + } + + # Under mod_perl, we look for templates using the absolute path of the + # template directory, which causes Template Toolkit to look for their + # *compiled* versions using the full absolute path under the data/template + # directory. (Like data/template/var/www/html/bugzilla/.) To avoid + # re-compiling templates under mod_perl, we symlink to the + # already-compiled templates. This doesn't work on Windows. + if (!ON_WINDOWS) { + + # We do these separately in case they're in different locations. + _do_template_symlink(bz_locations()->{'templatedir'}); + _do_template_symlink(bz_locations()->{'extensionsdir'}); + } - print install_string('done') . "\n" if $output; + # If anything created a Template object before now, clear it out. + delete Bugzilla->request_cache->{template}; + + print install_string('done') . "\n" if $output; } # Helper for precompile_templates sub _precompile_push { - my $name = $File::Find::name; - return if (-d $name); - return if ($name =~ /\/CVS\//); - return if ($name !~ /\.tmpl$/); - $_templates_to_precompile{$name} = 1; + my $name = $File::Find::name; + return if (-d $name); + return if ($name =~ /\/CVS\//); + return if ($name !~ /\.tmpl$/); + $_templates_to_precompile{$name} = 1; } # Helper for precompile_templates sub _do_template_symlink { - my $dir_to_symlink = shift; - - my $abs_path = abs_path($dir_to_symlink); - - # If $dir_to_symlink is already an absolute path (as might happen - # with packagers who set $libpath to an absolute path), then we don't - # need to do this symlink. - return if ($abs_path eq $dir_to_symlink); - - my $abs_root = dirname($abs_path); - my $dir_name = basename($abs_path); - my $cache_dir = bz_locations()->{'template_cache'}; - my $container = "$cache_dir$abs_root"; - mkpath($container); - my $target = "$cache_dir/$dir_name"; - # Check if the directory exists, because if there are no extensions, - # there won't be an "data/template/extensions" directory to link to. - if (-d $target) { - # We use abs2rel so that the symlink will look like - # "../../../../template" which works, while just - # "data/template/template/" doesn't work. - my $relative_target = File::Spec->abs2rel($target, $container); - - my $link_name = "$container/$dir_name"; - symlink($relative_target, $link_name) - or warn "Could not make $link_name a symlink to $relative_target: $!"; - } + my $dir_to_symlink = shift; + + my $abs_path = abs_path($dir_to_symlink); + + # If $dir_to_symlink is already an absolute path (as might happen + # with packagers who set $libpath to an absolute path), then we don't + # need to do this symlink. + return if ($abs_path eq $dir_to_symlink); + + my $abs_root = dirname($abs_path); + my $dir_name = basename($abs_path); + my $cache_dir = bz_locations()->{'template_cache'}; + my $container = "$cache_dir$abs_root"; + mkpath($container); + my $target = "$cache_dir/$dir_name"; + + # Check if the directory exists, because if there are no extensions, + # there won't be an "data/template/extensions" directory to link to. + if (-d $target) { + + # We use abs2rel so that the symlink will look like + # "../../../../template" which works, while just + # "data/template/template/" doesn't work. + my $relative_target = File::Spec->abs2rel($target, $container); + + my $link_name = "$container/$dir_name"; + symlink($relative_target, $link_name) + or warn "Could not make $link_name a symlink to $relative_target: $!"; + } } 1; diff --git a/Bugzilla/Template/Context.pm b/Bugzilla/Template/Context.pm index 3893504b3a..b9d84ab859 100644 --- a/Bugzilla/Template/Context.pm +++ b/Bugzilla/Template/Context.pm @@ -18,23 +18,24 @@ use Bugzilla::Hook; use Scalar::Util qw(blessed); sub process { - my $self = shift; - # We don't want to run the template_before_process hook for - # template hooks (but we do want it to run if a hook calls - # PROCESS inside itself). The problem is that the {component}->{name} of - # hooks is unreliable--sometimes it starts with ./ and it's the - # full path to the hook template, and sometimes it's just the relative - # name (like hook/global/field-descs-end.none.tmpl). Also, calling - # template_before_process for hook templates doesn't seem too useful, - # because that's already part of the extension and they should be able - # to modify their hook if they want (or just modify the variables in the - # calling template). - if (not delete $self->{bz_in_hook}) { - $self->{bz_in_process} = 1; - } - my $result = $self->SUPER::process(@_); - delete $self->{bz_in_process}; - return $result; + my $self = shift; + + # We don't want to run the template_before_process hook for + # template hooks (but we do want it to run if a hook calls + # PROCESS inside itself). The problem is that the {component}->{name} of + # hooks is unreliable--sometimes it starts with ./ and it's the + # full path to the hook template, and sometimes it's just the relative + # name (like hook/global/field-descs-end.none.tmpl). Also, calling + # template_before_process for hook templates doesn't seem too useful, + # because that's already part of the extension and they should be able + # to modify their hook if they want (or just modify the variables in the + # calling template). + if (not delete $self->{bz_in_hook}) { + $self->{bz_in_process} = 1; + } + my $result = $self->SUPER::process(@_); + delete $self->{bz_in_process}; + return $result; } # This method is called by Template-Toolkit exactly once per template or @@ -46,58 +47,59 @@ sub process { # in the PROCESS or INCLUDE directive haven't been set, and if we're # in an INCLUDE, the stash is not yet localized during process(). sub stash { - my $self = shift; - my $stash = $self->SUPER::stash(@_); - - my $name = $stash->{component}->{name}; - my $pre_process = $self->config->{PRE_PROCESS}; - - # Checking bz_in_process tells us that we were indeed called as part of a - # Context::process, and not at some other point. - # - # Checking $name makes sure that we're processing a file, and not just a - # block, by checking that the name has a period in it. We don't allow - # blocks because their names are too unreliable--an extension could have - # a block with the same name, or multiple files could have a same-named - # block, and then your extension would malfunction. - # - # We also make sure that we don't run, ever, during the PRE_PROCESS - # templates, because if somebody calls Throw*Error globally inside of - # template_before_process, that causes an infinite recursion into - # the PRE_PROCESS templates (because Bugzilla, while inside - # global/intialize.none.tmpl, loads the template again to create the - # template object for Throw*Error). - # - # Checking Bugzilla::Hook::in prevents infinite recursion on this hook. - if ($self->{bz_in_process} and $name =~ /\./ - and !grep($_ eq $name, @$pre_process) - and !Bugzilla::Hook::in('template_before_process')) - { - Bugzilla::Hook::process("template_before_process", - { vars => $stash, context => $self, - file => $name }); - } - - # This prevents other calls to stash() that might somehow happen - # later in the file from also triggering the hook. - delete $self->{bz_in_process}; - - return $stash; + my $self = shift; + my $stash = $self->SUPER::stash(@_); + + my $name = $stash->{component}->{name}; + my $pre_process = $self->config->{PRE_PROCESS}; + + # Checking bz_in_process tells us that we were indeed called as part of a + # Context::process, and not at some other point. + # + # Checking $name makes sure that we're processing a file, and not just a + # block, by checking that the name has a period in it. We don't allow + # blocks because their names are too unreliable--an extension could have + # a block with the same name, or multiple files could have a same-named + # block, and then your extension would malfunction. + # + # We also make sure that we don't run, ever, during the PRE_PROCESS + # templates, because if somebody calls Throw*Error globally inside of + # template_before_process, that causes an infinite recursion into + # the PRE_PROCESS templates (because Bugzilla, while inside + # global/intialize.none.tmpl, loads the template again to create the + # template object for Throw*Error). + # + # Checking Bugzilla::Hook::in prevents infinite recursion on this hook. + if ( $self->{bz_in_process} + and $name =~ /\./ + and !grep($_ eq $name, @$pre_process) + and !Bugzilla::Hook::in('template_before_process')) + { + Bugzilla::Hook::process("template_before_process", + {vars => $stash, context => $self, file => $name}); + } + + # This prevents other calls to stash() that might somehow happen + # later in the file from also triggering the hook. + delete $self->{bz_in_process}; + + return $stash; } sub filter { - my ($self, $name, $args) = @_; - # If we pass an alias for the filter name, the filter code is cached - # instead of looking for it at each call. - # If the filter has arguments, then we can't cache it. - $self->SUPER::filter($name, $args, $args ? undef : $name); + my ($self, $name, $args) = @_; + + # If we pass an alias for the filter name, the filter code is cached + # instead of looking for it at each call. + # If the filter has arguments, then we can't cache it. + $self->SUPER::filter($name, $args, $args ? undef : $name); } # We need a DESTROY sub for the same reason that Bugzilla::CGI does. sub DESTROY { - my $self = shift; - $self->SUPER::DESTROY(@_); -}; + my $self = shift; + $self->SUPER::DESTROY(@_); +} 1; diff --git a/Bugzilla/Template/Plugin/Bugzilla.pm b/Bugzilla/Template/Plugin/Bugzilla.pm index 4481e33d20..6e6100f6a6 100644 --- a/Bugzilla/Template/Plugin/Bugzilla.pm +++ b/Bugzilla/Template/Plugin/Bugzilla.pm @@ -16,20 +16,20 @@ use parent qw(Template::Plugin); use Bugzilla; sub new { - my ($class, $context) = @_; + my ($class, $context) = @_; - return bless {}, $class; + return bless {}, $class; } sub AUTOLOAD { - my $class = shift; - our $AUTOLOAD; + my $class = shift; + our $AUTOLOAD; - $AUTOLOAD =~ s/^.*:://; + $AUTOLOAD =~ s/^.*:://; - return if $AUTOLOAD eq 'DESTROY'; + return if $AUTOLOAD eq 'DESTROY'; - return Bugzilla->$AUTOLOAD(@_); + return Bugzilla->$AUTOLOAD(@_); } 1; diff --git a/Bugzilla/Template/Plugin/Hook.pm b/Bugzilla/Template/Plugin/Hook.pm index faba06b727..18da2983fb 100644 --- a/Bugzilla/Template/Plugin/Hook.pm +++ b/Bugzilla/Template/Plugin/Hook.pm @@ -14,81 +14,80 @@ use warnings; use parent qw(Template::Plugin); use Bugzilla::Constants; -use Bugzilla::Install::Util qw(template_include_path); +use Bugzilla::Install::Util qw(template_include_path); use Bugzilla::Util; use Bugzilla::Error; use File::Spec; sub new { - my ($class, $context) = @_; - return bless { _CONTEXT => $context }, $class; + my ($class, $context) = @_; + return bless {_CONTEXT => $context}, $class; } sub _context { return $_[0]->{_CONTEXT} } sub process { - my ($self, $hook_name, $template) = @_; - my $context = $self->_context(); - $template ||= $context->stash->{component}->{name}; - - # sanity check: - if (!$template =~ /[\w\.\/\-_\\]+/) { - ThrowCodeError('template_invalid', { name => $template }); - } - - my (undef, $path, $filename) = File::Spec->splitpath($template); - $path ||= ''; - $filename =~ m/(.+)\.(.+)\.tmpl$/; - my $template_name = $1; - my $type = $2; - - # Hooks are named like this: - my $extension_template = "$path$template_name-$hook_name.$type.tmpl"; - - # Get the hooks out of the cache if they exist. Otherwise, read them - # from the disk. - my $cache = Bugzilla->request_cache->{template_plugin_hook_cache} ||= {}; - my $lang = $context->{bz_language} || ''; - $cache->{"${lang}__$extension_template"} - ||= $self->_get_hooks($extension_template); - - # process() accepts an arrayref of templates, so we just pass the whole - # arrayref. - $context->{bz_in_hook} = 1; # See Bugzilla::Template::Context - return $context->process($cache->{"${lang}__$extension_template"}); + my ($self, $hook_name, $template) = @_; + my $context = $self->_context(); + $template ||= $context->stash->{component}->{name}; + + # sanity check: + if (!$template =~ /[\w\.\/\-_\\]+/) { + ThrowCodeError('template_invalid', {name => $template}); + } + + my (undef, $path, $filename) = File::Spec->splitpath($template); + $path ||= ''; + $filename =~ m/(.+)\.(.+)\.tmpl$/; + my $template_name = $1; + my $type = $2; + + # Hooks are named like this: + my $extension_template = "$path$template_name-$hook_name.$type.tmpl"; + + # Get the hooks out of the cache if they exist. Otherwise, read them + # from the disk. + my $cache = Bugzilla->request_cache->{template_plugin_hook_cache} ||= {}; + my $lang = $context->{bz_language} || ''; + $cache->{"${lang}__$extension_template"} + ||= $self->_get_hooks($extension_template); + + # process() accepts an arrayref of templates, so we just pass the whole + # arrayref. + $context->{bz_in_hook} = 1; # See Bugzilla::Template::Context + return $context->process($cache->{"${lang}__$extension_template"}); } sub _get_hooks { - my ($self, $extension_template) = @_; - - my $template_sets = $self->_template_hook_include_path(); - my @hooks; - foreach my $dir_set (@$template_sets) { - foreach my $template_dir (@$dir_set) { - my $file = "$template_dir/hook/$extension_template"; - if (-e $file) { - my $template = $self->_context->template($file); - push(@hooks, $template); - # Don't run the hook for more than one language. - last; - } - } + my ($self, $extension_template) = @_; + + my $template_sets = $self->_template_hook_include_path(); + my @hooks; + foreach my $dir_set (@$template_sets) { + foreach my $template_dir (@$dir_set) { + my $file = "$template_dir/hook/$extension_template"; + if (-e $file) { + my $template = $self->_context->template($file); + push(@hooks, $template); + + # Don't run the hook for more than one language. + last; + } } + } - return \@hooks; + return \@hooks; } sub _template_hook_include_path { - my $self = shift; - my $cache = Bugzilla->request_cache; - my $language = $self->_context->{bz_language} || ''; - my $cache_key = "template_plugin_hook_include_path_$language"; - $cache->{$cache_key} ||= template_include_path({ - language => $language, - hook => 1, - }); - return $cache->{$cache_key}; + my $self = shift; + my $cache = Bugzilla->request_cache; + my $language = $self->_context->{bz_language} || ''; + my $cache_key = "template_plugin_hook_include_path_$language"; + $cache->{$cache_key} + ||= template_include_path({language => $language, hook => 1,}); + return $cache->{$cache_key}; } 1; diff --git a/Bugzilla/Token.pm b/Bugzilla/Token.pm index 4f522bda95..2d69cea4fb 100644 --- a/Bugzilla/Token.pm +++ b/Bugzilla/Token.pm @@ -25,9 +25,9 @@ use Digest::SHA qw(hmac_sha256_base64); use parent qw(Exporter); @Bugzilla::Token::EXPORT = qw(issue_api_token issue_session_token - issue_auth_delegation_token check_auth_delegation_token - check_token_data delete_token - issue_hash_token check_hash_token); + issue_auth_delegation_token check_auth_delegation_token + check_token_data delete_token + issue_hash_token check_hash_token); # 128 bits password: # 128 * log10(2) / log10(62) = 21.49, round up to 22. @@ -42,436 +42,466 @@ use constant SEND_NOW => 1; # Create a token used for internal API authentication sub issue_api_token { - # Generates a random token, adds it to the tokens table if one does not - # already exist, and returns the token to the caller. - my $dbh = Bugzilla->dbh; - # Allow certain UI components to work if impersonating another user. - my $user = Bugzilla->sudoer || Bugzilla->user; - my ($token) = $dbh->selectrow_array(" + + # Generates a random token, adds it to the tokens table if one does not + # already exist, and returns the token to the caller. + my $dbh = Bugzilla->dbh; + + # Allow certain UI components to work if impersonating another user. + my $user = Bugzilla->sudoer || Bugzilla->user; + my ($token) = $dbh->selectrow_array(" SELECT token FROM tokens WHERE userid = ? AND tokentype = 'api_token' - AND (" . $dbh->sql_date_math('issuedate', '+', (MAX_TOKEN_AGE * 24 - 12), 'HOUR') . ") > NOW()", - undef, $user->id); - return $token // _create_token($user->id, 'api_token', ''); + AND (" + . $dbh->sql_date_math('issuedate', '+', (MAX_TOKEN_AGE * 24 - 12), 'HOUR') + . ") > NOW()", undef, $user->id); + return $token // _create_token($user->id, 'api_token', ''); } sub issue_auth_delegation_token { - my ($uri) = @_; - my $dbh = Bugzilla->dbh; - my $user = Bugzilla->user; - my $checksum = hmac_sha256_base64($user->id, $uri, Bugzilla->localconfig->{'site_wide_secret'}); + my ($uri) = @_; + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->user; + my $checksum = hmac_sha256_base64($user->id, $uri, + Bugzilla->localconfig->{'site_wide_secret'}); - return _create_token($user->id, 'auth_delegation', $checksum); + return _create_token($user->id, 'auth_delegation', $checksum); } sub check_auth_delegation_token { - my ($token, $uri) = @_; - my $dbh = Bugzilla->dbh; - my $user = Bugzilla->user; + my ($token, $uri) = @_; + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->user; - my ($eventdata) = $dbh->selectrow_array(" + my ($eventdata) = $dbh->selectrow_array(" SELECT eventdata FROM tokens WHERE token = ? AND tokentype = 'auth_delegation' - AND (" . $dbh->sql_date_math('issuedate', '+', (MAX_TOKEN_AGE * 24 - 12), 'HOUR') . ") > NOW()", - undef, $token); - - if ($eventdata) { - my $checksum = hmac_sha256_base64($user->id, $uri, Bugzilla->localconfig->{'site_wide_secret'}); - if ($eventdata eq $checksum) { - delete_token($token); - return 1; - } + AND (" + . $dbh->sql_date_math('issuedate', '+', (MAX_TOKEN_AGE * 24 - 12), 'HOUR') + . ") > NOW()", undef, $token); + + if ($eventdata) { + my $checksum = hmac_sha256_base64($user->id, $uri, + Bugzilla->localconfig->{'site_wide_secret'}); + if ($eventdata eq $checksum) { + delete_token($token); + return 1; } + } - return 0; + return 0; } # Creates and sends a token to create a new user account. # It assumes that the login has the correct format and is not already in use. sub issue_new_user_account_token { - my ($login, $email) = @_; - my $dbh = Bugzilla->dbh; - my $template = Bugzilla->template; - my $vars = {}; - - # Is there already a pending request for this email? If yes, do not throw - # an error because the user may have lost their email with the token inside. - # But to prevent using this way to mailbomb an email address, make sure - # the last request is old enough before sending a new email (default: 10 minutes). - - my $regexp = "^$email:"; - my $pending_requests = $dbh->selectrow_array( - 'SELECT COUNT(*) + my ($login, $email) = @_; + my $dbh = Bugzilla->dbh; + my $template = Bugzilla->template; + my $vars = {}; + +# Is there already a pending request for this email? If yes, do not throw +# an error because the user may have lost their email with the token inside. +# But to prevent using this way to mailbomb an email address, make sure +# the last request is old enough before sending a new email (default: 10 minutes). + + my $regexp = "^$email:"; + my $pending_requests = $dbh->selectrow_array( + 'SELECT COUNT(*) FROM tokens WHERE tokentype = ? AND ' . $dbh->sql_regexp('eventdata', $dbh->quote($regexp)) . ' AND issuedate > ' - . $dbh->sql_date_math('NOW()', '-', ACCOUNT_CHANGE_INTERVAL, 'MINUTE'), - undef, 'account'); + . $dbh->sql_date_math('NOW()', '-', ACCOUNT_CHANGE_INTERVAL, 'MINUTE'), + undef, 'account' + ); - ThrowUserError('too_soon_for_new_token', {'type' => 'account'}) if $pending_requests; + ThrowUserError('too_soon_for_new_token', {'type' => 'account'}) + if $pending_requests; - my ($token, $token_ts) = _create_token(undef, 'account', "$email:$login"); + my ($token, $token_ts) = _create_token(undef, 'account', "$email:$login"); - $vars->{'login'} = $login; - $vars->{'email'} = $email; - $vars->{'expiration_ts'} = ctime($token_ts + MAX_TOKEN_AGE * 86400); - $vars->{'token'} = $token; + $vars->{'login'} = $login; + $vars->{'email'} = $email; + $vars->{'expiration_ts'} = ctime($token_ts + MAX_TOKEN_AGE * 86400); + $vars->{'token'} = $token; - my $message; - $template->process('account/email/request-new.txt.tmpl', $vars, \$message) - || ThrowTemplateError($template->error()); + my $message; + $template->process('account/email/request-new.txt.tmpl', $vars, \$message) + || ThrowTemplateError($template->error()); - # In 99% of cases, the user getting the confirmation email is the same one - # who made the request, and so it is reasonable to send the email in the same - # language used to view the "Create a New Account" page (we cannot use their - # user prefs as the user has no account yet!). - MessageToMTA($message, SEND_NOW); + # In 99% of cases, the user getting the confirmation email is the same one + # who made the request, and so it is reasonable to send the email in the same + # language used to view the "Create a New Account" page (we cannot use their + # user prefs as the user has no account yet!). + MessageToMTA($message, SEND_NOW); } sub IssueEmailChangeToken { - my $new_email = shift; - my $user = Bugzilla->user; + my $new_email = shift; + my $user = Bugzilla->user; - my ($token, $token_ts) = _create_token($user->id, 'emailold', $user->email . ":$new_email"); - my $newtoken = _create_token($user->id, 'emailnew', $user->email . ":$new_email"); + my ($token, $token_ts) + = _create_token($user->id, 'emailold', $user->email . ":$new_email"); + my $newtoken + = _create_token($user->id, 'emailnew', $user->email . ":$new_email"); - # Mail the user the token along with instructions for using it. + # Mail the user the token along with instructions for using it. - my $template = Bugzilla->template_inner($user->setting('lang')); - my $vars = {}; + my $template = Bugzilla->template_inner($user->setting('lang')); + my $vars = {}; - $vars->{'newemailaddress'} = $new_email; - $vars->{'expiration_ts'} = ctime($token_ts + MAX_TOKEN_AGE * 86400); + $vars->{'newemailaddress'} = $new_email; + $vars->{'expiration_ts'} = ctime($token_ts + MAX_TOKEN_AGE * 86400); - # First send an email to the new address. If this one doesn't exist, - # then the whole process must stop immediately. This means the email must - # be sent immediately and must not be stored in the queue. - $vars->{'token'} = $newtoken; + # First send an email to the new address. If this one doesn't exist, + # then the whole process must stop immediately. This means the email must + # be sent immediately and must not be stored in the queue. + $vars->{'token'} = $newtoken; - my $message; - $template->process('account/email/change-new.txt.tmpl', $vars, \$message) - || ThrowTemplateError($template->error()); + my $message; + $template->process('account/email/change-new.txt.tmpl', $vars, \$message) + || ThrowTemplateError($template->error()); - MessageToMTA($message, SEND_NOW); + MessageToMTA($message, SEND_NOW); - # If we come here, then the new address exists. We now email the current - # address, but we don't want to stop the process if it no longer exists, - # to give a chance to the user to confirm the email address change. - $vars->{'token'} = $token; + # If we come here, then the new address exists. We now email the current + # address, but we don't want to stop the process if it no longer exists, + # to give a chance to the user to confirm the email address change. + $vars->{'token'} = $token; - $message = ''; - $template->process('account/email/change-old.txt.tmpl', $vars, \$message) - || ThrowTemplateError($template->error()); + $message = ''; + $template->process('account/email/change-old.txt.tmpl', $vars, \$message) + || ThrowTemplateError($template->error()); - eval { MessageToMTA($message, SEND_NOW); }; + eval { MessageToMTA($message, SEND_NOW); }; - # Give the user a chance to cancel the process even if he never got - # the email above. The token is required. - return $token; + # Give the user a chance to cancel the process even if he never got + # the email above. The token is required. + return $token; } # Generates a random token, adds it to the tokens table, and sends it # to the user with instructions for using it to change their password. sub IssuePasswordToken { - my $user = shift; - my $dbh = Bugzilla->dbh; + my $user = shift; + my $dbh = Bugzilla->dbh; - my $too_soon = $dbh->selectrow_array( - 'SELECT 1 FROM tokens + my $too_soon = $dbh->selectrow_array( + 'SELECT 1 FROM tokens WHERE userid = ? AND tokentype = ? - AND issuedate > ' - . $dbh->sql_date_math('NOW()', '-', ACCOUNT_CHANGE_INTERVAL, 'MINUTE'), - undef, ($user->id, 'password')); + AND issuedate > ' + . $dbh->sql_date_math('NOW()', '-', ACCOUNT_CHANGE_INTERVAL, 'MINUTE'), + undef, ($user->id, 'password') + ); - ThrowUserError('too_soon_for_new_token', {'type' => 'password'}) if $too_soon; + ThrowUserError('too_soon_for_new_token', {'type' => 'password'}) if $too_soon; - my $ip_addr = remote_ip(); - my ($token, $token_ts) = _create_token($user->id, 'password', $ip_addr); + my $ip_addr = remote_ip(); + my ($token, $token_ts) = _create_token($user->id, 'password', $ip_addr); - # Mail the user the token along with instructions for using it. - my $template = Bugzilla->template_inner($user->setting('lang')); - my $vars = {}; + # Mail the user the token along with instructions for using it. + my $template = Bugzilla->template_inner($user->setting('lang')); + my $vars = {}; - $vars->{'token'} = $token; - $vars->{'ip_addr'} = $ip_addr; - $vars->{'emailaddress'} = $user->email; - $vars->{'expiration_ts'} = ctime($token_ts + MAX_TOKEN_AGE * 86400); - # The user is not logged in (else they wouldn't request a new password). - # So we have to pass this information to the template. - $vars->{'timezone'} = $user->timezone; - - my $message = ""; - $template->process("account/password/forgotten-password.txt.tmpl", - $vars, \$message) - || ThrowTemplateError($template->error()); + $vars->{'token'} = $token; + $vars->{'ip_addr'} = $ip_addr; + $vars->{'emailaddress'} = $user->email; + $vars->{'expiration_ts'} = ctime($token_ts + MAX_TOKEN_AGE * 86400); + + # The user is not logged in (else they wouldn't request a new password). + # So we have to pass this information to the template. + $vars->{'timezone'} = $user->timezone; + + my $message = ""; + $template->process("account/password/forgotten-password.txt.tmpl", + $vars, \$message) + || ThrowTemplateError($template->error()); - MessageToMTA($message); + MessageToMTA($message); } sub issue_session_token { - # Generates a random token, adds it to the tokens table, and returns - # the token to the caller. - my $data = shift; - return _create_token(Bugzilla->user->id, 'session', $data); + # Generates a random token, adds it to the tokens table, and returns + # the token to the caller. + + my $data = shift; + return _create_token(Bugzilla->user->id, 'session', $data); } sub issue_hash_token { - my ($data, $time) = @_; - $data ||= []; - $time ||= time(); - - # For the user ID, use the actual ID if the user is logged in. - # Otherwise, use the remote IP, in case this is for something - # such as creating an account or logging in. - my $user_id = Bugzilla->user->id || remote_ip(); - - # The concatenated string is of the form - # token creation time + user ID (either ID or remote IP) + data - my @args = ($time, $user_id, @$data); - - my $token = join('*', @args); - # Wide characters cause Digest::SHA to die. - utf8::encode($token) if utf8::is_utf8($token); - $token = hmac_sha256_base64($token, Bugzilla->localconfig->{'site_wide_secret'}); - $token =~ s/\+/-/g; - $token =~ s/\//_/g; - - # Prepend the token creation time, unencrypted, so that the token - # lifetime can be validated. - return $time . '-' . $token; + my ($data, $time) = @_; + $data ||= []; + $time ||= time(); + + # For the user ID, use the actual ID if the user is logged in. + # Otherwise, use the remote IP, in case this is for something + # such as creating an account or logging in. + my $user_id = Bugzilla->user->id || remote_ip(); + + # The concatenated string is of the form + # token creation time + user ID (either ID or remote IP) + data + my @args = ($time, $user_id, @$data); + + my $token = join('*', @args); + + # Wide characters cause Digest::SHA to die. + utf8::encode($token) if utf8::is_utf8($token); + $token + = hmac_sha256_base64($token, Bugzilla->localconfig->{'site_wide_secret'}); + $token =~ s/\+/-/g; + $token =~ s/\//_/g; + + # Prepend the token creation time, unencrypted, so that the token + # lifetime can be validated. + return $time . '-' . $token; } sub check_hash_token { - my ($token, $data) = @_; - $data ||= []; - my ($time, $expected_token); - - if ($token) { - ($time, undef) = split(/-/, $token); - # Regenerate the token based on the information we have. - $expected_token = issue_hash_token($data, $time); - } + my ($token, $data) = @_; + $data ||= []; + my ($time, $expected_token); - if (!$token - || $expected_token ne $token - || time() - $time > MAX_TOKEN_AGE * 86400) - { - my $template = Bugzilla->template; - my $vars = {}; - $vars->{'script_name'} = basename($0); - $vars->{'token'} = issue_hash_token($data); - $vars->{'reason'} = (!$token) ? 'missing_token' : - ($expected_token ne $token) ? 'invalid_token' : - 'expired_token'; - print Bugzilla->cgi->header(); - $template->process('global/confirm-action.html.tmpl', $vars) - || ThrowTemplateError($template->error()); - exit; - } + if ($token) { + ($time, undef) = split(/-/, $token); + + # Regenerate the token based on the information we have. + $expected_token = issue_hash_token($data, $time); + } + + if (!$token + || $expected_token ne $token + || time() - $time > MAX_TOKEN_AGE * 86400) + { + my $template = Bugzilla->template; + my $vars = {}; + $vars->{'script_name'} = basename($0); + $vars->{'token'} = issue_hash_token($data); + $vars->{'reason'} + = (!$token) ? 'missing_token' + : ($expected_token ne $token) ? 'invalid_token' + : 'expired_token'; + print Bugzilla->cgi->header(); + $template->process('global/confirm-action.html.tmpl', $vars) + || ThrowTemplateError($template->error()); + exit; + } - # If we come here, then the token is valid and not too old. - return 1; + # If we come here, then the token is valid and not too old. + return 1; } sub CleanTokenTable { - my $dbh = Bugzilla->dbh; - $dbh->do('DELETE FROM tokens - WHERE ' . $dbh->sql_to_days('NOW()') . ' - ' . - $dbh->sql_to_days('issuedate') . ' >= ?', - undef, MAX_TOKEN_AGE); + my $dbh = Bugzilla->dbh; + $dbh->do( + 'DELETE FROM tokens + WHERE ' + . $dbh->sql_to_days('NOW()') . ' - ' + . $dbh->sql_to_days('issuedate') + . ' >= ?', undef, MAX_TOKEN_AGE + ); } sub GenerateUniqueToken { - # Generates a unique random token. Uses generate_random_password - # for the tokens themselves and checks uniqueness by searching for - # the token in the "tokens" table. Gives up if it can't come up - # with a token after about one hundred tries. - my ($table, $column) = @_; - - my $token; - my $duplicate = 1; - my $tries = 0; - $table ||= "tokens"; - $column ||= "token"; - - my $dbh = Bugzilla->dbh; - my $sth = $dbh->prepare("SELECT 1 FROM $table WHERE $column = ?"); - - while ($duplicate) { - ++$tries; - if ($tries > 100) { - ThrowCodeError("token_generation_error"); - } - $token = generate_random_password(TOKEN_LENGTH); - $sth->execute($token); - $duplicate = $sth->fetchrow_array; + + # Generates a unique random token. Uses generate_random_password + # for the tokens themselves and checks uniqueness by searching for + # the token in the "tokens" table. Gives up if it can't come up + # with a token after about one hundred tries. + my ($table, $column) = @_; + + my $token; + my $duplicate = 1; + my $tries = 0; + $table ||= "tokens"; + $column ||= "token"; + + my $dbh = Bugzilla->dbh; + my $sth = $dbh->prepare("SELECT 1 FROM $table WHERE $column = ?"); + + while ($duplicate) { + ++$tries; + if ($tries > 100) { + ThrowCodeError("token_generation_error"); } - return $token; + $token = generate_random_password(TOKEN_LENGTH); + $sth->execute($token); + $duplicate = $sth->fetchrow_array; + } + return $token; } # Cancels a previously issued token and notifies the user. # This should only happen when the user accidentally makes a token request # or when a malicious hacker makes a token request on behalf of a user. sub Cancel { - my ($token, $cancelaction, $vars) = @_; - my $dbh = Bugzilla->dbh; - $vars ||= {}; - - # Get information about the token being canceled. - trick_taint($token); - my ($db_token, $issuedate, $tokentype, $eventdata, $userid) = - $dbh->selectrow_array('SELECT token, ' . $dbh->sql_date_format('issuedate') . ', + my ($token, $cancelaction, $vars) = @_; + my $dbh = Bugzilla->dbh; + $vars ||= {}; + + # Get information about the token being canceled. + trick_taint($token); + my ($db_token, $issuedate, $tokentype, $eventdata, $userid) + = $dbh->selectrow_array( + 'SELECT token, ' + . $dbh->sql_date_format('issuedate') . ', tokentype, eventdata, userid FROM tokens - WHERE token = ?', - undef, $token); + WHERE token = ?', undef, $token + ); - # Some DBs such as MySQL are case-insensitive by default so we do - # a quick comparison to make sure the tokens are indeed the same. - (defined $db_token && $db_token eq $token) - || ThrowCodeError("cancel_token_does_not_exist"); + # Some DBs such as MySQL are case-insensitive by default so we do + # a quick comparison to make sure the tokens are indeed the same. + (defined $db_token && $db_token eq $token) + || ThrowCodeError("cancel_token_does_not_exist"); - # If we are canceling the creation of a new user account, then there - # is no entry in the 'profiles' table. - my $user = new Bugzilla::User($userid); + # If we are canceling the creation of a new user account, then there + # is no entry in the 'profiles' table. + my $user = new Bugzilla::User($userid); - if ($userid) { - $vars->{'emailaddress'} = $user->email; - $vars->{'login'} = $user->login; - } - else { - # Be careful! Some logins may contain ":" in them. - my ($email, $login) = split(':', $eventdata, 2); - $vars->{'emailaddress'} = $email; - $vars->{'login'} = $login; - } - - $vars->{'remoteaddress'} = remote_ip(); - $vars->{'token'} = $token; - $vars->{'tokentype'} = $tokentype; - $vars->{'issuedate'} = $issuedate; - # The user is probably not logged in. - # So we have to pass this information to the template. - $vars->{'timezone'} = $user->timezone; - $vars->{'eventdata'} = $eventdata; - $vars->{'cancelaction'} = $cancelaction; - - # Notify the user via email about the cancellation. - my $template = Bugzilla->template_inner($user->setting('lang')); - - my $message; - $template->process("account/cancel-token.txt.tmpl", $vars, \$message) - || ThrowTemplateError($template->error()); - - MessageToMTA($message); - - # Delete the token from the database. - delete_token($token); + if ($userid) { + $vars->{'emailaddress'} = $user->email; + $vars->{'login'} = $user->login; + } + else { + # Be careful! Some logins may contain ":" in them. + my ($email, $login) = split(':', $eventdata, 2); + $vars->{'emailaddress'} = $email; + $vars->{'login'} = $login; + } + + $vars->{'remoteaddress'} = remote_ip(); + $vars->{'token'} = $token; + $vars->{'tokentype'} = $tokentype; + $vars->{'issuedate'} = $issuedate; + + # The user is probably not logged in. + # So we have to pass this information to the template. + $vars->{'timezone'} = $user->timezone; + $vars->{'eventdata'} = $eventdata; + $vars->{'cancelaction'} = $cancelaction; + + # Notify the user via email about the cancellation. + my $template = Bugzilla->template_inner($user->setting('lang')); + + my $message; + $template->process("account/cancel-token.txt.tmpl", $vars, \$message) + || ThrowTemplateError($template->error()); + + MessageToMTA($message); + + # Delete the token from the database. + delete_token($token); } sub DeletePasswordTokens { - my ($userid, $reason) = @_; - my $dbh = Bugzilla->dbh; + my ($userid, $reason) = @_; + my $dbh = Bugzilla->dbh; - detaint_natural($userid); - my $tokens = $dbh->selectcol_arrayref('SELECT token FROM tokens + detaint_natural($userid); + my $tokens = $dbh->selectcol_arrayref( + 'SELECT token FROM tokens WHERE userid = ? AND tokentype = ?', - undef, ($userid, 'password')); + undef, ($userid, 'password') + ); - foreach my $token (@$tokens) { - Bugzilla::Token::Cancel($token, $reason); - } + foreach my $token (@$tokens) { + Bugzilla::Token::Cancel($token, $reason); + } } -# Returns an email change token if the user has one. +# Returns an email change token if the user has one. sub HasEmailChangeToken { - my $userid = shift; - my $dbh = Bugzilla->dbh; + my $userid = shift; + my $dbh = Bugzilla->dbh; - my $token = $dbh->selectrow_array('SELECT token FROM tokens + my $token = $dbh->selectrow_array( + 'SELECT token FROM tokens WHERE userid = ? - AND (tokentype = ? OR tokentype = ?) ' . - $dbh->sql_limit(1), - undef, ($userid, 'emailnew', 'emailold')); - return $token; + AND (tokentype = ? OR tokentype = ?) ' + . $dbh->sql_limit(1), undef, ($userid, 'emailnew', 'emailold') + ); + return $token; } # Returns the userid, issuedate and eventdata for the specified token sub GetTokenData { - my ($token) = @_; - my $dbh = Bugzilla->dbh; + my ($token) = @_; + my $dbh = Bugzilla->dbh; - return unless defined $token; - $token = clean_text($token); - trick_taint($token); + return unless defined $token; + $token = clean_text($token); + trick_taint($token); - my @token_data = $dbh->selectrow_array( - "SELECT token, userid, " . $dbh->sql_date_format('issuedate') . ", eventdata, tokentype + my @token_data = $dbh->selectrow_array( + "SELECT token, userid, " + . $dbh->sql_date_format('issuedate') + . ", eventdata, tokentype FROM tokens - WHERE token = ?", undef, $token); + WHERE token = ?", undef, $token + ); - # Some DBs such as MySQL are case-insensitive by default so we do - # a quick comparison to make sure the tokens are indeed the same. - my $db_token = shift @token_data; - return undef if (!defined $db_token || $db_token ne $token); + # Some DBs such as MySQL are case-insensitive by default so we do + # a quick comparison to make sure the tokens are indeed the same. + my $db_token = shift @token_data; + return undef if (!defined $db_token || $db_token ne $token); - return @token_data; + return @token_data; } # Deletes specified token sub delete_token { - my ($token) = @_; - my $dbh = Bugzilla->dbh; + my ($token) = @_; + my $dbh = Bugzilla->dbh; - return unless defined $token; - trick_taint($token); + return unless defined $token; + trick_taint($token); - $dbh->do("DELETE FROM tokens WHERE token = ?", undef, $token); + $dbh->do("DELETE FROM tokens WHERE token = ?", undef, $token); } # Given a token, makes sure it comes from the currently logged in user # and match the expected event. Returns 1 on success, else displays a warning. sub check_token_data { - my ($token, $expected_action, $alternate_script) = @_; - my $user = Bugzilla->user; - my $template = Bugzilla->template; - my $cgi = Bugzilla->cgi; - - my ($creator_id, $date, $token_action) = GetTokenData($token); - unless ($creator_id - && $creator_id == $user->id - && $token_action eq $expected_action) - { - # Something is going wrong. Ask confirmation before processing. - # It is possible that someone tried to trick an administrator. - # In this case, we want to know their name! - require Bugzilla::User; - - my $vars = {}; - $vars->{'abuser'} = Bugzilla::User->new($creator_id)->identity; - $vars->{'token_action'} = $token_action; - $vars->{'expected_action'} = $expected_action; - $vars->{'script_name'} = basename($0); - $vars->{'alternate_script'} = $alternate_script || basename($0); - - # Now is a good time to remove old tokens from the DB. - CleanTokenTable(); - - # If no token was found, create a valid token for the given action. - unless ($creator_id) { - $token = issue_session_token($expected_action); - $cgi->param('token', $token); - } - - print $cgi->header(); - - $template->process('admin/confirm-action.html.tmpl', $vars) - || ThrowTemplateError($template->error()); - exit; + my ($token, $expected_action, $alternate_script) = @_; + my $user = Bugzilla->user; + my $template = Bugzilla->template; + my $cgi = Bugzilla->cgi; + + my ($creator_id, $date, $token_action) = GetTokenData($token); + unless ($creator_id + && $creator_id == $user->id + && $token_action eq $expected_action) + { + # Something is going wrong. Ask confirmation before processing. + # It is possible that someone tried to trick an administrator. + # In this case, we want to know their name! + require Bugzilla::User; + + my $vars = {}; + $vars->{'abuser'} = Bugzilla::User->new($creator_id)->identity; + $vars->{'token_action'} = $token_action; + $vars->{'expected_action'} = $expected_action; + $vars->{'script_name'} = basename($0); + $vars->{'alternate_script'} = $alternate_script || basename($0); + + # Now is a good time to remove old tokens from the DB. + CleanTokenTable(); + + # If no token was found, create a valid token for the given action. + unless ($creator_id) { + $token = issue_session_token($expected_action); + $cgi->param('token', $token); } - return 1; + + print $cgi->header(); + + $template->process('admin/confirm-action.html.tmpl', $vars) + || ThrowTemplateError($template->error()); + exit; + } + return 1; } ################################################################################ @@ -481,34 +511,38 @@ sub check_token_data { # Generates a unique token and inserts it into the database # Returns the token and the token timestamp sub _create_token { - my ($userid, $tokentype, $eventdata) = @_; - my $dbh = Bugzilla->dbh; + my ($userid, $tokentype, $eventdata) = @_; + my $dbh = Bugzilla->dbh; - detaint_natural($userid) if defined $userid; - trick_taint($tokentype); - trick_taint($eventdata); + detaint_natural($userid) if defined $userid; + trick_taint($tokentype); + trick_taint($eventdata); - my $is_shadow = Bugzilla->is_shadow_db; - $dbh = Bugzilla->switch_to_main_db() if $is_shadow; + my $is_shadow = Bugzilla->is_shadow_db; + $dbh = Bugzilla->switch_to_main_db() if $is_shadow; - $dbh->bz_start_transaction(); + $dbh->bz_start_transaction(); - my $token = GenerateUniqueToken(); + my $token = GenerateUniqueToken(); - $dbh->do("INSERT INTO tokens (userid, issuedate, token, tokentype, eventdata) - VALUES (?, NOW(), ?, ?, ?)", undef, ($userid, $token, $tokentype, $eventdata)); + $dbh->do( + "INSERT INTO tokens (userid, issuedate, token, tokentype, eventdata) + VALUES (?, NOW(), ?, ?, ?)", undef, + ($userid, $token, $tokentype, $eventdata) + ); - $dbh->bz_commit_transaction(); + $dbh->bz_commit_transaction(); - if (wantarray) { - my (undef, $token_ts, undef) = GetTokenData($token); - $token_ts = str2time($token_ts); - Bugzilla->switch_to_shadow_db() if $is_shadow; - return ($token, $token_ts); - } else { - Bugzilla->switch_to_shadow_db() if $is_shadow; - return $token; - } + if (wantarray) { + my (undef, $token_ts, undef) = GetTokenData($token); + $token_ts = str2time($token_ts); + Bugzilla->switch_to_shadow_db() if $is_shadow; + return ($token, $token_ts); + } + else { + Bugzilla->switch_to_shadow_db() if $is_shadow; + return $token; + } } 1; diff --git a/Bugzilla/Update.pm b/Bugzilla/Update.pm index 8babf9864c..516de43e8b 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 b932a23c8e..ead64cc7d2 100644 --- a/Bugzilla/User.pm +++ b/Bugzilla/User.pm @@ -33,9 +33,9 @@ use URI::QueryParam; use parent qw(Bugzilla::Object Exporter); @Bugzilla::User::EXPORT = qw(is_available_email email_to_id - login_to_id validate_password validate_password_check - USER_MATCH_MULTIPLE USER_MATCH_FAILED USER_MATCH_SUCCESS - MATCH_SKIP_CONFIRM + login_to_id validate_password validate_password_check + USER_MATCH_MULTIPLE USER_MATCH_FAILED USER_MATCH_SUCCESS + MATCH_SKIP_CONFIRM ); ##################################################################### @@ -46,17 +46,17 @@ use constant USER_MATCH_MULTIPLE => -1; use constant USER_MATCH_FAILED => 0; use constant USER_MATCH_SUCCESS => 1; -use constant MATCH_SKIP_CONFIRM => 1; +use constant MATCH_SKIP_CONFIRM => 1; use constant DEFAULT_USER => { - 'userid' => 0, - 'login_name' => '', - 'email' => '', - 'realname' => '', - 'showmybugslink' => 0, - 'disabledtext' => '', - 'disable_mail' => 0, - 'is_enabled' => 1, + 'userid' => 0, + 'login_name' => '', + 'email' => '', + 'realname' => '', + 'showmybugslink' => 0, + 'disabledtext' => '', + 'disable_mail' => 0, + 'is_enabled' => 1, }; use constant DB_TABLE => 'profiles'; @@ -66,19 +66,20 @@ use constant DB_TABLE => 'profiles'; # Bugzilla::User used "name" for the realname field. This should be # fixed one day. sub DB_COLUMNS { - my $dbh = Bugzilla->dbh; - return ( - 'profiles.userid', - 'profiles.login_name', - 'profiles.email', - 'profiles.realname', - 'profiles.mybugslink AS showmybugslink', - 'profiles.disabledtext', - 'profiles.disable_mail', - 'profiles.extern_id', - 'profiles.is_enabled', - $dbh->sql_date_format('last_seen_date', '%Y-%m-%d') . ' AS last_seen_date', + my $dbh = Bugzilla->dbh; + return ( + 'profiles.userid', + 'profiles.login_name', + 'profiles.email', + 'profiles.realname', + 'profiles.mybugslink AS showmybugslink', + 'profiles.disabledtext', + 'profiles.disable_mail', + 'profiles.extern_id', + 'profiles.is_enabled', + $dbh->sql_date_format('last_seen_date', '%Y-%m-%d') . ' AS last_seen_date', ), + ; } use constant NAME_FIELD => 'login_name'; @@ -86,35 +87,33 @@ use constant ID_FIELD => 'userid'; use constant LIST_ORDER => NAME_FIELD; use constant VALIDATORS => { - cryptpassword => \&_check_password, - disable_mail => \&_check_disable_mail, - disabledtext => \&_check_disabledtext, - login_name => \&check_login_name, - email => \&check_email, - realname => \&_check_realname, - extern_id => \&_check_extern_id, - is_enabled => \&_check_is_enabled, + cryptpassword => \&_check_password, + disable_mail => \&_check_disable_mail, + disabledtext => \&_check_disabledtext, + login_name => \&check_login_name, + email => \&check_email, + realname => \&_check_realname, + extern_id => \&_check_extern_id, + is_enabled => \&_check_is_enabled, }; sub UPDATE_COLUMNS { - my $self = shift; - my @cols = qw( - disable_mail - disabledtext - login_name - email - realname - extern_id - is_enabled - ); - push(@cols, 'cryptpassword') if exists $self->{cryptpassword}; - return @cols; -}; - -use constant VALIDATOR_DEPENDENCIES => { - is_enabled => ['disabledtext'], - login_name => ['email'], -}; + my $self = shift; + my @cols = qw( + disable_mail + disabledtext + login_name + email + realname + extern_id + is_enabled + ); + push(@cols, 'cryptpassword') if exists $self->{cryptpassword}; + return @cols; +} + +use constant VALIDATOR_DEPENDENCIES => + {is_enabled => ['disabledtext'], login_name => ['email'],}; use constant EXTRA_REQUIRED_FIELDS => qw(is_enabled); @@ -123,130 +122,128 @@ use constant EXTRA_REQUIRED_FIELDS => qw(is_enabled); ################################################################################ sub new { - my $invocant = shift; - my $class = ref($invocant) || $invocant; - my ($param) = @_; - - my $user = { %{ DEFAULT_USER() } }; - bless ($user, $class); - return $user unless $param; - - if (ref($param) eq 'HASH') { - if (defined $param->{extern_id}) { - $param = { condition => 'extern_id = ?' , values => [$param->{extern_id}] }; - $_[0] = $param; - } + my $invocant = shift; + my $class = ref($invocant) || $invocant; + my ($param) = @_; + + my $user = {%{DEFAULT_USER()}}; + bless($user, $class); + return $user unless $param; + + if (ref($param) eq 'HASH') { + if (defined $param->{extern_id}) { + $param = {condition => 'extern_id = ?', values => [$param->{extern_id}]}; + $_[0] = $param; } - return $class->SUPER::new(@_); + } + return $class->SUPER::new(@_); } sub super_user { - my $invocant = shift; - my $class = ref($invocant) || $invocant; - my ($param) = @_; + my $invocant = shift; + my $class = ref($invocant) || $invocant; + my ($param) = @_; - my $user = { %{ DEFAULT_USER() } }; - $user->{groups} = [Bugzilla::Group->get_all]; - $user->{bless_groups} = [Bugzilla::Group->get_all]; - bless $user, $class; - return $user; + my $user = {%{DEFAULT_USER()}}; + $user->{groups} = [Bugzilla::Group->get_all]; + $user->{bless_groups} = [Bugzilla::Group->get_all]; + bless $user, $class; + return $user; } sub _update_groups { - my $self = shift; - my $group_changes = shift; - my $changes = shift; - my $dbh = Bugzilla->dbh; - - # Update group settings. - my $sth_add_mapping = $dbh->prepare( - qq{INSERT INTO user_group_map ( + my $self = shift; + my $group_changes = shift; + my $changes = shift; + my $dbh = Bugzilla->dbh; + + # Update group settings. + my $sth_add_mapping = $dbh->prepare( + qq{INSERT INTO user_group_map ( user_id, group_id, isbless, grant_type ) VALUES ( ?, ?, ?, ? ) - }); - my $sth_remove_mapping = $dbh->prepare( - qq{DELETE FROM user_group_map + } + ); + my $sth_remove_mapping = $dbh->prepare( + qq{DELETE FROM user_group_map WHERE user_id = ? AND group_id = ? AND isbless = ? AND grant_type = ? - }); + } + ); - foreach my $is_bless (keys %$group_changes) { - my ($removed, $added) = @{$group_changes->{$is_bless}}; + foreach my $is_bless (keys %$group_changes) { + my ($removed, $added) = @{$group_changes->{$is_bless}}; - foreach my $group (@$removed) { - $sth_remove_mapping->execute( - $self->id, $group->id, $is_bless, GRANT_DIRECT - ); - } - foreach my $group (@$added) { - $sth_add_mapping->execute( - $self->id, $group->id, $is_bless, GRANT_DIRECT - ); - } + foreach my $group (@$removed) { + $sth_remove_mapping->execute($self->id, $group->id, $is_bless, GRANT_DIRECT); + } + foreach my $group (@$added) { + $sth_add_mapping->execute($self->id, $group->id, $is_bless, GRANT_DIRECT); + } - if (! $is_bless) { - my $query = qq{ + if (!$is_bless) { + my $query = qq{ INSERT INTO profiles_activity (userid, who, profiles_when, fieldid, oldvalue, newvalue) VALUES ( ?, ?, now(), ?, ?, ?) }; - $dbh->do( - $query, undef, - $self->id, Bugzilla->user->id, - get_field_id('bug_group'), - join(', ', map { $_->name } @$removed), - join(', ', map { $_->name } @$added) - ); - } - else { - # XXX: should create profiles_activity entries for blesser changes. - } + $dbh->do( + $query, undef, $self->id, Bugzilla->user->id, + get_field_id('bug_group'), + join(', ', map { $_->name } @$removed), + join(', ', map { $_->name } @$added) + ); + } + else { + # XXX: should create profiles_activity entries for blesser changes. + } - Bugzilla->memcached->clear_config({ key => 'user_groups.' . $self->id }); + Bugzilla->memcached->clear_config({key => 'user_groups.' . $self->id}); - my $type = $is_bless ? 'bless_groups' : 'groups'; - $changes->{$type} = [ - [ map { $_->name } @$removed ], - [ map { $_->name } @$added ], - ]; - } + my $type = $is_bless ? 'bless_groups' : 'groups'; + $changes->{$type} = [[map { $_->name } @$removed], [map { $_->name } @$added],]; + } } sub update { - my $self = shift; - my $options = shift; + my $self = shift; + my $options = shift; - my $group_changes = delete $self->{_group_changes}; + my $group_changes = delete $self->{_group_changes}; - my $changes = $self->SUPER::update(@_); - my $dbh = Bugzilla->dbh; - $self->_update_groups($group_changes, $changes); + my $changes = $self->SUPER::update(@_); + my $dbh = Bugzilla->dbh; + $self->_update_groups($group_changes, $changes); - if (exists $changes->{email}) { - # Delete all the tokens related to the userid - $dbh->do('DELETE FROM tokens WHERE userid = ?', undef, $self->id) - unless $options->{keep_tokens}; - # And rederive regex groups - $self->derive_regexp_groups(); - } + if (exists $changes->{email}) { + + # Delete all the tokens related to the userid + $dbh->do('DELETE FROM tokens WHERE userid = ?', undef, $self->id) + unless $options->{keep_tokens}; - # Logout the user if necessary. - Bugzilla->logout_user($self) - if (!$options->{keep_session} - && (exists $changes->{login_name} - || exists $changes->{email} - || exists $changes->{disabledtext} - || exists $changes->{cryptpassword})); + # And rederive regex groups + $self->derive_regexp_groups(); + } - # XXX Can update profiles_activity here as soon as it understands - # field names like login_name. + # Logout the user if necessary. + Bugzilla->logout_user($self) + if ( + !$options->{keep_session} + && ( exists $changes->{login_name} + || exists $changes->{email} + || exists $changes->{disabledtext} + || exists $changes->{cryptpassword}) + ); - return $changes; + # XXX Can update profiles_activity here as soon as it understands + # field names like login_name. + + return $changes; } ################################################################################ @@ -258,92 +255,96 @@ sub _check_disabledtext { return trim($_[1]) || ''; } # Check whether the extern_id is unique. sub _check_extern_id { - my ($invocant, $extern_id) = @_; - $extern_id = trim($extern_id); - return undef unless defined($extern_id) && $extern_id ne ""; - if (!ref($invocant) || $invocant->extern_id ne $extern_id) { - my $existing_login = $invocant->new({ extern_id => $extern_id }); - if ($existing_login) { - ThrowUserError( 'extern_id_exists', - { extern_id => $extern_id, - existing_login_name => $existing_login->login }); - } + my ($invocant, $extern_id) = @_; + $extern_id = trim($extern_id); + return undef unless defined($extern_id) && $extern_id ne ""; + if (!ref($invocant) || $invocant->extern_id ne $extern_id) { + my $existing_login = $invocant->new({extern_id => $extern_id}); + if ($existing_login) { + ThrowUserError('extern_id_exists', + {extern_id => $extern_id, existing_login_name => $existing_login->login}); } - return $extern_id; + } + return $extern_id; } sub check_login_name { - my ($invocant, $login, undef, $data) = @_; - - # No control characters - $login = clean_text($login); - $login || ThrowUserError('user_login_required'); - # No whitespace - $login !~ /\s/ || ThrowUserError('login_illegal_character'); - - # No @ sign unless login is email (VALIDATOR_DEPENDENCIES means - # this will be set already) - if ($login =~ /@/) { - my $email = ref($invocant) ? $invocant->email : $data->{email}; - # We should really use fc() instead of lc(), but this requires Perl 5.16. - ThrowUserError('login_at_sign_disallowed') unless lc($login) eq lc($email); - } - # We set the max length to 127 to ensure logins aren't truncated when - # inserted into the tokens.eventdata field. - length($login) <= 127 or ThrowUserError('login_too_long'); + my ($invocant, $login, undef, $data) = @_; - trick_taint($login); + # No control characters + $login = clean_text($login); + $login || ThrowUserError('user_login_required'); - # Check the login name if it's a new user, or if we're changing the login name. - if (!ref($invocant) || lc($invocant->login) ne lc($login)) { - if (login_to_id($login)) { - ThrowUserError('account_exists', { login => $login }); - } + # No whitespace + $login !~ /\s/ || ThrowUserError('login_illegal_character'); + + # No @ sign unless login is email (VALIDATOR_DEPENDENCIES means + # this will be set already) + if ($login =~ /@/) { + my $email = ref($invocant) ? $invocant->email : $data->{email}; + + # We should really use fc() instead of lc(), but this requires Perl 5.16. + ThrowUserError('login_at_sign_disallowed') unless lc($login) eq lc($email); + } + + # We set the max length to 127 to ensure logins aren't truncated when + # inserted into the tokens.eventdata field. + length($login) <= 127 or ThrowUserError('login_too_long'); + + trick_taint($login); + + # Check the login name if it's a new user, or if we're changing the login name. + if (!ref($invocant) || lc($invocant->login) ne lc($login)) { + if (login_to_id($login)) { + ThrowUserError('account_exists', {login => $login}); } + } - return $login; + return $login; } sub check_email { - my ($invocant, $email) = @_; - $email = clean_text($email); - $email || ThrowUserError('email_required'); + my ($invocant, $email) = @_; + $email = clean_text($email); + $email || ThrowUserError('email_required'); - check_email_syntax($email); + check_email_syntax($email); - # Check the email if it's a new user, or if we're changing the email. - my $old_email = ref($invocant) ? $invocant->email : undef; - if (!defined($old_email) || lc($old_email) ne lc($email)) { - is_available_email($email, $old_email) - || ThrowUserError('account_exists', { email => $email }); - } + # Check the email if it's a new user, or if we're changing the email. + my $old_email = ref($invocant) ? $invocant->email : undef; + if (!defined($old_email) || lc($old_email) ne lc($email)) { + is_available_email($email, $old_email) + || ThrowUserError('account_exists', {email => $email}); + } - return $email; + return $email; } sub _check_password { - my ($self, $pass) = @_; + my ($self, $pass) = @_; - # If the password is '*', do not encrypt it or validate it further--we - # are creating a user who should not be able to log in using DB - # authentication. - return $pass if $pass eq '*'; + # If the password is '*', do not encrypt it or validate it further--we + # are creating a user who should not be able to log in using DB + # authentication. + return $pass if $pass eq '*'; - validate_password($pass); - my $cryptpassword = bz_crypt($pass); - return $cryptpassword; + validate_password($pass); + my $cryptpassword = bz_crypt($pass); + return $cryptpassword; } sub _check_realname { return trim($_[1]) || ''; } sub _check_is_enabled { - my ($invocant, $is_enabled, undef, $params) = @_; - # is_enabled is set automatically on creation depending on whether - # disabledtext is empty (enabled) or not empty (disabled). - # When updating the user, is_enabled is set by calling set_disabledtext(). - # Any value passed into this validator is ignored. - my $disabledtext = ref($invocant) ? $invocant->disabledtext : $params->{disabledtext}; - return $disabledtext ? 0 : 1; + my ($invocant, $is_enabled, undef, $params) = @_; + + # is_enabled is set automatically on creation depending on whether + # disabledtext is empty (enabled) or not empty (disabled). + # When updating the user, is_enabled is set by calling set_disabledtext(). + # Any value passed into this validator is ignored. + my $disabledtext + = ref($invocant) ? $invocant->disabledtext : $params->{disabledtext}; + return $disabledtext ? 0 : 1; } ################################################################################ @@ -352,156 +353,157 @@ sub _check_is_enabled { sub set_disable_mail { $_[0]->set('disable_mail', $_[1]); } sub set_email_enabled { $_[0]->set('disable_mail', !$_[1]); } -sub set_extern_id { $_[0]->set('extern_id', $_[1]); } +sub set_extern_id { $_[0]->set('extern_id', $_[1]); } sub set_login { - my ($self, $login) = @_; - $self->set('login_name', $login); - delete $self->{identity}; - delete $self->{nick}; + my ($self, $login) = @_; + $self->set('login_name', $login); + delete $self->{identity}; + delete $self->{nick}; } sub set_email { - my ($self, $email) = @_; - $self->set('email', $email); - $self->set_login($email) if Bugzilla->params->{'use_email_as_login'}; + my ($self, $email) = @_; + $self->set('email', $email); + $self->set_login($email) if Bugzilla->params->{'use_email_as_login'}; } sub set_name { - my ($self, $name) = @_; - $self->set('realname', $name); - delete $self->{identity}; + my ($self, $name) = @_; + $self->set('realname', $name); + delete $self->{identity}; } sub set_password { $_[0]->set('cryptpassword', $_[1]); } sub set_disabledtext { - $_[0]->set('disabledtext', $_[1]); - $_[0]->set('is_enabled', $_[1] ? 0 : 1); + $_[0]->set('disabledtext', $_[1]); + $_[0]->set('is_enabled', $_[1] ? 0 : 1); } sub set_groups { - my $self = shift; - $self->_set_groups(GROUP_MEMBERSHIP, @_); + my $self = shift; + $self->_set_groups(GROUP_MEMBERSHIP, @_); } sub set_bless_groups { - my $self = shift; + my $self = shift; - # The person making the change needs to be in the editusers group - Bugzilla->user->in_group('editusers') - || ThrowUserError("auth_failure", {group => "editusers", - reason => "cant_bless", - action => "edit", - object => "users"}); + # The person making the change needs to be in the editusers group + Bugzilla->user->in_group('editusers') || ThrowUserError( + "auth_failure", + { + group => "editusers", + reason => "cant_bless", + action => "edit", + object => "users" + } + ); - $self->_set_groups(GROUP_BLESS, @_); + $self->_set_groups(GROUP_BLESS, @_); } sub _set_groups { - my $self = shift; - my $is_bless = shift; - my $changes = shift; - my $dbh = Bugzilla->dbh; + my $self = shift; + my $is_bless = shift; + my $changes = shift; + my $dbh = Bugzilla->dbh; - # The person making the change is $user, $self is the person being changed - my $user = Bugzilla->user; + # The person making the change is $user, $self is the person being changed + my $user = Bugzilla->user; - # Input is a hash of arrays. Key is 'set', 'add' or 'remove'. The array - # is a list of group ids and/or names. + # Input is a hash of arrays. Key is 'set', 'add' or 'remove'. The array + # is a list of group ids and/or names. - # First turn the arrays into group objects. - $changes = $self->_set_groups_to_object($changes); + # First turn the arrays into group objects. + $changes = $self->_set_groups_to_object($changes); - # Get a list of the groups the user currently is a member of - my $ids = $dbh->selectcol_arrayref( - q{SELECT DISTINCT group_id + # Get a list of the groups the user currently is a member of + my $ids = $dbh->selectcol_arrayref( + q{SELECT DISTINCT group_id FROM user_group_map - WHERE user_id = ? AND isbless = ? AND grant_type = ?}, - undef, $self->id, $is_bless, GRANT_DIRECT); - - my $current_groups = Bugzilla::Group->new_from_list($ids); - my $new_groups = dclone($current_groups); - - # Record the changes - if (exists $changes->{set}) { - $new_groups = $changes->{set}; - - # We need to check the user has bless rights on the existing groups - # If they don't, then we need to add them back to new_groups - foreach my $group (@$current_groups) { - if (! $user->can_bless($group->id)) { - push @$new_groups, $group - unless grep { $_->id eq $group->id } @$new_groups; - } - } + WHERE user_id = ? AND isbless = ? AND grant_type = ?}, undef, $self->id, + $is_bless, GRANT_DIRECT + ); + + my $current_groups = Bugzilla::Group->new_from_list($ids); + my $new_groups = dclone($current_groups); + + # Record the changes + if (exists $changes->{set}) { + $new_groups = $changes->{set}; + + # We need to check the user has bless rights on the existing groups + # If they don't, then we need to add them back to new_groups + foreach my $group (@$current_groups) { + if (!$user->can_bless($group->id)) { + push @$new_groups, $group unless grep { $_->id eq $group->id } @$new_groups; + } } - else { - foreach my $group (@{$changes->{remove} // []}) { - @$new_groups = grep { $_->id ne $group->id } @$new_groups; - } - foreach my $group (@{$changes->{add} // []}) { - push @$new_groups, $group - unless grep { $_->id eq $group->id } @$new_groups; - } + } + else { + foreach my $group (@{$changes->{remove} // []}) { + @$new_groups = grep { $_->id ne $group->id } @$new_groups; } - - # Stash the changes, so self->update can actually make them - my @diffs = diff_arrays($current_groups, $new_groups, 'id'); - if (scalar(@{$diffs[0]}) || scalar(@{$diffs[1]})) { - $self->{_group_changes}{$is_bless} = \@diffs; + foreach my $group (@{$changes->{add} // []}) { + push @$new_groups, $group unless grep { $_->id eq $group->id } @$new_groups; } + } + + # Stash the changes, so self->update can actually make them + my @diffs = diff_arrays($current_groups, $new_groups, 'id'); + if (scalar(@{$diffs[0]}) || scalar(@{$diffs[1]})) { + $self->{_group_changes}{$is_bless} = \@diffs; + } } sub _set_groups_to_object { - my $self = shift; - my $changes = shift; - my $user = Bugzilla->user; - - foreach my $key (keys %$changes) { - # Check we were given an array - unless (ref($changes->{$key}) eq 'ARRAY') { - ThrowCodeError( - 'param_invalid', - { param => $changes->{$key}, function => $key } - ); - } + my $self = shift; + my $changes = shift; + my $user = Bugzilla->user; - # Go through the array, and turn items into group objects - my @groups = (); - foreach my $value (@{$changes->{$key}}) { - my $type = $value =~ /^\d+$/ ? 'id' : 'name'; - my $group = Bugzilla::Group->new({$type => $value}); - - if (! $group || ! $user->can_bless($group->id)) { - ThrowUserError('auth_failure', - { group => $value, reason => 'cant_bless', - action => 'edit', object => 'users' }); - } - push @groups, $group; - } - $changes->{$key} = \@groups; + foreach my $key (keys %$changes) { + + # Check we were given an array + unless (ref($changes->{$key}) eq 'ARRAY') { + ThrowCodeError('param_invalid', {param => $changes->{$key}, function => $key}); + } + + # Go through the array, and turn items into group objects + my @groups = (); + foreach my $value (@{$changes->{$key}}) { + my $type = $value =~ /^\d+$/ ? 'id' : 'name'; + my $group = Bugzilla::Group->new({$type => $value}); + + if (!$group || !$user->can_bless($group->id)) { + ThrowUserError('auth_failure', + {group => $value, reason => 'cant_bless', action => 'edit', object => 'users'}); + } + push @groups, $group; } + $changes->{$key} = \@groups; + } - return $changes; + return $changes; } sub update_last_seen_date { - my $self = shift; - return unless $self->id; - my $dbh = Bugzilla->dbh; - my $date = $dbh->selectrow_array( - 'SELECT ' . $dbh->sql_date_format('NOW()', '%Y-%m-%d')); - - if (!$self->last_seen_date or $date ne $self->last_seen_date) { - $self->{last_seen_date} = $date; - # We don't use the normal update() routine here as we only - # want to update the last_seen_date column, not any other - # pending changes - $dbh->do("UPDATE profiles SET last_seen_date = ? WHERE userid = ?", - undef, $date, $self->id); - Bugzilla->memcached->clear({ table => 'profiles', id => $self->id }); - } + my $self = shift; + return unless $self->id; + my $dbh = Bugzilla->dbh; + my $date = $dbh->selectrow_array( + 'SELECT ' . $dbh->sql_date_format('NOW()', '%Y-%m-%d')); + + if (!$self->last_seen_date or $date ne $self->last_seen_date) { + $self->{last_seen_date} = $date; + + # We don't use the normal update() routine here as we only + # want to update the last_seen_date column, not any other + # pending changes + $dbh->do("UPDATE profiles SET last_seen_date = ? WHERE userid = ?", + undef, $date, $self->id); + Bugzilla->memcached->clear({table => 'profiles', id => $self->id}); + } } ################################################################################ @@ -509,37 +511,41 @@ sub update_last_seen_date { ################################################################################ # Accessors for user attributes -sub name { $_[0]->{realname}; } -sub login { $_[0]->{login_name}; } -sub extern_id { $_[0]->{extern_id}; } -sub email { $_[0]->{email}; } -sub disabledtext { $_[0]->{'disabledtext'}; } -sub is_enabled { $_[0]->{'is_enabled'} ? 1 : 0; } +sub name { $_[0]->{realname}; } +sub login { $_[0]->{login_name}; } +sub extern_id { $_[0]->{extern_id}; } +sub email { $_[0]->{email}; } +sub disabledtext { $_[0]->{'disabledtext'}; } +sub is_enabled { $_[0]->{'is_enabled'} ? 1 : 0; } sub showmybugslink { $_[0]->{showmybugslink}; } sub email_disabled { $_[0]->{disable_mail}; } -sub email_enabled { !($_[0]->{disable_mail}); } +sub email_enabled { !($_[0]->{disable_mail}); } sub last_seen_date { $_[0]->{last_seen_date}; } + sub cryptpassword { - my $self = shift; - # We don't store it because we never want it in the object (we - # don't want to accidentally dump even the hash somewhere). - my ($pw) = Bugzilla->dbh->selectrow_array( - 'SELECT cryptpassword FROM profiles WHERE userid = ?', - undef, $self->id); - return $pw; + my $self = shift; + + # We don't store it because we never want it in the object (we + # don't want to accidentally dump even the hash somewhere). + my ($pw) + = Bugzilla->dbh->selectrow_array( + 'SELECT cryptpassword FROM profiles WHERE userid = ?', + undef, $self->id); + return $pw; } sub set_authorizer { - my ($self, $authorizer) = @_; - $self->{authorizer} = $authorizer; + my ($self, $authorizer) = @_; + $self->{authorizer} = $authorizer; } + sub authorizer { - my ($self) = @_; - if (!$self->{authorizer}) { - require Bugzilla::Auth; - $self->{authorizer} = new Bugzilla::Auth(); - } - return $self->{authorizer}; + my ($self) = @_; + if (!$self->{authorizer}) { + require Bugzilla::Auth; + $self->{authorizer} = new Bugzilla::Auth(); + } + return $self->{authorizer}; } # Generate a string to identify the user by name + login if the user @@ -548,144 +554,152 @@ sub authorizer { # See also get_userlist(), which constructs pseudo-Bugzilla::Users, including # the 'identity' value. sub identity { - my $self = shift; + my $self = shift; - return "" unless $self->id; + return "" unless $self->id; - if (!defined $self->{identity}) { - $self->{identity} = - $self->name ? $self->name . " (" . $self->login. ")" : $self->login; - } + if (!defined $self->{identity}) { + $self->{identity} + = $self->name ? $self->name . " (" . $self->login . ")" : $self->login; + } - return $self->{identity}; + return $self->{identity}; } sub nick { - my $self = shift; + my $self = shift; - return "" unless $self->id; + return "" unless $self->id; - if (!defined $self->{nick}) { - # This has the correct result even if the login does not contain an @. - $self->{nick} = (split(/@/, $self->login, 2))[0]; - } + if (!defined $self->{nick}) { - return $self->{nick}; + # This has the correct result even if the login does not contain an @. + $self->{nick} = (split(/@/, $self->login, 2))[0]; + } + + return $self->{nick}; } sub queries { - my $self = shift; - return $self->{queries} if defined $self->{queries}; - return [] unless $self->id; + my $self = shift; + return $self->{queries} if defined $self->{queries}; + return [] unless $self->id; - my $dbh = Bugzilla->dbh; - my $query_ids = $dbh->selectcol_arrayref( - 'SELECT id FROM namedqueries WHERE userid = ?', undef, $self->id); - require Bugzilla::Search::Saved; - $self->{queries} = Bugzilla::Search::Saved->new_from_list($query_ids); + my $dbh = Bugzilla->dbh; + my $query_ids + = $dbh->selectcol_arrayref('SELECT id FROM namedqueries WHERE userid = ?', + undef, $self->id); + require Bugzilla::Search::Saved; + $self->{queries} = Bugzilla::Search::Saved->new_from_list($query_ids); - # We preload link_in_footer from here as this information is always requested. - # This only works if the user object represents the current logged in user. - Bugzilla::Search::Saved::preload($self->{queries}) if $self->id == Bugzilla->user->id; + # We preload link_in_footer from here as this information is always requested. + # This only works if the user object represents the current logged in user. + Bugzilla::Search::Saved::preload($self->{queries}) + if $self->id == Bugzilla->user->id; - return $self->{queries}; + return $self->{queries}; } sub queries_subscribed { - my $self = shift; - return $self->{queries_subscribed} if defined $self->{queries_subscribed}; - return [] unless $self->id; - - # Exclude the user's own queries. - my @my_query_ids = map($_->id, @{$self->queries}); - my $query_id_string = join(',', @my_query_ids) || '-1'; - - # Only show subscriptions that we can still actually see. If a - # user changes the shared group of a query, our subscription - # will remain but we won't have access to the query anymore. - my $subscribed_query_ids = Bugzilla->dbh->selectcol_arrayref( - "SELECT lif.namedquery_id + my $self = shift; + return $self->{queries_subscribed} if defined $self->{queries_subscribed}; + return [] unless $self->id; + + # Exclude the user's own queries. + my @my_query_ids = map($_->id, @{$self->queries}); + my $query_id_string = join(',', @my_query_ids) || '-1'; + + # Only show subscriptions that we can still actually see. If a + # user changes the shared group of a query, our subscription + # will remain but we won't have access to the query anymore. + my $subscribed_query_ids = Bugzilla->dbh->selectcol_arrayref( + "SELECT lif.namedquery_id FROM namedqueries_link_in_footer lif INNER JOIN namedquery_group_map ngm ON ngm.namedquery_id = lif.namedquery_id WHERE lif.user_id = ? AND lif.namedquery_id NOT IN ($query_id_string) - AND " . $self->groups_in_sql, - undef, $self->id); - require Bugzilla::Search::Saved; - $self->{queries_subscribed} = - Bugzilla::Search::Saved->new_from_list($subscribed_query_ids); - return $self->{queries_subscribed}; + AND " . $self->groups_in_sql, undef, $self->id + ); + require Bugzilla::Search::Saved; + $self->{queries_subscribed} + = Bugzilla::Search::Saved->new_from_list($subscribed_query_ids); + return $self->{queries_subscribed}; } sub queries_available { - my $self = shift; - return $self->{queries_available} if defined $self->{queries_available}; - return [] unless $self->id; + my $self = shift; + return $self->{queries_available} if defined $self->{queries_available}; + return [] unless $self->id; - # Exclude the user's own queries. - my @my_query_ids = map($_->id, @{$self->queries}); - my $query_id_string = join(',', @my_query_ids) || '-1'; + # Exclude the user's own queries. + my @my_query_ids = map($_->id, @{$self->queries}); + my $query_id_string = join(',', @my_query_ids) || '-1'; - my $avail_query_ids = Bugzilla->dbh->selectcol_arrayref( - 'SELECT namedquery_id FROM namedquery_group_map - WHERE ' . $self->groups_in_sql . " - AND namedquery_id NOT IN ($query_id_string)"); - require Bugzilla::Search::Saved; - $self->{queries_available} = - Bugzilla::Search::Saved->new_from_list($avail_query_ids); - return $self->{queries_available}; + my $avail_query_ids = Bugzilla->dbh->selectcol_arrayref( + 'SELECT namedquery_id FROM namedquery_group_map + WHERE ' . $self->groups_in_sql . " + AND namedquery_id NOT IN ($query_id_string)" + ); + require Bugzilla::Search::Saved; + $self->{queries_available} + = Bugzilla::Search::Saved->new_from_list($avail_query_ids); + return $self->{queries_available}; } sub tags { - my $self = shift; - my $dbh = Bugzilla->dbh; - - if (!defined $self->{tags}) { - # We must use LEFT JOIN instead of INNER JOIN as we may be - # in the process of inserting a new tag to some bugs, - # in which case there are no bugs with this tag yet. - $self->{tags} = $dbh->selectall_hashref( - 'SELECT name, id, COUNT(bug_id) AS bug_count + my $self = shift; + my $dbh = Bugzilla->dbh; + + if (!defined $self->{tags}) { + + # We must use LEFT JOIN instead of INNER JOIN as we may be + # in the process of inserting a new tag to some bugs, + # in which case there are no bugs with this tag yet. + $self->{tags} = $dbh->selectall_hashref( + 'SELECT name, id, COUNT(bug_id) AS bug_count FROM tag LEFT JOIN bug_tag ON bug_tag.tag_id = tag.id - WHERE user_id = ? ' . $dbh->sql_group_by('id', 'name'), - 'name', undef, $self->id); - } - return $self->{tags}; + WHERE user_id = ? ' . $dbh->sql_group_by('id', 'name'), 'name', undef, + $self->id + ); + } + return $self->{tags}; } sub bugs_ignored { - my ($self) = @_; - my $dbh = Bugzilla->dbh; - if (!defined $self->{'bugs_ignored'}) { - $self->{'bugs_ignored'} = $dbh->selectall_arrayref( - 'SELECT bugs.bug_id AS id, + my ($self) = @_; + my $dbh = Bugzilla->dbh; + if (!defined $self->{'bugs_ignored'}) { + $self->{'bugs_ignored'} = $dbh->selectall_arrayref( + 'SELECT bugs.bug_id AS id, bugs.bug_status AS status, bugs.short_desc AS summary FROM bugs INNER JOIN email_bug_ignore ON bugs.bug_id = email_bug_ignore.bug_id - WHERE user_id = ?', - { Slice => {} }, $self->id); - # Go ahead and load these into the visible bugs cache - # to speed up can_see_bug checks later - $self->visible_bugs([ map { $_->{'id'} } @{ $self->{'bugs_ignored'} } ]); - } - return $self->{'bugs_ignored'}; + WHERE user_id = ?', {Slice => {}}, $self->id + ); + + # Go ahead and load these into the visible bugs cache + # to speed up can_see_bug checks later + $self->visible_bugs([map { $_->{'id'} } @{$self->{'bugs_ignored'}}]); + } + return $self->{'bugs_ignored'}; } sub is_bug_ignored { - my ($self, $bug_id) = @_; - return (grep {$_->{'id'} == $bug_id} @{$self->bugs_ignored}) ? 1 : 0; + my ($self, $bug_id) = @_; + return (grep { $_->{'id'} == $bug_id } @{$self->bugs_ignored}) ? 1 : 0; } sub use_markdown { - my ($self, $comment) = @_; - return Bugzilla->feature('markdown') - && $self->settings->{use_markdown}->{is_enabled} - && $self->settings->{use_markdown}->{value} eq 'on' - && (!defined $comment || $comment->is_markdown); + my ($self, $comment) = @_; + return + Bugzilla->feature('markdown') + && $self->settings->{use_markdown}->{is_enabled} + && $self->settings->{use_markdown}->{value} eq 'on' + && (!defined $comment || $comment->is_markdown); } ########################## @@ -693,309 +707,316 @@ sub use_markdown { ########################## sub recent_searches { - my $self = shift; - $self->{recent_searches} ||= - Bugzilla::Search::Recent->match({ user_id => $self->id }); - return $self->{recent_searches}; + my $self = shift; + $self->{recent_searches} + ||= Bugzilla::Search::Recent->match({user_id => $self->id}); + return $self->{recent_searches}; } sub recent_search_containing { - my ($self, $bug_id) = @_; - my $searches = $self->recent_searches; + my ($self, $bug_id) = @_; + my $searches = $self->recent_searches; - foreach my $search (@$searches) { - return $search if grep($_ == $bug_id, @{ $search->bug_list }); - } + foreach my $search (@$searches) { + return $search if grep($_ == $bug_id, @{$search->bug_list}); + } - return undef; + return undef; } sub recent_search_for { - my ($self, $bug) = @_; - my $params = Bugzilla->input_params; - my $cgi = Bugzilla->cgi; - - if ($self->id) { - # First see if there's a list_id parameter in the query string. - my $list_id = $params->{list_id}; - if (!$list_id) { - # If not, check for "list_id" in the query string of the referer. - my $referer = $cgi->referer; - if ($referer) { - my $uri = URI->new($referer); - if ($uri->path =~ /buglist\.cgi$/) { - $list_id = $uri->query_param('list_id') - || $uri->query_param('regetlastlist'); - } - } + my ($self, $bug) = @_; + my $params = Bugzilla->input_params; + my $cgi = Bugzilla->cgi; + + if ($self->id) { + + # First see if there's a list_id parameter in the query string. + my $list_id = $params->{list_id}; + if (!$list_id) { + + # If not, check for "list_id" in the query string of the referer. + my $referer = $cgi->referer; + if ($referer) { + my $uri = URI->new($referer); + if ($uri->path =~ /buglist\.cgi$/) { + $list_id = $uri->query_param('list_id') || $uri->query_param('regetlastlist'); } + } + } - if ($list_id && $list_id ne 'cookie') { - # If we got a bad list_id (either some other user's or an expired - # one) don't crash, just don't return that list. - my $search = Bugzilla::Search::Recent->check_quietly( - { id => $list_id }); - return $search if $search; - } + if ($list_id && $list_id ne 'cookie') { - # If there's no list_id, see if the current bug's id is contained - # in any of the user's saved lists. - my $search = $self->recent_search_containing($bug->id); - return $search if $search; + # If we got a bad list_id (either some other user's or an expired + # one) don't crash, just don't return that list. + my $search = Bugzilla::Search::Recent->check_quietly({id => $list_id}); + return $search if $search; } - # Finally (or always, if we're logged out), if there's a BUGLIST cookie - # and the selected bug is in the list, then return the cookie as a fake - # Search::Recent object. - if (my $list = $cgi->cookie('BUGLIST')) { - # Also split on colons, which was used as a separator in old cookies. - my @bug_ids = split(/[:-]/, $list); - if (grep { $_ == $bug->id } @bug_ids) { - my $search = Bugzilla::Search::Recent->new_from_cookie(\@bug_ids); - return $search; - } + # If there's no list_id, see if the current bug's id is contained + # in any of the user's saved lists. + my $search = $self->recent_search_containing($bug->id); + return $search if $search; + } + + # Finally (or always, if we're logged out), if there's a BUGLIST cookie + # and the selected bug is in the list, then return the cookie as a fake + # Search::Recent object. + if (my $list = $cgi->cookie('BUGLIST')) { + + # Also split on colons, which was used as a separator in old cookies. + my @bug_ids = split(/[:-]/, $list); + if (grep { $_ == $bug->id } @bug_ids) { + my $search = Bugzilla::Search::Recent->new_from_cookie(\@bug_ids); + return $search; } + } - return undef; + return undef; } sub save_last_search { - my ($self, $params) = @_; - my ($bug_ids, $order, $vars, $list_id) = - @$params{qw(bugs order vars list_id)}; - - my $cgi = Bugzilla->cgi; - if ($order) { - $cgi->send_cookie(-name => 'LASTORDER', - -value => $order, - -expires => 'Fri, 01-Jan-2038 00:00:00 GMT'); - } + my ($self, $params) = @_; + my ($bug_ids, $order, $vars, $list_id) = @$params{qw(bugs order vars list_id)}; + + my $cgi = Bugzilla->cgi; + if ($order) { + $cgi->send_cookie( + -name => 'LASTORDER', + -value => $order, + -expires => 'Fri, 01-Jan-2038 00:00:00 GMT' + ); + } - return if !@$bug_ids; - - my $search; - if ($self->id) { - on_main_db { - if ($list_id) { - $search = Bugzilla::Search::Recent->check_quietly({ id => $list_id }); - } - - if ($search) { - if (join(',', @{$search->bug_list}) ne join(',', @$bug_ids)) { - $search->set_bug_list($bug_ids); - } - if (!$search->list_order || $order ne $search->list_order) { - $search->set_list_order($order); - } - $search->update(); - } - else { - # If we already have an existing search with a totally - # identical bug list, then don't create a new one. This - # prevents people from writing over their whole - # recent-search list by just refreshing a saved search - # (which doesn't have list_id in the header) over and over. - my $list_string = join(',', @$bug_ids); - my $existing_search = Bugzilla::Search::Recent->match({ - user_id => $self->id, bug_list => $list_string }); - - if (!scalar(@$existing_search)) { - $search = Bugzilla::Search::Recent->create({ - user_id => $self->id, - bug_list => $bug_ids, - list_order => $order }); - } - else { - $search = $existing_search->[0]; - } - } - }; - delete $self->{recent_searches}; - } - # Logged-out users use a cookie to store a single last search. We don't - # override that cookie with the logged-in user's latest search, because - # if they did one search while logged out and another while logged in, - # they may still want to navigate through the search they made while - # logged out. - else { - my $bug_list = join('-', @$bug_ids); - if (length($bug_list) < 4000) { - $cgi->send_cookie(-name => 'BUGLIST', - -value => $bug_list, - -expires => 'Fri, 01-Jan-2038 00:00:00 GMT'); + return if !@$bug_ids; + + my $search; + if ($self->id) { + on_main_db { + if ($list_id) { + $search = Bugzilla::Search::Recent->check_quietly({id => $list_id}); + } + + if ($search) { + if (join(',', @{$search->bug_list}) ne join(',', @$bug_ids)) { + $search->set_bug_list($bug_ids); + } + if (!$search->list_order || $order ne $search->list_order) { + $search->set_list_order($order); + } + $search->update(); + } + else { + # If we already have an existing search with a totally + # identical bug list, then don't create a new one. This + # prevents people from writing over their whole + # recent-search list by just refreshing a saved search + # (which doesn't have list_id in the header) over and over. + my $list_string = join(',', @$bug_ids); + my $existing_search = Bugzilla::Search::Recent->match( + {user_id => $self->id, bug_list => $list_string}); + + if (!scalar(@$existing_search)) { + $search + = Bugzilla::Search::Recent->create({ + user_id => $self->id, bug_list => $bug_ids, list_order => $order + }); } else { - $cgi->remove_cookie('BUGLIST'); - $vars->{'toolong'} = 1; + $search = $existing_search->[0]; } + } + }; + delete $self->{recent_searches}; + } + + # Logged-out users use a cookie to store a single last search. We don't + # override that cookie with the logged-in user's latest search, because + # if they did one search while logged out and another while logged in, + # they may still want to navigate through the search they made while + # logged out. + else { + my $bug_list = join('-', @$bug_ids); + if (length($bug_list) < 4000) { + $cgi->send_cookie( + -name => 'BUGLIST', + -value => $bug_list, + -expires => 'Fri, 01-Jan-2038 00:00:00 GMT' + ); } - return $search; + else { + $cgi->remove_cookie('BUGLIST'); + $vars->{'toolong'} = 1; + } + } + return $search; } sub reports { - my $self = shift; - return $self->{reports} if defined $self->{reports}; - return [] unless $self->id; + my $self = shift; + return $self->{reports} if defined $self->{reports}; + return [] unless $self->id; - my $dbh = Bugzilla->dbh; - my $report_ids = $dbh->selectcol_arrayref( - 'SELECT id FROM reports WHERE user_id = ?', undef, $self->id); - require Bugzilla::Report; - $self->{reports} = Bugzilla::Report->new_from_list($report_ids); - return $self->{reports}; + my $dbh = Bugzilla->dbh; + my $report_ids + = $dbh->selectcol_arrayref('SELECT id FROM reports WHERE user_id = ?', + undef, $self->id); + require Bugzilla::Report; + $self->{reports} = Bugzilla::Report->new_from_list($report_ids); + return $self->{reports}; } sub flush_reports_cache { - my $self = shift; + my $self = shift; - delete $self->{reports}; + delete $self->{reports}; } sub settings { - my ($self) = @_; + my ($self) = @_; - return $self->{'settings'} if (defined $self->{'settings'}); + return $self->{'settings'} if (defined $self->{'settings'}); - # IF the user is logged in - # THEN get the user's settings - # ELSE get default settings - if ($self->id) { - $self->{'settings'} = get_all_settings($self->id); - } else { - $self->{'settings'} = get_defaults(); - } + # IF the user is logged in + # THEN get the user's settings + # ELSE get default settings + if ($self->id) { + $self->{'settings'} = get_all_settings($self->id); + } + else { + $self->{'settings'} = get_defaults(); + } - return $self->{'settings'}; + return $self->{'settings'}; } sub setting { - my ($self, $name) = @_; - return $self->settings->{$name}->{'value'}; + my ($self, $name) = @_; + return $self->settings->{$name}->{'value'}; } sub timezone { - my $self = shift; + my $self = shift; - if (!defined $self->{timezone}) { - my $tz = $self->setting('timezone'); - if ($tz eq 'local') { - # The user wants the local timezone of the server. - $self->{timezone} = Bugzilla->local_timezone; - } - else { - $self->{timezone} = DateTime::TimeZone->new(name => $tz); - } + if (!defined $self->{timezone}) { + my $tz = $self->setting('timezone'); + if ($tz eq 'local') { + + # The user wants the local timezone of the server. + $self->{timezone} = Bugzilla->local_timezone; + } + else { + $self->{timezone} = DateTime::TimeZone->new(name => $tz); } - return $self->{timezone}; + } + return $self->{timezone}; } sub flush_queries_cache { - my $self = shift; + my $self = shift; - delete $self->{queries}; - delete $self->{queries_subscribed}; - delete $self->{queries_available}; + delete $self->{queries}; + delete $self->{queries_subscribed}; + delete $self->{queries_available}; } sub groups { - my $self = shift; + my $self = shift; - return $self->{groups} if defined $self->{groups}; - return [] unless $self->id; + return $self->{groups} if defined $self->{groups}; + return [] unless $self->id; - my $user_groups_key = "user_groups." . $self->id; - my $groups = Bugzilla->memcached->get_config({ - key => $user_groups_key - }); + my $user_groups_key = "user_groups." . $self->id; + my $groups = Bugzilla->memcached->get_config({key => $user_groups_key}); - if (!$groups) { - my $dbh = Bugzilla->dbh; - my $groups_to_check = $dbh->selectcol_arrayref( - "SELECT DISTINCT group_id + if (!$groups) { + my $dbh = Bugzilla->dbh; + my $groups_to_check = $dbh->selectcol_arrayref( + "SELECT DISTINCT group_id FROM user_group_map - WHERE user_id = ? AND isbless = 0", undef, $self->id); - - my $grant_type_key = 'group_grant_type_' . GROUP_MEMBERSHIP; - my $membership_rows = Bugzilla->memcached->get_config({ - key => $grant_type_key, - }); - if (!$membership_rows) { - $membership_rows = $dbh->selectall_arrayref( - "SELECT DISTINCT grantor_id, member_id + WHERE user_id = ? AND isbless = 0", undef, $self->id + ); + + my $grant_type_key = 'group_grant_type_' . GROUP_MEMBERSHIP; + my $membership_rows + = Bugzilla->memcached->get_config({key => $grant_type_key,}); + if (!$membership_rows) { + $membership_rows = $dbh->selectall_arrayref( + "SELECT DISTINCT grantor_id, member_id FROM group_group_map - WHERE grant_type = " . GROUP_MEMBERSHIP); - Bugzilla->memcached->set_config({ - key => $grant_type_key, - data => $membership_rows, - }); - } + WHERE grant_type = " . GROUP_MEMBERSHIP + ); + Bugzilla->memcached->set_config({ + key => $grant_type_key, data => $membership_rows, + }); + } - my %group_membership; - foreach my $row (@$membership_rows) { - my ($grantor_id, $member_id) = @$row; - push (@{ $group_membership{$member_id} }, $grantor_id); - } + my %group_membership; + foreach my $row (@$membership_rows) { + my ($grantor_id, $member_id) = @$row; + push(@{$group_membership{$member_id}}, $grantor_id); + } - # Let's walk the groups hierarchy tree (using FIFO) - # On the first iteration it's pre-filled with direct groups - # membership. Later on, each group can add its own members into the - # FIFO. Circular dependencies are eliminated by checking - # $checked_groups{$member_id} hash values. - # As a result, %groups will have all the groups we are the member of. - my %checked_groups; - my %groups; - while (scalar(@$groups_to_check) > 0) { - # Pop the head group from FIFO - my $member_id = shift @$groups_to_check; - - # Skip the group if we have already checked it - if (!$checked_groups{$member_id}) { - # Mark group as checked - $checked_groups{$member_id} = 1; - - # Add all its members to the FIFO check list - # %group_membership contains arrays of group members - # for all groups. Accessible by group number. - my $members = $group_membership{$member_id}; - my @new_to_check = grep(!$checked_groups{$_}, @$members); - push(@$groups_to_check, @new_to_check); - - $groups{$member_id} = 1; - } - } - $groups = [ keys %groups ]; + # Let's walk the groups hierarchy tree (using FIFO) + # On the first iteration it's pre-filled with direct groups + # membership. Later on, each group can add its own members into the + # FIFO. Circular dependencies are eliminated by checking + # $checked_groups{$member_id} hash values. + # As a result, %groups will have all the groups we are the member of. + my %checked_groups; + my %groups; + while (scalar(@$groups_to_check) > 0) { + + # Pop the head group from FIFO + my $member_id = shift @$groups_to_check; - Bugzilla->memcached->set_config({ - key => $user_groups_key, - data => $groups, - }); + # Skip the group if we have already checked it + if (!$checked_groups{$member_id}) { + + # Mark group as checked + $checked_groups{$member_id} = 1; + + # Add all its members to the FIFO check list + # %group_membership contains arrays of group members + # for all groups. Accessible by group number. + my $members = $group_membership{$member_id}; + my @new_to_check = grep(!$checked_groups{$_}, @$members); + push(@$groups_to_check, @new_to_check); + + $groups{$member_id} = 1; + } } + $groups = [keys %groups]; - $self->{groups} = Bugzilla::Group->new_from_list($groups); - return $self->{groups}; + Bugzilla->memcached->set_config({key => $user_groups_key, data => $groups,}); + } + + $self->{groups} = Bugzilla::Group->new_from_list($groups); + return $self->{groups}; } sub last_visited { - my ($self, $ids) = @_; + my ($self, $ids) = @_; - return Bugzilla::BugUserLastVisit->match({ user_id => $self->id, - $ids ? ( bug_id => $ids ) : () }); + return Bugzilla::BugUserLastVisit->match({ + user_id => $self->id, $ids ? (bug_id => $ids) : () + }); } sub is_involved_in_bug { - my ($self, $bug) = @_; - my $user_id = $self->id; - my $user_login = $self->login; + my ($self, $bug) = @_; + my $user_id = $self->id; + my $user_login = $self->login; - return unless $user_id; - return 1 if $user_id == $bug->assigned_to->id; - return 1 if $user_id == $bug->reporter->id; + return unless $user_id; + return 1 if $user_id == $bug->assigned_to->id; + return 1 if $user_id == $bug->reporter->id; - if (Bugzilla->params->{'useqacontact'} and $bug->qa_contact) { - return 1 if $user_id == $bug->qa_contact->id; - } + if (Bugzilla->params->{'useqacontact'} and $bug->qa_contact) { + return 1 if $user_id == $bug->qa_contact->id; + } - return any { $user_login eq $_ } @{ $bug->cc }; + return any { $user_login eq $_ } @{$bug->cc}; } # It turns out that calling ->id on objects a few hundred thousand @@ -1003,296 +1024,311 @@ sub is_involved_in_bug { # when profiling xt/search.t.) So we cache the group ids separately from # groups for functions that need the group ids. sub _group_ids { - my ($self) = @_; - $self->{group_ids} ||= [map { $_->id } @{ $self->groups }]; - return $self->{group_ids}; + my ($self) = @_; + $self->{group_ids} ||= [map { $_->id } @{$self->groups}]; + return $self->{group_ids}; } sub groups_as_string { - my $self = shift; - my $ids = $self->_group_ids; - return scalar(@$ids) ? join(',', @$ids) : '-1'; + my $self = shift; + my $ids = $self->_group_ids; + return scalar(@$ids) ? join(',', @$ids) : '-1'; } sub groups_in_sql { - my ($self, $field) = @_; - $field ||= 'group_id'; - my $ids = $self->_group_ids; - $ids = [-1] if !scalar @$ids; - return Bugzilla->dbh->sql_in($field, $ids); + my ($self, $field) = @_; + $field ||= 'group_id'; + my $ids = $self->_group_ids; + $ids = [-1] if !scalar @$ids; + return Bugzilla->dbh->sql_in($field, $ids); } sub bless_groups { - my $self = shift; + my $self = shift; - return $self->{'bless_groups'} if defined $self->{'bless_groups'}; - return [] unless $self->id; + return $self->{'bless_groups'} if defined $self->{'bless_groups'}; + return [] unless $self->id; - if ($self->in_group('editusers')) { - # Users having editusers permissions may bless all groups. - $self->{'bless_groups'} = [Bugzilla::Group->get_all]; - return $self->{'bless_groups'}; - } + if ($self->in_group('editusers')) { - if (Bugzilla->params->{usevisibilitygroups} - && !@{ $self->visible_groups_inherited }) { - return []; - } + # Users having editusers permissions may bless all groups. + $self->{'bless_groups'} = [Bugzilla::Group->get_all]; + return $self->{'bless_groups'}; + } + + if (Bugzilla->params->{usevisibilitygroups} + && !@{$self->visible_groups_inherited}) + { + return []; + } - my $dbh = Bugzilla->dbh; + my $dbh = Bugzilla->dbh; - # Get all groups for the user where they have direct bless privileges. - my $query = " + # Get all groups for the user where they have direct bless privileges. + my $query = " SELECT DISTINCT group_id FROM user_group_map WHERE user_id = ? AND isbless = 1"; - if (Bugzilla->params->{usevisibilitygroups}) { - $query .= " AND " - . $dbh->sql_in('group_id', $self->visible_groups_inherited); - } - - # Get all groups for the user where they are a member of a group that - # inherits bless privs. - my @group_ids = map { $_->id } @{ $self->groups }; - if (@group_ids) { - $query .= " + if (Bugzilla->params->{usevisibilitygroups}) { + $query .= " AND " . $dbh->sql_in('group_id', $self->visible_groups_inherited); + } + + # Get all groups for the user where they are a member of a group that + # inherits bless privs. + my @group_ids = map { $_->id } @{$self->groups}; + if (@group_ids) { + $query .= " UNION SELECT DISTINCT grantor_id FROM group_group_map WHERE grant_type = " . GROUP_BLESS . " AND " . $dbh->sql_in('member_id', \@group_ids); - if (Bugzilla->params->{usevisibilitygroups}) { - $query .= " AND " - . $dbh->sql_in('grantor_id', $self->visible_groups_inherited); - } + if (Bugzilla->params->{usevisibilitygroups}) { + $query .= " AND " . $dbh->sql_in('grantor_id', $self->visible_groups_inherited); } + } - my $ids = $dbh->selectcol_arrayref($query, undef, $self->id); - return $self->{bless_groups} = Bugzilla::Group->new_from_list($ids); + my $ids = $dbh->selectcol_arrayref($query, undef, $self->id); + return $self->{bless_groups} = Bugzilla::Group->new_from_list($ids); } sub in_group { - my ($self, $group, $product_id) = @_; - $group = $group->name if blessed $group; - if (scalar grep($_->name eq $group, @{ $self->groups })) { - return 1; - } - elsif ($product_id && detaint_natural($product_id)) { - # Make sure $group exists on a per-product basis. - return 0 unless (grep {$_ eq $group} PER_PRODUCT_PRIVILEGES); - - $self->{"product_$product_id"} = {} unless exists $self->{"product_$product_id"}; - if (!defined $self->{"product_$product_id"}->{$group}) { - my $dbh = Bugzilla->dbh; - my $in_group = $dbh->selectrow_array( - "SELECT 1 + my ($self, $group, $product_id) = @_; + $group = $group->name if blessed $group; + if (scalar grep($_->name eq $group, @{$self->groups})) { + return 1; + } + elsif ($product_id && detaint_natural($product_id)) { + + # Make sure $group exists on a per-product basis. + return 0 unless (grep { $_ eq $group } PER_PRODUCT_PRIVILEGES); + + $self->{"product_$product_id"} = {} + unless exists $self->{"product_$product_id"}; + if (!defined $self->{"product_$product_id"}->{$group}) { + my $dbh = Bugzilla->dbh; + my $in_group = $dbh->selectrow_array( + "SELECT 1 FROM group_control_map WHERE product_id = ? AND $group != 0 - AND " . $self->groups_in_sql . ' ' . - $dbh->sql_limit(1), - undef, $product_id); + AND " + . $self->groups_in_sql . ' ' . $dbh->sql_limit(1), undef, $product_id + ); - $self->{"product_$product_id"}->{$group} = $in_group ? 1 : 0; - } - return $self->{"product_$product_id"}->{$group}; + $self->{"product_$product_id"}->{$group} = $in_group ? 1 : 0; } - # If we come here, then the user is not in the requested group. - return 0; + return $self->{"product_$product_id"}->{$group}; + } + + # If we come here, then the user is not in the requested group. + return 0; } sub in_group_id { - my ($self, $id) = @_; - return grep($_->id == $id, @{ $self->groups }) ? 1 : 0; + my ($self, $id) = @_; + return grep($_->id == $id, @{$self->groups}) ? 1 : 0; } # This is a helper to get all groups which have an icon to be displayed # besides the name of the commenter. sub groups_with_icon { - my $self = shift; + my $self = shift; - return $self->{groups_with_icon} //= [grep { $_->icon_url } @{ $self->groups }]; + return $self->{groups_with_icon} //= [grep { $_->icon_url } @{$self->groups}]; } sub get_products_by_permission { - my ($self, $group) = @_; - # Make sure $group exists on a per-product basis. - return [] unless (grep {$_ eq $group} PER_PRODUCT_PRIVILEGES); + my ($self, $group) = @_; + + # Make sure $group exists on a per-product basis. + return [] unless (grep { $_ eq $group } PER_PRODUCT_PRIVILEGES); - my $product_ids = Bugzilla->dbh->selectcol_arrayref( - "SELECT DISTINCT product_id + my $product_ids = Bugzilla->dbh->selectcol_arrayref( + "SELECT DISTINCT product_id FROM group_control_map WHERE $group != 0 - AND " . $self->groups_in_sql); + AND " . $self->groups_in_sql + ); - # No need to go further if the user has no "special" privs. - return [] unless scalar(@$product_ids); - my %product_map = map { $_ => 1 } @$product_ids; + # No need to go further if the user has no "special" privs. + return [] unless scalar(@$product_ids); + my %product_map = map { $_ => 1 } @$product_ids; - # We will restrict the list to products the user can see. - my $selectable_products = $self->get_selectable_products; - my @products = grep { $product_map{$_->id} } @$selectable_products; - return \@products; + # We will restrict the list to products the user can see. + my $selectable_products = $self->get_selectable_products; + my @products = grep { $product_map{$_->id} } @$selectable_products; + return \@products; } sub can_see_user { - my ($self, $otherUser) = @_; - my $query; + my ($self, $otherUser) = @_; + my $query; - if (Bugzilla->params->{'usevisibilitygroups'}) { - # If the user can see no groups, then no users are visible either. - my $visibleGroups = $self->visible_groups_as_string() || return 0; - $query = qq{SELECT COUNT(DISTINCT userid) + if (Bugzilla->params->{'usevisibilitygroups'}) { + + # If the user can see no groups, then no users are visible either. + my $visibleGroups = $self->visible_groups_as_string() || return 0; + $query = qq{SELECT COUNT(DISTINCT userid) FROM profiles, user_group_map WHERE userid = ? AND user_id = userid AND isbless = 0 AND group_id IN ($visibleGroups) }; - } else { - $query = qq{SELECT COUNT(userid) + } + else { + $query = qq{SELECT COUNT(userid) FROM profiles WHERE userid = ? }; - } - return Bugzilla->dbh->selectrow_array($query, undef, $otherUser->id); + } + return Bugzilla->dbh->selectrow_array($query, undef, $otherUser->id); } sub can_edit_product { - my ($self, $prod_id) = @_; - my $dbh = Bugzilla->dbh; + my ($self, $prod_id) = @_; + my $dbh = Bugzilla->dbh; - if (Bugzilla->params->{'or_groups'}) { - my $groups = $self->groups_as_string; - # For or-groups, we check if there are any can_edit groups for the - # product, and if the user is in any of them. If there are none or - # the user is in at least one of them, they can edit the product - my ($cnt_can_edit, $cnt_group_member) = $dbh->selectrow_array( - "SELECT SUM(p.cnt_can_edit), + if (Bugzilla->params->{'or_groups'}) { + my $groups = $self->groups_as_string; + + # For or-groups, we check if there are any can_edit groups for the + # product, and if the user is in any of them. If there are none or + # the user is in at least one of them, they can edit the product + my ($cnt_can_edit, $cnt_group_member) = $dbh->selectrow_array( + "SELECT SUM(p.cnt_can_edit), SUM(p.cnt_group_member) FROM (SELECT CASE WHEN canedit = 1 THEN 1 ELSE 0 END AS cnt_can_edit, CASE WHEN canedit = 1 AND group_id IN ($groups) THEN 1 ELSE 0 END AS cnt_group_member FROM group_control_map - WHERE product_id = $prod_id) AS p"); - return (!$cnt_can_edit or $cnt_group_member); - } - else { - # For and-groups, a user needs to be in all canedit groups. Therefore - # if the user is not in a can_edit group for the product, they cannot - # edit the product. - my $has_external_groups = - $dbh->selectrow_array('SELECT 1 + WHERE product_id = $prod_id) AS p" + ); + return (!$cnt_can_edit or $cnt_group_member); + } + else { + # For and-groups, a user needs to be in all canedit groups. Therefore + # if the user is not in a can_edit group for the product, they cannot + # edit the product. + my $has_external_groups = $dbh->selectrow_array( + 'SELECT 1 FROM group_control_map WHERE product_id = ? AND canedit != 0 - AND group_id NOT IN(' . $self->groups_as_string . ')', - undef, $prod_id); + AND group_id NOT IN(' + . $self->groups_as_string . ')', undef, $prod_id + ); - return !$has_external_groups; - } + return !$has_external_groups; + } } sub can_see_bug { - my ($self, $bug_id) = @_; - return @{ $self->visible_bugs([$bug_id]) } ? 1 : 0; + my ($self, $bug_id) = @_; + return @{$self->visible_bugs([$bug_id])} ? 1 : 0; } sub visible_bugs { - my ($self, $bugs) = @_; - # Allow users to pass in Bug objects and bug ids both. - my @bug_ids = map { blessed $_ ? $_->id : $_ } @$bugs; - - # We only check the visibility of bugs that we haven't - # checked yet. - # Bugzilla::Bug->update automatically removes updated bugs - # from the cache to force them to be checked again. - my $visible_cache = $self->{_visible_bugs_cache} ||= {}; - my @check_ids = grep(!exists $visible_cache->{$_}, @bug_ids); - - if (@check_ids) { - foreach my $id (@check_ids) { - my $orig_id = $id; - detaint_natural($id) - || ThrowCodeError('param_must_be_numeric', { param => $orig_id, - function => 'Bugzilla::User->visible_bugs'}); - } + my ($self, $bugs) = @_; - Bugzilla->params->{'or_groups'} - ? $self->_visible_bugs_check_or(\@check_ids) - : $self->_visible_bugs_check_and(\@check_ids); + # Allow users to pass in Bug objects and bug ids both. + my @bug_ids = map { blessed $_ ? $_->id : $_ } @$bugs; + + # We only check the visibility of bugs that we haven't + # checked yet. + # Bugzilla::Bug->update automatically removes updated bugs + # from the cache to force them to be checked again. + my $visible_cache = $self->{_visible_bugs_cache} ||= {}; + my @check_ids = grep(!exists $visible_cache->{$_}, @bug_ids); + + if (@check_ids) { + foreach my $id (@check_ids) { + my $orig_id = $id; + detaint_natural($id) + || ThrowCodeError('param_must_be_numeric', + {param => $orig_id, function => 'Bugzilla::User->visible_bugs'}); } - return [grep { $visible_cache->{blessed $_ ? $_->id : $_} } @$bugs]; + Bugzilla->params->{'or_groups'} + ? $self->_visible_bugs_check_or(\@check_ids) + : $self->_visible_bugs_check_and(\@check_ids); + } + + return [grep { $visible_cache->{blessed $_ ? $_->id : $_} } @$bugs]; } sub _visible_bugs_check_or { - my ($self, $check_ids) = @_; - my $visible_cache = $self->{_visible_bugs_cache}; - my $dbh = Bugzilla->dbh; - my $user_id = $self->id; - - my $sth; - # Speed up the can_see_bug case. - if (scalar(@$check_ids) == 1) { - $sth = $self->{_sth_one_visible_bug}; - } - my $query = qq{ + my ($self, $check_ids) = @_; + my $visible_cache = $self->{_visible_bugs_cache}; + my $dbh = Bugzilla->dbh; + my $user_id = $self->id; + + my $sth; + + # Speed up the can_see_bug case. + if (scalar(@$check_ids) == 1) { + $sth = $self->{_sth_one_visible_bug}; + } + my $query = qq{ SELECT DISTINCT bugs.bug_id FROM bugs LEFT JOIN bug_group_map AS security_map ON bugs.bug_id = security_map.bug_id LEFT JOIN cc AS security_cc ON bugs.bug_id = security_cc.bug_id AND security_cc.who = $user_id WHERE bugs.bug_id IN (} . join(',', ('?') x @$check_ids) . qq{) - AND ((security_map.group_id IS NULL OR security_map.group_id IN (} . $self->groups_as_string . qq{)) + AND ((security_map.group_id IS NULL OR security_map.group_id IN (} + . $self->groups_as_string . qq{)) OR (bugs.reporter_accessible = 1 AND bugs.reporter = $user_id) OR (bugs.cclist_accessible = 1 AND security_cc.who IS NOT NULL) OR bugs.assigned_to = $user_id }; - if (Bugzilla->params->{'useqacontact'}) { - $query .= " OR bugs.qa_contact = $user_id"; - } - $query .= ')'; + if (Bugzilla->params->{'useqacontact'}) { + $query .= " OR bugs.qa_contact = $user_id"; + } + $query .= ')'; - $sth ||= $dbh->prepare($query); - if (scalar(@$check_ids) == 1) { - $self->{_sth_one_visible_bug} = $sth; - } + $sth ||= $dbh->prepare($query); + if (scalar(@$check_ids) == 1) { + $self->{_sth_one_visible_bug} = $sth; + } - # Set all bugs as non visible - foreach my $bug_id (@$check_ids) { - $visible_cache->{$bug_id} = 0; - } + # Set all bugs as non visible + foreach my $bug_id (@$check_ids) { + $visible_cache->{$bug_id} = 0; + } - # Now get the bugs the user can see - my $visible_bug_ids = $dbh->selectcol_arrayref($sth, undef, @$check_ids); - foreach my $bug_id (@$visible_bug_ids) { - $visible_cache->{$bug_id} = 1; - } + # Now get the bugs the user can see + my $visible_bug_ids = $dbh->selectcol_arrayref($sth, undef, @$check_ids); + foreach my $bug_id (@$visible_bug_ids) { + $visible_cache->{$bug_id} = 1; + } } sub _visible_bugs_check_and { - my ($self, $check_ids) = @_; - my $visible_cache = $self->{_visible_bugs_cache}; - my $dbh = Bugzilla->dbh; - my $user_id = $self->id; - - my $sth; - # Speed up the can_see_bug case. - if (scalar(@$check_ids) == 1) { - $sth = $self->{_sth_one_visible_bug}; - } - $sth ||= $dbh->prepare( - # This checks for groups that the bug is in that the user - # *isn't* in. Then, in the Perl code below, we check if - # the user can otherwise access the bug (for example, by being - # the assignee or QA Contact). - # - # The DISTINCT exists because the bug could be in *several* - # groups that the user isn't in, but they will all return the - # same result for bug_group_map.bug_id (so DISTINCT filters - # out duplicate rows). - "SELECT DISTINCT bugs.bug_id, reporter, assigned_to, qa_contact, + my ($self, $check_ids) = @_; + my $visible_cache = $self->{_visible_bugs_cache}; + my $dbh = Bugzilla->dbh; + my $user_id = $self->id; + + my $sth; + + # Speed up the can_see_bug case. + if (scalar(@$check_ids) == 1) { + $sth = $self->{_sth_one_visible_bug}; + } + $sth ||= $dbh->prepare( + + # This checks for groups that the bug is in that the user + # *isn't* in. Then, in the Perl code below, we check if + # the user can otherwise access the bug (for example, by being + # the assignee or QA Contact). + # + # The DISTINCT exists because the bug could be in *several* + # groups that the user isn't in, but they will all return the + # same result for bug_group_map.bug_id (so DISTINCT filters + # out duplicate rows). + "SELECT DISTINCT bugs.bug_id, reporter, assigned_to, qa_contact, reporter_accessible, cclist_accessible, cc.who, bug_group_map.bug_id FROM bugs @@ -1302,1063 +1338,1132 @@ sub _visible_bugs_check_and { LEFT JOIN bug_group_map ON bugs.bug_id = bug_group_map.bug_id AND bug_group_map.group_id NOT IN (" - . $self->groups_as_string . ') + . $self->groups_as_string . ') WHERE bugs.bug_id IN (' . join(',', ('?') x @$check_ids) . ') - AND creation_ts IS NOT NULL '); - if (scalar(@$check_ids) == 1) { - $self->{_sth_one_visible_bug} = $sth; - } - - $sth->execute(@$check_ids); - my $use_qa_contact = Bugzilla->params->{'useqacontact'}; - while (my $row = $sth->fetchrow_arrayref) { - my ($bug_id, $reporter, $owner, $qacontact, $reporter_access, - $cclist_access, $isoncclist, $missinggroup) = @$row; - $visible_cache->{$bug_id} ||= - ((($reporter == $user_id) && $reporter_access) - || ($use_qa_contact - && $qacontact && ($qacontact == $user_id)) - || ($owner == $user_id) - || ($isoncclist && $cclist_access) - || !$missinggroup) ? 1 : 0; - } + AND creation_ts IS NOT NULL ' + ); + if (scalar(@$check_ids) == 1) { + $self->{_sth_one_visible_bug} = $sth; + } + + $sth->execute(@$check_ids); + my $use_qa_contact = Bugzilla->params->{'useqacontact'}; + while (my $row = $sth->fetchrow_arrayref) { + my ($bug_id, $reporter, $owner, $qacontact, $reporter_access, $cclist_access, + $isoncclist, $missinggroup) + = @$row; + $visible_cache->{$bug_id} + ||= ((($reporter == $user_id) && $reporter_access) + || ($use_qa_contact && $qacontact && ($qacontact == $user_id)) + || ($owner == $user_id) + || ($isoncclist && $cclist_access) + || !$missinggroup) ? 1 : 0; + } } sub clear_product_cache { - my $self = shift; - delete $self->{enterable_products}; - delete $self->{selectable_products}; - delete $self->{selectable_classifications}; + my $self = shift; + delete $self->{enterable_products}; + delete $self->{selectable_products}; + delete $self->{selectable_classifications}; } sub can_see_product { - my ($self, $product_name) = @_; + my ($self, $product_name) = @_; - return scalar(grep {$_->name eq $product_name} @{$self->get_selectable_products}); + return + scalar(grep { $_->name eq $product_name } @{$self->get_selectable_products}); } sub get_selectable_products { - my $self = shift; - my $class_id = shift; - my $class_restricted = Bugzilla->params->{'useclassification'} && $class_id; + my $self = shift; + my $class_id = shift; + my $class_restricted = Bugzilla->params->{'useclassification'} && $class_id; - if (!defined $self->{selectable_products}) { - my $query = "SELECT id + if (!defined $self->{selectable_products}) { + my $query = "SELECT id FROM products LEFT JOIN group_control_map ON group_control_map.product_id = products.id - AND group_control_map.membercontrol = " . CONTROLMAPMANDATORY; - - if (Bugzilla->params->{'or_groups'}) { - # Either the user is in at least one of the MANDATORY groups, or - # there are no such groups for the product. - $query .= " WHERE group_id IN (" . $self->groups_as_string . ") + AND group_control_map.membercontrol = " + . CONTROLMAPMANDATORY; + + if (Bugzilla->params->{'or_groups'}) { + + # Either the user is in at least one of the MANDATORY groups, or + # there are no such groups for the product. + $query .= " WHERE group_id IN (" . $self->groups_as_string . ") OR group_id IS NULL"; - } - else { - # There must be no MANDATORY groups that the user is not in. - $query .= " AND group_id NOT IN (" . $self->groups_as_string . ") + } + else { + # There must be no MANDATORY groups that the user is not in. + $query .= " AND group_id NOT IN (" . $self->groups_as_string . ") WHERE group_id IS NULL"; - } - - my $prod_ids = Bugzilla->dbh->selectcol_arrayref($query); - $self->{selectable_products} = Bugzilla::Product->new_from_list($prod_ids); } - # Restrict the list of products to those being in the classification, if any. - if ($class_restricted) { - return [grep {$_->classification_id == $class_id} @{$self->{selectable_products}}]; - } - # If we come here, then we want all selectable products. - return $self->{selectable_products}; + my $prod_ids = Bugzilla->dbh->selectcol_arrayref($query); + $self->{selectable_products} = Bugzilla::Product->new_from_list($prod_ids); + } + + # Restrict the list of products to those being in the classification, if any. + if ($class_restricted) { + return [grep { $_->classification_id == $class_id } + @{$self->{selectable_products}}]; + } + + # If we come here, then we want all selectable products. + return $self->{selectable_products}; } sub get_selectable_classifications { - my ($self) = @_; + my ($self) = @_; - if (!defined $self->{selectable_classifications}) { - my $products = $self->get_selectable_products; - my %class_ids = map { $_->classification_id => 1 } @$products; + if (!defined $self->{selectable_classifications}) { + my $products = $self->get_selectable_products; + my %class_ids = map { $_->classification_id => 1 } @$products; - $self->{selectable_classifications} = Bugzilla::Classification->new_from_list([keys %class_ids]); - } - return $self->{selectable_classifications}; + $self->{selectable_classifications} + = Bugzilla::Classification->new_from_list([keys %class_ids]); + } + return $self->{selectable_classifications}; } sub can_enter_product { - my ($self, $input, $warn) = @_; - my $dbh = Bugzilla->dbh; - $warn ||= 0; - - $input = trim($input) if !ref $input; - if (!defined $input or $input eq '') { - return unless $warn == THROW_ERROR; - ThrowUserError('object_not_specified', - { class => 'Bugzilla::Product' }); - } + my ($self, $input, $warn) = @_; + my $dbh = Bugzilla->dbh; + $warn ||= 0; - if (!scalar @{ $self->get_enterable_products }) { - return unless $warn == THROW_ERROR; - ThrowUserError('no_products'); - } + $input = trim($input) if !ref $input; + if (!defined $input or $input eq '') { + return unless $warn == THROW_ERROR; + ThrowUserError('object_not_specified', {class => 'Bugzilla::Product'}); + } - my $product = blessed($input) ? $input - : new Bugzilla::Product({ name => $input }); - my $can_enter = - $product && grep($_->name eq $product->name, - @{ $self->get_enterable_products }); + if (!scalar @{$self->get_enterable_products}) { + return unless $warn == THROW_ERROR; + ThrowUserError('no_products'); + } - return $product if $can_enter; + my $product + = blessed($input) ? $input : new Bugzilla::Product({name => $input}); + my $can_enter = $product + && grep($_->name eq $product->name, @{$self->get_enterable_products}); - return 0 unless $warn == THROW_ERROR; + return $product if $can_enter; - # Check why access was denied. These checks are slow, - # but that's fine, because they only happen if we fail. + return 0 unless $warn == THROW_ERROR; - # We don't just use $product->name for error messages, because if it - # changes case from $input, then that's a clue that the product does - # exist but is hidden. - my $name = blessed($input) ? $input->name : $input; + # Check why access was denied. These checks are slow, + # but that's fine, because they only happen if we fail. - # The product could not exist or you could be denied... - if (!$product || !$product->user_has_access($self)) { - ThrowUserError('entry_access_denied', { product => $name }); - } - # It could be closed for bug entry... - elsif (!$product->is_active) { - ThrowUserError('product_disabled', { product => $product }); - } - # It could have no components... - elsif (!@{$product->components} - || !grep { $_->is_active } @{$product->components}) - { - ThrowUserError('missing_component', { product => $product }); - } - # It could have no versions... - elsif (!@{$product->versions} - || !grep { $_->is_active } @{$product->versions}) - { - ThrowUserError ('missing_version', { product => $product }); - } + # We don't just use $product->name for error messages, because if it + # changes case from $input, then that's a clue that the product does + # exist but is hidden. + my $name = blessed($input) ? $input->name : $input; - die "can_enter_product reached an unreachable location."; + # The product could not exist or you could be denied... + if (!$product || !$product->user_has_access($self)) { + ThrowUserError('entry_access_denied', {product => $name}); + } + + # It could be closed for bug entry... + elsif (!$product->is_active) { + ThrowUserError('product_disabled', {product => $product}); + } + + # It could have no components... + elsif (!@{$product->components} + || !grep { $_->is_active } @{$product->components}) + { + ThrowUserError('missing_component', {product => $product}); + } + + # It could have no versions... + elsif (!@{$product->versions} || !grep { $_->is_active } @{$product->versions}) + { + ThrowUserError('missing_version', {product => $product}); + } + + die "can_enter_product reached an unreachable location."; } sub get_enterable_products { - my $self = shift; - my $dbh = Bugzilla->dbh; + my $self = shift; + my $dbh = Bugzilla->dbh; - if (defined $self->{enterable_products}) { - return $self->{enterable_products}; - } + if (defined $self->{enterable_products}) { + return $self->{enterable_products}; + } - # All products which the user has "Entry" access to. - my $query = - 'SELECT products.id FROM products + # All products which the user has "Entry" access to. + my $query = 'SELECT products.id FROM products LEFT JOIN group_control_map ON group_control_map.product_id = products.id AND group_control_map.entry != 0'; - if (Bugzilla->params->{'or_groups'}) { - $query .= " WHERE (group_id IN (" . $self->groups_as_string . ")" . - " OR group_id IS NULL)"; - } else { - $query .= " AND group_id NOT IN (" . $self->groups_as_string . ")" . - " WHERE group_id IS NULL" - } - $query .= " AND products.isactive = 1"; - my $enterable_ids = $dbh->selectcol_arrayref($query); - - if (scalar @$enterable_ids) { - # And all of these products must have at least one component - # and one version. - $enterable_ids = $dbh->selectcol_arrayref( - 'SELECT DISTINCT products.id FROM products - WHERE ' . $dbh->sql_in('products.id', $enterable_ids) . - ' AND products.id IN (SELECT DISTINCT components.product_id + if (Bugzilla->params->{'or_groups'}) { + $query + .= " WHERE (group_id IN (" + . $self->groups_as_string . ")" + . " OR group_id IS NULL)"; + } + else { + $query + .= " AND group_id NOT IN (" + . $self->groups_as_string . ")" + . " WHERE group_id IS NULL"; + } + $query .= " AND products.isactive = 1"; + my $enterable_ids = $dbh->selectcol_arrayref($query); + + if (scalar @$enterable_ids) { + + # And all of these products must have at least one component + # and one version. + $enterable_ids = $dbh->selectcol_arrayref( + 'SELECT DISTINCT products.id FROM products + WHERE ' + . $dbh->sql_in('products.id', $enterable_ids) + . ' AND products.id IN (SELECT DISTINCT components.product_id FROM components WHERE components.isactive = 1 AND products.id = components.product_id) AND products.id IN (SELECT DISTINCT versions.product_id FROM versions - WHERE versions.isactive = 1 AND products.id = versions.product_id)'); - } + WHERE versions.isactive = 1 AND products.id = versions.product_id)' + ); + } - $self->{enterable_products} = - Bugzilla::Product->new_from_list($enterable_ids); - return $self->{enterable_products}; + $self->{enterable_products} = Bugzilla::Product->new_from_list($enterable_ids); + return $self->{enterable_products}; } sub can_access_product { - my ($self, $product) = @_; - my $product_name = blessed($product) ? $product->name : $product; - return scalar(grep {$_->name eq $product_name} @{$self->get_accessible_products}); + my ($self, $product) = @_; + my $product_name = blessed($product) ? $product->name : $product; + return + scalar(grep { $_->name eq $product_name } @{$self->get_accessible_products}); } sub get_accessible_products { - my $self = shift; - - # Map the objects into a hash using the ids as keys - my %products = map { $_->id => $_ } - @{$self->get_selectable_products}, - @{$self->get_enterable_products}; - - return [ sort { $a->name cmp $b->name } values %products ]; + my $self = shift; + + # Map the objects into a hash using the ids as keys + my %products = map { $_->id => $_ } @{$self->get_selectable_products}, + @{$self->get_enterable_products}; + + return [sort { $a->name cmp $b->name } values %products]; } sub can_administer { - my $self = shift; - - if (not defined $self->{can_administer}) { - my $can_administer = 0; - - $can_administer = 1 if $self->in_group('admin') - || $self->in_group('tweakparams') - || $self->in_group('editusers') - || $self->can_bless - || (Bugzilla->params->{'useclassification'} && $self->in_group('editclassifications')) - || $self->in_group('editcomponents') - || scalar(@{$self->get_products_by_permission('editcomponents')}) - || $self->in_group('creategroups') - || $self->in_group('editkeywords') - || $self->in_group('bz_canusewhines'); - - Bugzilla::Hook::process('user_can_administer', { can_administer => \$can_administer }); - $self->{can_administer} = $can_administer; - } + my $self = shift; + + if (not defined $self->{can_administer}) { + my $can_administer = 0; + + $can_administer = 1 + if $self->in_group('admin') + || $self->in_group('tweakparams') + || $self->in_group('editusers') + || $self->can_bless + || (Bugzilla->params->{'useclassification'} + && $self->in_group('editclassifications')) + || $self->in_group('editcomponents') + || scalar(@{$self->get_products_by_permission('editcomponents')}) + || $self->in_group('creategroups') + || $self->in_group('editkeywords') + || $self->in_group('bz_canusewhines'); + + Bugzilla::Hook::process('user_can_administer', + {can_administer => \$can_administer}); + $self->{can_administer} = $can_administer; + } - return $self->{can_administer}; + return $self->{can_administer}; } sub check_can_admin_product { - my ($self, $product_name) = @_; + my ($self, $product_name) = @_; - # First make sure the product name is valid. - my $product = Bugzilla::Product->check($product_name); + # First make sure the product name is valid. + my $product = Bugzilla::Product->check($product_name); - ($self->in_group('editcomponents', $product->id) - && $self->can_see_product($product->name)) - || ThrowUserError('product_admin_denied', {product => $product->name}); + ( $self->in_group('editcomponents', $product->id) + && $self->can_see_product($product->name)) + || ThrowUserError('product_admin_denied', {product => $product->name}); - # Return the validated product object. - return $product; + # Return the validated product object. + return $product; } sub check_can_admin_flagtype { - my ($self, $flagtype_id) = @_; - - my $flagtype = Bugzilla::FlagType->check({ id => $flagtype_id }); - my $can_fully_edit = 1; - - if (!$self->in_group('editcomponents')) { - my $products = $self->get_products_by_permission('editcomponents'); - # You need editcomponents privs for at least one product to have - # a chance to edit the flagtype. - scalar(@$products) - || ThrowUserError('auth_failure', {group => 'editcomponents', - action => 'edit', - object => 'flagtypes'}); - my $can_admin = 0; - my $i = $flagtype->inclusions_as_hash; - my $e = $flagtype->exclusions_as_hash; - - # If there is at least one product for which the user doesn't have - # editcomponents privs, then don't allow them to do everything with - # this flagtype, independently of whether this product is in the - # exclusion list or not. - my %product_ids; - map { $product_ids{$_->id} = 1 } @$products; - $can_fully_edit = 0 if grep { !$product_ids{$_} } keys %$i; - - unless ($e->{0}->{0}) { - foreach my $product (@$products) { - my $id = $product->id; - next if $e->{$id}->{0}; - # If we are here, the product has not been explicitly excluded. - # Check whether it's explicitly included, or at least one of - # its components. - $can_admin = ($i->{0}->{0} || $i->{$id}->{0} - || scalar(grep { !$e->{$id}->{$_} } keys %{$i->{$id}})); - last if $can_admin; - } - } - $can_admin || ThrowUserError('flag_type_not_editable', { flagtype => $flagtype }); - } - return wantarray ? ($flagtype, $can_fully_edit) : $flagtype; + my ($self, $flagtype_id) = @_; + + my $flagtype = Bugzilla::FlagType->check({id => $flagtype_id}); + my $can_fully_edit = 1; + + if (!$self->in_group('editcomponents')) { + my $products = $self->get_products_by_permission('editcomponents'); + + # You need editcomponents privs for at least one product to have + # a chance to edit the flagtype. + scalar(@$products) + || ThrowUserError('auth_failure', + {group => 'editcomponents', action => 'edit', object => 'flagtypes'}); + my $can_admin = 0; + my $i = $flagtype->inclusions_as_hash; + my $e = $flagtype->exclusions_as_hash; + + # If there is at least one product for which the user doesn't have + # editcomponents privs, then don't allow them to do everything with + # this flagtype, independently of whether this product is in the + # exclusion list or not. + my %product_ids; + map { $product_ids{$_->id} = 1 } @$products; + $can_fully_edit = 0 if grep { !$product_ids{$_} } keys %$i; + + unless ($e->{0}->{0}) { + foreach my $product (@$products) { + my $id = $product->id; + next if $e->{$id}->{0}; + + # If we are here, the product has not been explicitly excluded. + # Check whether it's explicitly included, or at least one of + # its components. + $can_admin + = ( $i->{0}->{0} + || $i->{$id}->{0} + || scalar(grep { !$e->{$id}->{$_} } keys %{$i->{$id}})); + last if $can_admin; + } + } + $can_admin || ThrowUserError('flag_type_not_editable', {flagtype => $flagtype}); + } + return wantarray ? ($flagtype, $can_fully_edit) : $flagtype; } sub can_request_flag { - my ($self, $flag_type) = @_; + my ($self, $flag_type) = @_; - return ($self->can_set_flag($flag_type) - || !$flag_type->request_group_id - || $self->in_group_id($flag_type->request_group_id)) ? 1 : 0; + return ($self->can_set_flag($flag_type) + || !$flag_type->request_group_id + || $self->in_group_id($flag_type->request_group_id)) ? 1 : 0; } sub can_set_flag { - my ($self, $flag_type) = @_; + my ($self, $flag_type) = @_; - return (!$flag_type->grant_group_id - || $self->in_group_id($flag_type->grant_group_id)) ? 1 : 0; + return (!$flag_type->grant_group_id + || $self->in_group_id($flag_type->grant_group_id)) ? 1 : 0; } # visible_groups_inherited returns a reference to a list of all the groups # whose members are visible to this user. sub visible_groups_inherited { - my $self = shift; - return $self->{visible_groups_inherited} if defined $self->{visible_groups_inherited}; - return [] unless $self->id; - my @visgroups = @{$self->visible_groups_direct}; - @visgroups = @{Bugzilla::Group->flatten_group_membership(@visgroups)}; - $self->{visible_groups_inherited} = \@visgroups; - return $self->{visible_groups_inherited}; + my $self = shift; + return $self->{visible_groups_inherited} + if defined $self->{visible_groups_inherited}; + return [] unless $self->id; + my @visgroups = @{$self->visible_groups_direct}; + @visgroups = @{Bugzilla::Group->flatten_group_membership(@visgroups)}; + $self->{visible_groups_inherited} = \@visgroups; + return $self->{visible_groups_inherited}; } # visible_groups_direct returns a reference to a list of all the groups that # are visible to this user. sub visible_groups_direct { - my $self = shift; - my @visgroups = (); - return $self->{visible_groups_direct} if defined $self->{visible_groups_direct}; - return [] unless $self->id; - - my $dbh = Bugzilla->dbh; - my $sth; - - if (Bugzilla->params->{'usevisibilitygroups'}) { - $sth = $dbh->prepare("SELECT DISTINCT grantor_id + my $self = shift; + my @visgroups = (); + return $self->{visible_groups_direct} if defined $self->{visible_groups_direct}; + return [] unless $self->id; + + my $dbh = Bugzilla->dbh; + my $sth; + + if (Bugzilla->params->{'usevisibilitygroups'}) { + $sth = $dbh->prepare( + "SELECT DISTINCT grantor_id FROM group_group_map WHERE " . $self->groups_in_sql('member_id') . " - AND grant_type=" . GROUP_VISIBLE); - } - else { - # All groups are visible if usevisibilitygroups is off. - $sth = $dbh->prepare('SELECT id FROM groups'); - } - $sth->execute(); + AND grant_type=" . GROUP_VISIBLE + ); + } + else { + # All groups are visible if usevisibilitygroups is off. + $sth = $dbh->prepare('SELECT id FROM groups'); + } + $sth->execute(); - while (my ($row) = $sth->fetchrow_array) { - push @visgroups,$row; - } - $self->{visible_groups_direct} = \@visgroups; + while (my ($row) = $sth->fetchrow_array) { + push @visgroups, $row; + } + $self->{visible_groups_direct} = \@visgroups; - return $self->{visible_groups_direct}; + return $self->{visible_groups_direct}; } sub visible_groups_as_string { - my $self = shift; - return join(', ', @{$self->visible_groups_inherited()}); + my $self = shift; + return join(', ', @{$self->visible_groups_inherited()}); } # This function defines the groups a user may share a query with. # More restrictive sites may want to build this reference to a list of group IDs # from bless_groups instead of mirroring visible_groups_inherited, perhaps. sub queryshare_groups { - my $self = shift; - my @queryshare_groups; - - return $self->{queryshare_groups} if defined $self->{queryshare_groups}; - - if ($self->in_group(Bugzilla->params->{'querysharegroup'})) { - # We want to be allowed to share with groups we're in only. - # If usevisibilitygroups is on, then we need to restrict this to groups - # we may see. - if (Bugzilla->params->{'usevisibilitygroups'}) { - foreach(@{$self->visible_groups_inherited()}) { - next unless $self->in_group_id($_); - push(@queryshare_groups, $_); - } - } - else { - @queryshare_groups = @{ $self->_group_ids }; - } + my $self = shift; + my @queryshare_groups; + + return $self->{queryshare_groups} if defined $self->{queryshare_groups}; + + if ($self->in_group(Bugzilla->params->{'querysharegroup'})) { + + # We want to be allowed to share with groups we're in only. + # If usevisibilitygroups is on, then we need to restrict this to groups + # we may see. + if (Bugzilla->params->{'usevisibilitygroups'}) { + foreach (@{$self->visible_groups_inherited()}) { + next unless $self->in_group_id($_); + push(@queryshare_groups, $_); + } } + else { + @queryshare_groups = @{$self->_group_ids}; + } + } - return $self->{queryshare_groups} = \@queryshare_groups; + return $self->{queryshare_groups} = \@queryshare_groups; } sub queryshare_groups_as_string { - my $self = shift; - return join(', ', @{$self->queryshare_groups()}); + my $self = shift; + return join(', ', @{$self->queryshare_groups()}); } sub derive_regexp_groups { - my ($self) = @_; + my ($self) = @_; - my $id = $self->id; - return unless $id; + my $id = $self->id; + return unless $id; - my $dbh = Bugzilla->dbh; + my $dbh = Bugzilla->dbh; - my $sth; + my $sth; - # add derived records for any matching regexps + # add derived records for any matching regexps - $sth = $dbh->prepare("SELECT id, userregexp, user_group_map.group_id + $sth = $dbh->prepare( + "SELECT id, userregexp, user_group_map.group_id FROM groups LEFT JOIN user_group_map ON groups.id = user_group_map.group_id AND user_group_map.user_id = ? - AND user_group_map.grant_type = ?"); - $sth->execute($id, GRANT_REGEXP); + AND user_group_map.grant_type = ?" + ); + $sth->execute($id, GRANT_REGEXP); - my $group_insert = $dbh->prepare(q{INSERT INTO user_group_map + my $group_insert = $dbh->prepare( + q{INSERT INTO user_group_map (user_id, group_id, isbless, grant_type) - VALUES (?, ?, 0, ?)}); - my $group_delete = $dbh->prepare(q{DELETE FROM user_group_map + VALUES (?, ?, 0, ?)} + ); + my $group_delete = $dbh->prepare( + q{DELETE FROM user_group_map WHERE user_id = ? AND group_id = ? AND isbless = 0 - AND grant_type = ?}); - while (my ($group, $regexp, $present) = $sth->fetchrow_array()) { - if (($regexp ne '') && ($self->login =~ m/$regexp/i)) { - $group_insert->execute($id, $group, GRANT_REGEXP) unless $present; - } else { - $group_delete->execute($id, $group, GRANT_REGEXP) if $present; - } + AND grant_type = ?} + ); + while (my ($group, $regexp, $present) = $sth->fetchrow_array()) { + if (($regexp ne '') && ($self->login =~ m/$regexp/i)) { + $group_insert->execute($id, $group, GRANT_REGEXP) unless $present; } + else { + $group_delete->execute($id, $group, GRANT_REGEXP) if $present; + } + } - Bugzilla->memcached->clear_config({ key => "user_groups.$id" }); + Bugzilla->memcached->clear_config({key => "user_groups.$id"}); } sub product_responsibilities { - my $self = shift; - my $dbh = Bugzilla->dbh; + my $self = shift; + my $dbh = Bugzilla->dbh; - return $self->{'product_resp'} if defined $self->{'product_resp'}; - return [] unless $self->id; + return $self->{'product_resp'} if defined $self->{'product_resp'}; + return [] unless $self->id; - my $list = $dbh->selectall_arrayref('SELECT components.product_id, components.id + my $list = $dbh->selectall_arrayref( + 'SELECT components.product_id, components.id FROM components LEFT JOIN component_cc ON components.id = component_cc.component_id WHERE components.initialowner = ? OR components.initialqacontact = ? OR component_cc.user_id = ?', - {Slice => {}}, ($self->id, $self->id, $self->id)); + {Slice => {}}, ($self->id, $self->id, $self->id) + ); - unless ($list) { - $self->{'product_resp'} = []; - return $self->{'product_resp'}; - } - - my @prod_ids = map {$_->{'product_id'}} @$list; - my $products = Bugzilla::Product->new_from_list(\@prod_ids); - # We cannot |use| it, because Component.pm already |use|s User.pm. - require Bugzilla::Component; - my @comp_ids = map {$_->{'id'}} @$list; - my $components = Bugzilla::Component->new_from_list(\@comp_ids); - - my @prod_list; - # @$products is already sorted alphabetically. - foreach my $prod (@$products) { - # We use @components instead of $prod->components because we only want - # components where the user is either the default assignee or QA contact. - push(@prod_list, {product => $prod, - components => [grep {$_->product_id == $prod->id} @$components]}); - } - $self->{'product_resp'} = \@prod_list; + unless ($list) { + $self->{'product_resp'} = []; return $self->{'product_resp'}; + } + + my @prod_ids = map { $_->{'product_id'} } @$list; + my $products = Bugzilla::Product->new_from_list(\@prod_ids); + + # We cannot |use| it, because Component.pm already |use|s User.pm. + require Bugzilla::Component; + my @comp_ids = map { $_->{'id'} } @$list; + my $components = Bugzilla::Component->new_from_list(\@comp_ids); + + my @prod_list; + + # @$products is already sorted alphabetically. + foreach my $prod (@$products) { + + # We use @components instead of $prod->components because we only want + # components where the user is either the default assignee or QA contact. + push( + @prod_list, + { + product => $prod, + components => [grep { $_->product_id == $prod->id } @$components] + } + ); + } + $self->{'product_resp'} = \@prod_list; + return $self->{'product_resp'}; } sub can_bless { - my $self = shift; + my $self = shift; - if (!scalar(@_)) { - # If we're called without an argument, just return - # whether or not we can bless at all. - return scalar(@{ $self->bless_groups }) ? 1 : 0; - } + if (!scalar(@_)) { + + # If we're called without an argument, just return + # whether or not we can bless at all. + return scalar(@{$self->bless_groups}) ? 1 : 0; + } - # Otherwise, we're checking a specific group - my $group_id = shift; - return grep($_->id == $group_id, @{ $self->bless_groups }) ? 1 : 0; + # Otherwise, we're checking a specific group + my $group_id = shift; + return grep($_->id == $group_id, @{$self->bless_groups}) ? 1 : 0; } sub match { - # Generates a list of users whose login name or real name matches a - # substring or wildcard. - # This is also called if matches are disabled (for error checking), but - # in this case only the exact match code will end up running. - - # $str contains the string to match, while $limit contains the - # maximum number of records to retrieve. - my ($str, $limit, $exclude_disabled) = @_; - my $user = Bugzilla->user; - my $dbh = Bugzilla->dbh; - - $str = trim($str); - - my @users = (); - return \@users if $str =~ /^\s*$/; - - # The search order is wildcards, then exact match, then substring search. - # Wildcard matching is skipped if there is no '*', and exact matches will - # not (?) have a '*' in them. If any search comes up with something, the - # ones following it will not execute. - - # first try wildcards - my $wildstr = $str; - - # Do not do wildcards if there is no '*' in the string. - if ($wildstr =~ s/\*/\%/g && $user->id) { - # Build the query. - trick_taint($wildstr); - my $query = "SELECT DISTINCT userid FROM profiles "; - if (Bugzilla->params->{'usevisibilitygroups'}) { - $query .= "INNER JOIN user_group_map - ON user_group_map.user_id = profiles.userid "; - } - $query .= "WHERE (" - . $dbh->sql_istrcmp('login_name', '?', "LIKE") . " OR " . - $dbh->sql_istrcmp('realname', '?', "LIKE") . ") "; - if (Bugzilla->params->{'usevisibilitygroups'}) { - $query .= "AND isbless = 0 " . - "AND group_id IN(" . - join(', ', (-1, @{$user->visible_groups_inherited})) . ") "; - } - $query .= " AND is_enabled = 1 " if $exclude_disabled; - $query .= $dbh->sql_limit($limit) if $limit; - # Execute the query, retrieve the results, and make them into - # User objects. - my $user_ids = $dbh->selectcol_arrayref($query, undef, ($wildstr, $wildstr)); - @users = @{Bugzilla::User->new_from_list($user_ids)}; - } - else { # try an exact match - # Exact matches don't care if a user is disabled, and match - # login names only. - trick_taint($str); + # Generates a list of users whose login name or real name matches a + # substring or wildcard. + # This is also called if matches are disabled (for error checking), but + # in this case only the exact match code will end up running. + + # $str contains the string to match, while $limit contains the + # maximum number of records to retrieve. + my ($str, $limit, $exclude_disabled) = @_; + my $user = Bugzilla->user; + my $dbh = Bugzilla->dbh; + + $str = trim($str); - my $user_id = $dbh->selectrow_array('SELECT userid FROM profiles - WHERE ' . $dbh->sql_istrcmp('login_name', '?'), - undef, $str); + my @users = (); + return \@users if $str =~ /^\s*$/; - push(@users, new Bugzilla::User($user_id)) if $user_id; + # The search order is wildcards, then exact match, then substring search. + # Wildcard matching is skipped if there is no '*', and exact matches will + # not (?) have a '*' in them. If any search comes up with something, the + # ones following it will not execute. + + # first try wildcards + my $wildstr = $str; + + # Do not do wildcards if there is no '*' in the string. + if ($wildstr =~ s/\*/\%/g && $user->id) { + + # Build the query. + trick_taint($wildstr); + my $query = "SELECT DISTINCT userid FROM profiles "; + if (Bugzilla->params->{'usevisibilitygroups'}) { + $query .= "INNER JOIN user_group_map + ON user_group_map.user_id = profiles.userid "; } + $query + .= "WHERE (" + . $dbh->sql_istrcmp('login_name', '?', "LIKE") . " OR " + . $dbh->sql_istrcmp('realname', '?', "LIKE") . ") "; + if (Bugzilla->params->{'usevisibilitygroups'}) { + $query + .= "AND isbless = 0 " + . "AND group_id IN(" + . join(', ', (-1, @{$user->visible_groups_inherited})) . ") "; + } + $query .= " AND is_enabled = 1 " if $exclude_disabled; + $query .= $dbh->sql_limit($limit) if $limit; + + # Execute the query, retrieve the results, and make them into + # User objects. + my $user_ids = $dbh->selectcol_arrayref($query, undef, ($wildstr, $wildstr)); + @users = @{Bugzilla::User->new_from_list($user_ids)}; + } + else { # try an exact match + # Exact matches don't care if a user is disabled, and match + # login names only. + trick_taint($str); + + my $user_id = $dbh->selectrow_array( + 'SELECT userid FROM profiles + WHERE ' + . $dbh->sql_istrcmp('login_name', '?'), undef, $str + ); + + push(@users, new Bugzilla::User($user_id)) if $user_id; + } - # then try substring search - if (!scalar(@users) && length($str) >= 3 && $user->id) { - trick_taint($str); + # then try substring search + if (!scalar(@users) && length($str) >= 3 && $user->id) { + trick_taint($str); - my $query = "SELECT DISTINCT userid FROM profiles "; - if (Bugzilla->params->{'usevisibilitygroups'}) { - $query .= "INNER JOIN user_group_map + my $query = "SELECT DISTINCT userid FROM profiles "; + if (Bugzilla->params->{'usevisibilitygroups'}) { + $query .= "INNER JOIN user_group_map ON user_group_map.user_id = profiles.userid "; - } - $query .= " WHERE (" . - $dbh->sql_iposition('?', 'login_name') . " > 0" . " OR " . - $dbh->sql_iposition('?', 'realname') . " > 0) "; - if (Bugzilla->params->{'usevisibilitygroups'}) { - $query .= " AND isbless = 0" . - " AND group_id IN(" . - join(', ', (-1, @{$user->visible_groups_inherited})) . ") "; - } - $query .= " AND is_enabled = 1 " if $exclude_disabled; - $query .= $dbh->sql_limit($limit) if $limit; - my $user_ids = $dbh->selectcol_arrayref($query, undef, ($str, $str)); - @users = @{Bugzilla::User->new_from_list($user_ids)}; } - return \@users; + $query + .= " WHERE (" + . $dbh->sql_iposition('?', 'login_name') . " > 0" . " OR " + . $dbh->sql_iposition('?', 'realname') + . " > 0) "; + if (Bugzilla->params->{'usevisibilitygroups'}) { + $query + .= " AND isbless = 0" + . " AND group_id IN(" + . join(', ', (-1, @{$user->visible_groups_inherited})) . ") "; + } + $query .= " AND is_enabled = 1 " if $exclude_disabled; + $query .= $dbh->sql_limit($limit) if $limit; + my $user_ids = $dbh->selectcol_arrayref($query, undef, ($str, $str)); + @users = @{Bugzilla::User->new_from_list($user_ids)}; + } + return \@users; } sub match_field { - my $fields = shift; # arguments as a hash - my $data = shift || Bugzilla->input_params; # hash to look up fields in - my $behavior = shift || 0; # A constant that tells us how to act - my $matches = {}; # the values sent to the template - my $matchsuccess = 1; # did the match fail? - my $need_confirm = 0; # whether to display confirmation screen - my $match_multiple = 0; # whether we ever matched more than one user - my @non_conclusive_fields; # fields which don't have a unique user. - - my $params = Bugzilla->params; - - # prepare default form values - - # Fields can be regular expressions matching multiple form fields - # (f.e. "requestee-(\d+)"), so expand each non-literal field - # into the list of form fields it matches. - my $expanded_fields = {}; - foreach my $field_pattern (keys %{$fields}) { - # Check if the field has any non-word characters. Only those fields - # can be regular expressions, so don't expand the field if it doesn't - # have any of those characters. - if ($field_pattern =~ /^\w+$/) { - $expanded_fields->{$field_pattern} = $fields->{$field_pattern}; - } - else { - my @field_names = grep(/$field_pattern/, keys %$data); - - foreach my $field_name (@field_names) { - $expanded_fields->{$field_name} = - { type => $fields->{$field_pattern}->{'type'} }; - - # The field is a requestee field; in order for its name - # to show up correctly on the confirmation page, we need - # to find out the name of its flag type. - if ($field_name =~ /^requestee(_type)?-(\d+)$/a) { - my $flag_type; - if ($1) { - require Bugzilla::FlagType; - $flag_type = new Bugzilla::FlagType($2); - } - else { - require Bugzilla::Flag; - my $flag = new Bugzilla::Flag($2); - $flag_type = $flag->type if $flag; - } - if ($flag_type) { - $expanded_fields->{$field_name}->{'flag_type'} = $flag_type; - } - else { - # No need to look for a valid requestee if the flag(type) - # has been deleted (may occur in race conditions). - delete $expanded_fields->{$field_name}; - delete $data->{$field_name}; - } - } - } + my $fields = shift; # arguments as a hash + my $data = shift || Bugzilla->input_params; # hash to look up fields in + my $behavior = shift || 0; # A constant that tells us how to act + my $matches = {}; # the values sent to the template + my $matchsuccess = 1; # did the match fail? + my $need_confirm = 0; # whether to display confirmation screen + my $match_multiple = 0; # whether we ever matched more than one user + my @non_conclusive_fields; # fields which don't have a unique user. + + my $params = Bugzilla->params; + + # prepare default form values + + # Fields can be regular expressions matching multiple form fields + # (f.e. "requestee-(\d+)"), so expand each non-literal field + # into the list of form fields it matches. + my $expanded_fields = {}; + foreach my $field_pattern (keys %{$fields}) { + + # Check if the field has any non-word characters. Only those fields + # can be regular expressions, so don't expand the field if it doesn't + # have any of those characters. + if ($field_pattern =~ /^\w+$/) { + $expanded_fields->{$field_pattern} = $fields->{$field_pattern}; + } + else { + my @field_names = grep(/$field_pattern/, keys %$data); + + foreach my $field_name (@field_names) { + $expanded_fields->{$field_name} = {type => $fields->{$field_pattern}->{'type'}}; + + # The field is a requestee field; in order for its name + # to show up correctly on the confirmation page, we need + # to find out the name of its flag type. + if ($field_name =~ /^requestee(_type)?-(\d+)$/a) { + my $flag_type; + if ($1) { + require Bugzilla::FlagType; + $flag_type = new Bugzilla::FlagType($2); + } + else { + require Bugzilla::Flag; + my $flag = new Bugzilla::Flag($2); + $flag_type = $flag->type if $flag; + } + if ($flag_type) { + $expanded_fields->{$field_name}->{'flag_type'} = $flag_type; + } + else { + # No need to look for a valid requestee if the flag(type) + # has been deleted (may occur in race conditions). + delete $expanded_fields->{$field_name}; + delete $data->{$field_name}; + } } + } } - $fields = $expanded_fields; + } + $fields = $expanded_fields; - foreach my $field (keys %{$fields}) { - next unless defined $data->{$field}; + foreach my $field (keys %{$fields}) { + next unless defined $data->{$field}; - #Concatenate login names, so that we have a common way to handle them. - my $raw_field; - if (ref $data->{$field}) { - $raw_field = join(",", @{$data->{$field}}); - } - else { - $raw_field = $data->{$field}; - } - $raw_field = clean_text($raw_field || ''); - - # Now we either split $raw_field by spaces/commas and put the list - # into @queries, or in the case of fields which only accept single - # entries, we simply use the verbatim text. - my @queries; - if ($fields->{$field}->{'type'} eq 'single') { - @queries = ($raw_field); - # We will repopulate it later if a match is found, else it must - # be set to an empty string so that the field remains defined. - $data->{$field} = ''; - } - elsif ($fields->{$field}->{'type'} eq 'multi') { - @queries = split(/[,;]+/, $raw_field); - # We will repopulate it later if a match is found, else it must - # be undefined. - delete $data->{$field}; - } - else { - # bad argument - ThrowCodeError('bad_arg', - { argument => $fields->{$field}->{'type'}, - function => 'Bugzilla::User::match_field', - }); - } + #Concatenate login names, so that we have a common way to handle them. + my $raw_field; + if (ref $data->{$field}) { + $raw_field = join(",", @{$data->{$field}}); + } + else { + $raw_field = $data->{$field}; + } + $raw_field = clean_text($raw_field || ''); - # Tolerate fields that do not exist (in case you specify - # e.g. the QA contact, and it's currently not in use). - next unless (defined $raw_field && $raw_field ne ''); + # Now we either split $raw_field by spaces/commas and put the list + # into @queries, or in the case of fields which only accept single + # entries, we simply use the verbatim text. + my @queries; + if ($fields->{$field}->{'type'} eq 'single') { + @queries = ($raw_field); - my $limit = 0; - if ($params->{'maxusermatches'}) { - $limit = $params->{'maxusermatches'} + 1; - } + # We will repopulate it later if a match is found, else it must + # be set to an empty string so that the field remains defined. + $data->{$field} = ''; + } + elsif ($fields->{$field}->{'type'} eq 'multi') { + @queries = split(/[,;]+/, $raw_field); - my @logins; - for my $query (@queries) { - $query = trim($query); - next if $query eq ''; - - my $users = match( - $query, # match string - $limit, # match limit - 1 # exclude_disabled - ); - - # here is where it checks for multiple matches - if (scalar(@{$users}) == 1) { # exactly one match - push(@logins, @{$users}[0]->login); - - # skip confirmation for exact matches - next if (lc(@{$users}[0]->login) eq lc($query)); - - $matches->{$field}->{$query}->{'status'} = 'success'; - $need_confirm = 1 if $params->{'confirmuniqueusermatch'}; - - } - elsif ((scalar(@{$users}) > 1) - && ($params->{'maxusermatches'} != 1)) { - $need_confirm = 1; - $match_multiple = 1; - push(@non_conclusive_fields, $field); - - if (($params->{'maxusermatches'}) - && (scalar(@{$users}) > $params->{'maxusermatches'})) - { - $matches->{$field}->{$query}->{'status'} = 'trunc'; - pop @{$users}; # take the last one out - } - else { - $matches->{$field}->{$query}->{'status'} = 'success'; - } - - } - else { - # everything else fails - $matchsuccess = 0; # fail - push(@non_conclusive_fields, $field); - $matches->{$field}->{$query}->{'status'} = 'fail'; - $need_confirm = 1; # confirmation screen shows failures - } - - $matches->{$field}->{$query}->{'users'} = $users; + # We will repopulate it later if a match is found, else it must + # be undefined. + delete $data->{$field}; + } + else { + # bad argument + ThrowCodeError( + 'bad_arg', + { + argument => $fields->{$field}->{'type'}, + function => 'Bugzilla::User::match_field', } + ); + } - # If no match or more than one match has been found for a field - # expecting only one match (type eq "single"), we set it back to '' - # so that the caller of this function can still check whether this - # field was defined or not (and it was if we came here). - if ($fields->{$field}->{'type'} eq 'single') { - $data->{$field} = $logins[0] || ''; + # Tolerate fields that do not exist (in case you specify + # e.g. the QA contact, and it's currently not in use). + next unless (defined $raw_field && $raw_field ne ''); + + my $limit = 0; + if ($params->{'maxusermatches'}) { + $limit = $params->{'maxusermatches'} + 1; + } + + my @logins; + for my $query (@queries) { + $query = trim($query); + next if $query eq ''; + + my $users = match( + $query, # match string + $limit, # match limit + 1 # exclude_disabled + ); + + # here is where it checks for multiple matches + if (scalar(@{$users}) == 1) { # exactly one match + push(@logins, @{$users}[0]->login); + + # skip confirmation for exact matches + next if (lc(@{$users}[0]->login) eq lc($query)); + + $matches->{$field}->{$query}->{'status'} = 'success'; + $need_confirm = 1 if $params->{'confirmuniqueusermatch'}; + + } + elsif ((scalar(@{$users}) > 1) && ($params->{'maxusermatches'} != 1)) { + $need_confirm = 1; + $match_multiple = 1; + push(@non_conclusive_fields, $field); + + if ( ($params->{'maxusermatches'}) + && (scalar(@{$users}) > $params->{'maxusermatches'})) + { + $matches->{$field}->{$query}->{'status'} = 'trunc'; + pop @{$users}; # take the last one out } - elsif (scalar @logins) { - $data->{$field} = \@logins; + else { + $matches->{$field}->{$query}->{'status'} = 'success'; } - } - my $retval; - if (!$matchsuccess) { - $retval = USER_MATCH_FAILED; + } + else { + # everything else fails + $matchsuccess = 0; # fail + push(@non_conclusive_fields, $field); + $matches->{$field}->{$query}->{'status'} = 'fail'; + $need_confirm = 1; # confirmation screen shows failures + } + + $matches->{$field}->{$query}->{'users'} = $users; } - elsif ($match_multiple) { - $retval = USER_MATCH_MULTIPLE; + + # If no match or more than one match has been found for a field + # expecting only one match (type eq "single"), we set it back to '' + # so that the caller of this function can still check whether this + # field was defined or not (and it was if we came here). + if ($fields->{$field}->{'type'} eq 'single') { + $data->{$field} = $logins[0] || ''; } - else { - $retval = USER_MATCH_SUCCESS; + elsif (scalar @logins) { + $data->{$field} = \@logins; } + } - # Skip confirmation if we were told to, or if we don't need to confirm. - if ($behavior == MATCH_SKIP_CONFIRM || !$need_confirm) { - return wantarray ? ($retval, \@non_conclusive_fields) : $retval; - } + my $retval; + if (!$matchsuccess) { + $retval = USER_MATCH_FAILED; + } + elsif ($match_multiple) { + $retval = USER_MATCH_MULTIPLE; + } + else { + $retval = USER_MATCH_SUCCESS; + } + + # Skip confirmation if we were told to, or if we don't need to confirm. + if ($behavior == MATCH_SKIP_CONFIRM || !$need_confirm) { + return wantarray ? ($retval, \@non_conclusive_fields) : $retval; + } - my $template = Bugzilla->template; - my $cgi = Bugzilla->cgi; - my $vars = {}; + my $template = Bugzilla->template; + my $cgi = Bugzilla->cgi; + my $vars = {}; - $vars->{'script'} = $cgi->url(-relative => 1); # for self-referencing URLs - $vars->{'fields'} = $fields; # fields being matched - $vars->{'matches'} = $matches; # matches that were made - $vars->{'matchsuccess'} = $matchsuccess; # continue or fail - $vars->{'matchmultiple'} = $match_multiple; + $vars->{'script'} = $cgi->url(-relative => 1); # for self-referencing URLs + $vars->{'fields'} = $fields; # fields being matched + $vars->{'matches'} = $matches; # matches that were made + $vars->{'matchsuccess'} = $matchsuccess; # continue or fail + $vars->{'matchmultiple'} = $match_multiple; - print $cgi->header(); + print $cgi->header(); - $template->process("global/confirm-user-match.html.tmpl", $vars) - || ThrowTemplateError($template->error()); - exit; + $template->process("global/confirm-user-match.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + exit; } # Changes in some fields automatically trigger events. The field names are # from the fielddefs table. our %names_to_events = ( - 'resolution' => EVT_OPENED_CLOSED, - 'keywords' => EVT_KEYWORD, - 'cc' => EVT_CC, - 'bug_severity' => EVT_PROJ_MANAGEMENT, - 'priority' => EVT_PROJ_MANAGEMENT, - 'bug_status' => EVT_PROJ_MANAGEMENT, - 'target_milestone' => EVT_PROJ_MANAGEMENT, - 'attachments.description' => EVT_ATTACHMENT_DATA, - 'attachments.mimetype' => EVT_ATTACHMENT_DATA, - 'attachments.ispatch' => EVT_ATTACHMENT_DATA, - 'dependson' => EVT_DEPEND_BLOCK, - 'blocked' => EVT_DEPEND_BLOCK, - 'product' => EVT_COMPONENT, - 'component' => EVT_COMPONENT); + 'resolution' => EVT_OPENED_CLOSED, + 'keywords' => EVT_KEYWORD, + 'cc' => EVT_CC, + 'bug_severity' => EVT_PROJ_MANAGEMENT, + 'priority' => EVT_PROJ_MANAGEMENT, + 'bug_status' => EVT_PROJ_MANAGEMENT, + 'target_milestone' => EVT_PROJ_MANAGEMENT, + 'attachments.description' => EVT_ATTACHMENT_DATA, + 'attachments.mimetype' => EVT_ATTACHMENT_DATA, + 'attachments.ispatch' => EVT_ATTACHMENT_DATA, + 'dependson' => EVT_DEPEND_BLOCK, + 'blocked' => EVT_DEPEND_BLOCK, + 'product' => EVT_COMPONENT, + 'component' => EVT_COMPONENT +); # Returns true if the user wants mail for a given bug change. # Note: the "+" signs before the constants suppress bareword quoting. sub wants_bug_mail { - my $self = shift; - my ($bug, $relationship, $fieldDiffs, $comments, $dep_mail, $changer, - $minor_update) = @_; - - # Make a list of the events which have happened during this bug change, - # from the point of view of this user. - my %events; - foreach my $change (@$fieldDiffs) { - my $fieldName = $change->{field_name}; - # A change to any of the above fields sets the corresponding event - if (defined($names_to_events{$fieldName})) { - $events{$names_to_events{$fieldName}} = 1; - } - else { - # Catch-all for any change not caught by a more specific event - $events{+EVT_OTHER} = 1; - } + my $self = shift; + my ($bug, $relationship, $fieldDiffs, $comments, $dep_mail, $changer, + $minor_update) + = @_; - # If the user is in a particular role and the value of that role - # changed, we need the ADDED_REMOVED event. - if (($fieldName eq "assigned_to" && $relationship == REL_ASSIGNEE) || - ($fieldName eq "qa_contact" && $relationship == REL_QA)) - { - $events{+EVT_ADDED_REMOVED} = 1; - } - - if ($fieldName eq "cc") { - my $login = $self->login; - my $inold = ($change->{old} =~ /^(.*,\s*)?\Q$login\E(,.*)?$/); - my $innew = ($change->{new} =~ /^(.*,\s*)?\Q$login\E(,.*)?$/); - if ($inold != $innew) - { - $events{+EVT_ADDED_REMOVED} = 1; - } - } - } - - if (!$bug->lastdiffed) { - # Notify about new bugs. - $events{+EVT_BUG_CREATED} = 1; + # Make a list of the events which have happened during this bug change, + # from the point of view of this user. + my %events; + foreach my $change (@$fieldDiffs) { + my $fieldName = $change->{field_name}; - # You role is new if the bug itself is. - # Only makes sense for the assignee, QA contact and the CC list. - if ($relationship == REL_ASSIGNEE - || $relationship == REL_QA - || $relationship == REL_CC) - { - $events{+EVT_ADDED_REMOVED} = 1; - } + # A change to any of the above fields sets the corresponding event + if (defined($names_to_events{$fieldName})) { + $events{$names_to_events{$fieldName}} = 1; } - - if (grep { $_->type == CMT_ATTACHMENT_CREATED } @$comments) { - $events{+EVT_ATTACHMENT} = 1; - } - elsif (defined($$comments[0])) { - $events{+EVT_COMMENT} = 1; + else { + # Catch-all for any change not caught by a more specific event + $events{+EVT_OTHER} = 1; } - - # Dependent changed bugmails must have an event to ensure the bugmail is - # emailed. - if ($dep_mail) { - $events{+EVT_DEPEND_BLOCK} = 1; + + # If the user is in a particular role and the value of that role + # changed, we need the ADDED_REMOVED event. + if ( ($fieldName eq "assigned_to" && $relationship == REL_ASSIGNEE) + || ($fieldName eq "qa_contact" && $relationship == REL_QA)) + { + $events{+EVT_ADDED_REMOVED} = 1; } - my @event_list = keys %events; - - my $wants_mail = $self->wants_mail(\@event_list, $relationship); - - # The negative events are handled separately - they can't be incorporated - # into the first wants_mail call, because they are of the opposite sense. - # - # We do them separately because if _any_ of them are set, we don't want - # the mail. - if ($wants_mail && $changer && ($self->id == $changer->id)) { - $wants_mail &= $self->wants_mail([EVT_CHANGED_BY_ME], $relationship); - } - - if ($wants_mail && $bug->bug_status eq 'UNCONFIRMED') { - $wants_mail &= $self->wants_mail([EVT_UNCONFIRMED], $relationship); + if ($fieldName eq "cc") { + my $login = $self->login; + my $inold = ($change->{old} =~ /^(.*,\s*)?\Q$login\E(,.*)?$/); + my $innew = ($change->{new} =~ /^(.*,\s*)?\Q$login\E(,.*)?$/); + if ($inold != $innew) { + $events{+EVT_ADDED_REMOVED} = 1; + } } + } + + if (!$bug->lastdiffed) { - if ($wants_mail && $minor_update) { - $wants_mail &= $self->wants_mail([EVT_MINOR_UPDATE], $relationship); + # Notify about new bugs. + $events{+EVT_BUG_CREATED} = 1; + + # You role is new if the bug itself is. + # Only makes sense for the assignee, QA contact and the CC list. + if ( $relationship == REL_ASSIGNEE + || $relationship == REL_QA + || $relationship == REL_CC) + { + $events{+EVT_ADDED_REMOVED} = 1; } - - return $wants_mail; + } + + if (grep { $_->type == CMT_ATTACHMENT_CREATED } @$comments) { + $events{+EVT_ATTACHMENT} = 1; + } + elsif (defined($$comments[0])) { + $events{+EVT_COMMENT} = 1; + } + + # Dependent changed bugmails must have an event to ensure the bugmail is + # emailed. + if ($dep_mail) { + $events{+EVT_DEPEND_BLOCK} = 1; + } + + my @event_list = keys %events; + + my $wants_mail = $self->wants_mail(\@event_list, $relationship); + + # The negative events are handled separately - they can't be incorporated + # into the first wants_mail call, because they are of the opposite sense. + # + # We do them separately because if _any_ of them are set, we don't want + # the mail. + if ($wants_mail && $changer && ($self->id == $changer->id)) { + $wants_mail &= $self->wants_mail([EVT_CHANGED_BY_ME], $relationship); + } + + if ($wants_mail && $bug->bug_status eq 'UNCONFIRMED') { + $wants_mail &= $self->wants_mail([EVT_UNCONFIRMED], $relationship); + } + + if ($wants_mail && $minor_update) { + $wants_mail &= $self->wants_mail([EVT_MINOR_UPDATE], $relationship); + } + + return $wants_mail; } # Returns true if the user wants mail for a given set of events. sub wants_mail { - my $self = shift; - my ($events, $relationship) = @_; - - # Don't send any mail, ever, if account is disabled - # XXX Temporary Compatibility Change 1 of 2: - # This code is disabled for the moment to make the behaviour like the old - # system, which sent bugmail to disabled accounts. - # return 0 if $self->{'disabledtext'}; - - # No mail if there are no events - return 0 if !scalar(@$events); - - # If a relationship isn't given, default to REL_ANY. - if (!defined($relationship)) { - $relationship = REL_ANY; - } + my $self = shift; + my ($events, $relationship) = @_; - # Skip DB query if relationship is explicit - return 1 if $relationship == REL_GLOBAL_WATCHER; + # Don't send any mail, ever, if account is disabled + # XXX Temporary Compatibility Change 1 of 2: + # This code is disabled for the moment to make the behaviour like the old + # system, which sent bugmail to disabled accounts. + # return 0 if $self->{'disabledtext'}; - my $wants_mail = grep { $self->mail_settings->{$relationship}{$_} } @$events; - return $wants_mail ? 1 : 0; + # No mail if there are no events + return 0 if !scalar(@$events); + + # If a relationship isn't given, default to REL_ANY. + if (!defined($relationship)) { + $relationship = REL_ANY; + } + + # Skip DB query if relationship is explicit + return 1 if $relationship == REL_GLOBAL_WATCHER; + + my $wants_mail = grep { $self->mail_settings->{$relationship}{$_} } @$events; + return $wants_mail ? 1 : 0; } sub mail_settings { - my $self = shift; - my $dbh = Bugzilla->dbh; - - if (!defined $self->{'mail_settings'}) { - my $data = - $dbh->selectall_arrayref('SELECT relationship, event FROM email_setting - WHERE user_id = ?', undef, $self->id); - my %mail; - # The hash is of the form $mail{$relationship}{$event} = 1. - $mail{$_->[0]}{$_->[1]} = 1 foreach @$data; - - $self->{'mail_settings'} = \%mail; - } - return $self->{'mail_settings'}; + my $self = shift; + my $dbh = Bugzilla->dbh; + + if (!defined $self->{'mail_settings'}) { + my $data = $dbh->selectall_arrayref( + 'SELECT relationship, event FROM email_setting + WHERE user_id = ?', undef, $self->id + ); + my %mail; + + # The hash is of the form $mail{$relationship}{$event} = 1. + $mail{$_->[0]}{$_->[1]} = 1 foreach @$data; + + $self->{'mail_settings'} = \%mail; + } + return $self->{'mail_settings'}; } sub has_audit_entries { - my $self = shift; - my $dbh = Bugzilla->dbh; + my $self = shift; + my $dbh = Bugzilla->dbh; - if (!exists $self->{'has_audit_entries'}) { - $self->{'has_audit_entries'} = - $dbh->selectrow_array('SELECT 1 FROM audit_log WHERE user_id = ? ' . - $dbh->sql_limit(1), undef, $self->id); - } - return $self->{'has_audit_entries'}; + if (!exists $self->{'has_audit_entries'}) { + $self->{'has_audit_entries'} + = $dbh->selectrow_array( + 'SELECT 1 FROM audit_log WHERE user_id = ? ' . $dbh->sql_limit(1), + undef, $self->id); + } + return $self->{'has_audit_entries'}; } sub is_insider { - my $self = shift; + my $self = shift; - if (!defined $self->{'is_insider'}) { - my $insider_group = Bugzilla->params->{'insidergroup'}; - $self->{'is_insider'} = - ($insider_group && $self->in_group($insider_group)) ? 1 : 0; - } - return $self->{'is_insider'}; + if (!defined $self->{'is_insider'}) { + my $insider_group = Bugzilla->params->{'insidergroup'}; + $self->{'is_insider'} + = ($insider_group && $self->in_group($insider_group)) ? 1 : 0; + } + return $self->{'is_insider'}; } sub is_global_watcher { - my $self = shift; + my $self = shift; - if (!defined $self->{'is_global_watcher'}) { - my @watchers = split(/[,;]+/, Bugzilla->params->{'globalwatchers'}); - $self->{'is_global_watcher'} = scalar(grep { $_ eq $self->login } @watchers) ? 1 : 0; - } - return $self->{'is_global_watcher'}; + if (!defined $self->{'is_global_watcher'}) { + my @watchers = split(/[,;]+/, Bugzilla->params->{'globalwatchers'}); + $self->{'is_global_watcher'} + = scalar(grep { $_ eq $self->login } @watchers) ? 1 : 0; + } + return $self->{'is_global_watcher'}; } sub is_timetracker { - my $self = shift; + my $self = shift; - if (!defined $self->{'is_timetracker'}) { - my $tt_group = Bugzilla->params->{'timetrackinggroup'}; - $self->{'is_timetracker'} = - ($tt_group && $self->in_group($tt_group)) ? 1 : 0; - } - return $self->{'is_timetracker'}; + if (!defined $self->{'is_timetracker'}) { + my $tt_group = Bugzilla->params->{'timetrackinggroup'}; + $self->{'is_timetracker'} = ($tt_group && $self->in_group($tt_group)) ? 1 : 0; + } + return $self->{'is_timetracker'}; } sub can_tag_comments { - my $self = shift; + my $self = shift; - if (!defined $self->{'can_tag_comments'}) { - my $group = Bugzilla->params->{'comment_taggers_group'}; - $self->{'can_tag_comments'} = - ($group && $self->in_group($group)) ? 1 : 0; - } - return $self->{'can_tag_comments'}; + if (!defined $self->{'can_tag_comments'}) { + my $group = Bugzilla->params->{'comment_taggers_group'}; + $self->{'can_tag_comments'} = ($group && $self->in_group($group)) ? 1 : 0; + } + return $self->{'can_tag_comments'}; } sub get_userlist { - my $self = shift; - - return $self->{'userlist'} if defined $self->{'userlist'}; - - my $dbh = Bugzilla->dbh; - my $query = "SELECT DISTINCT login_name, realname,"; - if (Bugzilla->params->{'usevisibilitygroups'}) { - $query .= " COUNT(group_id) "; - } else { - $query .= " 1 "; - } - $query .= "FROM profiles "; - if (Bugzilla->params->{'usevisibilitygroups'}) { - $query .= "LEFT JOIN user_group_map " . - "ON user_group_map.user_id = userid AND isbless = 0 " . - "AND group_id IN(" . - join(', ', (-1, @{$self->visible_groups_inherited})) . ")"; - } - $query .= " WHERE is_enabled = 1 "; - $query .= $dbh->sql_group_by('userid', 'login_name, realname'); - - my $sth = $dbh->prepare($query); - $sth->execute; - - my @userlist; - while (my($login, $name, $visible) = $sth->fetchrow_array) { - push @userlist, { - login => $login, - identity => $name ? "$name ($login)" : $login, - visible => $visible, - }; - } - @userlist = sort { lc $$a{'identity'} cmp lc $$b{'identity'} } @userlist; - - $self->{'userlist'} = \@userlist; - return $self->{'userlist'}; + my $self = shift; + + return $self->{'userlist'} if defined $self->{'userlist'}; + + my $dbh = Bugzilla->dbh; + my $query = "SELECT DISTINCT login_name, realname,"; + if (Bugzilla->params->{'usevisibilitygroups'}) { + $query .= " COUNT(group_id) "; + } + else { + $query .= " 1 "; + } + $query .= "FROM profiles "; + if (Bugzilla->params->{'usevisibilitygroups'}) { + $query + .= "LEFT JOIN user_group_map " + . "ON user_group_map.user_id = userid AND isbless = 0 " + . "AND group_id IN(" + . join(', ', (-1, @{$self->visible_groups_inherited})) . ")"; + } + $query .= " WHERE is_enabled = 1 "; + $query .= $dbh->sql_group_by('userid', 'login_name, realname'); + + my $sth = $dbh->prepare($query); + $sth->execute; + + my @userlist; + while (my ($login, $name, $visible) = $sth->fetchrow_array) { + push @userlist, + { + login => $login, + identity => $name ? "$name ($login)" : $login, + visible => $visible, + }; + } + @userlist = sort { lc $$a{'identity'} cmp lc $$b{'identity'} } @userlist; + + $self->{'userlist'} = \@userlist; + return $self->{'userlist'}; } sub create { - my $invocant = shift; - my $class = ref($invocant) || $invocant; - my $dbh = Bugzilla->dbh; - - $dbh->bz_start_transaction(); - - my $user = $class->SUPER::create(@_); - - # Turn on all email for the new user - require Bugzilla::BugMail; - my %relationships = Bugzilla::BugMail::relationships(); - foreach my $rel (keys %relationships) { - foreach my $event (POS_EVENTS, NEG_EVENTS) { - # These "exceptions" define the default email preferences. - # - # We enable mail unless the change was made by the user, or it's - # just a CC list addition and the user is not the reporter. - next if ($event == EVT_CHANGED_BY_ME); - next if (($event == EVT_CC) && ($rel != REL_REPORTER)); - - $dbh->do('INSERT INTO email_setting (user_id, relationship, event) - VALUES (?, ?, ?)', undef, ($user->id, $rel, $event)); - } - } - - foreach my $event (GLOBAL_EVENTS) { - $dbh->do('INSERT INTO email_setting (user_id, relationship, event) - VALUES (?, ?, ?)', undef, ($user->id, REL_ANY, $event)); - } + my $invocant = shift; + my $class = ref($invocant) || $invocant; + my $dbh = Bugzilla->dbh; + + $dbh->bz_start_transaction(); + + my $user = $class->SUPER::create(@_); + + # Turn on all email for the new user + require Bugzilla::BugMail; + my %relationships = Bugzilla::BugMail::relationships(); + foreach my $rel (keys %relationships) { + foreach my $event (POS_EVENTS, NEG_EVENTS) { + + # These "exceptions" define the default email preferences. + # + # We enable mail unless the change was made by the user, or it's + # just a CC list addition and the user is not the reporter. + next if ($event == EVT_CHANGED_BY_ME); + next if (($event == EVT_CC) && ($rel != REL_REPORTER)); + + $dbh->do( + 'INSERT INTO email_setting (user_id, relationship, event) + VALUES (?, ?, ?)', undef, ($user->id, $rel, $event) + ); + } + } + + foreach my $event (GLOBAL_EVENTS) { + $dbh->do( + 'INSERT INTO email_setting (user_id, relationship, event) + VALUES (?, ?, ?)', undef, ($user->id, REL_ANY, $event) + ); + } - $user->derive_regexp_groups(); + $user->derive_regexp_groups(); - # Add the creation date to the profiles_activity table. - # $who is the user who created the new user account, i.e. either an - # admin or the new user himself. - my $who = Bugzilla->user->id || $user->id; - my $creation_date_fieldid = get_field_id('creation_ts'); + # Add the creation date to the profiles_activity table. + # $who is the user who created the new user account, i.e. either an + # admin or the new user himself. + my $who = Bugzilla->user->id || $user->id; + my $creation_date_fieldid = get_field_id('creation_ts'); - $dbh->do('INSERT INTO profiles_activity + $dbh->do( + 'INSERT INTO profiles_activity (userid, who, profiles_when, fieldid, newvalue) - VALUES (?, ?, NOW(), ?, NOW())', - undef, ($user->id, $who, $creation_date_fieldid)); + VALUES (?, ?, NOW(), ?, NOW())', undef, + ($user->id, $who, $creation_date_fieldid) + ); - $dbh->bz_commit_transaction(); + $dbh->bz_commit_transaction(); - # Return the newly created user account. - return $user; + # Return the newly created user account. + return $user; } ########################### @@ -2366,56 +2471,57 @@ sub create { ########################### sub account_is_locked_out { - my $self = shift; - my $login_failures = scalar @{ $self->account_ip_login_failures }; - return $login_failures >= MAX_LOGIN_ATTEMPTS ? 1 : 0; + my $self = shift; + my $login_failures = scalar @{$self->account_ip_login_failures}; + return $login_failures >= MAX_LOGIN_ATTEMPTS ? 1 : 0; } sub note_login_failure { - my $self = shift; - my $ip_addr = remote_ip(); - trick_taint($ip_addr); - Bugzilla->dbh->do("INSERT INTO login_failure (user_id, ip_addr, login_time) - VALUES (?, ?, LOCALTIMESTAMP(0))", - undef, $self->id, $ip_addr); - delete $self->{account_ip_login_failures}; + my $self = shift; + my $ip_addr = remote_ip(); + trick_taint($ip_addr); + Bugzilla->dbh->do( + "INSERT INTO login_failure (user_id, ip_addr, login_time) + VALUES (?, ?, LOCALTIMESTAMP(0))", undef, $self->id, $ip_addr + ); + delete $self->{account_ip_login_failures}; } sub clear_login_failures { - my $self = shift; - my $ip_addr = remote_ip(); - trick_taint($ip_addr); - Bugzilla->dbh->do( - 'DELETE FROM login_failure WHERE user_id = ? AND ip_addr = ?', - undef, $self->id, $ip_addr); - delete $self->{account_ip_login_failures}; + my $self = shift; + my $ip_addr = remote_ip(); + trick_taint($ip_addr); + Bugzilla->dbh->do('DELETE FROM login_failure WHERE user_id = ? AND ip_addr = ?', + undef, $self->id, $ip_addr); + delete $self->{account_ip_login_failures}; } sub account_ip_login_failures { - my $self = shift; - my $dbh = Bugzilla->dbh; - my $time = $dbh->sql_date_math('LOCALTIMESTAMP(0)', '-', - LOGIN_LOCKOUT_INTERVAL, 'MINUTE'); - my $ip_addr = remote_ip(); - trick_taint($ip_addr); - $self->{account_ip_login_failures} ||= Bugzilla->dbh->selectall_arrayref( - "SELECT login_time, ip_addr, user_id FROM login_failure + my $self = shift; + my $dbh = Bugzilla->dbh; + my $time = $dbh->sql_date_math('LOCALTIMESTAMP(0)', '-', LOGIN_LOCKOUT_INTERVAL, + 'MINUTE'); + my $ip_addr = remote_ip(); + trick_taint($ip_addr); + $self->{account_ip_login_failures} ||= Bugzilla->dbh->selectall_arrayref( + "SELECT login_time, ip_addr, user_id FROM login_failure WHERE user_id = ? AND login_time > $time AND ip_addr = ? - ORDER BY login_time", {Slice => {}}, $self->id, $ip_addr); - return $self->{account_ip_login_failures}; + ORDER BY login_time", {Slice => {}}, $self->id, $ip_addr + ); + return $self->{account_ip_login_failures}; } sub check_current_password { - my $self = shift; - my $password = shift || ThrowUserError("current_password_required"); + my $self = shift; + my $password = shift || ThrowUserError("current_password_required"); - my $cryptpwd - = $self->cryptpassword || ThrowCodeError("unable_to_retrieve_password"); + my $cryptpwd + = $self->cryptpassword || ThrowCodeError("unable_to_retrieve_password"); - if (bz_crypt($password, $cryptpwd) ne $cryptpwd) { - ThrowUserError("current_password_incorrect"); - } + if (bz_crypt($password, $cryptpwd) ne $cryptpwd) { + ThrowUserError("current_password_incorrect"); + } } @@ -2424,161 +2530,179 @@ sub check_current_password { ############### sub is_available_email { - my ($email, $old_email) = @_; + my ($email, $old_email) = @_; - return 0 if email_to_id($email); + return 0 if email_to_id($email); - my $dbh = Bugzilla->dbh; - # $email is safe because it is only used in SELECT placeholders. - trick_taint($email); - # Reject if the new email is part of an email change which is - # still in progress - # - # substring/locate stuff: bug 165221; this used to use regexes, but that - # was unsafe and required weird escaping; using substring to pull out - # the new/old email addresses and sql_position() to find the delimiter (':') - # is cleaner/safer - my ($tokentype, $eventdata) = $dbh->selectrow_array( - "SELECT tokentype, eventdata + my $dbh = Bugzilla->dbh; + + # $email is safe because it is only used in SELECT placeholders. + trick_taint($email); + + # Reject if the new email is part of an email change which is + # still in progress + # + # substring/locate stuff: bug 165221; this used to use regexes, but that + # was unsafe and required weird escaping; using substring to pull out + # the new/old email addresses and sql_position() to find the delimiter (':') + # is cleaner/safer + my ($tokentype, $eventdata) = $dbh->selectrow_array( + "SELECT tokentype, eventdata FROM tokens WHERE (tokentype = 'emailold' - AND SUBSTRING(eventdata, 1, (" . - $dbh->sql_position(q{':'}, 'eventdata') . "- 1)) = ?) + AND SUBSTRING(eventdata, 1, (" + . $dbh->sql_position(q{':'}, 'eventdata') . "- 1)) = ?) OR (tokentype = 'emailnew' - AND SUBSTRING(eventdata, (" . - $dbh->sql_position(q{':'}, 'eventdata') . "+ 1), LENGTH(eventdata)) = ?)", - undef, ($email, $email)); - - if ($eventdata) { - # Allow thru owner of token - if ($old_email - && (($tokentype eq 'emailnew' && $eventdata eq "$old_email:$email") - || ($tokentype eq 'emailold' && $eventdata eq "$email:$old_email"))) - { - return 1; - } - return 0; + AND SUBSTRING(eventdata, (" + . $dbh->sql_position(q{':'}, 'eventdata') + . "+ 1), LENGTH(eventdata)) = ?)", + undef, ($email, $email) + ); + + if ($eventdata) { + + # Allow thru owner of token + if ( + $old_email + && ( ($tokentype eq 'emailnew' && $eventdata eq "$old_email:$email") + || ($tokentype eq 'emailold' && $eventdata eq "$email:$old_email")) + ) + { + return 1; } + return 0; + } - return 1; + return 1; } sub check_account_creation_enabled { - my $self = shift; + my $self = shift; - # If we're using e.g. LDAP for login, then we can't create a new account. - $self->authorizer->user_can_create_account - || ThrowUserError('auth_cant_create_account'); + # If we're using e.g. LDAP for login, then we can't create a new account. + $self->authorizer->user_can_create_account + || ThrowUserError('auth_cant_create_account'); - Bugzilla->params->{'createemailregexp'} - || ThrowUserError('account_creation_disabled'); + Bugzilla->params->{'createemailregexp'} + || ThrowUserError('account_creation_disabled'); } sub check_and_send_account_creation_confirmation { - my ($self, $login, $email) = @_; - my $class = ref($self) || $self; - my $dbh = Bugzilla->dbh; + my ($self, $login, $email) = @_; + my $class = ref($self) || $self; + my $dbh = Bugzilla->dbh; - $dbh->bz_start_transaction; + $dbh->bz_start_transaction; - $email = $class->check_email($email); - $login = $class->check_login_name($login, undef, { email => $email }); - my $creation_regexp = Bugzilla->params->{'createemailregexp'}; + $email = $class->check_email($email); + $login = $class->check_login_name($login, undef, {email => $email}); + my $creation_regexp = Bugzilla->params->{'createemailregexp'}; - if ($email !~ /$creation_regexp/i) { - ThrowUserError('account_creation_restricted'); - } + if ($email !~ /$creation_regexp/i) { + ThrowUserError('account_creation_restricted'); + } - # Allow extensions to do extra checks. - Bugzilla::Hook::process('user_check_account_creation', - { login => $login, email => $email }); + # Allow extensions to do extra checks. + Bugzilla::Hook::process('user_check_account_creation', + {login => $login, email => $email}); - # Create and send a token for this new account. - require Bugzilla::Token; - Bugzilla::Token::issue_new_user_account_token($login, $email); + # Create and send a token for this new account. + require Bugzilla::Token; + Bugzilla::Token::issue_new_user_account_token($login, $email); - $dbh->bz_commit_transaction; + $dbh->bz_commit_transaction; } # This is used in a few performance-critical areas where we don't want to # do check() and pull all the user data from the database. sub login_to_id { - my ($login, $throw_error) = @_; - my $dbh = Bugzilla->dbh; - my $cache = Bugzilla->request_cache->{user_login_to_id} ||= {}; - - # We cache lookups because this function showed up as taking up a - # significant amount of time in profiles of xt/search.t. However, - # for users that don't exist, we re-do the check every time. - my $user_id; - if (defined $cache->{$login}) { - $user_id = $cache->{$login}; - } - else { - trick_taint($login); - $user_id = $dbh->selectrow_array( - "SELECT userid FROM profiles - WHERE " . $dbh->sql_istrcmp('login_name', '?'), undef, $login); - $cache->{$login} = $user_id; - } - - if ($user_id) { - return $user_id; - } elsif ($throw_error) { - ThrowUserError('invalid_username', { name => $login }); - } else { - return 0; - } + my ($login, $throw_error) = @_; + my $dbh = Bugzilla->dbh; + my $cache = Bugzilla->request_cache->{user_login_to_id} ||= {}; + + # We cache lookups because this function showed up as taking up a + # significant amount of time in profiles of xt/search.t. However, + # for users that don't exist, we re-do the check every time. + my $user_id; + if (defined $cache->{$login}) { + $user_id = $cache->{$login}; + } + else { + trick_taint($login); + $user_id = $dbh->selectrow_array( + "SELECT userid FROM profiles + WHERE " . $dbh->sql_istrcmp('login_name', '?'), undef, $login + ); + $cache->{$login} = $user_id; + } + + if ($user_id) { + return $user_id; + } + elsif ($throw_error) { + ThrowUserError('invalid_username', {name => $login}); + } + else { + return 0; + } } sub email_to_id { - my ($email, $throw_error) = @_; - my $dbh = Bugzilla->dbh; - trick_taint($email); - my $user_id = $dbh->selectrow_array("SELECT userid FROM profiles WHERE " . - $dbh->sql_istrcmp('email', '?'), - undef, $email); - if ($user_id) { - return $user_id; - } - elsif ($throw_error) { - ThrowUserError('invalid_email', { email => $email }); - } - else { - return 0; - } + my ($email, $throw_error) = @_; + my $dbh = Bugzilla->dbh; + trick_taint($email); + my $user_id + = $dbh->selectrow_array( + "SELECT userid FROM profiles WHERE " . $dbh->sql_istrcmp('email', '?'), + undef, $email); + if ($user_id) { + return $user_id; + } + elsif ($throw_error) { + ThrowUserError('invalid_email', {email => $email}); + } + else { + return 0; + } } sub validate_password { - my $check = validate_password_check(@_); - ThrowUserError($check) if $check; - return 1; + my $check = validate_password_check(@_); + ThrowUserError($check) if $check; + return 1; } sub validate_password_check { - my ($password, $matchpassword) = @_; - - if (length($password) < USER_PASSWORD_MIN_LENGTH) { - return 'password_too_short'; - } elsif ((defined $matchpassword) && ($password ne $matchpassword)) { - return 'passwords_dont_match'; - } - - my $complexity_level = Bugzilla->params->{password_complexity}; - if ($complexity_level eq 'letters_numbers_specialchars') { - return 'password_not_complex' - if ($password !~ /[[:alpha:]]/ || $password !~ /\d/ || $password !~ /[[:punct:]]/); - } elsif ($complexity_level eq 'letters_numbers') { - return 'password_not_complex' - if ($password !~ /[[:lower:]]/ || $password !~ /[[:upper:]]/ || $password !~ /\d/); - } elsif ($complexity_level eq 'mixed_letters') { - return 'password_not_complex' - if ($password !~ /[[:lower:]]/ || $password !~ /[[:upper:]]/); - } - - # Having done these checks makes us consider the password untainted. - trick_taint($_[0]); - return; + my ($password, $matchpassword) = @_; + + if (length($password) < USER_PASSWORD_MIN_LENGTH) { + return 'password_too_short'; + } + elsif ((defined $matchpassword) && ($password ne $matchpassword)) { + return 'passwords_dont_match'; + } + + my $complexity_level = Bugzilla->params->{password_complexity}; + if ($complexity_level eq 'letters_numbers_specialchars') { + return 'password_not_complex' + if ($password !~ /[[:alpha:]]/ + || $password !~ /\d/ + || $password !~ /[[:punct:]]/); + } + elsif ($complexity_level eq 'letters_numbers') { + return 'password_not_complex' + if ($password !~ /[[:lower:]]/ + || $password !~ /[[:upper:]]/ + || $password !~ /\d/); + } + elsif ($complexity_level eq 'mixed_letters') { + return 'password_not_complex' + if ($password !~ /[[:lower:]]/ || $password !~ /[[:upper:]]/); + } + + # Having done these checks makes us consider the password untainted. + trick_taint($_[0]); + return; } diff --git a/Bugzilla/User/APIKey.pm b/Bugzilla/User/APIKey.pm index 41a091193e..2ce8a256de 100644 --- a/Bugzilla/User/APIKey.pm +++ b/Bugzilla/User/APIKey.pm @@ -20,52 +20,54 @@ use Bugzilla::Util qw(generate_random_password trim); # Overridden Constants that are used as methods ##################################################################### -use constant DB_TABLE => 'user_api_keys'; -use constant DB_COLUMNS => qw( - id - user_id - api_key - description - revoked - last_used +use constant DB_TABLE => 'user_api_keys'; +use constant DB_COLUMNS => qw( + id + user_id + api_key + description + revoked + last_used ); use constant UPDATE_COLUMNS => qw(description revoked last_used); use constant VALIDATORS => { - api_key => \&_check_api_key, - description => \&_check_description, - revoked => \&Bugzilla::Object::check_boolean, + api_key => \&_check_api_key, + description => \&_check_description, + revoked => \&Bugzilla::Object::check_boolean, }; -use constant LIST_ORDER => 'id'; -use constant NAME_FIELD => 'api_key'; +use constant LIST_ORDER => 'id'; +use constant NAME_FIELD => 'api_key'; # turn off auditing and exclude these objects from memcached -use constant { AUDIT_CREATES => 0, - AUDIT_UPDATES => 0, - AUDIT_REMOVES => 0, - USE_MEMCACHED => 0 }; +use constant { + AUDIT_CREATES => 0, + AUDIT_UPDATES => 0, + AUDIT_REMOVES => 0, + USE_MEMCACHED => 0 +}; # Accessors -sub id { return $_[0]->{id} } -sub user_id { return $_[0]->{user_id} } -sub api_key { return $_[0]->{api_key} } -sub description { return $_[0]->{description} } -sub revoked { return $_[0]->{revoked} } -sub last_used { return $_[0]->{last_used} } +sub id { return $_[0]->{id} } +sub user_id { return $_[0]->{user_id} } +sub api_key { return $_[0]->{api_key} } +sub description { return $_[0]->{description} } +sub revoked { return $_[0]->{revoked} } +sub last_used { return $_[0]->{last_used} } # Helpers sub user { - my $self = shift; - $self->{user} //= Bugzilla::User->new({name => $self->user_id, cache => 1}); - return $self->{user}; + my $self = shift; + $self->{user} //= Bugzilla::User->new({name => $self->user_id, cache => 1}); + return $self->{user}; } sub update_last_used { - my $self = shift; - my $timestamp = shift - || Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); - $self->set('last_used', $timestamp); - $self->update; + my $self = shift; + my $timestamp + = shift || Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); + $self->set('last_used', $timestamp); + $self->update; } # Setters @@ -73,8 +75,8 @@ sub set_description { $_[0]->set('description', $_[1]); } sub set_revoked { $_[0]->set('revoked', $_[1]); } # Validators -sub _check_api_key { return generate_random_password(40); } -sub _check_description { return trim($_[1]) || ''; } +sub _check_api_key { return generate_random_password(40); } +sub _check_description { return trim($_[1]) || ''; } 1; __END__ diff --git a/Bugzilla/User/Setting.pm b/Bugzilla/User/Setting.pm index d0bf1f15b9..f55515bae6 100644 --- a/Bugzilla/User/Setting.pm +++ b/Bugzilla/User/Setting.pm @@ -17,10 +17,10 @@ use parent qw(Exporter); # Module stuff @Bugzilla::User::Setting::EXPORT = qw( - get_all_settings - get_defaults - add_setting - clear_settings_cache + get_all_settings + get_defaults + add_setting + clear_settings_cache ); use Bugzilla::Error; @@ -31,88 +31,84 @@ use Bugzilla::Util qw(trick_taint get_text); ############################### sub new { - my $invocant = shift; - my $setting_name = shift; - my $user_id = shift; - - my $class = ref($invocant) || $invocant; - my $subclass = ''; - - # Create a ref to an empty hash and bless it - my $self = {}; - - my $dbh = Bugzilla->dbh; - - # Confirm that the $setting_name is properly formed; - # if not, throw a code error. - # - # NOTE: due to the way that setting names are used in templates, - # they must conform to to the limitations set for HTML NAMEs and IDs. - # - if ( !($setting_name =~ /^[a-zA-Z][-.:\w]*$/) ) { - ThrowCodeError("setting_name_invalid", { name => $setting_name }); - } - - # If there were only two parameters passed in, then we need - # to retrieve the information for this setting ourselves. - if (scalar @_ == 0) { - - my ($default, $is_enabled, $value); - ($default, $is_enabled, $value, $subclass) = - $dbh->selectrow_array( - q{SELECT default_value, is_enabled, setting_value, subclass + my $invocant = shift; + my $setting_name = shift; + my $user_id = shift; + + my $class = ref($invocant) || $invocant; + my $subclass = ''; + + # Create a ref to an empty hash and bless it + my $self = {}; + + my $dbh = Bugzilla->dbh; + + # Confirm that the $setting_name is properly formed; + # if not, throw a code error. + # + # NOTE: due to the way that setting names are used in templates, + # they must conform to to the limitations set for HTML NAMEs and IDs. + # + if (!($setting_name =~ /^[a-zA-Z][-.:\w]*$/)) { + ThrowCodeError("setting_name_invalid", {name => $setting_name}); + } + + # If there were only two parameters passed in, then we need + # to retrieve the information for this setting ourselves. + if (scalar @_ == 0) { + + my ($default, $is_enabled, $value); + ($default, $is_enabled, $value, $subclass) = $dbh->selectrow_array( + q{SELECT default_value, is_enabled, setting_value, subclass FROM setting LEFT JOIN profile_setting ON setting.name = profile_setting.setting_name WHERE name = ? - AND profile_setting.user_id = ?}, - undef, - $setting_name, $user_id); - - # if not defined, then grab the default value - if (! defined $value) { - ($default, $is_enabled, $subclass) = - $dbh->selectrow_array( - q{SELECT default_value, is_enabled, subclass + AND profile_setting.user_id = ?}, undef, $setting_name, $user_id + ); + + # if not defined, then grab the default value + if (!defined $value) { + ($default, $is_enabled, $subclass) = $dbh->selectrow_array( + q{SELECT default_value, is_enabled, subclass FROM setting - WHERE name = ?}, - undef, - $setting_name); - } - - $self->{'is_enabled'} = $is_enabled; - $self->{'default_value'} = $default; - - # IF the setting is enabled, AND the user has chosen a setting - # THEN return that value - # ELSE return the site default, and note that it is the default. - if ( ($is_enabled) && (defined $value) ) { - $self->{'value'} = $value; - } else { - $self->{'value'} = $default; - $self->{'isdefault'} = 1; - } - } - else { - # If the values were passed in, simply assign them and return. - $self->{'is_enabled'} = shift; - $self->{'default_value'} = shift; - $self->{'value'} = shift; - $self->{'is_default'} = shift; - $subclass = shift; - } - if ($subclass) { - eval('require ' . $class . '::' . $subclass); - $@ && ThrowCodeError('setting_subclass_invalid', - {'subclass' => $subclass}); - $class = $class . '::' . $subclass; + WHERE name = ?}, undef, $setting_name + ); } - bless($self, $class); - $self->{'_setting_name'} = $setting_name; - $self->{'_user_id'} = $user_id; + $self->{'is_enabled'} = $is_enabled; + $self->{'default_value'} = $default; - return $self; + # IF the setting is enabled, AND the user has chosen a setting + # THEN return that value + # ELSE return the site default, and note that it is the default. + if (($is_enabled) && (defined $value)) { + $self->{'value'} = $value; + } + else { + $self->{'value'} = $default; + $self->{'isdefault'} = 1; + } + } + else { + # If the values were passed in, simply assign them and return. + $self->{'is_enabled'} = shift; + $self->{'default_value'} = shift; + $self->{'value'} = shift; + $self->{'is_default'} = shift; + $subclass = shift; + } + if ($subclass) { + eval('require ' . $class . '::' . $subclass); + $@ && ThrowCodeError('setting_subclass_invalid', {'subclass' => $subclass}); + $class = $class . '::' . $subclass; + } + bless($self, $class); + + $self->{'_setting_name'} = $setting_name; + $self->{'_user_id'} = $user_id; + + return $self; } ############################### @@ -120,191 +116,205 @@ sub new { ############################### sub add_setting { - my ($name, $values, $default_value, $subclass, $force_check, - $silently) = @_; - my $dbh = Bugzilla->dbh; - - my $exists = _setting_exists($name); - return if ($exists && !$force_check); - - ($name && length( $default_value // '' )) - || ThrowCodeError("setting_info_invalid"); - - if ($exists) { - # If this setting exists, we delete it and regenerate it. - $dbh->do('DELETE FROM setting_value WHERE name = ?', undef, $name); - $dbh->do('DELETE FROM setting WHERE name = ?', undef, $name); - # Remove obsolete user preferences for this setting. - if (defined $values && scalar(@$values)) { - my $list = join(', ', map {$dbh->quote($_)} @$values); - $dbh->do("DELETE FROM profile_setting - WHERE setting_name = ? AND setting_value NOT IN ($list)", - undef, $name); - } - } - elsif (!$silently) { - print get_text('install_setting_new', { name => $name }) . "\n"; - } - $dbh->do(q{INSERT INTO setting (name, default_value, is_enabled, subclass) - VALUES (?, ?, 1, ?)}, - undef, ($name, $default_value, $subclass)); + my ($name, $values, $default_value, $subclass, $force_check, $silently) = @_; + my $dbh = Bugzilla->dbh; + + my $exists = _setting_exists($name); + return if ($exists && !$force_check); + + ($name && length($default_value // '')) + || ThrowCodeError("setting_info_invalid"); - my $sth = $dbh->prepare(q{INSERT INTO setting_value (name, value, sortindex) - VALUES (?, ?, ?)}); + if ($exists) { - my $sortindex = 5; - foreach my $key (@$values){ - $sth->execute($name, $key, $sortindex); - $sortindex += 5; + # If this setting exists, we delete it and regenerate it. + $dbh->do('DELETE FROM setting_value WHERE name = ?', undef, $name); + $dbh->do('DELETE FROM setting WHERE name = ?', undef, $name); + + # Remove obsolete user preferences for this setting. + if (defined $values && scalar(@$values)) { + my $list = join(', ', map { $dbh->quote($_) } @$values); + $dbh->do( + "DELETE FROM profile_setting + WHERE setting_name = ? AND setting_value NOT IN ($list)", undef, + $name + ); } + } + elsif (!$silently) { + print get_text('install_setting_new', {name => $name}) . "\n"; + } + $dbh->do( + q{INSERT INTO setting (name, default_value, is_enabled, subclass) + VALUES (?, ?, 1, ?)}, undef, ($name, $default_value, $subclass) + ); + + my $sth = $dbh->prepare( + q{INSERT INTO setting_value (name, value, sortindex) + VALUES (?, ?, ?)} + ); + + my $sortindex = 5; + foreach my $key (@$values) { + $sth->execute($name, $key, $sortindex); + $sortindex += 5; + } } sub get_all_settings { - my ($user_id) = @_; - my $settings = {}; - my $dbh = Bugzilla->dbh; - - my $cache_key = "user_settings.$user_id"; - my $rows = Bugzilla->memcached->get_config({ key => $cache_key }); - if (!$rows) { - $rows = $dbh->selectall_arrayref( - q{SELECT name, default_value, is_enabled, setting_value, subclass + my ($user_id) = @_; + my $settings = {}; + my $dbh = Bugzilla->dbh; + + my $cache_key = "user_settings.$user_id"; + my $rows = Bugzilla->memcached->get_config({key => $cache_key}); + if (!$rows) { + $rows = $dbh->selectall_arrayref( + q{SELECT name, default_value, is_enabled, setting_value, subclass FROM setting LEFT JOIN profile_setting ON setting.name = profile_setting.setting_name - AND profile_setting.user_id = ?}, undef, ($user_id)); - Bugzilla->memcached->set_config({ key => $cache_key, data => $rows }); - } + AND profile_setting.user_id = ?}, undef, ($user_id) + ); + Bugzilla->memcached->set_config({key => $cache_key, data => $rows}); + } - foreach my $row (@$rows) { - my ($name, $default_value, $is_enabled, $value, $subclass) = @$row; + foreach my $row (@$rows) { + my ($name, $default_value, $is_enabled, $value, $subclass) = @$row; - my $is_default; + my $is_default; - if ( ($is_enabled) && (defined $value) ) { - $is_default = 0; - } else { - $value = $default_value; - $is_default = 1; - } - - $settings->{$name} = new Bugzilla::User::Setting( - $name, $user_id, $is_enabled, - $default_value, $value, $is_default, $subclass); + if (($is_enabled) && (defined $value)) { + $is_default = 0; } + else { + $value = $default_value; + $is_default = 1; + } + + $settings->{$name} + = new Bugzilla::User::Setting($name, $user_id, $is_enabled, $default_value, + $value, $is_default, $subclass); + } - return $settings; + return $settings; } sub clear_settings_cache { - my ($user_id) = @_; - Bugzilla->memcached->clear_config({ key => "user_settings.$user_id" }); + my ($user_id) = @_; + Bugzilla->memcached->clear_config({key => "user_settings.$user_id"}); } sub get_defaults { - my ($user_id) = @_; - my $dbh = Bugzilla->dbh; - my $default_settings = {}; + my ($user_id) = @_; + my $dbh = Bugzilla->dbh; + my $default_settings = {}; - $user_id ||= 0; + $user_id ||= 0; - my $rows = $dbh->selectall_arrayref(q{SELECT name, default_value, is_enabled, subclass - FROM setting}); + my $rows = $dbh->selectall_arrayref( + q{SELECT name, default_value, is_enabled, subclass + FROM setting} + ); - foreach my $row (@$rows) { - my ($name, $default_value, $is_enabled, $subclass) = @$row; + foreach my $row (@$rows) { + my ($name, $default_value, $is_enabled, $subclass) = @$row; - $default_settings->{$name} = new Bugzilla::User::Setting( - $name, $user_id, $is_enabled, $default_value, $default_value, 1, - $subclass); - } + $default_settings->{$name} + = new Bugzilla::User::Setting($name, $user_id, $is_enabled, $default_value, + $default_value, 1, $subclass); + } - return $default_settings; + return $default_settings; } sub set_default { - my ($setting_name, $default_value, $is_enabled) = @_; - my $dbh = Bugzilla->dbh; + my ($setting_name, $default_value, $is_enabled) = @_; + my $dbh = Bugzilla->dbh; - my $sth = $dbh->prepare(q{UPDATE setting + my $sth = $dbh->prepare( + q{UPDATE setting SET default_value = ?, is_enabled = ? - WHERE name = ?}); - $sth->execute($default_value, $is_enabled, $setting_name); + WHERE name = ?} + ); + $sth->execute($default_value, $is_enabled, $setting_name); } sub _setting_exists { - my ($setting_name) = @_; - my $dbh = Bugzilla->dbh; - return $dbh->selectrow_arrayref( - "SELECT 1 FROM setting WHERE name = ?", undef, $setting_name) || 0; + my ($setting_name) = @_; + my $dbh = Bugzilla->dbh; + return $dbh->selectrow_arrayref("SELECT 1 FROM setting WHERE name = ?", + undef, $setting_name) + || 0; } sub legal_values { - my ($self) = @_; + my ($self) = @_; - return $self->{'legal_values'} if defined $self->{'legal_values'}; + return $self->{'legal_values'} if defined $self->{'legal_values'}; - my $dbh = Bugzilla->dbh; - $self->{'legal_values'} = $dbh->selectcol_arrayref( - q{SELECT value + my $dbh = Bugzilla->dbh; + $self->{'legal_values'} = $dbh->selectcol_arrayref( + q{SELECT value FROM setting_value WHERE name = ? - ORDER BY sortindex}, - undef, $self->{'_setting_name'}); + ORDER BY sortindex}, undef, $self->{'_setting_name'} + ); - return $self->{'legal_values'}; + return $self->{'legal_values'}; } sub validate_value { - my $self = shift; - - if (grep(/^$_[0]$/, @{$self->legal_values()})) { - trick_taint($_[0]); - } - else { - ThrowCodeError('setting_value_invalid', - {'name' => $self->{'_setting_name'}, - 'value' => $_[0]}); - } + my $self = shift; + + if (grep(/^$_[0]$/, @{$self->legal_values()})) { + trick_taint($_[0]); + } + else { + ThrowCodeError('setting_value_invalid', + {'name' => $self->{'_setting_name'}, 'value' => $_[0]}); + } } sub reset_to_default { - my ($self) = @_; + my ($self) = @_; - my $dbh = Bugzilla->dbh; - my $sth = $dbh->do(q{ DELETE + my $dbh = Bugzilla->dbh; + my $sth = $dbh->do( + q{ DELETE FROM profile_setting WHERE setting_name = ? - AND user_id = ?}, - undef, $self->{'_setting_name'}, $self->{'_user_id'}); - $self->{'value'} = $self->{'default_value'}; - $self->{'is_default'} = 1; + AND user_id = ?}, undef, $self->{'_setting_name'}, + $self->{'_user_id'} + ); + $self->{'value'} = $self->{'default_value'}; + $self->{'is_default'} = 1; } sub set { - my ($self, $value) = @_; - my $dbh = Bugzilla->dbh; - my $query; + my ($self, $value) = @_; + my $dbh = Bugzilla->dbh; + my $query; - if ($self->{'is_default'}) { - $query = q{INSERT INTO profile_setting + if ($self->{'is_default'}) { + $query = q{INSERT INTO profile_setting (setting_value, setting_name, user_id) VALUES (?,?,?)}; - } else { - $query = q{UPDATE profile_setting + } + else { + $query = q{UPDATE profile_setting SET setting_value = ? WHERE setting_name = ? AND user_id = ?}; - } - $dbh->do($query, undef, $value, $self->{'_setting_name'}, $self->{'_user_id'}); + } + $dbh->do($query, undef, $value, $self->{'_setting_name'}, $self->{'_user_id'}); - $self->{'value'} = $value; - $self->{'is_default'} = 0; + $self->{'value'} = $value; + $self->{'is_default'} = 0; } - 1; __END__ diff --git a/Bugzilla/User/Setting/Lang.pm b/Bugzilla/User/Setting/Lang.pm index ba144aa309..d413910c29 100644 --- a/Bugzilla/User/Setting/Lang.pm +++ b/Bugzilla/User/Setting/Lang.pm @@ -16,11 +16,11 @@ use parent qw(Bugzilla::User::Setting); use Bugzilla::Constants; sub legal_values { - my ($self) = @_; + my ($self) = @_; - return $self->{'legal_values'} if defined $self->{'legal_values'}; + return $self->{'legal_values'} if defined $self->{'legal_values'}; - return $self->{'legal_values'} = Bugzilla->languages; + return $self->{'legal_values'} = Bugzilla->languages; } 1; diff --git a/Bugzilla/User/Setting/Skin.pm b/Bugzilla/User/Setting/Skin.pm index 81ce6fd2c3..0eb97d389a 100644 --- a/Bugzilla/User/Setting/Skin.pm +++ b/Bugzilla/User/Setting/Skin.pm @@ -21,24 +21,26 @@ use File::Basename; use constant BUILTIN_SKIN_NAMES => ['standard']; sub legal_values { - my ($self) = @_; + my ($self) = @_; - return $self->{'legal_values'} if defined $self->{'legal_values'}; + return $self->{'legal_values'} if defined $self->{'legal_values'}; - my $dirbase = bz_locations()->{'skinsdir'} . '/contrib'; - # Avoid modification of the list BUILTIN_SKIN_NAMES points to by copying the - # list over instead of simply writing $legal_values = BUILTIN_SKIN_NAMES. - my @legal_values = @{(BUILTIN_SKIN_NAMES)}; + my $dirbase = bz_locations()->{'skinsdir'} . '/contrib'; - foreach my $direntry (glob(catdir($dirbase, '*'))) { - if (-d $direntry) { - next if basename($direntry) =~ /^cvs$/i; - # Stylesheet set found - push(@legal_values, basename($direntry)); - } + # Avoid modification of the list BUILTIN_SKIN_NAMES points to by copying the + # list over instead of simply writing $legal_values = BUILTIN_SKIN_NAMES. + my @legal_values = @{(BUILTIN_SKIN_NAMES)}; + + foreach my $direntry (glob(catdir($dirbase, '*'))) { + if (-d $direntry) { + next if basename($direntry) =~ /^cvs$/i; + + # Stylesheet set found + push(@legal_values, basename($direntry)); } + } - return $self->{'legal_values'} = \@legal_values; + return $self->{'legal_values'} = \@legal_values; } 1; diff --git a/Bugzilla/User/Setting/Timezone.pm b/Bugzilla/User/Setting/Timezone.pm index 2810395e84..508f448ed1 100644 --- a/Bugzilla/User/Setting/Timezone.pm +++ b/Bugzilla/User/Setting/Timezone.pm @@ -18,19 +18,21 @@ use parent qw(Bugzilla::User::Setting); use Bugzilla::Constants; sub legal_values { - my ($self) = @_; + my ($self) = @_; - return $self->{'legal_values'} if defined $self->{'legal_values'}; + return $self->{'legal_values'} if defined $self->{'legal_values'}; - my @timezones = DateTime::TimeZone->all_names; - # Remove old formats, such as CST6CDT, EST, EST5EDT. - @timezones = grep { $_ =~ m#.+/.+#} @timezones; - # Append 'local' to the list, which will use the timezone - # given by the server. - push(@timezones, 'local'); - push(@timezones, 'UTC'); + my @timezones = DateTime::TimeZone->all_names; - return $self->{'legal_values'} = \@timezones; + # Remove old formats, such as CST6CDT, EST, EST5EDT. + @timezones = grep { $_ =~ m#.+/.+# } @timezones; + + # Append 'local' to the list, which will use the timezone + # given by the server. + push(@timezones, 'local'); + push(@timezones, 'UTC'); + + return $self->{'legal_values'} = \@timezones; } 1; diff --git a/Bugzilla/UserAgent.pm b/Bugzilla/UserAgent.pm index 22e2639a30..c677003dee 100644 --- a/Bugzilla/UserAgent.pm +++ b/Bugzilla/UserAgent.pm @@ -20,176 +20,200 @@ use List::MoreUtils qw(natatime); use constant DEFAULT_VALUE => 'Other'; use constant PLATFORMS_MAP => ( - # PowerPC - qr/\(.*PowerPC.*\)/i => ["PowerPC", "Macintosh"], - # AMD64, Intel x86_64 - qr/\(.*[ix0-9]86 (?:on |\()x86_64.*\)/ => ["IA32", "x86", "PC"], - qr/\(.*amd64.*\)/ => ["AMD64", "x86_64", "PC"], - qr/\(.*x86_64.*\)/ => ["AMD64", "x86_64", "PC"], - # Intel IA64 - qr/\(.*IA64.*\)/ => ["IA64", "PC"], - # Intel x86 - qr/\(.*Intel.*\)/ => ["IA32", "x86", "PC"], - qr/\(.*[ix0-9]86.*\)/ => ["IA32", "x86", "PC"], - # Versions of Windows that only run on Intel x86 - qr/\(.*Win(?:dows |)[39M].*\)/ => ["IA32", "x86", "PC"], - qr/\(.*Win(?:dows |)16.*\)/ => ["IA32", "x86", "PC"], - # Sparc - qr/\(.*sparc.*\)/ => ["Sparc", "Sun"], - qr/\(.*sun4.*\)/ => ["Sparc", "Sun"], - # Alpha - qr/\(.*AXP.*\)/i => ["Alpha", "DEC"], - qr/\(.*[ _]Alpha.\D/i => ["Alpha", "DEC"], - qr/\(.*[ _]Alpha\)/i => ["Alpha", "DEC"], - # MIPS - qr/\(.*IRIX.*\)/i => ["MIPS", "SGI"], - qr/\(.*MIPS.*\)/i => ["MIPS", "SGI"], - # 68k - qr/\(.*68K.*\)/ => ["68k", "Macintosh"], - qr/\(.*680[x0]0.*\)/ => ["68k", "Macintosh"], - # HP - qr/\(.*9000.*\)/ => ["PA-RISC", "HP"], - # ARM - qr/\(.*(?:iPod|iPad|iPhone).*\)/ => ["ARM"], - qr/\(.*ARM.*\)/ => ["ARM", "PocketPC"], - # PocketPC intentionally before PowerPC - qr/\(.*Windows CE.*PPC.*\)/ => ["ARM", "PocketPC"], - # PowerPC - qr/\(.*PPC.*\)/ => ["PowerPC", "Macintosh"], - qr/\(.*AIX.*\)/ => ["PowerPC", "Macintosh"], - # Stereotypical and broken - qr/\(.*Windows CE.*\)/ => ["ARM", "PocketPC"], - qr/\(.*Macintosh.*\)/ => ["68k", "Macintosh"], - qr/\(.*Mac OS [89].*\)/ => ["68k", "Macintosh"], - qr/\(.*WOW64.*\)/ => ["x86_64"], - qr/\(.*Win64.*\)/ => ["IA64"], - qr/\(Win.*\)/ => ["IA32", "x86", "PC"], - qr/\(.*Win(?:dows[ -])NT.*\)/ => ["IA32", "x86", "PC"], - qr/\(.*OSF.*\)/ => ["Alpha", "DEC"], - qr/\(.*HP-?UX.*\)/i => ["PA-RISC", "HP"], - qr/\(.*IRIX.*\)/i => ["MIPS", "SGI"], - qr/\(.*(SunOS|Solaris).*\)/ => ["Sparc", "Sun"], - # Braindead old browsers who didn't follow convention: - qr/Amiga/ => ["68k", "Macintosh"], - qr/WinMosaic/ => ["IA32", "x86", "PC"], + + # PowerPC + qr/\(.*PowerPC.*\)/i => ["PowerPC", "Macintosh"], + + # AMD64, Intel x86_64 + qr/\(.*[ix0-9]86 (?:on |\()x86_64.*\)/ => ["IA32", "x86", "PC"], + qr/\(.*amd64.*\)/ => ["AMD64", "x86_64", "PC"], + qr/\(.*x86_64.*\)/ => ["AMD64", "x86_64", "PC"], + + # Intel IA64 + qr/\(.*IA64.*\)/ => ["IA64", "PC"], + + # Intel x86 + qr/\(.*Intel.*\)/ => ["IA32", "x86", "PC"], + qr/\(.*[ix0-9]86.*\)/ => ["IA32", "x86", "PC"], + + # Versions of Windows that only run on Intel x86 + qr/\(.*Win(?:dows |)[39M].*\)/ => ["IA32", "x86", "PC"], + qr/\(.*Win(?:dows |)16.*\)/ => ["IA32", "x86", "PC"], + + # Sparc + qr/\(.*sparc.*\)/ => ["Sparc", "Sun"], + qr/\(.*sun4.*\)/ => ["Sparc", "Sun"], + + # Alpha + qr/\(.*AXP.*\)/i => ["Alpha", "DEC"], + qr/\(.*[ _]Alpha.\D/i => ["Alpha", "DEC"], + qr/\(.*[ _]Alpha\)/i => ["Alpha", "DEC"], + + # MIPS + qr/\(.*IRIX.*\)/i => ["MIPS", "SGI"], + qr/\(.*MIPS.*\)/i => ["MIPS", "SGI"], + + # 68k + qr/\(.*68K.*\)/ => ["68k", "Macintosh"], + qr/\(.*680[x0]0.*\)/ => ["68k", "Macintosh"], + + # HP + qr/\(.*9000.*\)/ => ["PA-RISC", "HP"], + + # ARM + qr/\(.*(?:iPod|iPad|iPhone).*\)/ => ["ARM"], + qr/\(.*ARM.*\)/ => ["ARM", "PocketPC"], + + # PocketPC intentionally before PowerPC + qr/\(.*Windows CE.*PPC.*\)/ => ["ARM", "PocketPC"], + + # PowerPC + qr/\(.*PPC.*\)/ => ["PowerPC", "Macintosh"], + qr/\(.*AIX.*\)/ => ["PowerPC", "Macintosh"], + + # Stereotypical and broken + qr/\(.*Windows CE.*\)/ => ["ARM", "PocketPC"], + qr/\(.*Macintosh.*\)/ => ["68k", "Macintosh"], + qr/\(.*Mac OS [89].*\)/ => ["68k", "Macintosh"], + qr/\(.*WOW64.*\)/ => ["x86_64"], + qr/\(.*Win64.*\)/ => ["IA64"], + qr/\(Win.*\)/ => ["IA32", "x86", "PC"], + qr/\(.*Win(?:dows[ -])NT.*\)/ => ["IA32", "x86", "PC"], + qr/\(.*OSF.*\)/ => ["Alpha", "DEC"], + qr/\(.*HP-?UX.*\)/i => ["PA-RISC", "HP"], + qr/\(.*IRIX.*\)/i => ["MIPS", "SGI"], + qr/\(.*(SunOS|Solaris).*\)/ => ["Sparc", "Sun"], + + # Braindead old browsers who didn't follow convention: + qr/Amiga/ => ["68k", "Macintosh"], + qr/WinMosaic/ => ["IA32", "x86", "PC"], ); use constant OS_MAP => ( - # Sun - qr/\(.*Solaris.*\)/ => ["Solaris"], - qr/\(.*SunOS 5.11.*\)/ => [("OpenSolaris", "Opensolaris", "Solaris 11")], - qr/\(.*SunOS 5.10.*\)/ => ["Solaris 10"], - qr/\(.*SunOS 5.9.*\)/ => ["Solaris 9"], - qr/\(.*SunOS 5.8.*\)/ => ["Solaris 8"], - qr/\(.*SunOS 5.7.*\)/ => ["Solaris 7"], - qr/\(.*SunOS 5.6.*\)/ => ["Solaris 6"], - qr/\(.*SunOS 5.5.*\)/ => ["Solaris 5"], - qr/\(.*SunOS 5.*\)/ => ["Solaris"], - qr/\(.*SunOS.*sun4u.*\)/ => ["Solaris"], - qr/\(.*SunOS.*i86pc.*\)/ => ["Solaris"], - qr/\(.*SunOS.*\)/ => ["SunOS"], - # BSD - qr/\(.*BSD\/(?:OS|386).*\)/ => ["BSDI"], - qr/\(.*FreeBSD.*\)/ => ["FreeBSD"], - qr/\(.*OpenBSD.*\)/ => ["OpenBSD"], - qr/\(.*NetBSD.*\)/ => ["NetBSD"], - # Misc POSIX - qr/\(.*IRIX.*\)/ => ["IRIX"], - qr/\(.*OSF.*\)/ => ["OSF/1"], - qr/\(.*Linux.*\)/ => ["Linux"], - qr/\(.*BeOS.*\)/ => ["BeOS"], - qr/\(.*AIX.*\)/ => ["AIX"], - qr/\(.*OS\/2.*\)/ => ["OS/2"], - qr/\(.*QNX.*\)/ => ["Neutrino"], - qr/\(.*VMS.*\)/ => ["OpenVMS"], - qr/\(.*HP-?UX.*\)/ => ["HP-UX"], - qr/\(.*Android.*\)/ => ["Android"], - # Windows - qr/\(.*Windows XP.*\)/ => ["Windows XP"], - qr/\(.*Windows NT 10\.0.*\)/ => ["Windows 10"], - qr/\(.*Windows NT 6\.4.*\)/ => ["Windows 10"], - qr/\(.*Windows NT 6\.3.*\)/ => ["Windows 8.1"], - qr/\(.*Windows NT 6\.2.*\)/ => ["Windows 8"], - qr/\(.*Windows NT 6\.1.*\)/ => ["Windows 7"], - qr/\(.*Windows NT 6\.0.*\)/ => ["Windows Vista"], - qr/\(.*Windows NT 5\.2.*\)/ => ["Windows Server 2003"], - qr/\(.*Windows NT 5\.1.*\)/ => ["Windows XP"], - qr/\(.*Windows 2000.*\)/ => ["Windows 2000"], - qr/\(.*Windows NT 5.*\)/ => ["Windows 2000"], - qr/\(.*Win.*9[8x].*4\.9.*\)/ => ["Windows ME"], - qr/\(.*Win(?:dows |)M[Ee].*\)/ => ["Windows ME"], - qr/\(.*Win(?:dows |)98.*\)/ => ["Windows 98"], - qr/\(.*Win(?:dows |)95.*\)/ => ["Windows 95"], - qr/\(.*Win(?:dows |)16.*\)/ => ["Windows 3.1"], - qr/\(.*Win(?:dows[ -]|)NT.*\)/ => ["Windows NT"], - qr/\(.*Windows.*NT.*\)/ => ["Windows NT"], - # OS X - qr/\(.*(?:iPad|iPhone).*OS 7.*\)/ => ["iOS 7"], - qr/\(.*(?:iPad|iPhone).*OS 6.*\)/ => ["iOS 6"], - qr/\(.*(?:iPad|iPhone).*OS 5.*\)/ => ["iOS 5"], - qr/\(.*(?:iPad|iPhone).*OS 4.*\)/ => ["iOS 4"], - qr/\(.*(?:iPad|iPhone).*OS 3.*\)/ => ["iOS 3"], - qr/\(.*(?:iPod|iPad|iPhone).*\)/ => ["iOS"], - qr/\(.*Mac OS X (?:|Mach-O |\()10.8.*\)/ => ["Mac OS X 10.8"], - qr/\(.*Mac OS X (?:|Mach-O |\()10.7.*\)/ => ["Mac OS X 10.7"], - qr/\(.*Mac OS X (?:|Mach-O |\()10.6.*\)/ => ["Mac OS X 10.6"], - qr/\(.*Mac OS X (?:|Mach-O |\()10.5.*\)/ => ["Mac OS X 10.5"], - qr/\(.*Mac OS X (?:|Mach-O |\()10.4.*\)/ => ["Mac OS X 10.4"], - qr/\(.*Mac OS X (?:|Mach-O |\()10.3.*\)/ => ["Mac OS X 10.3"], - qr/\(.*Mac OS X (?:|Mach-O |\()10.2.*\)/ => ["Mac OS X 10.2"], - qr/\(.*Mac OS X (?:|Mach-O |\()10.1.*\)/ => ["Mac OS X 10.1"], - # Unfortunately, OS X 10.4 was the first to support Intel. This is fallback - # support because some browsers refused to include the OS Version. - qr/\(.*Intel.*Mac OS X.*\)/ => ["Mac OS X 10.4"], - # OS X 10.3 is the most likely default version of PowerPC Macs - # OS X 10.0 is more for configurations which didn't setup 10.x versions - qr/\(.*Mac OS X.*\)/ => [("Mac OS X 10.3", "Mac OS X 10.0", "Mac OS X")], - qr/\(.*Mac OS 9.*\)/ => [("Mac System 9.x", "Mac System 9.0")], - qr/\(.*Mac OS 8\.6.*\)/ => [("Mac System 8.6", "Mac System 8.5")], - qr/\(.*Mac OS 8\.5.*\)/ => ["Mac System 8.5"], - qr/\(.*Mac OS 8\.1.*\)/ => [("Mac System 8.1", "Mac System 8.0")], - qr/\(.*Mac OS 8\.0.*\)/ => ["Mac System 8.0"], - qr/\(.*Mac OS 8[^.].*\)/ => ["Mac System 8.0"], - qr/\(.*Mac OS 8.*\)/ => ["Mac System 8.6"], - qr/\(.*Darwin.*\)/ => [("Mac OS X 10.0", "Mac OS X")], - # Silly - qr/\(.*Mac.*PowerPC.*\)/ => ["Mac System 9.x"], - qr/\(.*Mac.*PPC.*\)/ => ["Mac System 9.x"], - qr/\(.*Mac.*68k.*\)/ => ["Mac System 8.0"], - # Evil - qr/Amiga/i => ["Other"], - qr/WinMosaic/ => ["Windows 95"], - qr/\(.*32bit.*\)/ => ["Windows 95"], - qr/\(.*16bit.*\)/ => ["Windows 3.1"], - qr/\(.*PowerPC.*\)/ => ["Mac System 9.x"], - qr/\(.*PPC.*\)/ => ["Mac System 9.x"], - qr/\(.*68K.*\)/ => ["Mac System 8.0"], + + # Sun + qr/\(.*Solaris.*\)/ => ["Solaris"], + qr/\(.*SunOS 5.11.*\)/ => [("OpenSolaris", "Opensolaris", "Solaris 11")], + qr/\(.*SunOS 5.10.*\)/ => ["Solaris 10"], + qr/\(.*SunOS 5.9.*\)/ => ["Solaris 9"], + qr/\(.*SunOS 5.8.*\)/ => ["Solaris 8"], + qr/\(.*SunOS 5.7.*\)/ => ["Solaris 7"], + qr/\(.*SunOS 5.6.*\)/ => ["Solaris 6"], + qr/\(.*SunOS 5.5.*\)/ => ["Solaris 5"], + qr/\(.*SunOS 5.*\)/ => ["Solaris"], + qr/\(.*SunOS.*sun4u.*\)/ => ["Solaris"], + qr/\(.*SunOS.*i86pc.*\)/ => ["Solaris"], + qr/\(.*SunOS.*\)/ => ["SunOS"], + + # BSD + qr/\(.*BSD\/(?:OS|386).*\)/ => ["BSDI"], + qr/\(.*FreeBSD.*\)/ => ["FreeBSD"], + qr/\(.*OpenBSD.*\)/ => ["OpenBSD"], + qr/\(.*NetBSD.*\)/ => ["NetBSD"], + + # Misc POSIX + qr/\(.*IRIX.*\)/ => ["IRIX"], + qr/\(.*OSF.*\)/ => ["OSF/1"], + qr/\(.*Linux.*\)/ => ["Linux"], + qr/\(.*BeOS.*\)/ => ["BeOS"], + qr/\(.*AIX.*\)/ => ["AIX"], + qr/\(.*OS\/2.*\)/ => ["OS/2"], + qr/\(.*QNX.*\)/ => ["Neutrino"], + qr/\(.*VMS.*\)/ => ["OpenVMS"], + qr/\(.*HP-?UX.*\)/ => ["HP-UX"], + qr/\(.*Android.*\)/ => ["Android"], + + # Windows + qr/\(.*Windows XP.*\)/ => ["Windows XP"], + qr/\(.*Windows NT 10\.0.*\)/ => ["Windows 10"], + qr/\(.*Windows NT 6\.4.*\)/ => ["Windows 10"], + qr/\(.*Windows NT 6\.3.*\)/ => ["Windows 8.1"], + qr/\(.*Windows NT 6\.2.*\)/ => ["Windows 8"], + qr/\(.*Windows NT 6\.1.*\)/ => ["Windows 7"], + qr/\(.*Windows NT 6\.0.*\)/ => ["Windows Vista"], + qr/\(.*Windows NT 5\.2.*\)/ => ["Windows Server 2003"], + qr/\(.*Windows NT 5\.1.*\)/ => ["Windows XP"], + qr/\(.*Windows 2000.*\)/ => ["Windows 2000"], + qr/\(.*Windows NT 5.*\)/ => ["Windows 2000"], + qr/\(.*Win.*9[8x].*4\.9.*\)/ => ["Windows ME"], + qr/\(.*Win(?:dows |)M[Ee].*\)/ => ["Windows ME"], + qr/\(.*Win(?:dows |)98.*\)/ => ["Windows 98"], + qr/\(.*Win(?:dows |)95.*\)/ => ["Windows 95"], + qr/\(.*Win(?:dows |)16.*\)/ => ["Windows 3.1"], + qr/\(.*Win(?:dows[ -]|)NT.*\)/ => ["Windows NT"], + qr/\(.*Windows.*NT.*\)/ => ["Windows NT"], + + # OS X + qr/\(.*(?:iPad|iPhone).*OS 7.*\)/ => ["iOS 7"], + qr/\(.*(?:iPad|iPhone).*OS 6.*\)/ => ["iOS 6"], + qr/\(.*(?:iPad|iPhone).*OS 5.*\)/ => ["iOS 5"], + qr/\(.*(?:iPad|iPhone).*OS 4.*\)/ => ["iOS 4"], + qr/\(.*(?:iPad|iPhone).*OS 3.*\)/ => ["iOS 3"], + qr/\(.*(?:iPod|iPad|iPhone).*\)/ => ["iOS"], + qr/\(.*Mac OS X (?:|Mach-O |\()10.8.*\)/ => ["Mac OS X 10.8"], + qr/\(.*Mac OS X (?:|Mach-O |\()10.7.*\)/ => ["Mac OS X 10.7"], + qr/\(.*Mac OS X (?:|Mach-O |\()10.6.*\)/ => ["Mac OS X 10.6"], + qr/\(.*Mac OS X (?:|Mach-O |\()10.5.*\)/ => ["Mac OS X 10.5"], + qr/\(.*Mac OS X (?:|Mach-O |\()10.4.*\)/ => ["Mac OS X 10.4"], + qr/\(.*Mac OS X (?:|Mach-O |\()10.3.*\)/ => ["Mac OS X 10.3"], + qr/\(.*Mac OS X (?:|Mach-O |\()10.2.*\)/ => ["Mac OS X 10.2"], + qr/\(.*Mac OS X (?:|Mach-O |\()10.1.*\)/ => ["Mac OS X 10.1"], + + # Unfortunately, OS X 10.4 was the first to support Intel. This is fallback + # support because some browsers refused to include the OS Version. + qr/\(.*Intel.*Mac OS X.*\)/ => ["Mac OS X 10.4"], + + # OS X 10.3 is the most likely default version of PowerPC Macs + # OS X 10.0 is more for configurations which didn't setup 10.x versions + qr/\(.*Mac OS X.*\)/ => [("Mac OS X 10.3", "Mac OS X 10.0", "Mac OS X")], + qr/\(.*Mac OS 9.*\)/ => [("Mac System 9.x", "Mac System 9.0")], + qr/\(.*Mac OS 8\.6.*\)/ => [("Mac System 8.6", "Mac System 8.5")], + qr/\(.*Mac OS 8\.5.*\)/ => ["Mac System 8.5"], + qr/\(.*Mac OS 8\.1.*\)/ => [("Mac System 8.1", "Mac System 8.0")], + qr/\(.*Mac OS 8\.0.*\)/ => ["Mac System 8.0"], + qr/\(.*Mac OS 8[^.].*\)/ => ["Mac System 8.0"], + qr/\(.*Mac OS 8.*\)/ => ["Mac System 8.6"], + qr/\(.*Darwin.*\)/ => [("Mac OS X 10.0", "Mac OS X")], + + # Silly + qr/\(.*Mac.*PowerPC.*\)/ => ["Mac System 9.x"], + qr/\(.*Mac.*PPC.*\)/ => ["Mac System 9.x"], + qr/\(.*Mac.*68k.*\)/ => ["Mac System 8.0"], + + # Evil + qr/Amiga/i => ["Other"], + qr/WinMosaic/ => ["Windows 95"], + qr/\(.*32bit.*\)/ => ["Windows 95"], + qr/\(.*16bit.*\)/ => ["Windows 3.1"], + qr/\(.*PowerPC.*\)/ => ["Mac System 9.x"], + qr/\(.*PPC.*\)/ => ["Mac System 9.x"], + qr/\(.*68K.*\)/ => ["Mac System 8.0"], ); sub detect_platform { - my $userAgent = $ENV{'HTTP_USER_AGENT'}; - my @detected; - my $iterator = natatime(2, PLATFORMS_MAP); - while (my($re, $ra) = $iterator->()) { - if ($userAgent =~ $re) { - push @detected, @$ra; - } + my $userAgent = $ENV{'HTTP_USER_AGENT'}; + my @detected; + my $iterator = natatime(2, PLATFORMS_MAP); + while (my ($re, $ra) = $iterator->()) { + if ($userAgent =~ $re) { + push @detected, @$ra; } - return _pick_valid_field_value('rep_platform', @detected); + } + return _pick_valid_field_value('rep_platform', @detected); } sub detect_op_sys { - my $userAgent = $ENV{'HTTP_USER_AGENT'} || ''; - my @detected; - my $iterator = natatime(2, OS_MAP); - while (my($re, $ra) = $iterator->()) { - if ($userAgent =~ $re) { - push @detected, @$ra; - } + my $userAgent = $ENV{'HTTP_USER_AGENT'} || ''; + my @detected; + my $iterator = natatime(2, OS_MAP); + while (my ($re, $ra) = $iterator->()) { + if ($userAgent =~ $re) { + push @detected, @$ra; } - push(@detected, "Windows") if grep(/^Windows /, @detected); - push(@detected, "Mac OS") if grep(/^Mac /, @detected); - return _pick_valid_field_value('op_sys', @detected); + } + push(@detected, "Windows") if grep(/^Windows /, @detected); + push(@detected, "Mac OS") if grep(/^Mac /, @detected); + return _pick_valid_field_value('op_sys', @detected); } # Takes the name of a field and a list of possible values for that field. @@ -197,11 +221,11 @@ sub detect_op_sys { # field. # Returns 'Other' if none of the values match. sub _pick_valid_field_value { - my ($field, @values) = @_; - foreach my $value (@values) { - return $value if check_field($field, $value, undef, 1); - } - return DEFAULT_VALUE; + my ($field, @values) = @_; + foreach my $value (@values) { + return $value if check_field($field, $value, undef, 1); + } + return DEFAULT_VALUE; } 1; diff --git a/Bugzilla/Util.pm b/Bugzilla/Util.pm index fcd4aff918..0e33ede4e5 100644 --- a/Bugzilla/Util.pm +++ b/Bugzilla/Util.pm @@ -13,18 +13,18 @@ use warnings; use parent qw(Exporter); @Bugzilla::Util::EXPORT = qw(trick_taint detaint_natural detaint_signed - html_quote url_quote xml_quote - css_class_quote html_light_quote - i_am_cgi i_am_webservice correct_urlbase remote_ip - validate_ip do_ssl_redirect_if_required use_attachbase - diff_arrays on_main_db - trim wrap_hard wrap_comment find_wrap_point wrap_cite - format_time validate_date validate_time datetime_from - is_7bit_clean bz_crypt generate_random_password - validate_email_syntax check_email_syntax clean_text - get_text template_var display_value disable_utf8 - detect_encoding email_filter - join_activity_entries read_text write_text); + html_quote url_quote xml_quote + css_class_quote html_light_quote + i_am_cgi i_am_webservice correct_urlbase remote_ip + validate_ip do_ssl_redirect_if_required use_attachbase + diff_arrays on_main_db + trim wrap_hard wrap_comment find_wrap_point wrap_cite + format_time validate_date validate_time datetime_from + is_7bit_clean bz_crypt generate_random_password + validate_email_syntax check_email_syntax clean_text + get_text template_var display_value disable_utf8 + detect_encoding email_filter + join_activity_entries read_text write_text); use Bugzilla::Constants; use Bugzilla::RNG qw(irand); @@ -43,658 +43,704 @@ use File::Basename qw(dirname); use File::Temp qw(tempfile); sub trick_taint { - require Carp; - Carp::confess("Undef to trick_taint") unless defined $_[0]; - my $match = $_[0] =~ /^(.*)$/s; - $_[0] = $match ? $1 : undef; - return (defined($_[0])); + require Carp; + Carp::confess("Undef to trick_taint") unless defined $_[0]; + my $match = $_[0] =~ /^(.*)$/s; + $_[0] = $match ? $1 : undef; + return (defined($_[0])); } sub detaint_natural { - my $match = $_[0] =~ /^(\d+)$/a; - $_[0] = $match ? int($1) : undef; - return (defined($_[0])); + my $match = $_[0] =~ /^(\d+)$/a; + $_[0] = $match ? int($1) : undef; + return (defined($_[0])); } sub detaint_signed { - my $match = $_[0] =~ /^([-+]?\d+)$/a; - # The "int()" call removes any leading plus sign. - $_[0] = $match ? int($1) : undef; - return (defined($_[0])); + my $match = $_[0] =~ /^([-+]?\d+)$/a; + + # The "int()" call removes any leading plus sign. + $_[0] = $match ? int($1) : undef; + return (defined($_[0])); } # Bug 120030: Override html filter to obscure the '@' in user # visible strings. # Bug 319331: Handle BiDi disruptions. sub html_quote { - my $var = shift; - $var =~ s/&/&/g; - $var =~ s/</g; - $var =~ s/>/>/g; - $var =~ s/"/"/g; - # Obscure '@'. - $var =~ s/\@/\@/g; - - # Remove control characters. - $var =~ s/(?![\t\r\n])[[:cntrl:]]//g; - - # Remove the following characters because they're - # influencing BiDi: - # -------------------------------------------------------- - # |Code |Name |UTF-8 representation| - # |------|--------------------------|--------------------| - # |U+202a|Left-To-Right Embedding |0xe2 0x80 0xaa | - # |U+202b|Right-To-Left Embedding |0xe2 0x80 0xab | - # |U+202c|Pop Directional Formatting|0xe2 0x80 0xac | - # |U+202d|Left-To-Right Override |0xe2 0x80 0xad | - # |U+202e|Right-To-Left Override |0xe2 0x80 0xae | - # -------------------------------------------------------- - # - # The following are characters influencing BiDi, too, but - # they can be spared from filtering because they don't - # influence more than one character right or left: - # -------------------------------------------------------- - # |Code |Name |UTF-8 representation| - # |------|--------------------------|--------------------| - # |U+200e|Left-To-Right Mark |0xe2 0x80 0x8e | - # |U+200f|Right-To-Left Mark |0xe2 0x80 0x8f | - # -------------------------------------------------------- - $var =~ tr/\x{202a}-\x{202e}//d; - return $var; + my $var = shift; + $var =~ s/&/&/g; + $var =~ s/</g; + $var =~ s/>/>/g; + $var =~ s/"/"/g; + + # Obscure '@'. + $var =~ s/\@/\@/g; + + # Remove control characters. + $var =~ s/(?![\t\r\n])[[:cntrl:]]//g; + + # Remove the following characters because they're + # influencing BiDi: + # -------------------------------------------------------- + # |Code |Name |UTF-8 representation| + # |------|--------------------------|--------------------| + # |U+202a|Left-To-Right Embedding |0xe2 0x80 0xaa | + # |U+202b|Right-To-Left Embedding |0xe2 0x80 0xab | + # |U+202c|Pop Directional Formatting|0xe2 0x80 0xac | + # |U+202d|Left-To-Right Override |0xe2 0x80 0xad | + # |U+202e|Right-To-Left Override |0xe2 0x80 0xae | + # -------------------------------------------------------- + # + # The following are characters influencing BiDi, too, but + # they can be spared from filtering because they don't + # influence more than one character right or left: + # -------------------------------------------------------- + # |Code |Name |UTF-8 representation| + # |------|--------------------------|--------------------| + # |U+200e|Left-To-Right Mark |0xe2 0x80 0x8e | + # |U+200f|Right-To-Left Mark |0xe2 0x80 0x8f | + # -------------------------------------------------------- + $var =~ tr/\x{202a}-\x{202e}//d; + return $var; } sub read_text { - my ($filename) = @_; - open my $fh, '<:encoding(utf-8)', $filename; - local $/ = undef; - my $content = <$fh>; - close $fh; - return $content; + my ($filename) = @_; + open my $fh, '<:encoding(utf-8)', $filename; + local $/ = undef; + my $content = <$fh>; + close $fh; + return $content; } sub write_text { - my ($filename, $content) = @_; - my ($tmp_fh, $tmp_filename) = tempfile('.tmp.XXXXXXXXXX', - DIR => dirname($filename), - UNLINK => 0, - ); - binmode $tmp_fh, ':encoding(utf-8)'; - print $tmp_fh $content; - close $tmp_fh; - rename $tmp_filename, $filename; + my ($filename, $content) = @_; + my ($tmp_fh, $tmp_filename) + = tempfile('.tmp.XXXXXXXXXX', DIR => dirname($filename), UNLINK => 0,); + binmode $tmp_fh, ':encoding(utf-8)'; + print $tmp_fh $content; + close $tmp_fh; + rename $tmp_filename, $filename; } sub html_light_quote { - my ($text) = @_; - # admin/table.html.tmpl calls |FILTER html_light| many times. - # There is no need to recreate the HTML::Scrubber object again and again. - my $scrubber = Bugzilla->process_cache->{html_scrubber}; - - # List of allowed HTML elements having no attributes. - my @allow = qw(b strong em i u p br abbr acronym ins del cite code var - dfn samp kbd big small sub sup tt dd dt dl ul li ol - fieldset legend); - - if (!Bugzilla->feature('html_desc')) { - my $safe = join('|', @allow); - my $chr = chr(1); - - # First, escape safe elements. - $text =~ s#<($safe)>#$chr$1$chr#go; - $text =~ s#($safe)>#$chr/$1$chr#go; - # Now filter < and >. - $text =~ s#<#<#g; - $text =~ s#>#>#g; - # Restore safe elements. - $text =~ s#$chr/($safe)$chr#$1>#go; - $text =~ s#$chr($safe)$chr#<$1>#go; - return $text; - } - elsif (!$scrubber) { - # We can be less restrictive. We can accept elements with attributes. - push(@allow, qw(a blockquote q span)); - - # Allowed protocols. - my $safe_protocols = join('|', SAFE_PROTOCOLS); - my $protocol_regexp = qr{(^(?:$safe_protocols):|^[^:]+$)}i; - - # Deny all elements and attributes unless explicitly authorized. - my @default = (0 => { - id => 1, - name => 1, - class => 1, - '*' => 0, # Reject all other attributes. - } - ); - - # Specific rules for allowed elements. If no specific rule is set - # for a given element, then the default is used. - my @rules = (a => { - href => $protocol_regexp, - target => qr{^(?:_blank|_parent|_self|_top)$}i, - title => 1, - id => 1, - name => 1, - class => 1, - '*' => 0, # Reject all other attributes. - }, - blockquote => { - cite => $protocol_regexp, - id => 1, - name => 1, - class => 1, - '*' => 0, # Reject all other attributes. - }, - 'q' => { - cite => $protocol_regexp, - id => 1, - name => 1, - class => 1, - '*' => 0, # Reject all other attributes. - }, - ); - - Bugzilla->process_cache->{html_scrubber} = $scrubber = - HTML::Scrubber->new(default => \@default, - allow => \@allow, - rules => \@rules, - comment => 0, - process => 0); - } - return $scrubber->scrub($text); + my ($text) = @_; + + # admin/table.html.tmpl calls |FILTER html_light| many times. + # There is no need to recreate the HTML::Scrubber object again and again. + my $scrubber = Bugzilla->process_cache->{html_scrubber}; + + # List of allowed HTML elements having no attributes. + my @allow = qw(b strong em i u p br abbr acronym ins del cite code var + dfn samp kbd big small sub sup tt dd dt dl ul li ol + fieldset legend); + + if (!Bugzilla->feature('html_desc')) { + my $safe = join('|', @allow); + my $chr = chr(1); + + # First, escape safe elements. + $text =~ s#<($safe)>#$chr$1$chr#go; + $text =~ s#($safe)>#$chr/$1$chr#go; + + # Now filter < and >. + $text =~ s#<#<#g; + $text =~ s#>#>#g; + + # Restore safe elements. + $text =~ s#$chr/($safe)$chr#$1>#go; + $text =~ s#$chr($safe)$chr#<$1>#go; + return $text; + } + elsif (!$scrubber) { + + # We can be less restrictive. We can accept elements with attributes. + push(@allow, qw(a blockquote q span)); + + # Allowed protocols. + my $safe_protocols = join('|', SAFE_PROTOCOLS); + my $protocol_regexp = qr{(^(?:$safe_protocols):|^[^:]+$)}i; + + # Deny all elements and attributes unless explicitly authorized. + my @default = ( + 0 => { + id => 1, + name => 1, + class => 1, + '*' => 0, # Reject all other attributes. + } + ); + + # Specific rules for allowed elements. If no specific rule is set + # for a given element, then the default is used. + my @rules = ( + a => { + href => $protocol_regexp, + target => qr{^(?:_blank|_parent|_self|_top)$}i, + title => 1, + id => 1, + name => 1, + class => 1, + '*' => 0, # Reject all other attributes. + }, + blockquote => { + cite => $protocol_regexp, + id => 1, + name => 1, + class => 1, + '*' => 0, # Reject all other attributes. + }, + 'q' => { + cite => $protocol_regexp, + id => 1, + name => 1, + class => 1, + '*' => 0, # Reject all other attributes. + }, + ); + + Bugzilla->process_cache->{html_scrubber} = $scrubber = HTML::Scrubber->new( + default => \@default, + allow => \@allow, + rules => \@rules, + comment => 0, + process => 0 + ); + } + return $scrubber->scrub($text); } sub email_filter { - my ($toencode) = @_; - if (!Bugzilla->user->id) { - my @emails = Email::Address->parse($toencode); - if (scalar @emails) { - my @hosts = map { quotemeta($_->host) } @emails; - my $hosts_re = join('|', @hosts); - $toencode =~ s/\@(?:$hosts_re)//g; - return $toencode; - } + my ($toencode) = @_; + if (!Bugzilla->user->id) { + my @emails = Email::Address->parse($toencode); + if (scalar @emails) { + my @hosts = map { quotemeta($_->host) } @emails; + my $hosts_re = join('|', @hosts); + $toencode =~ s/\@(?:$hosts_re)//g; + return $toencode; } - return $toencode; + } + return $toencode; } # This originally came from CGI.pm, by Lincoln D. Stein sub url_quote { - my ($toencode) = (@_); - # The below regex works only on bytes - utf8::encode($toencode) if utf8::is_utf8($toencode); - $toencode =~ s/([^a-zA-Z0-9_\-.])/uc sprintf("%%%02x",ord($1))/eg; - return $toencode; + my ($toencode) = (@_); + + # The below regex works only on bytes + utf8::encode($toencode) if utf8::is_utf8($toencode); + $toencode =~ s/([^a-zA-Z0-9_\-.])/uc sprintf("%%%02x",ord($1))/eg; + return $toencode; } sub css_class_quote { - my ($toencode) = (@_); - $toencode =~ s#[ /]#_#g; - $toencode =~ s/([^a-zA-Z0-9_\-.])/uc sprintf("%x;",ord($1))/eg; - return $toencode; + my ($toencode) = (@_); + $toencode =~ s#[ /]#_#g; + $toencode =~ s/([^a-zA-Z0-9_\-.])/uc sprintf("%x;",ord($1))/eg; + return $toencode; } sub xml_quote { - my ($var) = (@_); - $var =~ s/\&/\&/g; - $var =~ s/\</g; - $var =~ s/>/\>/g; - $var =~ s/\"/\"/g; - $var =~ s/\'/\'/g; - - # the following nukes characters disallowed by the XML 1.0 - # spec, Production 2.2. 1.0 declares that only the following - # are valid: - # (#x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF]) - $var =~ s/([\x{0001}-\x{0008}]| + my ($var) = (@_); + $var =~ s/\&/\&/g; + $var =~ s/\</g; + $var =~ s/>/\>/g; + $var =~ s/\"/\"/g; + $var =~ s/\'/\'/g; + + # the following nukes characters disallowed by the XML 1.0 + # spec, Production 2.2. 1.0 declares that only the following + # are valid: + # (#x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF]) + $var =~ s/([\x{0001}-\x{0008}]| [\x{000B}-\x{000C}]| [\x{000E}-\x{001F}]| [\x{D800}-\x{DFFF}]| [\x{FFFE}-\x{FFFF}])//gx; - return $var; + return $var; } sub i_am_cgi { - # I use SERVER_SOFTWARE because it's required to be - # defined for all requests in the CGI spec. - return exists $ENV{'SERVER_SOFTWARE'} ? 1 : 0; + + # I use SERVER_SOFTWARE because it's required to be + # defined for all requests in the CGI spec. + return exists $ENV{'SERVER_SOFTWARE'} ? 1 : 0; } sub i_am_webservice { - my $usage_mode = Bugzilla->usage_mode; - return $usage_mode == USAGE_MODE_XMLRPC - || $usage_mode == USAGE_MODE_JSON - || $usage_mode == USAGE_MODE_REST; + my $usage_mode = Bugzilla->usage_mode; + return + $usage_mode == USAGE_MODE_XMLRPC + || $usage_mode == USAGE_MODE_JSON + || $usage_mode == USAGE_MODE_REST; } # This exists as a separate function from Bugzilla::CGI::redirect_to_https # because we don't want to create a CGI object during XML-RPC calls # (doing so can mess up XML-RPC). sub do_ssl_redirect_if_required { - return if !i_am_cgi(); - return if Bugzilla->params->{urlbase} =~ /^https/i; - return if !Bugzilla->params->{'ssl_redirect'}; - return if !Bugzilla->params->{'sslbase'}; - - # If we're already running under SSL, never redirect. - if (Bugzilla->params->{'inbound_proxies'} - && uc($ENV{HTTP_X_FORWARDED_PROTO} || '') eq 'HTTPS') { - return; - } - return if uc($ENV{HTTPS} || '') eq 'ON'; - - # If called from Bugzilla::CGI->new itself, use the newly created - # CGI object, to avoid deep recursions. - my $cgi = shift || Bugzilla->cgi; - $cgi->redirect_to_https(); + return if !i_am_cgi(); + return if Bugzilla->params->{urlbase} =~ /^https/i; + return if !Bugzilla->params->{'ssl_redirect'}; + return if !Bugzilla->params->{'sslbase'}; + + # If we're already running under SSL, never redirect. + if (Bugzilla->params->{'inbound_proxies'} + && uc($ENV{HTTP_X_FORWARDED_PROTO} || '') eq 'HTTPS') + { + return; + } + return if uc($ENV{HTTPS} || '') eq 'ON'; + + # If called from Bugzilla::CGI->new itself, use the newly created + # CGI object, to avoid deep recursions. + my $cgi = shift || Bugzilla->cgi; + $cgi->redirect_to_https(); } sub correct_urlbase { - my $urlbase = Bugzilla->params->{'urlbase'}; - my $sslbase = Bugzilla->params->{'sslbase'}; - - if (!$sslbase) { - return $urlbase; - } - elsif (Bugzilla->params->{'ssl_redirect'}) { - return $sslbase; - } - # Return what the user currently uses. - elsif (Bugzilla->params->{'inbound_proxies'}) { - return (uc($ENV{HTTP_X_FORWARDED_PROTO} || '') eq 'HTTPS') ? $sslbase : $urlbase; - } - else { - return (uc($ENV{HTTPS} || '') eq 'ON') ? $sslbase : $urlbase; - } + my $urlbase = Bugzilla->params->{'urlbase'}; + my $sslbase = Bugzilla->params->{'sslbase'}; + + if (!$sslbase) { + return $urlbase; + } + elsif (Bugzilla->params->{'ssl_redirect'}) { + return $sslbase; + } + + # Return what the user currently uses. + elsif (Bugzilla->params->{'inbound_proxies'}) { + return (uc($ENV{HTTP_X_FORWARDED_PROTO} || '') eq 'HTTPS') + ? $sslbase + : $urlbase; + } + else { + return (uc($ENV{HTTPS} || '') eq 'ON') ? $sslbase : $urlbase; + } } # Returns the real remote address of the client, sub remote_ip { - my $remote_ip = $ENV{'REMOTE_ADDR'} || '127.0.0.1'; - my @proxies = split(/[\s,]+/, Bugzilla->params->{inbound_proxies}); - my @x_forwarded_for = split(/[\s,]+/, $ENV{HTTP_X_FORWARDED_FOR} // ''); - - return $remote_ip unless @x_forwarded_for; - return $x_forwarded_for[0] if (@proxies && $proxies[0] eq '*'); - return $remote_ip if none { $_ eq $remote_ip } @proxies; - - foreach my $ip (reverse @x_forwarded_for) { - if (none { $_ eq $ip } @proxies) { - # Keep the original IP address if the remote IP is invalid. - return validate_ip($ip) || $remote_ip; - } + my $remote_ip = $ENV{'REMOTE_ADDR'} || '127.0.0.1'; + my @proxies = split(/[\s,]+/, Bugzilla->params->{inbound_proxies}); + my @x_forwarded_for = split(/[\s,]+/, $ENV{HTTP_X_FORWARDED_FOR} // ''); + + return $remote_ip unless @x_forwarded_for; + return $x_forwarded_for[0] if (@proxies && $proxies[0] eq '*'); + return $remote_ip if none { $_ eq $remote_ip } @proxies; + + foreach my $ip (reverse @x_forwarded_for) { + if (none { $_ eq $ip } @proxies) { + + # Keep the original IP address if the remote IP is invalid. + return validate_ip($ip) || $remote_ip; } - return $remote_ip; + } + return $remote_ip; } sub validate_ip { - my $ip = shift; - return is_ipv4($ip) || is_ipv6($ip); + my $ip = shift; + return is_ipv4($ip) || is_ipv6($ip); } # Copied from Data::Validate::IP::is_ipv4(). sub is_ipv4 { - my $ip = shift; - return unless defined $ip; + my $ip = shift; + return unless defined $ip; - my @octets = $ip =~ /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/; - return unless scalar(@octets) == 4; + my @octets = $ip =~ /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/; + return unless scalar(@octets) == 4; - foreach my $octet (@octets) { - return unless ($octet >= 0 && $octet <= 255 && $octet !~ /^0\d{1,2}$/); - } + foreach my $octet (@octets) { + return unless ($octet >= 0 && $octet <= 255 && $octet !~ /^0\d{1,2}$/); + } - # The IP address is valid and can now be detainted. - return join('.', @octets); + # The IP address is valid and can now be detainted. + return join('.', @octets); } # Copied from Data::Validate::IP::is_ipv6(). sub is_ipv6 { - my $ip = shift; - return unless defined $ip; - - # If there is a :: then there must be only one :: and the length - # can be variable. Without it, the length must be 8 groups. - my @chunks = split(':', $ip); - - # Need to check if the last chunk is an IPv4 address, if it is we - # pop it off and exempt it from the normal IPv6 checking and stick - # it back on at the end. If there is only one chunk and it's an IPv4 - # address, then it isn't an IPv6 address. - my $ipv4; - my $expected_chunks = 8; - if (@chunks > 1 && is_ipv4($chunks[$#chunks])) { - $ipv4 = pop(@chunks); - $expected_chunks--; - } + my $ip = shift; + return unless defined $ip; + + # If there is a :: then there must be only one :: and the length + # can be variable. Without it, the length must be 8 groups. + my @chunks = split(':', $ip); + + # Need to check if the last chunk is an IPv4 address, if it is we + # pop it off and exempt it from the normal IPv6 checking and stick + # it back on at the end. If there is only one chunk and it's an IPv4 + # address, then it isn't an IPv6 address. + my $ipv4; + my $expected_chunks = 8; + if (@chunks > 1 && is_ipv4($chunks[$#chunks])) { + $ipv4 = pop(@chunks); + $expected_chunks--; + } + + my $empty = 0; + + # Workaround to handle trailing :: being valid. + if ($ip =~ /[0-9a-f]{1,4}::$/) { + $empty++; - my $empty = 0; - # Workaround to handle trailing :: being valid. - if ($ip =~ /[0-9a-f]{1,4}::$/) { - $empty++; # Single trailing ':' is invalid. - } elsif ($ip =~ /:$/) { - return; - } + } + elsif ($ip =~ /:$/) { + return; + } - foreach my $chunk (@chunks) { - return unless $chunk =~ /^[0-9a-f]{0,4}$/i; - $empty++ if $chunk eq ''; - } - # More than one :: block is bad, but if it starts with :: it will - # look like two, so we need an exception. - if ($empty == 2 && $ip =~ /^::/) { - # This is ok - } elsif ($empty > 1) { - return; - } + foreach my $chunk (@chunks) { + return unless $chunk =~ /^[0-9a-f]{0,4}$/i; + $empty++ if $chunk eq ''; + } + + # More than one :: block is bad, but if it starts with :: it will + # look like two, so we need an exception. + if ($empty == 2 && $ip =~ /^::/) { + + # This is ok + } + elsif ($empty > 1) { + return; + } - push(@chunks, $ipv4) if $ipv4; - # Need 8 chunks, or we need an empty section that could be filled - # to represent the missing '0' sections. - return unless (@chunks == $expected_chunks || @chunks < $expected_chunks && $empty); + push(@chunks, $ipv4) if $ipv4; - my $ipv6 = join(':', @chunks); - # The IP address is valid and can now be detainted. - trick_taint($ipv6); + # Need 8 chunks, or we need an empty section that could be filled + # to represent the missing '0' sections. + return + unless (@chunks == $expected_chunks || @chunks < $expected_chunks && $empty); - # Need to handle the exception of trailing :: being valid. - return "${ipv6}::" if $ip =~ /::$/; - return $ipv6; + my $ipv6 = join(':', @chunks); + + # The IP address is valid and can now be detainted. + trick_taint($ipv6); + + # Need to handle the exception of trailing :: being valid. + return "${ipv6}::" if $ip =~ /::$/; + return $ipv6; } sub use_attachbase { - my $attachbase = Bugzilla->params->{'attachment_base'}; - return ($attachbase ne '' - && $attachbase ne Bugzilla->params->{'urlbase'} - && $attachbase ne Bugzilla->params->{'sslbase'}) ? 1 : 0; + my $attachbase = Bugzilla->params->{'attachment_base'}; + return ($attachbase ne '' + && $attachbase ne Bugzilla->params->{'urlbase'} + && $attachbase ne Bugzilla->params->{'sslbase'}) ? 1 : 0; } sub diff_arrays { - my ($old_ref, $new_ref, $attrib) = @_; - $attrib ||= 'name'; - - my (%counts, %pos); - # We are going to alter the old array. - my @old = @$old_ref; - my $i = 0; - - # $counts{foo}-- means old, $counts{foo}++ means new. - # If $counts{foo} becomes positive, then we are adding new items, - # else we simply cancel one old existing item. Remaining items - # in the old list have been removed. - foreach (@old) { - next unless defined $_; - my $value = blessed($_) ? $_->$attrib : $_; - $counts{$value}--; - push @{$pos{$value}}, $i++; + my ($old_ref, $new_ref, $attrib) = @_; + $attrib ||= 'name'; + + my (%counts, %pos); + + # We are going to alter the old array. + my @old = @$old_ref; + my $i = 0; + + # $counts{foo}-- means old, $counts{foo}++ means new. + # If $counts{foo} becomes positive, then we are adding new items, + # else we simply cancel one old existing item. Remaining items + # in the old list have been removed. + foreach (@old) { + next unless defined $_; + my $value = blessed($_) ? $_->$attrib : $_; + $counts{$value}--; + push @{$pos{$value}}, $i++; + } + my @added; + foreach (@$new_ref) { + next unless defined $_; + my $value = blessed($_) ? $_->$attrib : $_; + if (++$counts{$value} > 0) { + + # Ignore empty strings, but objects having an empty string + # as attribute are fine. + push(@added, $_) unless ($value eq '' && !blessed($_)); } - my @added; - foreach (@$new_ref) { - next unless defined $_; - my $value = blessed($_) ? $_->$attrib : $_; - if (++$counts{$value} > 0) { - # Ignore empty strings, but objects having an empty string - # as attribute are fine. - push(@added, $_) unless ($value eq '' && !blessed($_)); - } - else { - my $old_pos = shift @{$pos{$value}}; - $old[$old_pos] = undef; - } + else { + my $old_pos = shift @{$pos{$value}}; + $old[$old_pos] = undef; } - # Ignore canceled items as well as empty strings. - my @removed = grep { defined $_ && $_ ne '' } @old; - return (\@removed, \@added); + } + + # Ignore canceled items as well as empty strings. + my @removed = grep { defined $_ && $_ ne '' } @old; + return (\@removed, \@added); } sub trim { - my ($str) = @_; - if ($str) { - $str =~ s/^\s+//g; - $str =~ s/\s+$//g; - } - return $str; + my ($str) = @_; + if ($str) { + $str =~ s/^\s+//g; + $str =~ s/\s+$//g; + } + return $str; } sub wrap_comment { - my ($comment, $cols) = @_; - my $wrappedcomment = ""; - - # Use 'local', as recommended by Text::Wrap's perldoc. - local $Text::Wrap::columns = $cols || COMMENT_COLS; - # Make words that are longer than COMMENT_COLS not wrap. - local $Text::Wrap::huge = 'overflow'; - # Don't mess with tabs. - local $Text::Wrap::unexpand = 0; - - # If the line starts with ">", don't wrap it. Otherwise, wrap. - foreach my $line (split(/\r\n|\r|\n/, $comment)) { - if ($line =~ qr/^>/) { - $wrappedcomment .= ($line . "\n"); - } - else { - $wrappedcomment .= (wrap('', '', $line) . "\n"); - } + my ($comment, $cols) = @_; + my $wrappedcomment = ""; + + # Use 'local', as recommended by Text::Wrap's perldoc. + local $Text::Wrap::columns = $cols || COMMENT_COLS; + + # Make words that are longer than COMMENT_COLS not wrap. + local $Text::Wrap::huge = 'overflow'; + + # Don't mess with tabs. + local $Text::Wrap::unexpand = 0; + + # If the line starts with ">", don't wrap it. Otherwise, wrap. + foreach my $line (split(/\r\n|\r|\n/, $comment)) { + if ($line =~ qr/^>/) { + $wrappedcomment .= ($line . "\n"); } + else { + $wrappedcomment .= (wrap('', '', $line) . "\n"); + } + } - chomp($wrappedcomment); # Text::Wrap adds an extra newline at the end. - return $wrappedcomment; + chomp($wrappedcomment); # Text::Wrap adds an extra newline at the end. + return $wrappedcomment; } sub wrap_cite { - my ($comment, $cols) = @_; - my $wrappedcomment = ""; - - # Use 'local', as recommended by Text::Wrap's perldoc. - local $Text::Wrap::columns = $cols || COMMENT_COLS; - # Make words that are longer than COMMENT_COLS not wrap. - local $Text::Wrap::huge = 'overflow'; - # Don't mess with tabs. - local $Text::Wrap::unexpand = 0; - - foreach my $line (split(/\r\n|\r|\n/, $comment)) { - if ($line =~ /^(>+ *)/) { - $wrappedcomment .= wrap('', $1, $line) . "\n"; - } else { - $wrappedcomment .= $line . "\n"; - } + my ($comment, $cols) = @_; + my $wrappedcomment = ""; + + # Use 'local', as recommended by Text::Wrap's perldoc. + local $Text::Wrap::columns = $cols || COMMENT_COLS; + + # Make words that are longer than COMMENT_COLS not wrap. + local $Text::Wrap::huge = 'overflow'; + + # Don't mess with tabs. + local $Text::Wrap::unexpand = 0; + + foreach my $line (split(/\r\n|\r|\n/, $comment)) { + if ($line =~ /^(>+ *)/) { + $wrappedcomment .= wrap('', $1, $line) . "\n"; } - chomp($wrappedcomment); # remove extra newline at the end - return $wrappedcomment; + else { + $wrappedcomment .= $line . "\n"; + } + } + chomp($wrappedcomment); # remove extra newline at the end + return $wrappedcomment; } sub find_wrap_point { - my ($string, $maxpos) = @_; - if (!$string) { return 0 } - if (length($string) < $maxpos) { return length($string) } - my $wrappoint = rindex($string, ",", $maxpos); # look for comma - if ($wrappoint <= 0) { # can't find comma - $wrappoint = rindex($string, " ", $maxpos); # look for space - if ($wrappoint <= 0) { # can't find space - $wrappoint = rindex($string, "-", $maxpos); # look for hyphen - if ($wrappoint <= 0) { # can't find hyphen - $wrappoint = $maxpos; # just truncate it - } else { - $wrappoint++; # leave hyphen on the left side - } - } + my ($string, $maxpos) = @_; + if (!$string) { return 0 } + if (length($string) < $maxpos) { return length($string) } + my $wrappoint = rindex($string, ",", $maxpos); # look for comma + if ($wrappoint <= 0) { # can't find comma + $wrappoint = rindex($string, " ", $maxpos); # look for space + if ($wrappoint <= 0) { # can't find space + $wrappoint = rindex($string, "-", $maxpos); # look for hyphen + if ($wrappoint <= 0) { # can't find hyphen + $wrappoint = $maxpos; # just truncate it + } + else { + $wrappoint++; # leave hyphen on the left side + } } - return $wrappoint; + } + return $wrappoint; } sub join_activity_entries { - my ($field, $current_change, $new_change) = @_; - # We need to insert characters as these were removed by old - # LogActivityEntry code. - - return $new_change if $current_change eq ''; - - # Buglists and see_also need the comma restored - if ($field eq 'dependson' || $field eq 'blocked' || $field eq 'see_also') { - if (substr($new_change, 0, 1) eq ',' || substr($new_change, 0, 1) eq ' ') { - return $current_change . $new_change; - } else { - return $current_change . ', ' . $new_change; - } - } + my ($field, $current_change, $new_change) = @_; - # Assume bug_file_loc contain a single url, don't insert a delimiter - if ($field eq 'bug_file_loc') { - return $current_change . $new_change; - } + # We need to insert characters as these were removed by old + # LogActivityEntry code. - # All other fields get a space unless the first character of the second - # string is a comma or space + return $new_change if $current_change eq ''; + + # Buglists and see_also need the comma restored + if ($field eq 'dependson' || $field eq 'blocked' || $field eq 'see_also') { if (substr($new_change, 0, 1) eq ',' || substr($new_change, 0, 1) eq ' ') { - return $current_change . $new_change; - } else { - return $current_change . ' ' . $new_change; + return $current_change . $new_change; + } + else { + return $current_change . ', ' . $new_change; } + } + + # Assume bug_file_loc contain a single url, don't insert a delimiter + if ($field eq 'bug_file_loc') { + return $current_change . $new_change; + } + + # All other fields get a space unless the first character of the second + # string is a comma or space + if (substr($new_change, 0, 1) eq ',' || substr($new_change, 0, 1) eq ' ') { + return $current_change . $new_change; + } + else { + return $current_change . ' ' . $new_change; + } } sub wrap_hard { - my ($string, $columns) = @_; - local $Text::Wrap::columns = $columns; - local $Text::Wrap::unexpand = 0; - local $Text::Wrap::huge = 'wrap'; - - my $wrapped = wrap('', '', $string); - chomp($wrapped); - return $wrapped; + my ($string, $columns) = @_; + local $Text::Wrap::columns = $columns; + local $Text::Wrap::unexpand = 0; + local $Text::Wrap::huge = 'wrap'; + + my $wrapped = wrap('', '', $string); + chomp($wrapped); + return $wrapped; } sub format_time { - my ($date, $format, $timezone) = @_; - - # If $format is not set, try to guess the correct date format. - if (!$format) { - if (!ref $date - && $date =~ /^(\d{4})[-\.](\d{2})[-\.](\d{2}) (\d{2}):(\d{2})(:(\d{2}))?$/) - { - my $sec = $7; - if (defined $sec) { - $format = "%Y-%m-%d %T %Z"; - } else { - $format = "%Y-%m-%d %R %Z"; - } - } else { - # Default date format. See DateTime for other formats available. - $format = "%Y-%m-%d %R %Z"; - } - } - - my $dt = ref $date ? $date : datetime_from($date, $timezone); - $date = defined $dt ? $dt->strftime($format) : ''; - return trim($date); -} - -sub datetime_from { - my ($date, $timezone) = @_; - - # In the database, this is the "0" date. - return undef if $date =~ /^0000/; + my ($date, $format, $timezone) = @_; - my @time; - # Most dates will be in this format, avoid strptime's generic parser - if ($date =~ /^(\d{4})[\.-](\d{2})[\.-](\d{2})(?: (\d{2}):(\d{2}):(\d{2}))?$/) { - @time = ($6, $5, $4, $3, $2 - 1, $1 - 1900, undef); + # If $format is not set, try to guess the correct date format. + if (!$format) { + if (!ref $date + && $date =~ /^(\d{4})[-\.](\d{2})[-\.](\d{2}) (\d{2}):(\d{2})(:(\d{2}))?$/) + { + my $sec = $7; + if (defined $sec) { + $format = "%Y-%m-%d %T %Z"; + } + else { + $format = "%Y-%m-%d %R %Z"; + } } else { - @time = strptime($date); + # Default date format. See DateTime for other formats available. + $format = "%Y-%m-%d %R %Z"; } + } - unless (scalar @time) { - # If an unknown timezone is passed (such as MSK, for Moskow), - # strptime() is unable to parse the date. We try again, but we first - # remove the timezone. - $date =~ s/\s+\S+$//; - @time = strptime($date); - } - - return undef if !@time; - - # strptime() counts years from 1900, except if they are older than 1901 - # in which case it returns the full year (so 1890 -> 1890, but 1984 -> 84, - # and 3790 -> 1890). We make a guess and assume that 1100 <= year < 3000. - $time[5] += 1900 if $time[5] < 1100; - - my %args = ( - year => $time[5], - # Months start from 0 (January). - month => $time[4] + 1, - day => $time[3], - hour => $time[2], - minute => $time[1], - # DateTime doesn't like fractional seconds. - # Also, sometimes seconds are undef. - second => defined($time[0]) ? int($time[0]) : undef, - # If a timezone was specified, use it. Otherwise, use the - # local timezone. - time_zone => DateTime::TimeZone->offset_as_string($time[6]) - || Bugzilla->local_timezone, - ); - - # If something wasn't specified in the date, it's best to just not - # pass it to DateTime at all. (This is important for doing datetime_from - # on the deadline field, which is usually just a date with no time.) - foreach my $arg (keys %args) { - delete $args{$arg} if !defined $args{$arg}; - } - - # This module takes time to load and is only used here, so we - # |require| it here rather than |use| it. - require DateTime; - my $dt = new DateTime(\%args); + my $dt = ref $date ? $date : datetime_from($date, $timezone); + $date = defined $dt ? $dt->strftime($format) : ''; + return trim($date); +} - # Now display the date using the given timezone, - # or the user's timezone if none is given. - $dt->set_time_zone($timezone || Bugzilla->user->timezone); - return $dt; +sub datetime_from { + my ($date, $timezone) = @_; + + # In the database, this is the "0" date. + return undef if $date =~ /^0000/; + + my @time; + + # Most dates will be in this format, avoid strptime's generic parser + if ($date =~ /^(\d{4})[\.-](\d{2})[\.-](\d{2})(?: (\d{2}):(\d{2}):(\d{2}))?$/) { + @time = ($6, $5, $4, $3, $2 - 1, $1 - 1900, undef); + } + else { + @time = strptime($date); + } + + unless (scalar @time) { + + # If an unknown timezone is passed (such as MSK, for Moskow), + # strptime() is unable to parse the date. We try again, but we first + # remove the timezone. + $date =~ s/\s+\S+$//; + @time = strptime($date); + } + + return undef if !@time; + + # strptime() counts years from 1900, except if they are older than 1901 + # in which case it returns the full year (so 1890 -> 1890, but 1984 -> 84, + # and 3790 -> 1890). We make a guess and assume that 1100 <= year < 3000. + $time[5] += 1900 if $time[5] < 1100; + + my %args = ( + year => $time[5], + + # Months start from 0 (January). + month => $time[4] + 1, + day => $time[3], + hour => $time[2], + minute => $time[1], + + # DateTime doesn't like fractional seconds. + # Also, sometimes seconds are undef. + second => defined($time[0]) ? int($time[0]) : undef, + + # If a timezone was specified, use it. Otherwise, use the + # local timezone. + time_zone => DateTime::TimeZone->offset_as_string($time[6]) + || Bugzilla->local_timezone, + ); + + # If something wasn't specified in the date, it's best to just not + # pass it to DateTime at all. (This is important for doing datetime_from + # on the deadline field, which is usually just a date with no time.) + foreach my $arg (keys %args) { + delete $args{$arg} if !defined $args{$arg}; + } + + # This module takes time to load and is only used here, so we + # |require| it here rather than |use| it. + require DateTime; + my $dt = new DateTime(\%args); + + # Now display the date using the given timezone, + # or the user's timezone if none is given. + $dt->set_time_zone($timezone || Bugzilla->user->timezone); + return $dt; } sub bz_crypt { - my ($password, $salt) = @_; - - my $algorithm; - if (!defined $salt) { - # If you don't use a salt, then people can create tables of - # hashes that map to particular passwords, and then break your - # hashing very easily if they have a large-enough table of common - # (or even uncommon) passwords. So we generate a unique salt for - # each password in the database, and then just prepend it to - # the hash. - $salt = generate_random_password(PASSWORD_SALT_LENGTH); - $algorithm = PASSWORD_DIGEST_ALGORITHM; - } - - # We append the algorithm used to the string. This is good because then - # we can change the algorithm being used, in the future, without - # disrupting the validation of existing passwords. Also, this tells - # us if a password is using the old "crypt" method of hashing passwords, - # because the algorithm will be missing from the string. - if ($salt =~ /{([^}]+)}$/) { - $algorithm = $1; - } - - # Wide characters cause crypt and Digest to die. - utf8::encode($password) if utf8::is_utf8($password); - - my $crypted_password; - if (!$algorithm) { - # Crypt the password. - $crypted_password = crypt($password, $salt); - } - else { - my $hasher = Digest->new($algorithm); - # Newly created salts won't yet have a comma. - ($salt) = $salt =~ /^([^,]+),?/; - $hasher->add($password, $salt); - $crypted_password = $salt . ',' . $hasher->b64digest . "{$algorithm}"; - } - - # Return the crypted password. - return $crypted_password; + my ($password, $salt) = @_; + + my $algorithm; + if (!defined $salt) { + + # If you don't use a salt, then people can create tables of + # hashes that map to particular passwords, and then break your + # hashing very easily if they have a large-enough table of common + # (or even uncommon) passwords. So we generate a unique salt for + # each password in the database, and then just prepend it to + # the hash. + $salt = generate_random_password(PASSWORD_SALT_LENGTH); + $algorithm = PASSWORD_DIGEST_ALGORITHM; + } + + # We append the algorithm used to the string. This is good because then + # we can change the algorithm being used, in the future, without + # disrupting the validation of existing passwords. Also, this tells + # us if a password is using the old "crypt" method of hashing passwords, + # because the algorithm will be missing from the string. + if ($salt =~ /{([^}]+)}$/) { + $algorithm = $1; + } + + # Wide characters cause crypt and Digest to die. + utf8::encode($password) if utf8::is_utf8($password); + + my $crypted_password; + if (!$algorithm) { + + # Crypt the password. + $crypted_password = crypt($password, $salt); + } + else { + my $hasher = Digest->new($algorithm); + + # Newly created salts won't yet have a comma. + ($salt) = $salt =~ /^([^,]+),?/; + $hasher->add($password, $salt); + $crypted_password = $salt . ',' . $hasher->b64digest . "{$algorithm}"; + } + + # Return the crypted password. + return $crypted_password; } # If you want to understand the security of strings generated by this @@ -704,188 +750,197 @@ sub bz_crypt { # by the number of characters you generate, and that gets you the equivalent # strength of the string in bits. sub generate_random_password { - my $size = shift || 10; # default to 10 chars if nothing specified - return join("", map{ ('0'..'9','a'..'z','A'..'Z')[irand 62] } (1..$size)); + my $size = shift || 10; # default to 10 chars if nothing specified + return + join("", map { ('0' .. '9', 'a' .. 'z', 'A' .. 'Z')[irand 62] } (1 .. $size)); } sub validate_email_syntax { - my ($email) = @_; - my $match = Bugzilla->params->{'emailregexp'}; - # This regexp follows RFC 2822 section 3.4.1. - my $addr_spec = $Email::Address::addr_spec; - # RFC 2822 section 2.1 specifies that email addresses must - # be made of US-ASCII characters only. - # Email::Address::addr_spec doesn't enforce this. - # We set the max length to 127 to ensure addresses aren't truncated when - # inserted into the tokens.eventdata field. - if ($email =~ /$match/ - && $email !~ /\P{ASCII}/ - && $email =~ /^$addr_spec$/ - && length($email) <= 127) - { - # We assume these checks to suffice to consider the address untainted. - trick_taint($_[0]); - return 1; - } - return 0; + my ($email) = @_; + my $match = Bugzilla->params->{'emailregexp'}; + + # This regexp follows RFC 2822 section 3.4.1. + my $addr_spec = $Email::Address::addr_spec; + + # RFC 2822 section 2.1 specifies that email addresses must + # be made of US-ASCII characters only. + # Email::Address::addr_spec doesn't enforce this. + # We set the max length to 127 to ensure addresses aren't truncated when + # inserted into the tokens.eventdata field. + if ( $email =~ /$match/ + && $email !~ /\P{ASCII}/ + && $email =~ /^$addr_spec$/ + && length($email) <= 127) + { + # We assume these checks to suffice to consider the address untainted. + trick_taint($_[0]); + return 1; + } + return 0; } sub check_email_syntax { - my ($email) = @_; + my ($email) = @_; - unless (validate_email_syntax(@_)) { - ThrowUserError('illegal_email_address', { email => $email }); - } + unless (validate_email_syntax(@_)) { + ThrowUserError('illegal_email_address', {email => $email}); + } } sub validate_date { - my ($date) = @_; - my $date2; - - # $ts is undefined if the parser fails. - my $ts = str2time($date); - if ($ts) { - $date2 = time2str("%Y-%m-%d", $ts); - - $date =~ s/(\d+)-0*(\d+?)-0*(\d+?)/$1-$2-$3/a; - $date2 =~ s/(\d+)-0*(\d+?)-0*(\d+?)/$1-$2-$3/a; - } - my $ret = ($ts && $date eq $date2); - return $ret ? 1 : 0; + my ($date) = @_; + my $date2; + + # $ts is undefined if the parser fails. + my $ts = str2time($date); + if ($ts) { + $date2 = time2str("%Y-%m-%d", $ts); + + $date =~ s/(\d+)-0*(\d+?)-0*(\d+?)/$1-$2-$3/a; + $date2 =~ s/(\d+)-0*(\d+?)-0*(\d+?)/$1-$2-$3/a; + } + my $ret = ($ts && $date eq $date2); + return $ret ? 1 : 0; } sub validate_time { - my ($time) = @_; - my $time2; - - # $ts is undefined if the parser fails. - my $ts = str2time($time); - if ($ts) { - $time2 = time2str("%H:%M:%S", $ts); - if ($time =~ /^(\d{1,2}):(\d\d)(?::(\d\d))?$/) { - $time = sprintf("%02d:%02d:%02d", $1, $2, $3 || 0); - } + my ($time) = @_; + my $time2; + + # $ts is undefined if the parser fails. + my $ts = str2time($time); + if ($ts) { + $time2 = time2str("%H:%M:%S", $ts); + if ($time =~ /^(\d{1,2}):(\d\d)(?::(\d\d))?$/) { + $time = sprintf("%02d:%02d:%02d", $1, $2, $3 || 0); } - my $ret = ($ts && $time eq $time2); - return $ret ? 1 : 0; + } + my $ret = ($ts && $time eq $time2); + return $ret ? 1 : 0; } sub is_7bit_clean { - return $_[0] !~ /[^\x20-\x7E\x0A\x0D]/; + return $_[0] !~ /[^\x20-\x7E\x0A\x0D]/; } sub clean_text { - my $dtext = shift; - if ($dtext) { - # change control characters into a space - $dtext =~ s/[\x00-\x1F\x7F]+/ /g; - } - return trim($dtext); + my $dtext = shift; + if ($dtext) { + + # change control characters into a space + $dtext =~ s/[\x00-\x1F\x7F]+/ /g; + } + return trim($dtext); } sub on_main_db (&) { - my $code = shift; - my $original_dbh = Bugzilla->dbh; - Bugzilla->request_cache->{dbh} = Bugzilla->dbh_main; - $code->(); - Bugzilla->request_cache->{dbh} = $original_dbh; + my $code = shift; + my $original_dbh = Bugzilla->dbh; + Bugzilla->request_cache->{dbh} = Bugzilla->dbh_main; + $code->(); + Bugzilla->request_cache->{dbh} = $original_dbh; } sub get_text { - my ($name, $vars) = @_; - my $template = Bugzilla->template_inner; - $vars ||= {}; - $vars->{'message'} = $name; - my $message; - $template->process('global/message.txt.tmpl', $vars, \$message) - || ThrowTemplateError($template->error()); - - # Remove the indenting that exists in messages.html.tmpl. - $message =~ s/^ //gm; - return $message; + my ($name, $vars) = @_; + my $template = Bugzilla->template_inner; + $vars ||= {}; + $vars->{'message'} = $name; + my $message; + $template->process('global/message.txt.tmpl', $vars, \$message) + || ThrowTemplateError($template->error()); + + # Remove the indenting that exists in messages.html.tmpl. + $message =~ s/^ //gm; + return $message; } sub template_var { - my $name = shift; - my $request_cache = Bugzilla->request_cache; - my $cache = $request_cache->{util_template_var} ||= {}; - my $lang = $request_cache->{template_current_lang}->[0] || ''; - return $cache->{$lang}->{$name} if defined $cache->{$lang}; - - my $template = Bugzilla->template_inner($lang); - my %vars; - # Note: If we suddenly start needing a lot of template_var variables, - # they should move into their own template, not field-descs. - $template->process('global/field-descs.none.tmpl', - { vars => \%vars, in_template_var => 1 }) - || ThrowTemplateError($template->error()); - - $cache->{$lang} = \%vars; - return $vars{$name}; + my $name = shift; + my $request_cache = Bugzilla->request_cache; + my $cache = $request_cache->{util_template_var} ||= {}; + my $lang = $request_cache->{template_current_lang}->[0] || ''; + return $cache->{$lang}->{$name} if defined $cache->{$lang}; + + my $template = Bugzilla->template_inner($lang); + my %vars; + + # Note: If we suddenly start needing a lot of template_var variables, + # they should move into their own template, not field-descs. + $template->process('global/field-descs.none.tmpl', + {vars => \%vars, in_template_var => 1}) + || ThrowTemplateError($template->error()); + + $cache->{$lang} = \%vars; + return $vars{$name}; } sub display_value { - my ($field, $value) = @_; - return template_var('value_descs')->{$field}->{$value} // $value; + my ($field, $value) = @_; + return template_var('value_descs')->{$field}->{$value} // $value; } sub disable_utf8 { - # Turn off UTF8 encoding. - binmode STDOUT, ':bytes'; + + # Turn off UTF8 encoding. + binmode STDOUT, ':bytes'; } use constant UTF8_ACCIDENTAL => qw(shiftjis big5-eten euc-kr euc-jp); sub detect_encoding { - my $data = shift; - - Bugzilla->feature('detect_charset') - || ThrowUserError('feature_disabled', { feature => 'detect_charset' }); - - require Encode::Detect::Detector; - import Encode::Detect::Detector 'detect'; - - my $encoding = detect($data); - $encoding = resolve_alias($encoding) if $encoding; - - # Encode::Detect is bad at detecting certain charsets, but Encode::Guess - # is better at them. Here's the details: - - # shiftjis, big5-eten, euc-kr, and euc-jp: (Encode::Detect - # tends to accidentally mis-detect UTF-8 strings as being - # these encodings.) - if ($encoding && grep($_ eq $encoding, UTF8_ACCIDENTAL)) { - $encoding = undef; - my $decoder = guess_encoding($data, UTF8_ACCIDENTAL); - $encoding = $decoder->name if ref $decoder; - } - - # Encode::Detect sometimes mis-detects various ISO encodings as iso-8859-8, - # or cp1255, but Encode::Guess can usually tell which one it is. - if ($encoding && ($encoding eq 'iso-8859-8' || $encoding eq 'cp1255')) { - my $decoded_as = _guess_iso($data, 'iso-8859-8', - # These are ordered this way because it gives the most - # accurate results. - qw(cp1252 iso-8859-7 iso-8859-2)); - $encoding = $decoded_as if $decoded_as; - } + my $data = shift; + + Bugzilla->feature('detect_charset') + || ThrowUserError('feature_disabled', {feature => 'detect_charset'}); + + require Encode::Detect::Detector; + import Encode::Detect::Detector 'detect'; + + my $encoding = detect($data); + $encoding = resolve_alias($encoding) if $encoding; + + # Encode::Detect is bad at detecting certain charsets, but Encode::Guess + # is better at them. Here's the details: + + # shiftjis, big5-eten, euc-kr, and euc-jp: (Encode::Detect + # tends to accidentally mis-detect UTF-8 strings as being + # these encodings.) + if ($encoding && grep($_ eq $encoding, UTF8_ACCIDENTAL)) { + $encoding = undef; + my $decoder = guess_encoding($data, UTF8_ACCIDENTAL); + $encoding = $decoder->name if ref $decoder; + } + + # Encode::Detect sometimes mis-detects various ISO encodings as iso-8859-8, + # or cp1255, but Encode::Guess can usually tell which one it is. + if ($encoding && ($encoding eq 'iso-8859-8' || $encoding eq 'cp1255')) { + my $decoded_as = _guess_iso( + $data, 'iso-8859-8', + + # These are ordered this way because it gives the most + # accurate results. + qw(cp1252 iso-8859-7 iso-8859-2) + ); + $encoding = $decoded_as if $decoded_as; + } - return $encoding; + return $encoding; } # A helper for detect_encoding. sub _guess_iso { - my ($data, $versus, @isos) = (shift, shift, shift); - - my $encoding; - foreach my $iso (@isos) { - my $decoder = guess_encoding($data, ($iso, $versus)); - if (ref $decoder) { - $encoding = $decoder->name if ref $decoder; - last; - } + my ($data, $versus, @isos) = (shift, shift, shift); + + my $encoding; + foreach my $iso (@isos) { + my $decoder = guess_encoding($data, ($iso, $versus)); + if (ref $decoder) { + $encoding = $decoder->name if ref $decoder; + last; } - return $encoding; + } + return $encoding; } 1; diff --git a/Bugzilla/Version.pm b/Bugzilla/Version.pm index d135c4a7af..143475c494 100644 --- a/Bugzilla/Version.pm +++ b/Bugzilla/Version.pm @@ -26,134 +26,131 @@ use Scalar::Util qw(blessed); use constant DEFAULT_VERSION => 'unspecified'; -use constant DB_TABLE => 'versions'; +use constant DB_TABLE => 'versions'; use constant NAME_FIELD => 'value'; + # This is "id" because it has to be filled in and id is probably the fastest. # We do a custom sort in new_from_list below. use constant LIST_ORDER => 'id'; use constant DB_COLUMNS => qw( - id - value - product_id - isactive + id + value + product_id + isactive ); -use constant REQUIRED_FIELD_MAP => { - product_id => 'product', -}; +use constant REQUIRED_FIELD_MAP => {product_id => 'product',}; use constant UPDATE_COLUMNS => qw( - value - isactive + value + isactive ); use constant VALIDATORS => { - product => \&_check_product, - value => \&_check_value, - isactive => \&Bugzilla::Object::check_boolean, + product => \&_check_product, + value => \&_check_value, + isactive => \&Bugzilla::Object::check_boolean, }; -use constant VALIDATOR_DEPENDENCIES => { - value => ['product'], -}; +use constant VALIDATOR_DEPENDENCIES => {value => ['product'],}; ################################ # Methods ################################ sub new { - my $class = shift; - my $param = shift; - my $dbh = Bugzilla->dbh; - - my $product; - if (ref $param and !defined $param->{id}) { - $product = $param->{product}; - my $name = $param->{name}; - if (!defined $product) { - ThrowCodeError('bad_arg', - {argument => 'product', - function => "${class}::new"}); - } - if (!defined $name) { - ThrowCodeError('bad_arg', - {argument => 'name', - function => "${class}::new"}); - } - - my $condition = 'product_id = ? AND value = ?'; - my @values = ($product->id, $name); - $param = { condition => $condition, values => \@values }; + my $class = shift; + my $param = shift; + my $dbh = Bugzilla->dbh; + + my $product; + if (ref $param and !defined $param->{id}) { + $product = $param->{product}; + my $name = $param->{name}; + if (!defined $product) { + ThrowCodeError('bad_arg', {argument => 'product', function => "${class}::new"}); + } + if (!defined $name) { + ThrowCodeError('bad_arg', {argument => 'name', function => "${class}::new"}); } - unshift @_, $param; - return $class->SUPER::new(@_); + my $condition = 'product_id = ? AND value = ?'; + my @values = ($product->id, $name); + $param = {condition => $condition, values => \@values}; + } + + unshift @_, $param; + return $class->SUPER::new(@_); } sub new_from_list { - my $self = shift; - my $list = $self->SUPER::new_from_list(@_); - return [sort { vers_cmp(lc($a->name), lc($b->name)) } @$list]; + my $self = shift; + my $list = $self->SUPER::new_from_list(@_); + return [sort { vers_cmp(lc($a->name), lc($b->name)) } @$list]; } sub run_create_validators { - my $class = shift; - my $params = $class->SUPER::run_create_validators(@_); - my $product = delete $params->{product}; - $params->{product_id} = $product->id; - return $params; + my $class = shift; + my $params = $class->SUPER::run_create_validators(@_); + my $product = delete $params->{product}; + $params->{product_id} = $product->id; + return $params; } sub bug_count { - my $self = shift; - my $dbh = Bugzilla->dbh; + my $self = shift; + my $dbh = Bugzilla->dbh; - if (!defined $self->{'bug_count'}) { - $self->{'bug_count'} = $dbh->selectrow_array(qq{ + if (!defined $self->{'bug_count'}) { + $self->{'bug_count'} = $dbh->selectrow_array( + qq{ SELECT COUNT(*) FROM bugs WHERE product_id = ? AND version = ?}, undef, - ($self->product_id, $self->name)) || 0; - } - return $self->{'bug_count'}; + ($self->product_id, $self->name) + ) || 0; + } + return $self->{'bug_count'}; } sub update { - my $self = shift; - my $dbh = Bugzilla->dbh; - - $dbh->bz_start_transaction(); - my ($changes, $old_self) = $self->SUPER::update(@_); - - if (exists $changes->{value}) { - $dbh->do('UPDATE bugs SET version = ? - WHERE version = ? AND product_id = ?', - undef, ($self->name, $old_self->name, $self->product_id)); - } - $dbh->bz_commit_transaction(); - - return $changes; + my $self = shift; + my $dbh = Bugzilla->dbh; + + $dbh->bz_start_transaction(); + my ($changes, $old_self) = $self->SUPER::update(@_); + + if (exists $changes->{value}) { + $dbh->do( + 'UPDATE bugs SET version = ? + WHERE version = ? AND product_id = ?', undef, + ($self->name, $old_self->name, $self->product_id) + ); + } + $dbh->bz_commit_transaction(); + + return $changes; } sub remove_from_db { - my $self = shift; - my $dbh = Bugzilla->dbh; + my $self = shift; + my $dbh = Bugzilla->dbh; - $dbh->bz_start_transaction(); + $dbh->bz_start_transaction(); - # Products must have at least one version. - if (scalar(@{$self->product->versions}) == 1) { - ThrowUserError('version_is_last', { version => $self }); - } + # Products must have at least one version. + if (scalar(@{$self->product->versions}) == 1) { + ThrowUserError('version_is_last', {version => $self}); + } - # The version cannot be removed if there are bugs - # associated with it. - if ($self->bug_count) { - ThrowUserError("version_has_bugs", { nb => $self->bug_count }); - } - $self->SUPER::remove_from_db(); + # The version cannot be removed if there are bugs + # associated with it. + if ($self->bug_count) { + ThrowUserError("version_has_bugs", {nb => $self->bug_count}); + } + $self->SUPER::remove_from_db(); - $dbh->bz_commit_transaction(); + $dbh->bz_commit_transaction(); } ############################### @@ -161,45 +158,48 @@ sub remove_from_db { ############################### sub product_id { return $_[0]->{'product_id'}; } -sub is_active { return $_[0]->{'isactive'}; } +sub is_active { return $_[0]->{'isactive'}; } sub product { - my $self = shift; + my $self = shift; - require Bugzilla::Product; - $self->{'product'} ||= Bugzilla::Product->new({ id => $self->product_id, cache => 1 }); - return $self->{'product'}; + require Bugzilla::Product; + $self->{'product'} + ||= Bugzilla::Product->new({id => $self->product_id, cache => 1}); + return $self->{'product'}; } ################################ # Validators ################################ -sub set_value { $_[0]->set('value', $_[1]); } +sub set_value { $_[0]->set('value', $_[1]); } sub set_isactive { $_[0]->set('isactive', $_[1]); } sub _check_value { - my ($invocant, $name, undef, $params) = @_; - my $product = blessed($invocant) ? $invocant->product : $params->{product}; - - $name = trim($name); - $name || ThrowUserError('version_blank_name'); - # Remove unprintable characters - $name = clean_text($name); - - my $version = new Bugzilla::Version({ product => $product, name => $name }); - if ($version && (!ref $invocant || $version->id != $invocant->id)) { - ThrowUserError('version_already_exists', { name => $version->name, - product => $product->name }); - } - return $name; + my ($invocant, $name, undef, $params) = @_; + my $product = blessed($invocant) ? $invocant->product : $params->{product}; + + $name = trim($name); + $name || ThrowUserError('version_blank_name'); + + # Remove unprintable characters + $name = clean_text($name); + + my $version = new Bugzilla::Version({product => $product, name => $name}); + if ($version && (!ref $invocant || $version->id != $invocant->id)) { + ThrowUserError('version_already_exists', + {name => $version->name, product => $product->name}); + } + return $name; } sub _check_product { - my ($invocant, $product) = @_; - $product || ThrowCodeError('param_required', - { function => "$invocant->create", param => 'product' }); - return Bugzilla->user->check_can_admin_product($product->name); + my ($invocant, $product) = @_; + $product + || ThrowCodeError('param_required', + {function => "$invocant->create", param => 'product'}); + return Bugzilla->user->check_can_admin_product($product->name); } ############################### @@ -209,44 +209,52 @@ sub _check_product { # This is taken straight from Sort::Versions 1.5, which is not included # with perl by default. sub vers_cmp { - my ($a, $b) = @_; - - # Remove leading zeroes - Bug 344661 - $a =~ s/^0*(\d.+)/$1/; - $b =~ s/^0*(\d.+)/$1/; - - my @A = ($a =~ /([-.]|\d+|[^-.\d]+)/g); - my @B = ($b =~ /([-.]|\d+|[^-.\d]+)/g); - - my ($A, $B); - while (@A and @B) { - $A = shift @A; - $B = shift @B; - if ($A eq '-' and $B eq '-') { - next; - } elsif ( $A eq '-' ) { - return -1; - } elsif ( $B eq '-') { - return 1; - } elsif ($A eq '.' and $B eq '.') { - next; - } elsif ( $A eq '.' ) { - return -1; - } elsif ( $B eq '.' ) { - return 1; - } elsif ($A =~ /^\d+$/ and $B =~ /^\d+$/) { - if ($A =~ /^0/ || $B =~ /^0/) { - return $A cmp $B if $A cmp $B; - } else { - return $A <=> $B if $A <=> $B; - } - } else { - $A = uc $A; - $B = uc $B; - return $A cmp $B if $A cmp $B; - } + my ($a, $b) = @_; + + # Remove leading zeroes - Bug 344661 + $a =~ s/^0*(\d.+)/$1/; + $b =~ s/^0*(\d.+)/$1/; + + my @A = ($a =~ /([-.]|\d+|[^-.\d]+)/g); + my @B = ($b =~ /([-.]|\d+|[^-.\d]+)/g); + + my ($A, $B); + while (@A and @B) { + $A = shift @A; + $B = shift @B; + if ($A eq '-' and $B eq '-') { + next; + } + elsif ($A eq '-') { + return -1; + } + elsif ($B eq '-') { + return 1; + } + elsif ($A eq '.' and $B eq '.') { + next; + } + elsif ($A eq '.') { + return -1; + } + elsif ($B eq '.') { + return 1; + } + elsif ($A =~ /^\d+$/ and $B =~ /^\d+$/) { + if ($A =~ /^0/ || $B =~ /^0/) { + return $A cmp $B if $A cmp $B; + } + else { + return $A <=> $B if $A <=> $B; + } + } + else { + $A = uc $A; + $B = uc $B; + return $A cmp $B if $A cmp $B; } - return @A <=> @B; + } + return @A <=> @B; } 1; diff --git a/Bugzilla/WebService.pm b/Bugzilla/WebService.pm index 743d54c879..16bc86e615 100644 --- a/Bugzilla/WebService.pm +++ b/Bugzilla/WebService.pm @@ -5,7 +5,7 @@ # This Source Code Form is "Incompatible With Secondary Licenses", as # defined by the Mozilla Public License, v. 2.0. -# This is the base class for $self in WebService method calls. For the +# This is the base class for $self in WebService method calls. For the # actual RPC server, see Bugzilla::WebService::Server and its subclasses. package Bugzilla::WebService; @@ -17,11 +17,12 @@ use Bugzilla::WebService::Server; # Used by the JSON-RPC server to convert incoming date fields apprpriately. use constant DATE_FIELDS => {}; + # Used by the JSON-RPC server to convert incoming base64 fields appropriately. use constant BASE64_FIELDS => {}; # For some methods, we shouldn't call Bugzilla->login before we call them -use constant LOGIN_EXEMPT => { }; +use constant LOGIN_EXEMPT => {}; # Used to allow methods to be called in the JSON-RPC WebService via GET. # Methods that can modify data MUST not be listed here. @@ -32,8 +33,8 @@ use constant READ_ONLY => (); use constant PUBLIC_METHODS => (); sub login_exempt { - my ($class, $method) = @_; - return $class->LOGIN_EXEMPT->{$method}; + my ($class, $method) = @_; + return $class->LOGIN_EXEMPT->{$method}; } 1; diff --git a/Bugzilla/WebService/Bug.pm b/Bugzilla/WebService/Bug.pm index 82676b0b62..f128d3949b 100644 --- a/Bugzilla/WebService/Bug.pm +++ b/Bugzilla/WebService/Bug.pm @@ -19,7 +19,8 @@ use Bugzilla::Constants; use Bugzilla::Error; use Bugzilla::Field; use Bugzilla::WebService::Constants; -use Bugzilla::WebService::Util qw(extract_flags filter filter_wants validate translate); +use Bugzilla::WebService::Util + qw(extract_flags filter filter_wants validate translate); use Bugzilla::Bug; use Bugzilla::BugMail; use Bugzilla::Util qw(trick_taint trim diff_arrays detaint_natural); @@ -43,58 +44,54 @@ use Storable qw(dclone); use constant PRODUCT_SPECIFIC_FIELDS => qw(version target_milestone component); use constant DATE_FIELDS => { - comments => ['new_since'], - history => ['new_since'], - search => ['last_change_time', 'creation_time'], + comments => ['new_since'], + history => ['new_since'], + search => ['last_change_time', 'creation_time'], }; -use constant BASE64_FIELDS => { - add_attachment => ['data'], -}; +use constant BASE64_FIELDS => {add_attachment => ['data'],}; use constant READ_ONLY => qw( - attachments - comments - fields - get - history - legal_values - search + attachments + comments + fields + get + history + legal_values + search ); use constant PUBLIC_METHODS => qw( - add_attachment - add_comment - attachments - comments - create - fields - get - history - legal_values - possible_duplicates - render_comment - search - search_comment_tags - update - update_attachment - update_comment_tags - update_see_also - update_tags + add_attachment + add_comment + attachments + comments + create + fields + get + history + legal_values + possible_duplicates + render_comment + search + search_comment_tags + update + update_attachment + update_comment_tags + update_see_also + update_tags ); -use constant ATTACHMENT_MAPPED_SETTERS => { - file_name => 'filename', - summary => 'description', -}; +use constant ATTACHMENT_MAPPED_SETTERS => + {file_name => 'filename', summary => 'description',}; use constant ATTACHMENT_MAPPED_RETURNS => { - description => 'summary', - ispatch => 'is_patch', - isprivate => 'is_private', - isobsolete => 'is_obsolete', - filename => 'file_name', - mimetype => 'content_type', + description => 'summary', + ispatch => 'is_patch', + isprivate => 'is_private', + isobsolete => 'is_obsolete', + filename => 'file_name', + mimetype => 'content_type', }; ########### @@ -102,1170 +99,1197 @@ use constant ATTACHMENT_MAPPED_RETURNS => { ########### sub fields { - my ($self, $params) = validate(@_, 'ids', 'names'); + my ($self, $params) = validate(@_, 'ids', 'names'); - Bugzilla->switch_to_shadow_db(); + Bugzilla->switch_to_shadow_db(); - my @fields; - if (defined $params->{ids}) { - my $ids = $params->{ids}; - foreach my $id (@$ids) { - my $loop_field = Bugzilla::Field->check({ id => $id }); - push(@fields, $loop_field); - } + my @fields; + if (defined $params->{ids}) { + my $ids = $params->{ids}; + foreach my $id (@$ids) { + my $loop_field = Bugzilla::Field->check({id => $id}); + push(@fields, $loop_field); } - - if (defined $params->{names}) { - my $names = $params->{names}; - foreach my $field_name (@$names) { - my $loop_field = Bugzilla::Field->check($field_name); - # Don't push in duplicate fields if we also asked for this field - # in "ids". - if (!grep($_->id == $loop_field->id, @fields)) { - push(@fields, $loop_field); - } - } + } + + if (defined $params->{names}) { + my $names = $params->{names}; + foreach my $field_name (@$names) { + my $loop_field = Bugzilla::Field->check($field_name); + + # Don't push in duplicate fields if we also asked for this field + # in "ids". + if (!grep($_->id == $loop_field->id, @fields)) { + push(@fields, $loop_field); + } } - - if (!defined $params->{ids} and !defined $params->{names}) { - @fields = @{ Bugzilla->fields({ obsolete => 0 }) }; + } + + if (!defined $params->{ids} and !defined $params->{names}) { + @fields = @{Bugzilla->fields({obsolete => 0})}; + } + + my @fields_out; + foreach my $field (@fields) { + my $visibility_field + = $field->visibility_field ? $field->visibility_field->name : undef; + my $vis_values = $field->visibility_values; + my $value_field = $field->value_field ? $field->value_field->name : undef; + + my (@values, $has_values); + if ( ($field->is_select and $field->name ne 'product') + or grep($_ eq $field->name, PRODUCT_SPECIFIC_FIELDS) + or $field->name eq 'keywords') + { + $has_values = 1; + @values = @{$self->_legal_field_values({field => $field})}; } - my @fields_out; - foreach my $field (@fields) { - my $visibility_field = $field->visibility_field - ? $field->visibility_field->name : undef; - my $vis_values = $field->visibility_values; - my $value_field = $field->value_field - ? $field->value_field->name : undef; - - my (@values, $has_values); - if ( ($field->is_select and $field->name ne 'product') - or grep($_ eq $field->name, PRODUCT_SPECIFIC_FIELDS) - or $field->name eq 'keywords') - { - $has_values = 1; - @values = @{ $self->_legal_field_values({ field => $field }) }; - } - - if (grep($_ eq $field->name, PRODUCT_SPECIFIC_FIELDS)) { - $value_field = 'product'; - } + if (grep($_ eq $field->name, PRODUCT_SPECIFIC_FIELDS)) { + $value_field = 'product'; + } - my %field_data = ( - id => $self->type('int', $field->id), - type => $self->type('int', $field->type), - is_custom => $self->type('boolean', $field->custom), - name => $self->type('string', $field->name), - display_name => $self->type('string', $field->description), - is_mandatory => $self->type('boolean', $field->is_mandatory), - is_on_bug_entry => $self->type('boolean', $field->enter_bug), - visibility_field => $self->type('string', $visibility_field), - visibility_values => - [ map { $self->type('string', $_->name) } @$vis_values ], - ); - if ($has_values) { - $field_data{value_field} = $self->type('string', $value_field); - $field_data{values} = \@values; - }; - push(@fields_out, filter $params, \%field_data); + my %field_data = ( + id => $self->type('int', $field->id), + type => $self->type('int', $field->type), + is_custom => $self->type('boolean', $field->custom), + name => $self->type('string', $field->name), + display_name => $self->type('string', $field->description), + is_mandatory => $self->type('boolean', $field->is_mandatory), + is_on_bug_entry => $self->type('boolean', $field->enter_bug), + visibility_field => $self->type('string', $visibility_field), + visibility_values => [map { $self->type('string', $_->name) } @$vis_values], + ); + if ($has_values) { + $field_data{value_field} = $self->type('string', $value_field); + $field_data{values} = \@values; } + push(@fields_out, filter $params, \%field_data); + } - return { fields => \@fields_out }; + return {fields => \@fields_out}; } sub _legal_field_values { - my ($self, $params) = @_; - my $field = $params->{field}; - my $field_name = $field->name; - my $user = Bugzilla->user; - - my @result; - if (grep($_ eq $field_name, PRODUCT_SPECIFIC_FIELDS)) { - my @list; - if ($field_name eq 'version') { - @list = Bugzilla::Version->get_all; - } - elsif ($field_name eq 'component') { - @list = Bugzilla::Component->get_all; - } - else { - @list = Bugzilla::Milestone->get_all; - } + my ($self, $params) = @_; + my $field = $params->{field}; + my $field_name = $field->name; + my $user = Bugzilla->user; + + my @result; + if (grep($_ eq $field_name, PRODUCT_SPECIFIC_FIELDS)) { + my @list; + if ($field_name eq 'version') { + @list = Bugzilla::Version->get_all; + } + elsif ($field_name eq 'component') { + @list = Bugzilla::Component->get_all; + } + else { + @list = Bugzilla::Milestone->get_all; + } - foreach my $value (@list) { - my $sortkey = $field_name eq 'target_milestone' - ? $value->sortkey : 0; - # XXX This is very slow for large numbers of values. - my $product_name = $value->product->name; - if ($user->can_see_product($product_name)) { - push(@result, { - name => $self->type('string', $value->name), - sort_key => $self->type('int', $sortkey), - sortkey => $self->type('int', $sortkey), # deprecated - visibility_values => [$self->type('string', $product_name)], - is_active => $self->type('boolean', $value->is_active), - }); - } - } + foreach my $value (@list) { + my $sortkey = $field_name eq 'target_milestone' ? $value->sortkey : 0; + + # XXX This is very slow for large numbers of values. + my $product_name = $value->product->name; + if ($user->can_see_product($product_name)) { + push( + @result, + { + name => $self->type('string', $value->name), + sort_key => $self->type('int', $sortkey), + sortkey => $self->type('int', $sortkey), # deprecated + visibility_values => [$self->type('string', $product_name)], + is_active => $self->type('boolean', $value->is_active), + } + ); + } } + } + + elsif ($field_name eq 'bug_status') { + my @status_all = Bugzilla::Status->get_all; + my $initial_status = bless( + { + id => 0, + name => '', + is_open => 1, + sortkey => 0, + can_change_to => Bugzilla::Status->can_change_to + }, + 'Bugzilla::Status' + ); + unshift(@status_all, $initial_status); + + foreach my $status (@status_all) { + my @can_change_to; + foreach my $change_to (@{$status->can_change_to}) { + + # There's no need to note that a status can transition + # to itself. + next if $change_to->id == $status->id; + my %change_to_hash = ( + name => $self->type('string', $change_to->name), + comment_required => + $self->type('boolean', $change_to->comment_required_on_change_from($status)), + ); + push(@can_change_to, \%change_to_hash); + } - elsif ($field_name eq 'bug_status') { - my @status_all = Bugzilla::Status->get_all; - my $initial_status = bless({ id => 0, name => '', is_open => 1, sortkey => 0, - can_change_to => Bugzilla::Status->can_change_to }, - 'Bugzilla::Status'); - unshift(@status_all, $initial_status); - - foreach my $status (@status_all) { - my @can_change_to; - foreach my $change_to (@{ $status->can_change_to }) { - # There's no need to note that a status can transition - # to itself. - next if $change_to->id == $status->id; - my %change_to_hash = ( - name => $self->type('string', $change_to->name), - comment_required => $self->type('boolean', - $change_to->comment_required_on_change_from($status)), - ); - push(@can_change_to, \%change_to_hash); - } - - push (@result, { - name => $self->type('string', $status->name), - is_open => $self->type('boolean', $status->is_open), - sort_key => $self->type('int', $status->sortkey), - sortkey => $self->type('int', $status->sortkey), # deprecated - can_change_to => \@can_change_to, - visibility_values => [], - }); + push( + @result, + { + name => $self->type('string', $status->name), + is_open => $self->type('boolean', $status->is_open), + sort_key => $self->type('int', $status->sortkey), + sortkey => $self->type('int', $status->sortkey), # deprecated + can_change_to => \@can_change_to, + visibility_values => [], } + ); } - - elsif ($field_name eq 'keywords') { - my @legal_keywords = Bugzilla::Keyword->get_all; - foreach my $value (@legal_keywords) { - next unless $value->is_active; - push (@result, { - name => $self->type('string', $value->name), - description => $self->type('string', $value->description), - }); + } + + elsif ($field_name eq 'keywords') { + my @legal_keywords = Bugzilla::Keyword->get_all; + foreach my $value (@legal_keywords) { + next unless $value->is_active; + push( + @result, + { + name => $self->type('string', $value->name), + description => $self->type('string', $value->description), } + ); } - else { - my @values = Bugzilla::Field::Choice->type($field)->get_all(); - foreach my $value (@values) { - my $vis_val = $value->visibility_value; - push(@result, { - name => $self->type('string', $value->name), - sort_key => $self->type('int' , $value->sortkey), - sortkey => $self->type('int' , $value->sortkey), # deprecated - visibility_values => [ - defined $vis_val ? $self->type('string', $vis_val->name) - : () - ], - }); + } + else { + my @values = Bugzilla::Field::Choice->type($field)->get_all(); + foreach my $value (@values) { + my $vis_val = $value->visibility_value; + push( + @result, + { + name => $self->type('string', $value->name), + sort_key => $self->type('int', $value->sortkey), + sortkey => $self->type('int', $value->sortkey), # deprecated + visibility_values => + [defined $vis_val ? $self->type('string', $vis_val->name) : ()], } + ); } + } - return \@result; + return \@result; } sub comments { - my ($self, $params) = validate(@_, 'ids', 'comment_ids'); + my ($self, $params) = validate(@_, 'ids', 'comment_ids'); - if (!(defined $params->{ids} || defined $params->{comment_ids})) { - ThrowCodeError('params_required', - { function => 'Bug.comments', - params => ['ids', 'comment_ids'] }); - } + if (!(defined $params->{ids} || defined $params->{comment_ids})) { + ThrowCodeError('params_required', + {function => 'Bug.comments', params => ['ids', 'comment_ids']}); + } - my $bug_ids = $params->{ids} || []; - my $comment_ids = $params->{comment_ids} || []; - - my $dbh = Bugzilla->switch_to_shadow_db(); - my $user = Bugzilla->user; - - my %bugs; - foreach my $bug_id (@$bug_ids) { - my $bug = Bugzilla::Bug->check($bug_id); - # We want the API to always return comments in the same order. - - my $comments = $bug->comments({ order => 'oldest_to_newest', - after => $params->{new_since} }); - my @result; - foreach my $comment (@$comments) { - next if $comment->is_private && !$user->is_insider; - push(@result, $self->_translate_comment($comment, $params)); - } - $bugs{$bug->id}{'comments'} = \@result; + my $bug_ids = $params->{ids} || []; + my $comment_ids = $params->{comment_ids} || []; + + my $dbh = Bugzilla->switch_to_shadow_db(); + my $user = Bugzilla->user; + + my %bugs; + foreach my $bug_id (@$bug_ids) { + my $bug = Bugzilla::Bug->check($bug_id); + + # We want the API to always return comments in the same order. + + my $comments + = $bug->comments({order => 'oldest_to_newest', after => $params->{new_since} + }); + my @result; + foreach my $comment (@$comments) { + next if $comment->is_private && !$user->is_insider; + push(@result, $self->_translate_comment($comment, $params)); + } + $bugs{$bug->id}{'comments'} = \@result; + } + + my %comments; + if (scalar @$comment_ids) { + my @ids = map { trim($_) } @$comment_ids; + my $comment_data = Bugzilla::Comment->new_from_list(\@ids); + + # See if we were passed any invalid comment ids. + my %got_ids = map { $_->id => 1 } @$comment_data; + foreach my $comment_id (@ids) { + if (!$got_ids{$comment_id}) { + ThrowUserError('comment_id_invalid', {id => $comment_id}); + } } - my %comments; - if (scalar @$comment_ids) { - my @ids = map { trim($_) } @$comment_ids; - my $comment_data = Bugzilla::Comment->new_from_list(\@ids); - - # See if we were passed any invalid comment ids. - my %got_ids = map { $_->id => 1 } @$comment_data; - foreach my $comment_id (@ids) { - if (!$got_ids{$comment_id}) { - ThrowUserError('comment_id_invalid', { id => $comment_id }); - } - } - - # Now make sure that we can see all the associated bugs. - my %got_bug_ids = map { $_->bug_id => 1 } @$comment_data; - Bugzilla::Bug->check($_) foreach (keys %got_bug_ids); - - foreach my $comment (@$comment_data) { - if ($comment->is_private && !$user->is_insider) { - ThrowUserError('comment_is_private', { id => $comment->id }); - } - $comments{$comment->id} = - $self->_translate_comment($comment, $params); - } + # Now make sure that we can see all the associated bugs. + my %got_bug_ids = map { $_->bug_id => 1 } @$comment_data; + Bugzilla::Bug->check($_) foreach (keys %got_bug_ids); + + foreach my $comment (@$comment_data) { + if ($comment->is_private && !$user->is_insider) { + ThrowUserError('comment_is_private', {id => $comment->id}); + } + $comments{$comment->id} = $self->_translate_comment($comment, $params); } + } - return { bugs => \%bugs, comments => \%comments }; + return {bugs => \%bugs, comments => \%comments}; } sub render_comment { - my ($self, $params) = @_; + my ($self, $params) = @_; - unless (defined $params->{text}) { - ThrowCodeError('params_required', - { function => 'Bug.render_comment', - params => ['text'] }); - } + unless (defined $params->{text}) { + ThrowCodeError('params_required', + {function => 'Bug.render_comment', params => ['text']}); + } - Bugzilla->switch_to_shadow_db(); - my $bug = $params->{id} ? Bugzilla::Bug->check($params->{id}) : undef; - - my $tmpl - = $params->{markdown} - ? '[% text FILTER markdown(bug, { is_markdown => 1 }) %]' - : '[% text FILTER wrap_cite FILTER markdown(bug) %]'; - my $html; - my $template = Bugzilla->template; - $template->process( - \$tmpl, - { bug => $bug, text => $params->{text}}, - \$html - ); + Bugzilla->switch_to_shadow_db(); + my $bug = $params->{id} ? Bugzilla::Bug->check($params->{id}) : undef; - return { html => $html }; + my $tmpl + = $params->{markdown} + ? '[% text FILTER markdown(bug, { is_markdown => 1 }) %]' + : '[% text FILTER wrap_cite FILTER markdown(bug) %]'; + my $html; + my $template = Bugzilla->template; + $template->process(\$tmpl, {bug => $bug, text => $params->{text}}, \$html); + + return {html => $html}; } # Helper for Bug.comments sub _translate_comment { - my ($self, $comment, $filters, $types, $prefix) = @_; - my $attach_id = $comment->is_about_attachment ? $comment->extra_data - : undef; - - my $comment_hash = { - id => $self->type('int', $comment->id), - bug_id => $self->type('int', $comment->bug_id), - creator => $self->type('login', $comment->author->login), - time => $self->type('dateTime', $comment->creation_ts), - creation_time => $self->type('dateTime', $comment->creation_ts), - is_private => $self->type('boolean', $comment->is_private), - is_markdown => $self->type('boolean', $comment->is_markdown), - text => $self->type('string', $comment->body_full), - attachment_id => $self->type('int', $attach_id), - count => $self->type('int', $comment->count), - }; - - # Don't load comment tags unless enabled - if (Bugzilla->params->{'comment_taggers_group'}) { - $comment_hash->{tags} = [ - map { $self->type('string', $_) } - @{ $comment->tags } - ]; - } - - return filter($filters, $comment_hash, $types, $prefix); + my ($self, $comment, $filters, $types, $prefix) = @_; + my $attach_id = $comment->is_about_attachment ? $comment->extra_data : undef; + + my $comment_hash = { + id => $self->type('int', $comment->id), + bug_id => $self->type('int', $comment->bug_id), + creator => $self->type('login', $comment->author->login), + time => $self->type('dateTime', $comment->creation_ts), + creation_time => $self->type('dateTime', $comment->creation_ts), + is_private => $self->type('boolean', $comment->is_private), + is_markdown => $self->type('boolean', $comment->is_markdown), + text => $self->type('string', $comment->body_full), + attachment_id => $self->type('int', $attach_id), + count => $self->type('int', $comment->count), + }; + + # Don't load comment tags unless enabled + if (Bugzilla->params->{'comment_taggers_group'}) { + $comment_hash->{tags} = [map { $self->type('string', $_) } @{$comment->tags}]; + } + + return filter($filters, $comment_hash, $types, $prefix); } sub get { - my ($self, $params) = validate(@_, 'ids'); - - Bugzilla->switch_to_shadow_db() unless Bugzilla->user->id; - - my $ids = $params->{ids}; - defined $ids || ThrowCodeError('param_required', { param => 'ids' }); - - my (@bugs, @faults, @hashes); - - # Cache permissions for bugs. This highly reduces the number of calls to the DB. - # visible_bugs() is only able to handle bug IDs, so we have to skip aliases. - my @int = grep { $_ =~ /^\d+$/ } @$ids; - Bugzilla->user->visible_bugs(\@int); - - foreach my $bug_id (@$ids) { - my $bug; - if ($params->{permissive}) { - eval { $bug = Bugzilla::Bug->check($bug_id); }; - if ($@) { - push(@faults, {id => $bug_id, - faultString => $@->faultstring, - faultCode => $@->faultcode, - } - ); - undef $@; - next; - } - } - else { - $bug = Bugzilla::Bug->check($bug_id); - } - push(@bugs, $bug); - push(@hashes, $self->_bug_to_hash($bug, $params)); + my ($self, $params) = validate(@_, 'ids'); + + Bugzilla->switch_to_shadow_db() unless Bugzilla->user->id; + + my $ids = $params->{ids}; + defined $ids || ThrowCodeError('param_required', {param => 'ids'}); + + my (@bugs, @faults, @hashes); + + # Cache permissions for bugs. This highly reduces the number of calls to the DB. + # visible_bugs() is only able to handle bug IDs, so we have to skip aliases. + my @int = grep { $_ =~ /^\d+$/ } @$ids; + Bugzilla->user->visible_bugs(\@int); + + foreach my $bug_id (@$ids) { + my $bug; + if ($params->{permissive}) { + eval { $bug = Bugzilla::Bug->check($bug_id); }; + if ($@) { + push(@faults, + {id => $bug_id, faultString => $@->faultstring, faultCode => $@->faultcode,}); + undef $@; + next; + } } + else { + $bug = Bugzilla::Bug->check($bug_id); + } + push(@bugs, $bug); + push(@hashes, $self->_bug_to_hash($bug, $params)); + } - # Set the ETag before inserting the update tokens - # since the tokens will always be unique even if - # the data has not changed. - $self->bz_etag(\@hashes); + # Set the ETag before inserting the update tokens + # since the tokens will always be unique even if + # the data has not changed. + $self->bz_etag(\@hashes); - $self->_add_update_tokens($params, \@bugs, \@hashes); + $self->_add_update_tokens($params, \@bugs, \@hashes); - return { bugs => \@hashes, faults => \@faults }; + return {bugs => \@hashes, faults => \@faults}; } -# this is a function that gets bug activity for list of bug ids +# this is a function that gets bug activity for list of bug ids # it can be called as the following: # $call = $rpc->call( 'Bug.history', { ids => [1,2] }); sub history { - my ($self, $params) = validate(@_, 'ids'); - - Bugzilla->switch_to_shadow_db(); - - my $ids = $params->{ids}; - defined $ids || ThrowCodeError('param_required', { param => 'ids' }); - - my %api_name = reverse %{ Bugzilla::Bug::FIELD_MAP() }; - $api_name{'bug_group'} = 'groups'; - - my @return; - foreach my $bug_id (@$ids) { - my %item; - my $bug = Bugzilla::Bug->check($bug_id); - $bug_id = $bug->id; - $item{id} = $self->type('int', $bug_id); - - my ($activity) = $bug->get_activity(undef, $params->{new_since}); - - my @history; - foreach my $changeset (@$activity) { - my %bug_history; - $bug_history{when} = $self->type('dateTime', $changeset->{when}); - $bug_history{who} = $self->type('string', $changeset->{who}); - $bug_history{changes} = []; - foreach my $change (@{ $changeset->{changes} }) { - my $api_field = $api_name{$change->{fieldname}} || $change->{fieldname}; - my $attach_id = delete $change->{attachid}; - if ($attach_id) { - $change->{attachment_id} = $self->type('int', $attach_id); - } - $change->{removed} = $self->type('string', $change->{removed}); - $change->{added} = $self->type('string', $change->{added}); - $change->{field_name} = $self->type('string', $api_field); - delete $change->{fieldname}; - push (@{$bug_history{changes}}, $change); - } - - push (@history, \%bug_history); + my ($self, $params) = validate(@_, 'ids'); + + Bugzilla->switch_to_shadow_db(); + + my $ids = $params->{ids}; + defined $ids || ThrowCodeError('param_required', {param => 'ids'}); + + my %api_name = reverse %{Bugzilla::Bug::FIELD_MAP()}; + $api_name{'bug_group'} = 'groups'; + + my @return; + foreach my $bug_id (@$ids) { + my %item; + my $bug = Bugzilla::Bug->check($bug_id); + $bug_id = $bug->id; + $item{id} = $self->type('int', $bug_id); + + my ($activity) = $bug->get_activity(undef, $params->{new_since}); + + my @history; + foreach my $changeset (@$activity) { + my %bug_history; + $bug_history{when} = $self->type('dateTime', $changeset->{when}); + $bug_history{who} = $self->type('string', $changeset->{who}); + $bug_history{changes} = []; + foreach my $change (@{$changeset->{changes}}) { + my $api_field = $api_name{$change->{fieldname}} || $change->{fieldname}; + my $attach_id = delete $change->{attachid}; + if ($attach_id) { + $change->{attachment_id} = $self->type('int', $attach_id); } + $change->{removed} = $self->type('string', $change->{removed}); + $change->{added} = $self->type('string', $change->{added}); + $change->{field_name} = $self->type('string', $api_field); + delete $change->{fieldname}; + push(@{$bug_history{changes}}, $change); + } + + push(@history, \%bug_history); + } - $item{history} = \@history; + $item{history} = \@history; - # alias is returned in case users passes a mixture of ids and aliases - # then they get to know which bug activity relates to which value - # they passed - $item{alias} = [ map { $self->type('string', $_) } @{ $bug->alias } ]; + # alias is returned in case users passes a mixture of ids and aliases + # then they get to know which bug activity relates to which value + # they passed + $item{alias} = [map { $self->type('string', $_) } @{$bug->alias}]; - push(@return, \%item); - } + push(@return, \%item); + } - return { bugs => \@return }; + return {bugs => \@return}; } sub search { - my ($self, $params) = @_; - my $user = Bugzilla->user; - my $dbh = Bugzilla->dbh; + my ($self, $params) = @_; + my $user = Bugzilla->user; + my $dbh = Bugzilla->dbh; - Bugzilla->switch_to_shadow_db(); + Bugzilla->switch_to_shadow_db(); - my $match_params = dclone($params); - delete $match_params->{include_fields}; - delete $match_params->{exclude_fields}; + my $match_params = dclone($params); + delete $match_params->{include_fields}; + delete $match_params->{exclude_fields}; - # Determine whether this is a quicksearch query - if (exists $match_params->{quicksearch}) { - my $quicksearch = quicksearch($match_params->{'quicksearch'}); - my $cgi = Bugzilla::CGI->new($quicksearch); - $match_params = $cgi->Vars; - } - - if ( defined($match_params->{offset}) and !defined($match_params->{limit}) ) { - ThrowCodeError('param_required', - { param => 'limit', function => 'Bug.search()' }); - } - - my $max_results = Bugzilla->params->{max_search_results}; - unless (defined $match_params->{limit} && $match_params->{limit} == 0) { - if (!defined $match_params->{limit} || $match_params->{limit} > $max_results) { - $match_params->{limit} = $max_results; - } - } - else { - delete $match_params->{limit}; - delete $match_params->{offset}; - } + # Determine whether this is a quicksearch query + if (exists $match_params->{quicksearch}) { + my $quicksearch = quicksearch($match_params->{'quicksearch'}); + my $cgi = Bugzilla::CGI->new($quicksearch); + $match_params = $cgi->Vars; + } - $match_params = Bugzilla::Bug::map_fields($match_params); - - my %options = ( fields => ['bug_id'] ); - - # Find the highest custom field id - my @field_ids = grep(/^f(\d+)$/a, keys %$match_params); - my $last_field_id = @field_ids ? max @field_ids + 1 : 1; - - # Do special search types for certain fields. - if (my $change_when = delete $match_params->{'delta_ts'}) { - $match_params->{"f${last_field_id}"} = 'delta_ts'; - $match_params->{"o${last_field_id}"} = 'greaterthaneq'; - $match_params->{"v${last_field_id}"} = $change_when; - $last_field_id++; - } - if (my $creation_when = delete $match_params->{'creation_ts'}) { - $match_params->{"f${last_field_id}"} = 'creation_ts'; - $match_params->{"o${last_field_id}"} = 'greaterthaneq'; - $match_params->{"v${last_field_id}"} = $creation_when; - $last_field_id++; - } - - # Some fields require a search type such as short desc, keywords, etc. - foreach my $param (qw(short_desc longdesc status_whiteboard bug_file_loc)) { - if (defined $match_params->{$param} && !defined $match_params->{$param . '_type'}) { - $match_params->{$param . '_type'} = 'allwordssubstr'; - } - } - if (defined $match_params->{'keywords'} && !defined $match_params->{'keywords_type'}) { - $match_params->{'keywords_type'} = 'allwords'; - } + if (defined($match_params->{offset}) and !defined($match_params->{limit})) { + ThrowCodeError('param_required', + {param => 'limit', function => 'Bug.search()'}); + } - # Backwards compatibility with old method regarding role search - $match_params->{'reporter'} = delete $match_params->{'creator'} if $match_params->{'creator'}; - foreach my $role (qw(assigned_to reporter qa_contact longdesc cc)) { - next if !exists $match_params->{$role}; - my $value = delete $match_params->{$role}; - $match_params->{"f${last_field_id}"} = $role; - $match_params->{"o${last_field_id}"} = "anywordssubstr"; - $match_params->{"v${last_field_id}"} = ref $value ? join(" ", @{$value}) : $value; - $last_field_id++; + my $max_results = Bugzilla->params->{max_search_results}; + unless (defined $match_params->{limit} && $match_params->{limit} == 0) { + if (!defined $match_params->{limit} || $match_params->{limit} > $max_results) { + $match_params->{limit} = $max_results; } - - # If no other parameters have been passed other than limit and offset - # then we throw error if system is configured to do so. - if (!grep(!/^(limit|offset)$/, keys %$match_params) - && !Bugzilla->params->{search_allow_no_criteria}) + } + else { + delete $match_params->{limit}; + delete $match_params->{offset}; + } + + $match_params = Bugzilla::Bug::map_fields($match_params); + + my %options = (fields => ['bug_id']); + + # Find the highest custom field id + my @field_ids = grep(/^f(\d+)$/a, keys %$match_params); + my $last_field_id = @field_ids ? max @field_ids + 1 : 1; + + # Do special search types for certain fields. + if (my $change_when = delete $match_params->{'delta_ts'}) { + $match_params->{"f${last_field_id}"} = 'delta_ts'; + $match_params->{"o${last_field_id}"} = 'greaterthaneq'; + $match_params->{"v${last_field_id}"} = $change_when; + $last_field_id++; + } + if (my $creation_when = delete $match_params->{'creation_ts'}) { + $match_params->{"f${last_field_id}"} = 'creation_ts'; + $match_params->{"o${last_field_id}"} = 'greaterthaneq'; + $match_params->{"v${last_field_id}"} = $creation_when; + $last_field_id++; + } + + # Some fields require a search type such as short desc, keywords, etc. + foreach my $param (qw(short_desc longdesc status_whiteboard bug_file_loc)) { + if (defined $match_params->{$param} + && !defined $match_params->{$param . '_type'}) { - ThrowUserError('buglist_parameters_required'); + $match_params->{$param . '_type'} = 'allwordssubstr'; } - - # Allow the use of order shortcuts similar to web UI - if ($match_params->{order}) { - # Convert the value of the "order" form field into a list of columns - # by which to sort the results. - my %order_types = ( - "Bug Number" => [ "bug_id" ], - "Importance" => [ "priority", "bug_severity" ], - "Assignee" => [ "assigned_to", "bug_status", "priority", "bug_id" ], - "Last Changed" => [ "changeddate", "bug_status", "priority", - "assigned_to", "bug_id" ], - ); - if ($order_types{$match_params->{order}}) { - $options{order} = $order_types{$match_params->{order}}; - } - else { - $options{order} = [ split(/\s*,\s*/, $match_params->{order}) ]; - } + } + if (defined $match_params->{'keywords'} + && !defined $match_params->{'keywords_type'}) + { + $match_params->{'keywords_type'} = 'allwords'; + } + + # Backwards compatibility with old method regarding role search + $match_params->{'reporter'} = delete $match_params->{'creator'} + if $match_params->{'creator'}; + foreach my $role (qw(assigned_to reporter qa_contact longdesc cc)) { + next if !exists $match_params->{$role}; + my $value = delete $match_params->{$role}; + $match_params->{"f${last_field_id}"} = $role; + $match_params->{"o${last_field_id}"} = "anywordssubstr"; + $match_params->{"v${last_field_id}"} + = ref $value ? join(" ", @{$value}) : $value; + $last_field_id++; + } + + # If no other parameters have been passed other than limit and offset + # then we throw error if system is configured to do so. + if ( !grep(!/^(limit|offset)$/, keys %$match_params) + && !Bugzilla->params->{search_allow_no_criteria}) + { + ThrowUserError('buglist_parameters_required'); + } + + # Allow the use of order shortcuts similar to web UI + if ($match_params->{order}) { + + # Convert the value of the "order" form field into a list of columns + # by which to sort the results. + my %order_types = ( + "Bug Number" => ["bug_id"], + "Importance" => ["priority", "bug_severity"], + "Assignee" => ["assigned_to", "bug_status", "priority", "bug_id"], + "Last Changed" => + ["changeddate", "bug_status", "priority", "assigned_to", "bug_id"], + ); + if ($order_types{$match_params->{order}}) { + $options{order} = $order_types{$match_params->{order}}; + } + else { + $options{order} = [split(/\s*,\s*/, $match_params->{order})]; } + } - $options{params} = $match_params; + $options{params} = $match_params; - my $search = new Bugzilla::Search(%options); - my ($data) = $search->data; + my $search = new Bugzilla::Search(%options); + my ($data) = $search->data; - if (!scalar @$data) { - return { bugs => [] }; - } + if (!scalar @$data) { + return {bugs => []}; + } - # Search.pm won't return bugs that the user shouldn't see so no filtering is needed. - my @bug_ids = map { $_->[0] } @$data; - my %bug_objects = map { $_->id => $_ } @{ Bugzilla::Bug->new_from_list(\@bug_ids) }; - my @bugs = map { $bug_objects{$_} } @bug_ids; - @bugs = map { $self->_bug_to_hash($_, $params) } @bugs; +# Search.pm won't return bugs that the user shouldn't see so no filtering is needed. + my @bug_ids = map { $_->[0] } @$data; + my %bug_objects + = map { $_->id => $_ } @{Bugzilla::Bug->new_from_list(\@bug_ids)}; + my @bugs = map { $bug_objects{$_} } @bug_ids; + @bugs = map { $self->_bug_to_hash($_, $params) } @bugs; - return { bugs => \@bugs }; + return {bugs => \@bugs}; } sub possible_duplicates { - my ($self, $params) = validate(@_, 'products'); - my $user = Bugzilla->user; - - Bugzilla->switch_to_shadow_db(); - - # Undo the array-ification that validate() does, for "summary". - $params->{summary} || ThrowCodeError('param_required', - { function => 'Bug.possible_duplicates', param => 'summary' }); - - my @products; - foreach my $name (@{ $params->{'products'} || [] }) { - my $object = $user->can_enter_product($name, THROW_ERROR); - push(@products, $object); - } - - my $possible_dupes = Bugzilla::Bug->possible_duplicates( - { summary => $params->{summary}, products => \@products, - limit => $params->{limit} }); - my @hashes = map { $self->_bug_to_hash($_, $params) } @$possible_dupes; - $self->_add_update_tokens($params, $possible_dupes, \@hashes); - return { bugs => \@hashes }; + my ($self, $params) = validate(@_, 'products'); + my $user = Bugzilla->user; + + Bugzilla->switch_to_shadow_db(); + + # Undo the array-ification that validate() does, for "summary". + $params->{summary} + || ThrowCodeError('param_required', + {function => 'Bug.possible_duplicates', param => 'summary'}); + + my @products; + foreach my $name (@{$params->{'products'} || []}) { + my $object = $user->can_enter_product($name, THROW_ERROR); + push(@products, $object); + } + + my $possible_dupes = Bugzilla::Bug->possible_duplicates({ + summary => $params->{summary}, + products => \@products, + limit => $params->{limit} + }); + my @hashes = map { $self->_bug_to_hash($_, $params) } @$possible_dupes; + $self->_add_update_tokens($params, $possible_dupes, \@hashes); + return {bugs => \@hashes}; } sub update { - my ($self, $params) = validate(@_, 'ids'); + my ($self, $params) = validate(@_, 'ids'); - my $user = Bugzilla->login(LOGIN_REQUIRED); - my $dbh = Bugzilla->dbh; + my $user = Bugzilla->login(LOGIN_REQUIRED); + my $dbh = Bugzilla->dbh; - # We skip certain fields because their set_ methods actually use - # the external names instead of the internal names. - $params = Bugzilla::Bug::map_fields($params, - { summary => 1, platform => 1, severity => 1, url => 1 }); + # We skip certain fields because their set_ methods actually use + # the external names instead of the internal names. + $params = Bugzilla::Bug::map_fields($params, + {summary => 1, platform => 1, severity => 1, url => 1}); - my $ids = delete $params->{ids}; - defined $ids || ThrowCodeError('param_required', { param => 'ids' }); + my $ids = delete $params->{ids}; + defined $ids || ThrowCodeError('param_required', {param => 'ids'}); - my @bugs = map { Bugzilla::Bug->check_for_edit($_) } @$ids; + my @bugs = map { Bugzilla::Bug->check_for_edit($_) } @$ids; - my $minor_update = delete $params->{minor_update} ? 1 : 0; - my %values = %$params; - $values{other_bugs} = \@bugs; + my $minor_update = delete $params->{minor_update} ? 1 : 0; + my %values = %$params; + $values{other_bugs} = \@bugs; - if (exists $values{comment} and exists $values{comment}{comment}) { - $values{comment}{body} = delete $values{comment}{comment}; - } + if (exists $values{comment} and exists $values{comment}{comment}) { + $values{comment}{body} = delete $values{comment}{comment}; + } - # Prevent bugs that could be triggered by specifying fields that - # have valid "set_" functions in Bugzilla::Bug, but shouldn't be - # called using those field names. - delete $values{dependencies}; + # Prevent bugs that could be triggered by specifying fields that + # have valid "set_" functions in Bugzilla::Bug, but shouldn't be + # called using those field names. + delete $values{dependencies}; - # For backwards compatibility, treat alias string or array as a set action - if (exists $values{alias}) { - if (not ref $values{alias}) { - $values{alias} = { set => [ $values{alias} ] }; - } - elsif (ref $values{alias} eq 'ARRAY') { - $values{alias} = { set => $values{alias} }; - } + # For backwards compatibility, treat alias string or array as a set action + if (exists $values{alias}) { + if (not ref $values{alias}) { + $values{alias} = {set => [$values{alias}]}; } - - my $flags = delete $values{flags}; - - foreach my $bug (@bugs) { - $bug->set_all(\%values); - if ($flags) { - my ($old_flags, $new_flags) = extract_flags($flags, $bug->flag_types, $bug->flags); - $bug->set_flags($old_flags, $new_flags); - } + elsif (ref $values{alias} eq 'ARRAY') { + $values{alias} = {set => $values{alias}}; } + } - my %all_changes; - my %minor_updates; - $dbh->bz_start_transaction(); - foreach my $bug (@bugs) { - $minor_updates{$bug->id} = $bug->has_unsent_changes ? 0 : $minor_update; - $all_changes{$bug->id} = $bug->update(); + my $flags = delete $values{flags}; + + foreach my $bug (@bugs) { + $bug->set_all(\%values); + if ($flags) { + my ($old_flags, $new_flags) + = extract_flags($flags, $bug->flag_types, $bug->flags); + $bug->set_flags($old_flags, $new_flags); } - $dbh->bz_commit_transaction(); + } + + my %all_changes; + my %minor_updates; + $dbh->bz_start_transaction(); + foreach my $bug (@bugs) { + $minor_updates{$bug->id} = $bug->has_unsent_changes ? 0 : $minor_update; + $all_changes{$bug->id} = $bug->update(); + } + $dbh->bz_commit_transaction(); + + foreach my $bug (@bugs) { + $bug->send_changes($all_changes{$bug->id}, undef, $minor_updates{$bug->id}); + } + + my %api_name = reverse %{Bugzilla::Bug::FIELD_MAP()}; + + # This doesn't normally belong in FIELD_MAP, but we do want to translate + # "bug_group" back into "groups". + $api_name{'bug_group'} = 'groups'; + + my @result; + foreach my $bug (@bugs) { + my %hash = ( + id => $self->type('int', $bug->id), + last_change_time => $self->type('dateTime', $bug->delta_ts), + changes => {}, + ); - foreach my $bug (@bugs) { - $bug->send_changes($all_changes{$bug->id}, undef, $minor_updates{$bug->id}); + # alias is returned in case users pass a mixture of ids and aliases, + # so that they can know which set of changes relates to which value + # they passed. + $hash{alias} = [map { $self->type('string', $_) } @{$bug->alias}]; + + my %changes = %{$all_changes{$bug->id}}; + foreach my $field (keys %changes) { + my $change = $changes{$field}; + my $api_field = $api_name{$field} || $field; + + # We normalize undef to an empty string, so that the API + # stays consistent for things like Deadline that can become + # empty. + $change->[0] = '' if !defined $change->[0]; + $change->[1] = '' if !defined $change->[1]; + $hash{changes}->{$api_field} = { + removed => $self->type('string', $change->[0]), + added => $self->type('string', $change->[1]) + }; } - my %api_name = reverse %{ Bugzilla::Bug::FIELD_MAP() }; - # This doesn't normally belong in FIELD_MAP, but we do want to translate - # "bug_group" back into "groups". - $api_name{'bug_group'} = 'groups'; + push(@result, \%hash); + } - my @result; - foreach my $bug (@bugs) { - my %hash = ( - id => $self->type('int', $bug->id), - last_change_time => $self->type('dateTime', $bug->delta_ts), - changes => {}, - ); + return {bugs => \@result}; +} - # alias is returned in case users pass a mixture of ids and aliases, - # so that they can know which set of changes relates to which value - # they passed. - $hash{alias} = [ map { $self->type('string', $_) } @{ $bug->alias } ]; - - my %changes = %{ $all_changes{$bug->id} }; - foreach my $field (keys %changes) { - my $change = $changes{$field}; - my $api_field = $api_name{$field} || $field; - # We normalize undef to an empty string, so that the API - # stays consistent for things like Deadline that can become - # empty. - $change->[0] = '' if !defined $change->[0]; - $change->[1] = '' if !defined $change->[1]; - $hash{changes}->{$api_field} = { - removed => $self->type('string', $change->[0]), - added => $self->type('string', $change->[1]) - }; - } +sub create { + my ($self, $params) = @_; + my $dbh = Bugzilla->dbh; - push(@result, \%hash); - } + Bugzilla->login(LOGIN_REQUIRED); - return { bugs => \@result }; -} + $params = Bugzilla::Bug::map_fields($params); -sub create { - my ($self, $params) = @_; - my $dbh = Bugzilla->dbh; + my $flags = delete $params->{flags}; - Bugzilla->login(LOGIN_REQUIRED); + # Set bug flags + if ($flags) { + my $product = Bugzilla::Product->check($params->{product}); + my $component + = Bugzilla::Component->check({ + product => $product, name => $params->{component} + }); + my $flag_types + = Bugzilla::FlagType::match({ + product_id => $product->id, component_id => $component->id, is_active => 1, + }); - $params = Bugzilla::Bug::map_fields($params); + my (undef, $new_flags) = extract_flags($flags, $flag_types); - my $flags = delete $params->{flags}; - # Set bug flags - if ($flags) { - my $product = Bugzilla::Product->check($params->{product}); - my $component = Bugzilla::Component->check({ - product => $product, - name => $params->{component} - }); - my $flag_types = Bugzilla::FlagType::match({ - product_id => $product->id, - component_id => $component->id, - is_active => 1, - }); - - my(undef, $new_flags) = extract_flags($flags, $flag_types); - - $params->{flags} = $new_flags; - } + $params->{flags} = $new_flags; + } - # We start a nested transaction in case flag setting fails - # we want the bug creation to roll back as well. - $dbh->bz_start_transaction(); + # We start a nested transaction in case flag setting fails + # we want the bug creation to roll back as well. + $dbh->bz_start_transaction(); - my $bug = Bugzilla::Bug->create($params); + my $bug = Bugzilla::Bug->create($params); - $dbh->bz_commit_transaction(); + $dbh->bz_commit_transaction(); - $bug->send_changes(); + $bug->send_changes(); - return { id => $self->type('int', $bug->bug_id) }; + return {id => $self->type('int', $bug->bug_id)}; } sub legal_values { - my ($self, $params) = @_; + my ($self, $params) = @_; - Bugzilla->switch_to_shadow_db(); + Bugzilla->switch_to_shadow_db(); - defined $params->{field} - or ThrowCodeError('param_required', { param => 'field' }); + defined $params->{field} + or ThrowCodeError('param_required', {param => 'field'}); - my $field = Bugzilla::Bug::FIELD_MAP->{$params->{field}} - || $params->{field}; + my $field = Bugzilla::Bug::FIELD_MAP->{$params->{field}} || $params->{field}; - my @global_selects = - @{ Bugzilla->fields({ is_select => 1, is_abnormal => 0 }) }; + my @global_selects = @{Bugzilla->fields({is_select => 1, is_abnormal => 0})}; - my $values; - if (grep($_->name eq $field, @global_selects)) { - # The field is a valid one. - trick_taint($field); - $values = get_legal_field_values($field); - } - elsif (grep($_ eq $field, PRODUCT_SPECIFIC_FIELDS)) { - my $id = $params->{product_id}; - defined $id || ThrowCodeError('param_required', - { function => 'Bug.legal_values', param => 'product_id' }); - grep($_->id eq $id, @{Bugzilla->user->get_accessible_products}) - || ThrowUserError('product_access_denied', { id => $id }); - - my $product = new Bugzilla::Product($id); - my @objects; - if ($field eq 'version') { - @objects = @{$product->versions}; - } - elsif ($field eq 'target_milestone') { - @objects = @{$product->milestones}; - } - elsif ($field eq 'component') { - @objects = @{$product->components}; - } + my $values; + if (grep($_->name eq $field, @global_selects)) { - $values = [map { $_->name } @objects]; + # The field is a valid one. + trick_taint($field); + $values = get_legal_field_values($field); + } + elsif (grep($_ eq $field, PRODUCT_SPECIFIC_FIELDS)) { + my $id = $params->{product_id}; + defined $id + || ThrowCodeError('param_required', + {function => 'Bug.legal_values', param => 'product_id'}); + grep($_->id eq $id, @{Bugzilla->user->get_accessible_products}) + || ThrowUserError('product_access_denied', {id => $id}); + + my $product = new Bugzilla::Product($id); + my @objects; + if ($field eq 'version') { + @objects = @{$product->versions}; } - else { - ThrowCodeError('invalid_field_name', { field => $params->{field} }); + elsif ($field eq 'target_milestone') { + @objects = @{$product->milestones}; } - - my @result; - foreach my $val (@$values) { - push(@result, $self->type('string', $val)); + elsif ($field eq 'component') { + @objects = @{$product->components}; } - return { values => \@result }; -} + $values = [map { $_->name } @objects]; + } + else { + ThrowCodeError('invalid_field_name', {field => $params->{field}}); + } -sub add_attachment { - my ($self, $params) = validate(@_, 'ids'); - my $dbh = Bugzilla->dbh; - - Bugzilla->login(LOGIN_REQUIRED); - defined $params->{ids} - || ThrowCodeError('param_required', { param => 'ids' }); - defined $params->{data} - || ThrowCodeError('param_required', { param => 'data' }); - - my @bugs = map { Bugzilla::Bug->check_for_edit($_) } @{ $params->{ids} }; - my $minor_update = delete $params->{minor_update} ? 1 : 0; - - my @created; - $dbh->bz_start_transaction(); - my $timestamp = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); - - my $flags = delete $params->{flags}; - - foreach my $bug (@bugs) { - my $attachment = Bugzilla::Attachment->create({ - bug => $bug, - creation_ts => $timestamp, - data => $params->{data}, - description => $params->{summary}, - filename => $params->{file_name}, - mimetype => $params->{content_type}, - ispatch => $params->{is_patch}, - isprivate => $params->{is_private}, - }); - - if ($flags) { - my ($old_flags, $new_flags) = extract_flags($flags, - $attachment->flag_types, - $attachment->flags); - - $attachment->set_flags($old_flags, $new_flags); - } + my @result; + foreach my $val (@$values) { + push(@result, $self->type('string', $val)); + } - $attachment->update($timestamp); - my $comment = $params->{comment} || ''; + return {values => \@result}; +} - my $is_markdown = 0; - if (ref $params->{comment} eq 'HASH') { - $is_markdown = $params->{comment}->{is_markdown}; - $comment = $params->{comment}->{body}; - } +sub add_attachment { + my ($self, $params) = validate(@_, 'ids'); + my $dbh = Bugzilla->dbh; + + Bugzilla->login(LOGIN_REQUIRED); + defined $params->{ids} || ThrowCodeError('param_required', {param => 'ids'}); + defined $params->{data} || ThrowCodeError('param_required', {param => 'data'}); + + my @bugs = map { Bugzilla::Bug->check_for_edit($_) } @{$params->{ids}}; + my $minor_update = delete $params->{minor_update} ? 1 : 0; + + my @created; + $dbh->bz_start_transaction(); + my $timestamp = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); + + my $flags = delete $params->{flags}; + + foreach my $bug (@bugs) { + my $attachment = Bugzilla::Attachment->create({ + bug => $bug, + creation_ts => $timestamp, + data => $params->{data}, + description => $params->{summary}, + filename => $params->{file_name}, + mimetype => $params->{content_type}, + ispatch => $params->{is_patch}, + isprivate => $params->{is_private}, + }); - ThrowUserError('markdown_disabled') - if $is_markdown && !Bugzilla->user->use_markdown(); + if ($flags) { + my ($old_flags, $new_flags) + = extract_flags($flags, $attachment->flag_types, $attachment->flags); - $attachment->bug->add_comment($comment, - { is_markdown => $is_markdown, - isprivate => $attachment->isprivate, - type => CMT_ATTACHMENT_CREATED, - extra_data => $attachment->id }); - push(@created, $attachment); - } - my %minor_updates; - foreach my $attachment (@created) { - my $bug = $attachment->bug; - $minor_updates{$bug->id} = $bug->has_unsent_changes ? 0 : $minor_update; - $bug->update($timestamp); + $attachment->set_flags($old_flags, $new_flags); } - $dbh->bz_commit_transaction(); - foreach my $bug (@bugs) { - $bug->send_changes(undef, undef, $minor_updates{$bug->id}); - } + $attachment->update($timestamp); + my $comment = $params->{comment} || ''; - my @created_ids = map { $_->id } @created; + my $is_markdown = 0; + if (ref $params->{comment} eq 'HASH') { + $is_markdown = $params->{comment}->{is_markdown}; + $comment = $params->{comment}->{body}; + } - return { ids => \@created_ids }; + ThrowUserError('markdown_disabled') + if $is_markdown && !Bugzilla->user->use_markdown(); + + $attachment->bug->add_comment( + $comment, + { + is_markdown => $is_markdown, + isprivate => $attachment->isprivate, + type => CMT_ATTACHMENT_CREATED, + extra_data => $attachment->id + } + ); + push(@created, $attachment); + } + my %minor_updates; + foreach my $attachment (@created) { + my $bug = $attachment->bug; + $minor_updates{$bug->id} = $bug->has_unsent_changes ? 0 : $minor_update; + $bug->update($timestamp); + } + $dbh->bz_commit_transaction(); + + foreach my $bug (@bugs) { + $bug->send_changes(undef, undef, $minor_updates{$bug->id}); + } + + my @created_ids = map { $_->id } @created; + + return {ids => \@created_ids}; } sub update_attachment { - my ($self, $params) = validate(@_, 'ids'); - - my $user = Bugzilla->login(LOGIN_REQUIRED); - my $dbh = Bugzilla->dbh; - - my $ids = delete $params->{ids}; - defined $ids || ThrowCodeError('param_required', { param => 'ids' }); - - my $req_minor_update = delete $params->{minor_update} ? 1 : 0; - # Some fields cannot be sent to set_all - foreach my $key (qw(login password token)) { - delete $params->{$key}; + my ($self, $params) = validate(@_, 'ids'); + + my $user = Bugzilla->login(LOGIN_REQUIRED); + my $dbh = Bugzilla->dbh; + + my $ids = delete $params->{ids}; + defined $ids || ThrowCodeError('param_required', {param => 'ids'}); + + my $req_minor_update = delete $params->{minor_update} ? 1 : 0; + + # Some fields cannot be sent to set_all + foreach my $key (qw(login password token)) { + delete $params->{$key}; + } + + $params = translate($params, ATTACHMENT_MAPPED_SETTERS); + + # Get all the attachments, after verifying that they exist and are editable + my @attachments = (); + my %bugs = (); + foreach my $id (@$ids) { + my $attachment = Bugzilla::Attachment->new($id) + || ThrowUserError("invalid_attach_id", {attach_id => $id}); + my $bug = $attachment->bug; + $attachment->_check_bug; + + push @attachments, $attachment; + $bugs{$bug->id} = $bug; + } + + my $flags = delete $params->{flags}; + my $comment = delete $params->{comment}; + my $is_markdown = 0; + + if (ref $comment eq 'HASH') { + $is_markdown = $comment->{is_markdown}; + $comment = $comment->{body}; + } + + ThrowUserError('markdown_disabled') if $is_markdown && !$user->use_markdown(); + + # Update the values + foreach my $attachment (@attachments) { + my ($update_flags, $new_flags) + = $flags + ? extract_flags($flags, $attachment->flag_types, $attachment->flags) + : ([], []); + if ($attachment->validate_can_edit) { + $attachment->set_all($params); + $attachment->set_flags($update_flags, $new_flags) if $flags; } + elsif (scalar @$update_flags && !scalar(@$new_flags) && !scalar keys %$params) { + + # Requestees can set flags targeted to them, even if they cannot + # edit the attachment. Flag setters can edit their own flags too. + my %flag_list = map { $_->{id} => $_ } @$update_flags; + my $flag_objs = Bugzilla::Flag->new_from_list([keys %flag_list]); + my @editable_flags; + foreach my $flag_obj (@$flag_objs) { + if ($flag_obj->setter_id == $user->id + || ($flag_obj->requestee_id && $flag_obj->requestee_id == $user->id)) + { + push(@editable_flags, $flag_list{$flag_obj->id}); + } + } + if (!scalar @editable_flags) { + ThrowUserError("illegal_attachment_edit", {attach_id => $attachment->id}); + } + $attachment->set_flags(\@editable_flags, []); + } + else { + ThrowUserError("illegal_attachment_edit", {attach_id => $attachment->id}); + } + } - $params = translate($params, ATTACHMENT_MAPPED_SETTERS); + $dbh->bz_start_transaction(); - # Get all the attachments, after verifying that they exist and are editable - my @attachments = (); - my %bugs = (); - foreach my $id (@$ids) { - my $attachment = Bugzilla::Attachment->new($id) - || ThrowUserError("invalid_attach_id", { attach_id => $id }); - my $bug = $attachment->bug; - $attachment->_check_bug; + # Do the actual update and get information to return to user + my @result; + foreach my $attachment (@attachments) { + my $changes = $attachment->update(); - push @attachments, $attachment; - $bugs{$bug->id} = $bug; + if ($comment = trim($comment)) { + $attachment->bug->add_comment( + $comment, + { + is_markdown => $is_markdown, + isprivate => $attachment->isprivate, + type => CMT_ATTACHMENT_UPDATED, + extra_data => $attachment->id + } + ); } - my $flags = delete $params->{flags}; - my $comment = delete $params->{comment}; - my $is_markdown = 0; + $changes = translate($changes, ATTACHMENT_MAPPED_RETURNS); - if (ref $comment eq 'HASH') { - $is_markdown = $comment->{is_markdown}; - $comment = $comment->{body}; - } + my %hash = ( + id => $self->type('int', $attachment->id), + last_change_time => $self->type('dateTime', $attachment->modification_time), + changes => {}, + ); - ThrowUserError('markdown_disabled') - if $is_markdown && !$user->use_markdown(); - - # Update the values - foreach my $attachment (@attachments) { - my ($update_flags, $new_flags) = $flags - ? extract_flags($flags, $attachment->flag_types, $attachment->flags) - : ([], []); - if ($attachment->validate_can_edit) { - $attachment->set_all($params); - $attachment->set_flags($update_flags, $new_flags) if $flags; - } - elsif (scalar @$update_flags && !scalar(@$new_flags) && !scalar keys %$params) { - # Requestees can set flags targeted to them, even if they cannot - # edit the attachment. Flag setters can edit their own flags too. - my %flag_list = map { $_->{id} => $_ } @$update_flags; - my $flag_objs = Bugzilla::Flag->new_from_list([ keys %flag_list ]); - my @editable_flags; - foreach my $flag_obj (@$flag_objs) { - if ($flag_obj->setter_id == $user->id - || ($flag_obj->requestee_id && $flag_obj->requestee_id == $user->id)) - { - push(@editable_flags, $flag_list{$flag_obj->id}); - } - } - if (!scalar @editable_flags) { - ThrowUserError("illegal_attachment_edit", { attach_id => $attachment->id }); - } - $attachment->set_flags(\@editable_flags, []); - } - else { - ThrowUserError("illegal_attachment_edit", { attach_id => $attachment->id }); - } + foreach my $field (keys %$changes) { + my $change = $changes->{$field}; + + # We normalize undef to an empty string, so that the API + # stays consistent for things like Deadline that can become + # empty. + $hash{changes}->{$field} = { + removed => $self->type('string', $change->[0] // ''), + added => $self->type('string', $change->[1] // '') + }; } - $dbh->bz_start_transaction(); + push(@result, \%hash); + } - # Do the actual update and get information to return to user - my @result; - foreach my $attachment (@attachments) { - my $changes = $attachment->update(); - - if ($comment = trim($comment)) { - $attachment->bug->add_comment($comment, - { is_markdown => $is_markdown, - isprivate => $attachment->isprivate, - type => CMT_ATTACHMENT_UPDATED, - extra_data => $attachment->id }); - } + $dbh->bz_commit_transaction(); - $changes = translate($changes, ATTACHMENT_MAPPED_RETURNS); + # Email users about the change + foreach my $bug (values %bugs) { + my $minor_update = $bug->has_unsent_changes ? 0 : $req_minor_update; + $bug->update(); + $bug->send_changes(undef, undef, $minor_update); + } - my %hash = ( - id => $self->type('int', $attachment->id), - last_change_time => $self->type('dateTime', $attachment->modification_time), - changes => {}, - ); + # Return the information to the user + return {attachments => \@result}; +} - foreach my $field (keys %$changes) { - my $change = $changes->{$field}; +sub add_comment { + my ($self, $params) = @_; - # We normalize undef to an empty string, so that the API - # stays consistent for things like Deadline that can become - # empty. - $hash{changes}->{$field} = { - removed => $self->type('string', $change->[0] // ''), - added => $self->type('string', $change->[1] // '') - }; - } + # The user must login in order add a comment + my $user = Bugzilla->login(LOGIN_REQUIRED); - push(@result, \%hash); - } + # Check parameters + defined $params->{id} || ThrowCodeError('param_required', {param => 'id'}); + my $comment = $params->{comment}; + (defined $comment && trim($comment) ne '') + || ThrowCodeError('param_required', {param => 'comment'}); - $dbh->bz_commit_transaction(); + my $bug = Bugzilla::Bug->check_for_edit($params->{id}); + my $minor_update = delete $params->{minor_update} ? 1 : 0; + $minor_update = $bug->has_unsent_changes ? 0 : $minor_update; - # Email users about the change - foreach my $bug (values %bugs) { - my $minor_update = $bug->has_unsent_changes ? 0 : $req_minor_update; - $bug->update(); - $bug->send_changes(undef, undef, $minor_update); - } + # Backwards-compatibility for versions before 3.6 + if (defined $params->{private}) { + $params->{is_private} = delete $params->{private}; + } - # Return the information to the user - return { attachments => \@result }; -} + ThrowUserError('markdown_disabled') + if $params->{is_markdown} && !$user->use_markdown(); -sub add_comment { - my ($self, $params) = @_; - - # The user must login in order add a comment - my $user = Bugzilla->login(LOGIN_REQUIRED); - - # Check parameters - defined $params->{id} - || ThrowCodeError('param_required', { param => 'id' }); - my $comment = $params->{comment}; - (defined $comment && trim($comment) ne '') - || ThrowCodeError('param_required', { param => 'comment' }); - - my $bug = Bugzilla::Bug->check_for_edit($params->{id}); - my $minor_update = delete $params->{minor_update} ? 1 : 0; - $minor_update = $bug->has_unsent_changes ? 0 : $minor_update; - - # Backwards-compatibility for versions before 3.6 - if (defined $params->{private}) { - $params->{is_private} = delete $params->{private}; + # Append comment + $bug->add_comment( + $comment, + { + isprivate => $params->{is_private}, + is_markdown => $params->{is_markdown}, + work_time => $params->{work_time} } + ); + $bug->update(); - ThrowUserError('markdown_disabled') - if $params->{is_markdown} && !$user->use_markdown(); - - # Append comment - $bug->add_comment($comment, { isprivate => $params->{is_private}, - is_markdown => $params->{is_markdown}, - work_time => $params->{work_time} }); - $bug->update(); - - my $new_comment_id = $bug->{added_comments}[0]->id; + my $new_comment_id = $bug->{added_comments}[0]->id; - # Send mail. - Bugzilla::BugMail::Send($bug->bug_id, { changer => $user }, - { minor_update => $minor_update }); + # Send mail. + Bugzilla::BugMail::Send( + $bug->bug_id, + {changer => $user}, + {minor_update => $minor_update} + ); - return { id => $self->type('int', $new_comment_id) }; + return {id => $self->type('int', $new_comment_id)}; } sub update_see_also { - my ($self, $params) = @_; - - my $user = Bugzilla->login(LOGIN_REQUIRED); - - # Check parameters - $params->{ids} - || ThrowCodeError('param_required', { param => 'id' }); - my ($add, $remove) = @$params{qw(add remove)}; - ($add || $remove) - or ThrowCodeError('params_required', { params => ['add', 'remove'] }); - my $req_minor_update = delete $params->{minor_update} ? 1 : 0; - - my @bugs; - foreach my $id (@{ $params->{ids} }) { - my $bug = Bugzilla::Bug->check_for_edit($id); - push(@bugs, $bug); - if ($remove) { - $bug->remove_see_also($_) foreach @$remove; - } - if ($add) { - $bug->add_see_also($_) foreach @$add; - } + my ($self, $params) = @_; + + my $user = Bugzilla->login(LOGIN_REQUIRED); + + # Check parameters + $params->{ids} || ThrowCodeError('param_required', {param => 'id'}); + my ($add, $remove) = @$params{qw(add remove)}; + ($add || $remove) + or ThrowCodeError('params_required', {params => ['add', 'remove']}); + my $req_minor_update = delete $params->{minor_update} ? 1 : 0; + + my @bugs; + foreach my $id (@{$params->{ids}}) { + my $bug = Bugzilla::Bug->check_for_edit($id); + push(@bugs, $bug); + if ($remove) { + $bug->remove_see_also($_) foreach @$remove; } - - my %changes; - foreach my $bug (@bugs) { - my $minor_update = $bug->has_unsent_changes ? 0 : $req_minor_update; - my $change = $bug->update(); - if (my $see_also = $change->{see_also}) { - $changes{$bug->id}->{see_also} = { - removed => [split(', ', $see_also->[0])], - added => [split(', ', $see_also->[1])], - }; - } - else { - # We still want a changes entry, for API consistency. - $changes{$bug->id}->{see_also} = { added => [], removed => [] }; - } - - Bugzilla::BugMail::Send($bug->id, { changer => $user }, - { minor_update => $minor_update }); + if ($add) { + $bug->add_see_also($_) foreach @$add; + } + } + + my %changes; + foreach my $bug (@bugs) { + my $minor_update = $bug->has_unsent_changes ? 0 : $req_minor_update; + my $change = $bug->update(); + if (my $see_also = $change->{see_also}) { + $changes{$bug->id}->{see_also} = { + removed => [split(', ', $see_also->[0])], + added => [split(', ', $see_also->[1])], + }; } + else { + # We still want a changes entry, for API consistency. + $changes{$bug->id}->{see_also} = {added => [], removed => []}; + } + + Bugzilla::BugMail::Send( + $bug->id, + {changer => $user}, + {minor_update => $minor_update} + ); + } - return { changes => \%changes }; + return {changes => \%changes}; } sub attachments { - my ($self, $params) = validate(@_, 'ids', 'attachment_ids'); + my ($self, $params) = validate(@_, 'ids', 'attachment_ids'); - Bugzilla->switch_to_shadow_db() unless Bugzilla->user->id; - - if (!(defined $params->{ids} - or defined $params->{attachment_ids})) - { - ThrowCodeError('param_required', - { function => 'Bug.attachments', - params => ['ids', 'attachment_ids'] }); - } + Bugzilla->switch_to_shadow_db() unless Bugzilla->user->id; - my $ids = $params->{ids} || []; - my $attach_ids = $params->{attachment_ids} || []; - - my %bugs; - foreach my $bug_id (@$ids) { - my $bug = Bugzilla::Bug->check($bug_id); - $bugs{$bug->id} = []; - foreach my $attach (@{$bug->attachments}) { - push @{$bugs{$bug->id}}, - $self->_attachment_to_hash($attach, $params); - } + if (!(defined $params->{ids} or defined $params->{attachment_ids})) { + ThrowCodeError('param_required', + {function => 'Bug.attachments', params => ['ids', 'attachment_ids']}); + } + + my $ids = $params->{ids} || []; + my $attach_ids = $params->{attachment_ids} || []; + + my %bugs; + foreach my $bug_id (@$ids) { + my $bug = Bugzilla::Bug->check($bug_id); + $bugs{$bug->id} = []; + foreach my $attach (@{$bug->attachments}) { + push @{$bugs{$bug->id}}, $self->_attachment_to_hash($attach, $params); } - - my %attachments; - foreach my $attach (@{Bugzilla::Attachment->new_from_list($attach_ids)}) { - Bugzilla::Bug->check($attach->bug_id); - if ($attach->isprivate && !Bugzilla->user->is_insider) { - ThrowUserError('auth_failure', {action => 'access', - object => 'attachment', - attach_id => $attach->id}); - } - $attachments{$attach->id} = - $self->_attachment_to_hash($attach, $params); + } + + my %attachments; + foreach my $attach (@{Bugzilla::Attachment->new_from_list($attach_ids)}) { + Bugzilla::Bug->check($attach->bug_id); + if ($attach->isprivate && !Bugzilla->user->is_insider) { + ThrowUserError('auth_failure', + {action => 'access', object => 'attachment', attach_id => $attach->id}); } + $attachments{$attach->id} = $self->_attachment_to_hash($attach, $params); + } - return { bugs => \%bugs, attachments => \%attachments }; + return {bugs => \%bugs, attachments => \%attachments}; } sub update_tags { - my ($self, $params) = @_; + my ($self, $params) = @_; - Bugzilla->login(LOGIN_REQUIRED); + Bugzilla->login(LOGIN_REQUIRED); - my $ids = $params->{ids}; - my $tags = $params->{tags}; + my $ids = $params->{ids}; + my $tags = $params->{tags}; - ThrowCodeError('param_required', - { function => 'Bug.update_tags', - param => 'ids' }) if !defined $ids; + ThrowCodeError('param_required', + {function => 'Bug.update_tags', param => 'ids'}) + if !defined $ids; - ThrowCodeError('param_required', - { function => 'Bug.update_tags', - param => 'tags' }) if !defined $tags; + ThrowCodeError('param_required', + {function => 'Bug.update_tags', param => 'tags'}) + if !defined $tags; - my %changes; - foreach my $bug_id (@$ids) { - my $bug = Bugzilla::Bug->check($bug_id); - my @old_tags = @{ $bug->tags }; + my %changes; + foreach my $bug_id (@$ids) { + my $bug = Bugzilla::Bug->check($bug_id); + my @old_tags = @{$bug->tags}; - $bug->remove_tag($_) foreach @{ $tags->{remove} || [] }; - $bug->add_tag($_) foreach @{ $tags->{add} || [] }; + $bug->remove_tag($_) foreach @{$tags->{remove} || []}; + $bug->add_tag($_) foreach @{$tags->{add} || []}; - my ($removed, $added) = diff_arrays(\@old_tags, $bug->tags); + my ($removed, $added) = diff_arrays(\@old_tags, $bug->tags); - my @removed = map { $self->type('string', $_) } @$removed; - my @added = map { $self->type('string', $_) } @$added; + my @removed = map { $self->type('string', $_) } @$removed; + my @added = map { $self->type('string', $_) } @$added; - $changes{$bug->id}->{tags} = { - removed => \@removed, - added => \@added - }; - } + $changes{$bug->id}->{tags} = {removed => \@removed, added => \@added}; + } - return { changes => \%changes }; + return {changes => \%changes}; } sub update_comment_tags { - my ($self, $params) = @_; - - my $user = Bugzilla->login(LOGIN_REQUIRED); - Bugzilla->params->{'comment_taggers_group'} - || ThrowUserError("comment_tag_disabled"); - $user->can_tag_comments - || ThrowUserError("auth_failure", - { group => Bugzilla->params->{'comment_taggers_group'}, - action => "update", - object => "comment_tags" }); - - my $comment_id = $params->{comment_id} - // ThrowCodeError('param_required', - { function => 'Bug.update_comment_tags', - param => 'comment_id' }); - - ThrowCodeError('param_integer_required', { function => 'Bug.update_comment_tags', - param => 'comment_id' }) - unless $comment_id =~ /^\d+$/a; - - my $comment = Bugzilla::Comment->new($comment_id) - || return []; - $comment->bug->check_is_visible(); - if ($comment->is_private && !$user->is_insider) { - ThrowUserError('comment_is_private', { id => $comment_id }); - } + my ($self, $params) = @_; - my $dbh = Bugzilla->dbh; - $dbh->bz_start_transaction(); - foreach my $tag (@{ $params->{add} || [] }) { - $comment->add_tag($tag) if defined $tag; - } - foreach my $tag (@{ $params->{remove} || [] }) { - $comment->remove_tag($tag) if defined $tag; + my $user = Bugzilla->login(LOGIN_REQUIRED); + Bugzilla->params->{'comment_taggers_group'} + || ThrowUserError("comment_tag_disabled"); + $user->can_tag_comments || ThrowUserError( + "auth_failure", + { + group => Bugzilla->params->{'comment_taggers_group'}, + action => "update", + object => "comment_tags" } - $comment->update(); - $dbh->bz_commit_transaction(); - - return $comment->tags; + ); + + my $comment_id = $params->{comment_id} // ThrowCodeError('param_required', + {function => 'Bug.update_comment_tags', param => 'comment_id'}); + + ThrowCodeError('param_integer_required', + {function => 'Bug.update_comment_tags', param => 'comment_id'}) + unless $comment_id =~ /^\d+$/a; + + my $comment = Bugzilla::Comment->new($comment_id) || return []; + $comment->bug->check_is_visible(); + if ($comment->is_private && !$user->is_insider) { + ThrowUserError('comment_is_private', {id => $comment_id}); + } + + my $dbh = Bugzilla->dbh; + $dbh->bz_start_transaction(); + foreach my $tag (@{$params->{add} || []}) { + $comment->add_tag($tag) if defined $tag; + } + foreach my $tag (@{$params->{remove} || []}) { + $comment->remove_tag($tag) if defined $tag; + } + $comment->update(); + $dbh->bz_commit_transaction(); + + return $comment->tags; } sub search_comment_tags { - my ($self, $params) = @_; - - Bugzilla->login(LOGIN_REQUIRED); - Bugzilla->params->{'comment_taggers_group'} - || ThrowUserError("comment_tag_disabled"); - Bugzilla->user->can_tag_comments - || ThrowUserError("auth_failure", { group => Bugzilla->params->{'comment_taggers_group'}, - action => "search", - object => "comment_tags"}); - - my $query = $params->{query}; - $query - // ThrowCodeError('param_required', { param => 'query' }); - my $limit = $params->{limit} || 7; - detaint_natural($limit) - || ThrowCodeError('param_must_be_numeric', { param => 'limit', - function => 'Bug.search_comment_tags' }); - - - my $tags = Bugzilla::Comment::TagWeights->match({ - WHERE => { - 'tag LIKE ?' => "\%$query\%", - }, - LIMIT => $limit, + my ($self, $params) = @_; + + Bugzilla->login(LOGIN_REQUIRED); + Bugzilla->params->{'comment_taggers_group'} + || ThrowUserError("comment_tag_disabled"); + Bugzilla->user->can_tag_comments || ThrowUserError( + "auth_failure", + { + group => Bugzilla->params->{'comment_taggers_group'}, + action => "search", + object => "comment_tags" + } + ); + + my $query = $params->{query}; + $query // ThrowCodeError('param_required', {param => 'query'}); + my $limit = $params->{limit} || 7; + detaint_natural($limit) + || ThrowCodeError('param_must_be_numeric', + {param => 'limit', function => 'Bug.search_comment_tags'}); + + + my $tags + = Bugzilla::Comment::TagWeights->match({ + WHERE => {'tag LIKE ?' => "\%$query\%",}, LIMIT => $limit, }); - return [ map { $_->tag } @$tags ]; + return [map { $_->tag } @$tags]; } ############################## @@ -1278,234 +1302,240 @@ sub search_comment_tags { # return them directly. sub _bug_to_hash { - my ($self, $bug, $params) = @_; - - # All the basic bug attributes are here, in alphabetical order. - # A bug attribute is "basic" if it doesn't require an additional - # database call to get the info. - my %item = %{ filter $params, { - # No need to format $bug->deadline specially, because Bugzilla::Bug - # already does it for us. - deadline => $self->type('string', $bug->deadline), - id => $self->type('int', $bug->bug_id), - is_confirmed => $self->type('boolean', $bug->everconfirmed), - op_sys => $self->type('string', $bug->op_sys), - platform => $self->type('string', $bug->rep_platform), - priority => $self->type('string', $bug->priority), - resolution => $self->type('string', $bug->resolution), - severity => $self->type('string', $bug->bug_severity), - status => $self->type('string', $bug->bug_status), - summary => $self->type('string', $bug->short_desc), - target_milestone => $self->type('string', $bug->target_milestone), - url => $self->type('string', $bug->bug_file_loc), - version => $self->type('string', $bug->version), - whiteboard => $self->type('string', $bug->status_whiteboard), - } }; - - # First we handle any fields that require extra work (such as date parsing - # or SQL calls). - if (filter_wants $params, 'alias') { - $item{alias} = [ map { $self->type('string', $_) } @{ $bug->alias } ]; - } - if (filter_wants $params, 'assigned_to') { - $item{'assigned_to'} = $self->type('login', $bug->assigned_to->login); - $item{'assigned_to_detail'} = $self->_user_to_hash($bug->assigned_to, $params, undef, 'assigned_to'); - } - if (filter_wants $params, 'blocks') { - my @blocks = map { $self->type('int', $_) } @{ $bug->blocked }; - $item{'blocks'} = \@blocks; - } - if (filter_wants $params, 'classification') { - $item{classification} = $self->type('string', $bug->classification); - } - if (filter_wants $params, 'component') { - $item{component} = $self->type('string', $bug->component); - } - if (filter_wants $params, 'cc') { - my @cc = map { $self->type('login', $_) } @{ $bug->cc }; - $item{'cc'} = \@cc; - $item{'cc_detail'} = [ map { $self->_user_to_hash($_, $params, undef, 'cc') } @{ $bug->cc_users } ]; - } - if (filter_wants $params, 'creation_time') { - $item{'creation_time'} = $self->type('dateTime', $bug->creation_ts); - } - if (filter_wants $params, 'creator') { - $item{'creator'} = $self->type('login', $bug->reporter->login); - $item{'creator_detail'} = $self->_user_to_hash($bug->reporter, $params, undef, 'creator'); - } - if (filter_wants $params, 'depends_on') { - my @depends_on = map { $self->type('int', $_) } @{ $bug->dependson }; - $item{'depends_on'} = \@depends_on; - } - if (filter_wants $params, 'dupe_of') { - $item{'dupe_of'} = $self->type('int', $bug->dup_id); - } - if (filter_wants $params, 'groups') { - my @groups = map { $self->type('string', $_->name) } - @{ $bug->groups_in }; - $item{'groups'} = \@groups; - } - if (filter_wants $params, 'is_open') { - $item{'is_open'} = $self->type('boolean', $bug->status->is_open); - } - if (filter_wants $params, 'keywords') { - my @keywords = map { $self->type('string', $_->name) } - @{ $bug->keyword_objects }; - $item{'keywords'} = \@keywords; - } - if (filter_wants $params, 'last_change_time') { - $item{'last_change_time'} = $self->type('dateTime', $bug->delta_ts); - } - if (filter_wants $params, 'product') { - $item{product} = $self->type('string', $bug->product); - } - if (filter_wants $params, 'qa_contact') { - my $qa_login = $bug->qa_contact ? $bug->qa_contact->login : ''; - $item{'qa_contact'} = $self->type('login', $qa_login); - if ($bug->qa_contact) { - $item{'qa_contact_detail'} = $self->_user_to_hash($bug->qa_contact, $params, undef, 'qa_contact'); - } + my ($self, $bug, $params) = @_; + + # All the basic bug attributes are here, in alphabetical order. + # A bug attribute is "basic" if it doesn't require an additional + # database call to get the info. + my %item = %{filter $params, + { + # No need to format $bug->deadline specially, because Bugzilla::Bug + # already does it for us. + deadline => $self->type('string', $bug->deadline), + id => $self->type('int', $bug->bug_id), + is_confirmed => $self->type('boolean', $bug->everconfirmed), + op_sys => $self->type('string', $bug->op_sys), + platform => $self->type('string', $bug->rep_platform), + priority => $self->type('string', $bug->priority), + resolution => $self->type('string', $bug->resolution), + severity => $self->type('string', $bug->bug_severity), + status => $self->type('string', $bug->bug_status), + summary => $self->type('string', $bug->short_desc), + target_milestone => $self->type('string', $bug->target_milestone), + url => $self->type('string', $bug->bug_file_loc), + version => $self->type('string', $bug->version), + whiteboard => $self->type('string', $bug->status_whiteboard), } - if (filter_wants $params, 'see_also') { - my @see_also = map { $self->type('string', $_->name) } - @{ $bug->see_also }; - $item{'see_also'} = \@see_also; + }; + + # First we handle any fields that require extra work (such as date parsing + # or SQL calls). + if (filter_wants $params, 'alias') { + $item{alias} = [map { $self->type('string', $_) } @{$bug->alias}]; + } + if (filter_wants $params, 'assigned_to') { + $item{'assigned_to'} = $self->type('login', $bug->assigned_to->login); + $item{'assigned_to_detail'} + = $self->_user_to_hash($bug->assigned_to, $params, undef, 'assigned_to'); + } + if (filter_wants $params, 'blocks') { + my @blocks = map { $self->type('int', $_) } @{$bug->blocked}; + $item{'blocks'} = \@blocks; + } + if (filter_wants $params, 'classification') { + $item{classification} = $self->type('string', $bug->classification); + } + if (filter_wants $params, 'component') { + $item{component} = $self->type('string', $bug->component); + } + if (filter_wants $params, 'cc') { + my @cc = map { $self->type('login', $_) } @{$bug->cc}; + $item{'cc'} = \@cc; + $item{'cc_detail'} + = [map { $self->_user_to_hash($_, $params, undef, 'cc') } @{$bug->cc_users}]; + } + if (filter_wants $params, 'creation_time') { + $item{'creation_time'} = $self->type('dateTime', $bug->creation_ts); + } + if (filter_wants $params, 'creator') { + $item{'creator'} = $self->type('login', $bug->reporter->login); + $item{'creator_detail'} + = $self->_user_to_hash($bug->reporter, $params, undef, 'creator'); + } + if (filter_wants $params, 'depends_on') { + my @depends_on = map { $self->type('int', $_) } @{$bug->dependson}; + $item{'depends_on'} = \@depends_on; + } + if (filter_wants $params, 'dupe_of') { + $item{'dupe_of'} = $self->type('int', $bug->dup_id); + } + if (filter_wants $params, 'groups') { + my @groups = map { $self->type('string', $_->name) } @{$bug->groups_in}; + $item{'groups'} = \@groups; + } + if (filter_wants $params, 'is_open') { + $item{'is_open'} = $self->type('boolean', $bug->status->is_open); + } + if (filter_wants $params, 'keywords') { + my @keywords = map { $self->type('string', $_->name) } @{$bug->keyword_objects}; + $item{'keywords'} = \@keywords; + } + if (filter_wants $params, 'last_change_time') { + $item{'last_change_time'} = $self->type('dateTime', $bug->delta_ts); + } + if (filter_wants $params, 'product') { + $item{product} = $self->type('string', $bug->product); + } + if (filter_wants $params, 'qa_contact') { + my $qa_login = $bug->qa_contact ? $bug->qa_contact->login : ''; + $item{'qa_contact'} = $self->type('login', $qa_login); + if ($bug->qa_contact) { + $item{'qa_contact_detail'} + = $self->_user_to_hash($bug->qa_contact, $params, undef, 'qa_contact'); } - if (filter_wants $params, 'flags') { - $item{'flags'} = [ map { $self->_flag_to_hash($_) } @{$bug->flags} ]; + } + if (filter_wants $params, 'see_also') { + my @see_also = map { $self->type('string', $_->name) } @{$bug->see_also}; + $item{'see_also'} = \@see_also; + } + if (filter_wants $params, 'flags') { + $item{'flags'} = [map { $self->_flag_to_hash($_) } @{$bug->flags}]; + } + if (filter_wants $params, 'tags', 'extra') { + $item{'tags'} = $bug->tags; + } + if (filter_wants $params, 'duplicates', 'extra') { + $item{'duplicates'} = [map { $self->type('int', $_->id) } @{$bug->duplicates}]; + } + + # And now custom fields + my @custom_fields = Bugzilla->active_custom_fields; + foreach my $field (@custom_fields) { + my $name = $field->name; + next if !filter_wants($params, $name, ['default', 'custom']); + if ($field->type == FIELD_TYPE_BUG_ID) { + $item{$name} = $self->type('int', $bug->$name); } - if (filter_wants $params, 'tags', 'extra') { - $item{'tags'} = $bug->tags; + elsif ($field->type == FIELD_TYPE_DATETIME || $field->type == FIELD_TYPE_DATE) { + $item{$name} = $self->type('dateTime', $bug->$name); } - if (filter_wants $params, 'duplicates', 'extra') { - $item{'duplicates'} = [ map { $self->type('int', $_->id) } @{ $bug->duplicates } ]; + elsif ($field->type == FIELD_TYPE_MULTI_SELECT) { + my @values = map { $self->type('string', $_) } @{$bug->$name}; + $item{$name} = \@values; } - - # And now custom fields - my @custom_fields = Bugzilla->active_custom_fields; - foreach my $field (@custom_fields) { - my $name = $field->name; - next if !filter_wants($params, $name, ['default', 'custom']); - if ($field->type == FIELD_TYPE_BUG_ID) { - $item{$name} = $self->type('int', $bug->$name); - } - elsif ($field->type == FIELD_TYPE_DATETIME - || $field->type == FIELD_TYPE_DATE) - { - $item{$name} = $self->type('dateTime', $bug->$name); - } - elsif ($field->type == FIELD_TYPE_MULTI_SELECT) { - my @values = map { $self->type('string', $_) } @{ $bug->$name }; - $item{$name} = \@values; - } - else { - $item{$name} = $self->type('string', $bug->$name); - } + else { + $item{$name} = $self->type('string', $bug->$name); } + } - # Timetracking fields are only sent if the user can see them. - if (Bugzilla->user->is_timetracker) { - if (filter_wants $params, 'estimated_time') { - $item{'estimated_time'} = $self->type('double', $bug->estimated_time); - } - if (filter_wants $params, 'remaining_time') { - $item{'remaining_time'} = $self->type('double', $bug->remaining_time); - } - if (filter_wants $params, 'actual_time') { - $item{'actual_time'} = $self->type('double', $bug->actual_time); - } + # Timetracking fields are only sent if the user can see them. + if (Bugzilla->user->is_timetracker) { + if (filter_wants $params, 'estimated_time') { + $item{'estimated_time'} = $self->type('double', $bug->estimated_time); } - - # The "accessible" bits go here because they have long names and it - # makes the code look nicer to separate them out. - if (filter_wants $params, 'is_cc_accessible') { - $item{'is_cc_accessible'} = $self->type('boolean', $bug->cclist_accessible); + if (filter_wants $params, 'remaining_time') { + $item{'remaining_time'} = $self->type('double', $bug->remaining_time); } - if (filter_wants $params, 'is_creator_accessible') { - $item{'is_creator_accessible'} = $self->type('boolean', $bug->reporter_accessible); + if (filter_wants $params, 'actual_time') { + $item{'actual_time'} = $self->type('double', $bug->actual_time); } - - return \%item; + } + + # The "accessible" bits go here because they have long names and it + # makes the code look nicer to separate them out. + if (filter_wants $params, 'is_cc_accessible') { + $item{'is_cc_accessible'} = $self->type('boolean', $bug->cclist_accessible); + } + if (filter_wants $params, 'is_creator_accessible') { + $item{'is_creator_accessible'} + = $self->type('boolean', $bug->reporter_accessible); + } + + return \%item; } sub _user_to_hash { - my ($self, $user, $filters, $types, $prefix) = @_; - my $item = filter $filters, { - id => $self->type('int', $user->id), - real_name => $self->type('string', $user->name), - name => $self->type('login', $user->login), - }, $types, $prefix; - return $item; + my ($self, $user, $filters, $types, $prefix) = @_; + my $item = filter $filters, + { + id => $self->type('int', $user->id), + real_name => $self->type('string', $user->name), + name => $self->type('login', $user->login), + }, + $types, $prefix; + return $item; } sub _attachment_to_hash { - my ($self, $attach, $filters, $types, $prefix) = @_; - - my $item = filter $filters, { - creation_time => $self->type('dateTime', $attach->attached), - last_change_time => $self->type('dateTime', $attach->modification_time), - id => $self->type('int', $attach->id), - bug_id => $self->type('int', $attach->bug_id), - file_name => $self->type('string', $attach->filename), - summary => $self->type('string', $attach->description), - content_type => $self->type('string', $attach->contenttype), - is_private => $self->type('int', $attach->isprivate), - is_obsolete => $self->type('int', $attach->isobsolete), - is_patch => $self->type('int', $attach->ispatch), - }, $types, $prefix; - - # creator requires an extra lookup, so we only send it if - # the filter wants it. - if (filter_wants $filters, 'creator', $types, $prefix) { - $item->{'creator'} = $self->type('login', $attach->attacher->login); - } - - if (filter_wants $filters, 'data', $types, $prefix) { - $item->{'data'} = $self->type('base64', $attach->data); - } + my ($self, $attach, $filters, $types, $prefix) = @_; - if (filter_wants $filters, 'size', $types, $prefix) { - $item->{'size'} = $self->type('int', $attach->datasize); - } - - if (filter_wants $filters, 'flags', $types, $prefix) { - $item->{'flags'} = [ map { $self->_flag_to_hash($_) } @{$attach->flags} ]; - } - - return $item; + my $item = filter $filters, + { + creation_time => $self->type('dateTime', $attach->attached), + last_change_time => $self->type('dateTime', $attach->modification_time), + id => $self->type('int', $attach->id), + bug_id => $self->type('int', $attach->bug_id), + file_name => $self->type('string', $attach->filename), + summary => $self->type('string', $attach->description), + content_type => $self->type('string', $attach->contenttype), + is_private => $self->type('int', $attach->isprivate), + is_obsolete => $self->type('int', $attach->isobsolete), + is_patch => $self->type('int', $attach->ispatch), + }, + $types, $prefix; + + # creator requires an extra lookup, so we only send it if + # the filter wants it. + if (filter_wants $filters, 'creator', $types, $prefix) { + $item->{'creator'} = $self->type('login', $attach->attacher->login); + } + + if (filter_wants $filters, 'data', $types, $prefix) { + $item->{'data'} = $self->type('base64', $attach->data); + } + + if (filter_wants $filters, 'size', $types, $prefix) { + $item->{'size'} = $self->type('int', $attach->datasize); + } + + if (filter_wants $filters, 'flags', $types, $prefix) { + $item->{'flags'} = [map { $self->_flag_to_hash($_) } @{$attach->flags}]; + } + + return $item; } sub _flag_to_hash { - my ($self, $flag) = @_; - - my $item = { - id => $self->type('int', $flag->id), - name => $self->type('string', $flag->name), - type_id => $self->type('int', $flag->type_id), - creation_date => $self->type('dateTime', $flag->creation_date), - modification_date => $self->type('dateTime', $flag->modification_date), - status => $self->type('string', $flag->status) - }; - - foreach my $field (qw(setter requestee)) { - my $field_id = $field . "_id"; - $item->{$field} = $self->type('login', $flag->$field->login) - if $flag->$field_id; - } - - return $item; + my ($self, $flag) = @_; + + my $item = { + id => $self->type('int', $flag->id), + name => $self->type('string', $flag->name), + type_id => $self->type('int', $flag->type_id), + creation_date => $self->type('dateTime', $flag->creation_date), + modification_date => $self->type('dateTime', $flag->modification_date), + status => $self->type('string', $flag->status) + }; + + foreach my $field (qw(setter requestee)) { + my $field_id = $field . "_id"; + $item->{$field} = $self->type('login', $flag->$field->login) + if $flag->$field_id; + } + + return $item; } sub _add_update_tokens { - my ($self, $params, $bugs, $hashes) = @_; + my ($self, $params, $bugs, $hashes) = @_; - return if !Bugzilla->user->id; - return if !filter_wants($params, 'update_token'); + return if !Bugzilla->user->id; + return if !filter_wants($params, 'update_token'); - for(my $i = 0; $i < @$bugs; $i++) { - my $token = issue_hash_token([$bugs->[$i]->id, $bugs->[$i]->delta_ts]); - $hashes->[$i]->{'update_token'} = $self->type('string', $token); - } + for (my $i = 0; $i < @$bugs; $i++) { + my $token = issue_hash_token([$bugs->[$i]->id, $bugs->[$i]->delta_ts]); + $hashes->[$i]->{'update_token'} = $self->type('string', $token); + } } 1; diff --git a/Bugzilla/WebService/BugUserLastVisit.pm b/Bugzilla/WebService/BugUserLastVisit.pm index b82faa33a1..8e0042a84d 100644 --- a/Bugzilla/WebService/BugUserLastVisit.pm +++ b/Bugzilla/WebService/BugUserLastVisit.pm @@ -19,80 +19,83 @@ use Bugzilla::WebService::Util qw( validate filter ); use Bugzilla::Constants; use constant PUBLIC_METHODS => qw( - get - update + get + update ); sub update { - my ($self, $params) = validate(@_, 'ids'); - my $user = Bugzilla->user; - my $dbh = Bugzilla->dbh; + my ($self, $params) = validate(@_, 'ids'); + my $user = Bugzilla->user; + my $dbh = Bugzilla->dbh; - $user->login(LOGIN_REQUIRED); + $user->login(LOGIN_REQUIRED); - my $ids = $params->{ids} // []; - ThrowCodeError('param_required', { param => 'ids' }) unless @$ids; + my $ids = $params->{ids} // []; + ThrowCodeError('param_required', {param => 'ids'}) unless @$ids; - # Cache permissions for bugs. This highly reduces the number of calls to the - # DB. visible_bugs() is only able to handle bug IDs, so we have to skip - # aliases. - $user->visible_bugs([grep /^[0-9]+$/, @$ids]); + # Cache permissions for bugs. This highly reduces the number of calls to the + # DB. visible_bugs() is only able to handle bug IDs, so we have to skip + # aliases. + $user->visible_bugs([grep /^[0-9]+$/, @$ids]); - $dbh->bz_start_transaction(); - my @results; - my $last_visit_ts = $dbh->selectrow_array('SELECT NOW()'); - foreach my $bug_id (@$ids) { - my $bug = Bugzilla::Bug->check({ id => $bug_id, cache => 1 }); + $dbh->bz_start_transaction(); + my @results; + my $last_visit_ts = $dbh->selectrow_array('SELECT NOW()'); + foreach my $bug_id (@$ids) { + my $bug = Bugzilla::Bug->check({id => $bug_id, cache => 1}); - ThrowUserError('user_not_involved', { bug_id => $bug->id }) - unless $user->is_involved_in_bug($bug); + ThrowUserError('user_not_involved', {bug_id => $bug->id}) + unless $user->is_involved_in_bug($bug); - $bug->update_user_last_visit($user, $last_visit_ts); + $bug->update_user_last_visit($user, $last_visit_ts); - push( - @results, - $self->_bug_user_last_visit_to_hash( - $bug->bug_id, $last_visit_ts, $params - )); - } - $dbh->bz_commit_transaction(); + push(@results, + $self->_bug_user_last_visit_to_hash($bug->bug_id, $last_visit_ts, $params)); + } + $dbh->bz_commit_transaction(); - return \@results; + return \@results; } sub get { - my ($self, $params) = validate(@_, 'ids'); - my $user = Bugzilla->user; - my $ids = $params->{ids}; - - $user->login(LOGIN_REQUIRED); - - my @last_visits; - if ($ids) { - # Cache permissions for bugs. This highly reduces the number of calls to - # the DB. visible_bugs() is only able to handle bug IDs, so we have to - # skip aliases. - $user->visible_bugs([grep /^[0-9]+$/, @$ids]); - - my %last_visit = map { $_->bug_id => $_->last_visit_ts } @{ $user->last_visited($ids) }; - @last_visits = map { $self->_bug_user_last_visit_to_hash($_, $last_visit{$_}, $params) } @$ids; - } - else { - @last_visits = map { - $self->_bug_user_last_visit_to_hash($_->bug_id, $_->last_visit_ts, $params) - } @{ $user->last_visited }; - } - - return \@last_visits; + my ($self, $params) = validate(@_, 'ids'); + my $user = Bugzilla->user; + my $ids = $params->{ids}; + + $user->login(LOGIN_REQUIRED); + + my @last_visits; + if ($ids) { + + # Cache permissions for bugs. This highly reduces the number of calls to + # the DB. visible_bugs() is only able to handle bug IDs, so we have to + # skip aliases. + $user->visible_bugs([grep /^[0-9]+$/, @$ids]); + + my %last_visit + = map { $_->bug_id => $_->last_visit_ts } @{$user->last_visited($ids)}; + @last_visits + = map { $self->_bug_user_last_visit_to_hash($_, $last_visit{$_}, $params) } + @$ids; + } + else { + @last_visits = map { + $self->_bug_user_last_visit_to_hash($_->bug_id, $_->last_visit_ts, $params) + } @{$user->last_visited}; + } + + return \@last_visits; } sub _bug_user_last_visit_to_hash { - my ($self, $bug_id, $last_visit_ts, $params) = @_; + my ($self, $bug_id, $last_visit_ts, $params) = @_; - my %result = (id => $self->type('int', $bug_id), - last_visit_ts => $self->type('dateTime', $last_visit_ts)); + my %result = ( + id => $self->type('int', $bug_id), + last_visit_ts => $self->type('dateTime', $last_visit_ts) + ); - return filter($params, \%result); + return filter($params, \%result); } 1; diff --git a/Bugzilla/WebService/Bugzilla.pm b/Bugzilla/WebService/Bugzilla.pm index c0df4754f7..a9595d867c 100644 --- a/Bugzilla/WebService/Bugzilla.pm +++ b/Bugzilla/WebService/Bugzilla.pm @@ -20,152 +20,149 @@ use Bugzilla::Util qw(trick_taint); use DateTime; # Basic info that is needed before logins -use constant LOGIN_EXEMPT => { - parameters => 1, - timezone => 1, - version => 1, -}; +use constant LOGIN_EXEMPT => {parameters => 1, timezone => 1, version => 1,}; use constant READ_ONLY => qw( - extensions - parameters - timezone - time - version + extensions + parameters + timezone + time + version ); use constant PUBLIC_METHODS => qw( - extensions - last_audit_time - parameters - time - timezone - version + extensions + last_audit_time + parameters + time + timezone + version ); # Logged-out users do not need to know more than that. use constant PARAMETERS_LOGGED_OUT => qw( - maintainer - requirelogin + maintainer + requirelogin ); # These parameters are guessable from the web UI when the user # is logged in. So it's safe to access them. use constant PARAMETERS_LOGGED_IN => qw( - allowemailchange - attachment_base - commentonchange_resolution - commentonduplicate - defaultopsys - defaultplatform - defaultpriority - defaultseverity - duplicate_or_move_bug_status - emailregexpdesc - letsubmitterchoosemilestone - letsubmitterchoosepriority - mailfrom - maintainer - maxattachmentsize - maxlocalattachment - password_complexity - rememberlogin - requirelogin - resolution_forbidden_with_open_blockers - search_allow_no_criteria - urlbase - use_email_as_login - use_see_also - useclassification - usemenuforusers - useqacontact - usestatuswhiteboard - usetargetmilestone + allowemailchange + attachment_base + commentonchange_resolution + commentonduplicate + defaultopsys + defaultplatform + defaultpriority + defaultseverity + duplicate_or_move_bug_status + emailregexpdesc + letsubmitterchoosemilestone + letsubmitterchoosepriority + mailfrom + maintainer + maxattachmentsize + maxlocalattachment + password_complexity + rememberlogin + requirelogin + resolution_forbidden_with_open_blockers + search_allow_no_criteria + urlbase + use_email_as_login + use_see_also + useclassification + usemenuforusers + useqacontact + usestatuswhiteboard + usetargetmilestone ); sub version { - my $self = shift; - return { version => $self->type('string', BUGZILLA_VERSION) }; + my $self = shift; + return {version => $self->type('string', BUGZILLA_VERSION)}; } sub extensions { - my $self = shift; - - my %retval; - foreach my $extension (@{ Bugzilla->extensions }) { - my $version = $extension->VERSION || 0; - my $name = $extension->NAME; - $retval{$name}->{version} = $self->type('string', $version); - } - return { extensions => \%retval }; + my $self = shift; + + my %retval; + foreach my $extension (@{Bugzilla->extensions}) { + my $version = $extension->VERSION || 0; + my $name = $extension->NAME; + $retval{$name}->{version} = $self->type('string', $version); + } + return {extensions => \%retval}; } sub timezone { - my $self = shift; - # All Webservices return times in UTC; Use UTC here for backwards compat. - return { timezone => $self->type('string', "+0000") }; + my $self = shift; + + # All Webservices return times in UTC; Use UTC here for backwards compat. + return {timezone => $self->type('string', "+0000")}; } sub time { - my ($self) = @_; - # All Webservices return times in UTC; Use UTC here for backwards compat. - # Hardcode values where appropriate - my $dbh = Bugzilla->dbh; - - my $db_time = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); - $db_time = datetime_from($db_time, 'UTC'); - my $now_utc = DateTime->now(); - - return { - db_time => $self->type('dateTime', $db_time), - web_time => $self->type('dateTime', $now_utc), - }; + my ($self) = @_; + + # All Webservices return times in UTC; Use UTC here for backwards compat. + # Hardcode values where appropriate + my $dbh = Bugzilla->dbh; + + my $db_time = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); + $db_time = datetime_from($db_time, 'UTC'); + my $now_utc = DateTime->now(); + + return { + db_time => $self->type('dateTime', $db_time), + web_time => $self->type('dateTime', $now_utc), + }; } sub last_audit_time { - my ($self, $params) = validate(@_, 'class'); - my $dbh = Bugzilla->dbh; - - my $sql_statement = "SELECT MAX(at_time) FROM audit_log"; - my $class_values = $params->{class}; - my @class_values_quoted; - foreach my $class_value (@$class_values) { - push (@class_values_quoted, $dbh->quote($class_value)) - if $class_value =~ /^Bugzilla(::[a-zA-Z0-9_]+)*$/; - } - - if (@class_values_quoted) { - $sql_statement .= " WHERE " . $dbh->sql_in('class', \@class_values_quoted); - } - - my $last_audit_time = $dbh->selectrow_array("$sql_statement"); - - # All Webservices return times in UTC; Use UTC here for backwards compat. - # Hardcode values where appropriate - $last_audit_time = datetime_from($last_audit_time, 'UTC'); - - return { - last_audit_time => $self->type('dateTime', $last_audit_time) - }; + my ($self, $params) = validate(@_, 'class'); + my $dbh = Bugzilla->dbh; + + my $sql_statement = "SELECT MAX(at_time) FROM audit_log"; + my $class_values = $params->{class}; + my @class_values_quoted; + foreach my $class_value (@$class_values) { + push(@class_values_quoted, $dbh->quote($class_value)) + if $class_value =~ /^Bugzilla(::[a-zA-Z0-9_]+)*$/; + } + + if (@class_values_quoted) { + $sql_statement .= " WHERE " . $dbh->sql_in('class', \@class_values_quoted); + } + + my $last_audit_time = $dbh->selectrow_array("$sql_statement"); + + # All Webservices return times in UTC; Use UTC here for backwards compat. + # Hardcode values where appropriate + $last_audit_time = datetime_from($last_audit_time, 'UTC'); + + return {last_audit_time => $self->type('dateTime', $last_audit_time)}; } sub parameters { - my ($self, $args) = @_; - my $user = Bugzilla->login(LOGIN_OPTIONAL); - my $params = Bugzilla->params; - $args ||= {}; - - my @params_list = $user->in_group('tweakparams') - ? keys(%$params) - : $user->id ? PARAMETERS_LOGGED_IN : PARAMETERS_LOGGED_OUT; - - my %parameters; - foreach my $param (@params_list) { - next unless filter_wants($args, $param); - $parameters{$param} = $self->type('string', $params->{$param}); - } - - return { parameters => \%parameters }; + my ($self, $args) = @_; + my $user = Bugzilla->login(LOGIN_OPTIONAL); + my $params = Bugzilla->params; + $args ||= {}; + + my @params_list + = $user->in_group('tweakparams') ? keys(%$params) + : $user->id ? PARAMETERS_LOGGED_IN + : PARAMETERS_LOGGED_OUT; + + my %parameters; + foreach my $param (@params_list) { + next unless filter_wants($args, $param); + $parameters{$param} = $self->type('string', $params->{$param}); + } + + return {parameters => \%parameters}; } 1; diff --git a/Bugzilla/WebService/Classification.pm b/Bugzilla/WebService/Classification.pm index e904440619..008b4974e2 100644 --- a/Bugzilla/WebService/Classification.pm +++ b/Bugzilla/WebService/Classification.pm @@ -18,65 +18,76 @@ use Bugzilla::Error; use Bugzilla::WebService::Util qw(filter validate params_to_objects); use constant READ_ONLY => qw( - get + get ); use constant PUBLIC_METHODS => qw( - get + get ); sub get { - my ($self, $params) = validate(@_, 'names', 'ids'); + my ($self, $params) = validate(@_, 'names', 'ids'); - defined $params->{names} || defined $params->{ids} - || ThrowCodeError('params_required', { function => 'Classification.get', - params => ['names', 'ids'] }); + defined $params->{names} + || defined $params->{ids} + || ThrowCodeError('params_required', + {function => 'Classification.get', params => ['names', 'ids']}); - my $user = Bugzilla->user; + my $user = Bugzilla->user; - Bugzilla->params->{'useclassification'} - || $user->in_group('editclassifications') - || ThrowUserError('auth_classification_not_enabled'); + Bugzilla->params->{'useclassification'} + || $user->in_group('editclassifications') + || ThrowUserError('auth_classification_not_enabled'); - Bugzilla->switch_to_shadow_db; + Bugzilla->switch_to_shadow_db; - my @classification_objs = @{ params_to_objects($params, 'Bugzilla::Classification') }; - unless ($user->in_group('editclassifications')) { - my %selectable_class = map { $_->id => 1 } @{$user->get_selectable_classifications}; - @classification_objs = grep { $selectable_class{$_->id} } @classification_objs; - } + my @classification_objs + = @{params_to_objects($params, 'Bugzilla::Classification')}; + unless ($user->in_group('editclassifications')) { + my %selectable_class + = map { $_->id => 1 } @{$user->get_selectable_classifications}; + @classification_objs = grep { $selectable_class{$_->id} } @classification_objs; + } - my @classifications = map { $self->_classification_to_hash($_, $params) } @classification_objs; + my @classifications + = map { $self->_classification_to_hash($_, $params) } @classification_objs; - return { classifications => \@classifications }; + return {classifications => \@classifications}; } sub _classification_to_hash { - my ($self, $classification, $params) = @_; - - my $user = Bugzilla->user; - return unless (Bugzilla->params->{'useclassification'} || $user->in_group('editclassifications')); - - my $products = $user->in_group('editclassifications') ? - $classification->products : $user->get_selectable_products($classification->id); - - return filter $params, { - id => $self->type('int', $classification->id), - name => $self->type('string', $classification->name), - description => $self->type('string', $classification->description), - sort_key => $self->type('int', $classification->sortkey), - products => [ map { $self->_product_to_hash($_, $params) } @$products ], + my ($self, $classification, $params) = @_; + + my $user = Bugzilla->user; + return + unless (Bugzilla->params->{'useclassification'} + || $user->in_group('editclassifications')); + + my $products + = $user->in_group('editclassifications') + ? $classification->products + : $user->get_selectable_products($classification->id); + + return filter $params, + { + id => $self->type('int', $classification->id), + name => $self->type('string', $classification->name), + description => $self->type('string', $classification->description), + sort_key => $self->type('int', $classification->sortkey), + products => [map { $self->_product_to_hash($_, $params) } @$products], }; } sub _product_to_hash { - my ($self, $product, $params) = @_; - - return filter $params, { - id => $self->type('int', $product->id), - name => $self->type('string', $product->name), - description => $self->type('string', $product->description), - }, undef, 'products'; + my ($self, $product, $params) = @_; + + return filter $params, + { + id => $self->type('int', $product->id), + name => $self->type('string', $product->name), + description => $self->type('string', $product->description), + }, + undef, 'products'; } 1; diff --git a/Bugzilla/WebService/Component.pm b/Bugzilla/WebService/Component.pm index 1410a12f63..92def92063 100644 --- a/Bugzilla/WebService/Component.pm +++ b/Bugzilla/WebService/Component.pm @@ -20,212 +20,209 @@ use Bugzilla::WebService::Constants; use Bugzilla::WebService::Util qw(translate params_to_objects validate); use constant PUBLIC_METHODS => qw( - create - delete - update + create + delete + update ); use constant CREATE_MAPPED_FIELDS => { - default_assignee => 'initialowner', - default_qa_contact => 'initialqacontact', - default_cc => 'initial_cc', - is_open => 'isactive', + default_assignee => 'initialowner', + default_qa_contact => 'initialqacontact', + default_cc => 'initial_cc', + is_open => 'isactive', }; -use constant MAPPED_FIELDS => { - is_open => 'is_active', -}; +use constant MAPPED_FIELDS => {is_open => 'is_active',}; use constant MAPPED_RETURNS => { - initialowner => 'default_assignee', - initialqacontact => 'default_qa_contact', - cc_list => 'default_cc', - isactive => 'isopen', + initialowner => 'default_assignee', + initialqacontact => 'default_qa_contact', + cc_list => 'default_cc', + isactive => 'isopen', }; sub create { - my ($self, $params) = @_; + my ($self, $params) = @_; - my $user = Bugzilla->login(LOGIN_REQUIRED); + my $user = Bugzilla->login(LOGIN_REQUIRED); - $user->in_group('editcomponents') - || scalar @{ $user->get_products_by_permission('editcomponents') } - || ThrowUserError('auth_failure', { group => 'editcomponents', - action => 'edit', - object => 'components' }); + $user->in_group('editcomponents') + || scalar @{$user->get_products_by_permission('editcomponents')} + || ThrowUserError('auth_failure', + {group => 'editcomponents', action => 'edit', object => 'components'}); - my $product = $user->check_can_admin_product($params->{product}); + my $product = $user->check_can_admin_product($params->{product}); - # Translate the fields - my $values = translate($params, CREATE_MAPPED_FIELDS); - $values->{product} = $product; + # Translate the fields + my $values = translate($params, CREATE_MAPPED_FIELDS); + $values->{product} = $product; - # Create the component and return the newly created id. - my $component = Bugzilla::Component->create($values); - return { id => $self->type('int', $component->id) }; + # Create the component and return the newly created id. + my $component = Bugzilla::Component->create($values); + return {id => $self->type('int', $component->id)}; } sub _component_params_to_objects { - # We can't use Util's _param_to_objects since name is a hash - my $params = shift; - my $user = Bugzilla->user; - my @components = (); + # We can't use Util's _param_to_objects since name is a hash + my $params = shift; + my $user = Bugzilla->user; - if (defined $params->{ids}) { - push @components, @{ Bugzilla::Component->new_from_list($params->{ids}) }; - } + my @components = (); + + if (defined $params->{ids}) { + push @components, @{Bugzilla::Component->new_from_list($params->{ids})}; + } + + if (defined $params->{names}) { - if (defined $params->{names}) { - # To get the component objects for product/component combination - # first obtain the product object from the passed product name - foreach my $name_hash (@{$params->{names}}) { - my $product = $user->check_can_admin_product($name_hash->{product}); - push @components, @{ Bugzilla::Component->match({ - product_id => $product->id, - name => $name_hash->{component} - })}; - } + # To get the component objects for product/component combination + # first obtain the product object from the passed product name + foreach my $name_hash (@{$params->{names}}) { + my $product = $user->check_can_admin_product($name_hash->{product}); + push @components, + @{Bugzilla::Component->match({ + product_id => $product->id, name => $name_hash->{component} + })}; } + } - my %seen_component_ids = (); + my %seen_component_ids = (); - my @accessible_components; - foreach my $component (@components) { - # Skip if we already included this component - next if $seen_component_ids{$component->id}++; + my @accessible_components; + foreach my $component (@components) { - # Can the user see and admin this product? - my $product = $component->product; - $user->check_can_admin_product($product->name); + # Skip if we already included this component + next if $seen_component_ids{$component->id}++; - push @accessible_components, $component; - } + # Can the user see and admin this product? + my $product = $component->product; + $user->check_can_admin_product($product->name); + + push @accessible_components, $component; + } - return \@accessible_components; + return \@accessible_components; } sub update { - my ($self, $params) = @_; - my $dbh = Bugzilla->dbh; - my $user = Bugzilla->login(LOGIN_REQUIRED); - - $user->in_group('editcomponents') - || scalar @{ $user->get_products_by_permission('editcomponents') } - || ThrowUserError("auth_failure", { group => "editcomponents", - action => "edit", - object => "components" }); - - defined($params->{names}) || defined($params->{ids}) - || ThrowCodeError('params_required', - { function => 'Component.update', params => ['ids', 'names'] }); - - my $component_objects = _component_params_to_objects($params); - - # If the user tries to change component name for several - # components of the same product then throw an error - if ($params->{name}) { - my %unique_product_comps; - foreach my $comp (@$component_objects) { - if($unique_product_comps{$comp->product_id}) { - ThrowUserError("multiple_components_update_not_allowed"); - } - else { - $unique_product_comps{$comp->product_id} = 1; - } - } + my ($self, $params) = @_; + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->login(LOGIN_REQUIRED); + + $user->in_group('editcomponents') + || scalar @{$user->get_products_by_permission('editcomponents')} + || ThrowUserError("auth_failure", + {group => "editcomponents", action => "edit", object => "components"}); + + defined($params->{names}) + || defined($params->{ids}) + || ThrowCodeError('params_required', + {function => 'Component.update', params => ['ids', 'names']}); + + my $component_objects = _component_params_to_objects($params); + + # If the user tries to change component name for several + # components of the same product then throw an error + if ($params->{name}) { + my %unique_product_comps; + foreach my $comp (@$component_objects) { + if ($unique_product_comps{$comp->product_id}) { + ThrowUserError("multiple_components_update_not_allowed"); + } + else { + $unique_product_comps{$comp->product_id} = 1; + } } - - my $values = translate($params, MAPPED_FIELDS); - - # We delete names and ids to keep only new values to set. - delete $values->{names}; - delete $values->{ids}; - - $dbh->bz_start_transaction(); - foreach my $component (@$component_objects) { - $component->set_all($values); + } + + my $values = translate($params, MAPPED_FIELDS); + + # We delete names and ids to keep only new values to set. + delete $values->{names}; + delete $values->{ids}; + + $dbh->bz_start_transaction(); + foreach my $component (@$component_objects) { + $component->set_all($values); + } + + my %changes; + foreach my $component (@$component_objects) { + my $returned_changes = $component->update(); + $changes{$component->id} = translate($returned_changes, MAPPED_RETURNS); + } + $dbh->bz_commit_transaction(); + + my @result; + foreach my $component (@$component_objects) { + my %hash = (id => $component->id, changes => {},); + + foreach my $field (keys %{$changes{$component->id}}) { + my $change = $changes{$component->id}->{$field}; + + if ( $field eq 'default_assignee' + || $field eq 'default_qa_contact' + || $field eq 'default_cc') + { + # We need to convert user ids to login names + my @old_user_ids = split(/[,\s]+/, $change->[0]); + my @new_user_ids = split(/[,\s]+/, $change->[1]); + + my @old_users + = map { $_->login } @{Bugzilla::User->new_from_list(\@old_user_ids)}; + my @new_users + = map { $_->login } @{Bugzilla::User->new_from_list(\@new_user_ids)}; + + $hash{changes}{$field} = { + removed => $self->type('string', join(', ', @old_users)), + added => $self->type('string', join(', ', @new_users)), + }; + } + else { + $hash{changes}{$field} = { + removed => $self->type('string', $change->[0]), + added => $self->type('string', $change->[1]) + }; + } } - my %changes; - foreach my $component (@$component_objects) { - my $returned_changes = $component->update(); - $changes{$component->id} = translate($returned_changes, MAPPED_RETURNS); - } - $dbh->bz_commit_transaction(); - - my @result; - foreach my $component (@$component_objects) { - my %hash = ( - id => $component->id, - changes => {}, - ); - - foreach my $field (keys %{ $changes{$component->id} }) { - my $change = $changes{$component->id}->{$field}; - - if ($field eq 'default_assignee' - || $field eq 'default_qa_contact' - || $field eq 'default_cc' - ) { - # We need to convert user ids to login names - my @old_user_ids = split(/[,\s]+/, $change->[0]); - my @new_user_ids = split(/[,\s]+/, $change->[1]); - - my @old_users = map { $_->login } - @{Bugzilla::User->new_from_list(\@old_user_ids)}; - my @new_users = map { $_->login } - @{Bugzilla::User->new_from_list(\@new_user_ids)}; - - $hash{changes}{$field} = { - removed => $self->type('string', join(', ', @old_users)), - added => $self->type('string', join(', ', @new_users)), - }; - } - else { - $hash{changes}{$field} = { - removed => $self->type('string', $change->[0]), - added => $self->type('string', $change->[1]) - }; - } - } - - push(@result, \%hash); - } + push(@result, \%hash); + } - return { components => \@result }; + return {components => \@result}; } sub delete { - my ($self, $params) = @_; - my $dbh = Bugzilla->dbh; - my $user = Bugzilla->login(LOGIN_REQUIRED); - - $user->in_group('editcomponents') - || scalar @{ $user->get_products_by_permission('editcomponents') } - || ThrowUserError("auth_failure", { group => "editcomponents", - action => "edit", - object => "components" }); - - defined($params->{names}) || defined($params->{ids}) - || ThrowCodeError('params_required', - { function => 'Component.delete', params => ['ids', 'names'] }); - - my $component_objects = _component_params_to_objects($params); - - $dbh->bz_start_transaction(); - my %changes; - foreach my $component (@$component_objects) { - my $returned_changes = $component->remove_from_db(); - } - $dbh->bz_commit_transaction(); - - my @result; - foreach my $component (@$component_objects) { - push @result, { id => $component->id }; - } - - return { components => \@result }; + my ($self, $params) = @_; + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->login(LOGIN_REQUIRED); + + $user->in_group('editcomponents') + || scalar @{$user->get_products_by_permission('editcomponents')} + || ThrowUserError("auth_failure", + {group => "editcomponents", action => "edit", object => "components"}); + + defined($params->{names}) + || defined($params->{ids}) + || ThrowCodeError('params_required', + {function => 'Component.delete', params => ['ids', 'names']}); + + my $component_objects = _component_params_to_objects($params); + + $dbh->bz_start_transaction(); + my %changes; + foreach my $component (@$component_objects) { + my $returned_changes = $component->remove_from_db(); + } + $dbh->bz_commit_transaction(); + + my @result; + foreach my $component (@$component_objects) { + push @result, {id => $component->id}; + } + + return {components => \@result}; } 1; diff --git a/Bugzilla/WebService/Constants.pm b/Bugzilla/WebService/Constants.pm index 3331669964..f18f9f0ecb 100644 --- a/Bugzilla/WebService/Constants.pm +++ b/Bugzilla/WebService/Constants.pm @@ -14,27 +14,27 @@ use warnings; use parent qw(Exporter); our @EXPORT = qw( - WS_ERROR_CODE + WS_ERROR_CODE - STATUS_OK - STATUS_CREATED - STATUS_ACCEPTED - STATUS_NO_CONTENT - STATUS_MULTIPLE_CHOICES - STATUS_BAD_REQUEST - STATUS_NOT_FOUND - STATUS_GONE - REST_STATUS_CODE_MAP + STATUS_OK + STATUS_CREATED + STATUS_ACCEPTED + STATUS_NO_CONTENT + STATUS_MULTIPLE_CHOICES + STATUS_BAD_REQUEST + STATUS_NOT_FOUND + STATUS_GONE + REST_STATUS_CODE_MAP - ERROR_UNKNOWN_FATAL - ERROR_UNKNOWN_TRANSIENT + ERROR_UNKNOWN_FATAL + ERROR_UNKNOWN_TRANSIENT - XMLRPC_CONTENT_TYPE_WHITELIST - REST_CONTENT_TYPE_WHITELIST + XMLRPC_CONTENT_TYPE_WHITELIST + REST_CONTENT_TYPE_WHITELIST - WS_DISPATCH + WS_DISPATCH - API_AUTH_HEADERS + API_AUTH_HEADERS ); # This maps the error names in global/*-error.html.tmpl to numbers. @@ -56,182 +56,205 @@ our @EXPORT = qw( # comment that it was retired. Also, if an error changes its name, you'll # have to fix it here. use constant WS_ERROR_CODE => { - # Generic errors (Bugzilla::Object and others) are 50-99. - object_not_specified => 50, - reassign_to_empty => 50, - param_required => 50, - params_required => 50, - undefined_field => 50, - object_does_not_exist => 51, - param_must_be_numeric => 52, - number_not_numeric => 52, - param_invalid => 53, - number_too_large => 54, - number_too_small => 55, - illegal_date => 56, - param_integer_required => 57, - param_scalar_array_required => 58, - # Bug errors usually occupy the 100-200 range. - improper_bug_id_field_value => 100, - bug_id_does_not_exist => 101, - bug_access_denied => 102, - bug_access_query => 102, - # These all mean "invalid alias" - alias_too_long => 103, - alias_in_use => 103, - alias_is_numeric => 103, - alias_has_comma_or_space => 103, - multiple_alias_not_allowed => 103, - # Misc. bug field errors - illegal_field => 104, - freetext_too_long => 104, - # Component errors - require_component => 105, - component_name_too_long => 105, - product_unknown_component => 105, - # Invalid Product - no_products => 106, - entry_access_denied => 106, - product_access_denied => 106, - product_disabled => 106, - # Invalid Summary - require_summary => 107, - # Invalid field name - invalid_field_name => 108, - # Not authorized to edit the bug - product_edit_denied => 109, - # Comment-related errors - comment_is_private => 110, - comment_id_invalid => 111, - comment_too_long => 114, - comment_invalid_isprivate => 117, - markdown_disabled => 140, - # Comment tagging - comment_tag_disabled => 125, - comment_tag_invalid => 126, - comment_tag_too_long => 127, - comment_tag_too_short => 128, - # See Also errors - bug_url_invalid => 112, - bug_url_too_long => 112, - # Insidergroup Errors - user_not_insider => 113, - # Note: 114 is above in the Comment-related section. - # Bug update errors - illegal_change => 115, - # Dependency errors - dependency_loop_single => 116, - dependency_loop_multi => 116, - # Note: 117 is above in the Comment-related section. - # Dup errors - dupe_loop_detected => 118, - dupe_id_required => 119, - # Bug-related group errors - group_invalid_removal => 120, - group_restriction_not_allowed => 120, - # Status/Resolution errors - missing_resolution => 121, - resolution_not_allowed => 122, - illegal_bug_status_transition => 123, - # Flag errors - flag_status_invalid => 129, - flag_update_denied => 130, - flag_type_requestee_disabled => 131, - flag_not_unique => 132, - flag_type_not_unique => 133, - flag_type_inactive => 134, - - # Authentication errors are usually 300-400. - invalid_login_or_password => 300, - account_disabled => 301, - auth_invalid_email => 302, - extern_id_conflict => -303, - auth_failure => 304, - password_too_short => 305, - password_not_complex => 305, - api_key_not_valid => 306, - api_key_revoked => 306, - auth_invalid_token => 307, - - # Except, historically, AUTH_NODATA, which is 410. - login_required => 410, - - # User errors are 500-600. - account_exists => 500, - illegal_email_address => 501, - auth_cant_create_account => 501, - account_creation_disabled => 501, - account_creation_restricted => 501, - password_too_short => 502, - # Error 503 password_too_long no longer exists. - invalid_username => 504, - # This is from strict_isolation, but it also basically means - # "invalid user." - invalid_user_group => 504, - user_access_by_id_denied => 505, - user_access_by_match_denied => 505, - - # Attachment errors are 600-700. - file_too_large => 600, - invalid_content_type => 601, - # Error 602 attachment_illegal_url no longer exists. - file_not_specified => 603, - missing_attachment_description => 604, - # Error 605 attachment_url_disabled no longer exists. - zero_length_file => 606, - - # Product erros are 700-800 - product_blank_name => 700, - product_name_too_long => 701, - product_name_already_in_use => 702, - product_name_diff_in_case => 702, - product_must_have_description => 703, - product_must_have_version => 704, - product_must_define_defaultmilestone => 705, - product_admin_denied => 706, - - # Group errors are 800-900 - empty_group_name => 800, - group_exists => 801, - empty_group_description => 802, - invalid_regexp => 803, - invalid_group_name => 804, - group_cannot_view => 805, - - # Classification errors are 900-1000 - auth_classification_not_enabled => 900, - - # Search errors are 1000-1100 - buglist_parameters_required => 1000, - - # Flag type errors are 1100-1200 - flag_type_name_invalid => 1101, - flag_type_description_invalid => 1102, - flag_type_cc_list_invalid => 1103, - flag_type_sortkey_invalid => 1104, - flag_type_not_editable => 1105, - - # Component errors are 1200-1300 - component_already_exists => 1200, - component_is_last => 1201, - component_has_bugs => 1202, - component_blank_name => 1210, - component_blank_description => 1211, - multiple_components_update_not_allowed => 1212, - component_need_initialowner => 1213, - - # BugUserLastVisited errors - user_not_involved => 1300, - - # Errors thrown by the WebService itself. The ones that are negative - # conform to http://xmlrpc-epi.sourceforge.net/specs/rfc.fault_codes.php - xmlrpc_invalid_value => -32600, - unknown_method => -32601, - json_rpc_post_only => 32610, - json_rpc_invalid_callback => 32611, - xmlrpc_illegal_content_type => 32612, - json_rpc_illegal_content_type => 32613, - rest_invalid_resource => 32614, + + # Generic errors (Bugzilla::Object and others) are 50-99. + object_not_specified => 50, + reassign_to_empty => 50, + param_required => 50, + params_required => 50, + undefined_field => 50, + object_does_not_exist => 51, + param_must_be_numeric => 52, + number_not_numeric => 52, + param_invalid => 53, + number_too_large => 54, + number_too_small => 55, + illegal_date => 56, + param_integer_required => 57, + param_scalar_array_required => 58, + + # Bug errors usually occupy the 100-200 range. + improper_bug_id_field_value => 100, + bug_id_does_not_exist => 101, + bug_access_denied => 102, + bug_access_query => 102, + + # These all mean "invalid alias" + alias_too_long => 103, + alias_in_use => 103, + alias_is_numeric => 103, + alias_has_comma_or_space => 103, + multiple_alias_not_allowed => 103, + + # Misc. bug field errors + illegal_field => 104, + freetext_too_long => 104, + + # Component errors + require_component => 105, + component_name_too_long => 105, + product_unknown_component => 105, + + # Invalid Product + no_products => 106, + entry_access_denied => 106, + product_access_denied => 106, + product_disabled => 106, + + # Invalid Summary + require_summary => 107, + + # Invalid field name + invalid_field_name => 108, + + # Not authorized to edit the bug + product_edit_denied => 109, + + # Comment-related errors + comment_is_private => 110, + comment_id_invalid => 111, + comment_too_long => 114, + comment_invalid_isprivate => 117, + markdown_disabled => 140, + + # Comment tagging + comment_tag_disabled => 125, + comment_tag_invalid => 126, + comment_tag_too_long => 127, + comment_tag_too_short => 128, + + # See Also errors + bug_url_invalid => 112, + bug_url_too_long => 112, + + # Insidergroup Errors + user_not_insider => 113, + + # Note: 114 is above in the Comment-related section. + # Bug update errors + illegal_change => 115, + + # Dependency errors + dependency_loop_single => 116, + dependency_loop_multi => 116, + + # Note: 117 is above in the Comment-related section. + # Dup errors + dupe_loop_detected => 118, + dupe_id_required => 119, + + # Bug-related group errors + group_invalid_removal => 120, + group_restriction_not_allowed => 120, + + # Status/Resolution errors + missing_resolution => 121, + resolution_not_allowed => 122, + illegal_bug_status_transition => 123, + + # Flag errors + flag_status_invalid => 129, + flag_update_denied => 130, + flag_type_requestee_disabled => 131, + flag_not_unique => 132, + flag_type_not_unique => 133, + flag_type_inactive => 134, + + # Authentication errors are usually 300-400. + invalid_login_or_password => 300, + account_disabled => 301, + auth_invalid_email => 302, + extern_id_conflict => -303, + auth_failure => 304, + password_too_short => 305, + password_not_complex => 305, + api_key_not_valid => 306, + api_key_revoked => 306, + auth_invalid_token => 307, + + # Except, historically, AUTH_NODATA, which is 410. + login_required => 410, + + # User errors are 500-600. + account_exists => 500, + illegal_email_address => 501, + auth_cant_create_account => 501, + account_creation_disabled => 501, + account_creation_restricted => 501, + password_too_short => 502, + + # Error 503 password_too_long no longer exists. + invalid_username => 504, + + # This is from strict_isolation, but it also basically means + # "invalid user." + invalid_user_group => 504, + user_access_by_id_denied => 505, + user_access_by_match_denied => 505, + + # Attachment errors are 600-700. + file_too_large => 600, + invalid_content_type => 601, + + # Error 602 attachment_illegal_url no longer exists. + file_not_specified => 603, + missing_attachment_description => 604, + + # Error 605 attachment_url_disabled no longer exists. + zero_length_file => 606, + + # Product erros are 700-800 + product_blank_name => 700, + product_name_too_long => 701, + product_name_already_in_use => 702, + product_name_diff_in_case => 702, + product_must_have_description => 703, + product_must_have_version => 704, + product_must_define_defaultmilestone => 705, + product_admin_denied => 706, + + # Group errors are 800-900 + empty_group_name => 800, + group_exists => 801, + empty_group_description => 802, + invalid_regexp => 803, + invalid_group_name => 804, + group_cannot_view => 805, + + # Classification errors are 900-1000 + auth_classification_not_enabled => 900, + + # Search errors are 1000-1100 + buglist_parameters_required => 1000, + + # Flag type errors are 1100-1200 + flag_type_name_invalid => 1101, + flag_type_description_invalid => 1102, + flag_type_cc_list_invalid => 1103, + flag_type_sortkey_invalid => 1104, + flag_type_not_editable => 1105, + + # Component errors are 1200-1300 + component_already_exists => 1200, + component_is_last => 1201, + component_has_bugs => 1202, + component_blank_name => 1210, + component_blank_description => 1211, + multiple_components_update_not_allowed => 1212, + component_need_initialowner => 1213, + + # BugUserLastVisited errors + user_not_involved => 1300, + + # Errors thrown by the WebService itself. The ones that are negative + # conform to http://xmlrpc-epi.sourceforge.net/specs/rfc.fault_codes.php + xmlrpc_invalid_value => -32600, + unknown_method => -32601, + json_rpc_post_only => 32610, + json_rpc_invalid_callback => 32611, + xmlrpc_illegal_content_type => 32612, + json_rpc_illegal_content_type => 32613, + rest_invalid_resource => 32614, }; # RESTful webservices use the http status code @@ -252,82 +275,83 @@ use constant STATUS_GONE => 410; # http status code based on the error code or use the # default STATUS_BAD_REQUEST. sub REST_STATUS_CODE_MAP { - my $status_code_map = { - 51 => STATUS_NOT_FOUND, - 101 => STATUS_NOT_FOUND, - 102 => STATUS_NOT_AUTHORIZED, - 106 => STATUS_NOT_AUTHORIZED, - 109 => STATUS_NOT_AUTHORIZED, - 110 => STATUS_NOT_AUTHORIZED, - 113 => STATUS_NOT_AUTHORIZED, - 115 => STATUS_NOT_AUTHORIZED, - 120 => STATUS_NOT_AUTHORIZED, - 300 => STATUS_NOT_AUTHORIZED, - 301 => STATUS_NOT_AUTHORIZED, - 302 => STATUS_NOT_AUTHORIZED, - 303 => STATUS_NOT_AUTHORIZED, - 304 => STATUS_NOT_AUTHORIZED, - 410 => STATUS_NOT_AUTHORIZED, - 504 => STATUS_NOT_AUTHORIZED, - 505 => STATUS_NOT_AUTHORIZED, - 32614 => STATUS_NOT_FOUND, - _default => STATUS_BAD_REQUEST - }; - - Bugzilla::Hook::process('webservice_status_code_map', - { status_code_map => $status_code_map }); - - return $status_code_map; -}; + my $status_code_map = { + 51 => STATUS_NOT_FOUND, + 101 => STATUS_NOT_FOUND, + 102 => STATUS_NOT_AUTHORIZED, + 106 => STATUS_NOT_AUTHORIZED, + 109 => STATUS_NOT_AUTHORIZED, + 110 => STATUS_NOT_AUTHORIZED, + 113 => STATUS_NOT_AUTHORIZED, + 115 => STATUS_NOT_AUTHORIZED, + 120 => STATUS_NOT_AUTHORIZED, + 300 => STATUS_NOT_AUTHORIZED, + 301 => STATUS_NOT_AUTHORIZED, + 302 => STATUS_NOT_AUTHORIZED, + 303 => STATUS_NOT_AUTHORIZED, + 304 => STATUS_NOT_AUTHORIZED, + 410 => STATUS_NOT_AUTHORIZED, + 504 => STATUS_NOT_AUTHORIZED, + 505 => STATUS_NOT_AUTHORIZED, + 32614 => STATUS_NOT_FOUND, + _default => STATUS_BAD_REQUEST + }; + + Bugzilla::Hook::process('webservice_status_code_map', + {status_code_map => $status_code_map}); + + return $status_code_map; +} # These are the fallback defaults for errors not in ERROR_CODE. use constant ERROR_UNKNOWN_FATAL => -32000; use constant ERROR_UNKNOWN_TRANSIENT => 32000; -use constant ERROR_GENERAL => 999; +use constant ERROR_GENERAL => 999; use constant XMLRPC_CONTENT_TYPE_WHITELIST => qw( - text/xml - application/xml + text/xml + application/xml ); # The first content type specified is used as the default. use constant REST_CONTENT_TYPE_WHITELIST => qw( - application/json - application/javascript - text/javascript - text/html + application/json + application/javascript + text/javascript + text/html ); sub WS_DISPATCH { - # We "require" here instead of "use" above to avoid a dependency loop. - require Bugzilla::Hook; - my %hook_dispatch; - Bugzilla::Hook::process('webservice', { dispatch => \%hook_dispatch }); - - my $dispatch = { - 'Bugzilla' => 'Bugzilla::WebService::Bugzilla', - 'Bug' => 'Bugzilla::WebService::Bug', - 'Classification' => 'Bugzilla::WebService::Classification', - 'Component' => 'Bugzilla::WebService::Component', - 'FlagType' => 'Bugzilla::WebService::FlagType', - 'Group' => 'Bugzilla::WebService::Group', - 'Product' => 'Bugzilla::WebService::Product', - 'User' => 'Bugzilla::WebService::User', - 'BugUserLastVisit' => 'Bugzilla::WebService::BugUserLastVisit', - %hook_dispatch - }; - return $dispatch; -}; + + # We "require" here instead of "use" above to avoid a dependency loop. + require Bugzilla::Hook; + my %hook_dispatch; + Bugzilla::Hook::process('webservice', {dispatch => \%hook_dispatch}); + + my $dispatch = { + 'Bugzilla' => 'Bugzilla::WebService::Bugzilla', + 'Bug' => 'Bugzilla::WebService::Bug', + 'Classification' => 'Bugzilla::WebService::Classification', + 'Component' => 'Bugzilla::WebService::Component', + 'FlagType' => 'Bugzilla::WebService::FlagType', + 'Group' => 'Bugzilla::WebService::Group', + 'Product' => 'Bugzilla::WebService::Product', + 'User' => 'Bugzilla::WebService::User', + 'BugUserLastVisit' => 'Bugzilla::WebService::BugUserLastVisit', + %hook_dispatch + }; + return $dispatch; +} # Custom HTTP headers that can be used for API authentication rather than # passing as URL parameters. This is useful if you do not want sensitive # information to show up in webserver log files. use constant API_AUTH_HEADERS => { - X_BUGZILLA_LOGIN => 'Bugzilla_login', - X_BUGZILLA_PASSWORD => 'Bugzilla_password', - X_BUGZILLA_API_KEY => 'Bugzilla_api_key', - X_BUGZILLA_TOKEN => 'Bugzilla_token', + X_BUGZILLA_LOGIN => 'Bugzilla_login', + X_BUGZILLA_PASSWORD => 'Bugzilla_password', + X_BUGZILLA_API_KEY => 'Bugzilla_api_key', + X_BUGZILLA_TOKEN => 'Bugzilla_token', }; 1; diff --git a/Bugzilla/WebService/FlagType.pm b/Bugzilla/WebService/FlagType.pm index 4c626914fa..0b0aff4393 100644 --- a/Bugzilla/WebService/FlagType.pm +++ b/Bugzilla/WebService/FlagType.pm @@ -22,292 +22,308 @@ use Bugzilla::Util qw(trim); use List::MoreUtils qw(uniq); use constant PUBLIC_METHODS => qw( - create - get - update + create + get + update ); sub get { - my ($self, $params) = @_; - my $dbh = Bugzilla->switch_to_shadow_db(); - my $user = Bugzilla->user; - - defined $params->{product} - || ThrowCodeError('param_required', - { function => 'Bug.flag_types', - param => 'product' }); - - my $product = delete $params->{product}; - my $component = delete $params->{component}; - - $product = Bugzilla::Product->check({ name => $product, cache => 1 }); - $component = Bugzilla::Component->check( - { name => $component, product => $product, cache => 1 }) if $component; - - my $flag_params = { product_id => $product->id }; - $flag_params->{component_id} = $component->id if $component; - my $matched_flag_types = Bugzilla::FlagType::match($flag_params); - - my $flag_types = { bug => [], attachment => [] }; - foreach my $flag_type (@$matched_flag_types) { - push(@{ $flag_types->{bug} }, $self->_flagtype_to_hash($flag_type, $product)) - if $flag_type->target_type eq 'bug'; - push(@{ $flag_types->{attachment} }, $self->_flagtype_to_hash($flag_type, $product)) - if $flag_type->target_type eq 'attachment'; - } - - return $flag_types; + my ($self, $params) = @_; + my $dbh = Bugzilla->switch_to_shadow_db(); + my $user = Bugzilla->user; + + defined $params->{product} + || ThrowCodeError('param_required', + {function => 'Bug.flag_types', param => 'product'}); + + my $product = delete $params->{product}; + my $component = delete $params->{component}; + + $product = Bugzilla::Product->check({name => $product, cache => 1}); + $component + = Bugzilla::Component->check( + {name => $component, product => $product, cache => 1}) + if $component; + + my $flag_params = {product_id => $product->id}; + $flag_params->{component_id} = $component->id if $component; + my $matched_flag_types = Bugzilla::FlagType::match($flag_params); + + my $flag_types = {bug => [], attachment => []}; + foreach my $flag_type (@$matched_flag_types) { + push(@{$flag_types->{bug}}, $self->_flagtype_to_hash($flag_type, $product)) + if $flag_type->target_type eq 'bug'; + push( + @{$flag_types->{attachment}}, + $self->_flagtype_to_hash($flag_type, $product) + ) if $flag_type->target_type eq 'attachment'; + } + + return $flag_types; } sub create { - my ($self, $params) = @_; - my $user = Bugzilla->login(LOGIN_REQUIRED); - - $user->in_group('editcomponents') - || scalar(@{$user->get_products_by_permission('editcomponents')}) - || ThrowUserError("auth_failure", { group => "editcomponents", - action => "add", - object => "flagtypes" }); - - $params->{name} || ThrowCodeError('param_required', { param => 'name' }); - $params->{description} || ThrowCodeError('param_required', { param => 'description' }); - - my %args = ( - sortkey => 1, - name => undef, - inclusions => ['0:0'], # Default to __ALL__:__ALL__ - cc_list => '', - description => undef, - is_requestable => 'on', - exclusions => [], - is_multiplicable => 'on', - request_group => '', - is_active => 'on', - is_specifically_requestable => 'on', - target_type => 'bug', - grant_group => '', - ); - - foreach my $key (keys %args) { - $args{$key} = $params->{$key} if defined($params->{$key}); - } - - $args{name} = trim($params->{name}); - $args{description} = trim($params->{description}); - - # Is specifically requestable is actually is_requesteeable - if (exists $args{is_specifically_requestable}) { - $args{is_requesteeble} = delete $args{is_specifically_requestable}; - } - - # Default is on for the tickbox flags. - # If the user has set them to 'off' then undefine them so the flags are not ticked - foreach my $arg_name (qw(is_requestable is_multiplicable is_active is_requesteeble)) { - if (defined($args{$arg_name}) && ($args{$arg_name} eq '0')) { - $args{$arg_name} = undef; - } + my ($self, $params) = @_; + my $user = Bugzilla->login(LOGIN_REQUIRED); + + $user->in_group('editcomponents') + || scalar(@{$user->get_products_by_permission('editcomponents')}) + || ThrowUserError("auth_failure", + {group => "editcomponents", action => "add", object => "flagtypes"}); + + $params->{name} || ThrowCodeError('param_required', {param => 'name'}); + $params->{description} + || ThrowCodeError('param_required', {param => 'description'}); + + my %args = ( + sortkey => 1, + name => undef, + inclusions => ['0:0'], # Default to __ALL__:__ALL__ + cc_list => '', + description => undef, + is_requestable => 'on', + exclusions => [], + is_multiplicable => 'on', + request_group => '', + is_active => 'on', + is_specifically_requestable => 'on', + target_type => 'bug', + grant_group => '', + ); + + foreach my $key (keys %args) { + $args{$key} = $params->{$key} if defined($params->{$key}); + } + + $args{name} = trim($params->{name}); + $args{description} = trim($params->{description}); + + # Is specifically requestable is actually is_requesteeable + if (exists $args{is_specifically_requestable}) { + $args{is_requesteeble} = delete $args{is_specifically_requestable}; + } + +# Default is on for the tickbox flags. +# If the user has set them to 'off' then undefine them so the flags are not ticked + foreach + my $arg_name (qw(is_requestable is_multiplicable is_active is_requesteeble)) + { + if (defined($args{$arg_name}) && ($args{$arg_name} eq '0')) { + $args{$arg_name} = undef; } + } - # Process group inclusions and exclusions - $args{inclusions} = _process_lists($params->{inclusions}) if defined $params->{inclusions}; - $args{exclusions} = _process_lists($params->{exclusions}) if defined $params->{exclusions}; + # Process group inclusions and exclusions + $args{inclusions} = _process_lists($params->{inclusions}) + if defined $params->{inclusions}; + $args{exclusions} = _process_lists($params->{exclusions}) + if defined $params->{exclusions}; - my $flagtype = Bugzilla::FlagType->create(\%args); + my $flagtype = Bugzilla::FlagType->create(\%args); - return { id => $self->type('int', $flagtype->id) }; + return {id => $self->type('int', $flagtype->id)}; } sub update { - my ($self, $params) = @_; - my $dbh = Bugzilla->dbh; - my $user = Bugzilla->login(LOGIN_REQUIRED); - - $user->in_group('editcomponents') - || scalar(@{$user->get_products_by_permission('editcomponents')}) - || ThrowUserError("auth_failure", { group => "editcomponents", - action => "edit", - object => "flagtypes" }); - - defined($params->{names}) || defined($params->{ids}) - || ThrowCodeError('params_required', - { function => 'FlagType.update', params => ['ids', 'names'] }); - - # Get the list of unique flag type ids we are updating - my @flag_type_ids = defined($params->{ids}) ? @{$params->{ids}} : (); - if (defined $params->{names}) { - push @flag_type_ids, map { $_->id } - @{ Bugzilla::FlagType::match({ name => $params->{names} }) }; + my ($self, $params) = @_; + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->login(LOGIN_REQUIRED); + + $user->in_group('editcomponents') + || scalar(@{$user->get_products_by_permission('editcomponents')}) + || ThrowUserError("auth_failure", + {group => "editcomponents", action => "edit", object => "flagtypes"}); + + defined($params->{names}) + || defined($params->{ids}) + || ThrowCodeError('params_required', + {function => 'FlagType.update', params => ['ids', 'names']}); + + # Get the list of unique flag type ids we are updating + my @flag_type_ids = defined($params->{ids}) ? @{$params->{ids}} : (); + if (defined $params->{names}) { + push @flag_type_ids, + map { $_->id } @{Bugzilla::FlagType::match({name => $params->{names}})}; + } + @flag_type_ids = uniq @flag_type_ids; + + # We delete names and ids to keep only new values to set. + delete $params->{names}; + delete $params->{ids}; + + # Process group inclusions and exclusions + # We removed them from $params because these are handled differently + my $inclusions = _process_lists(delete $params->{inclusions}) + if defined $params->{inclusions}; + my $exclusions = _process_lists(delete $params->{exclusions}) + if defined $params->{exclusions}; + + $dbh->bz_start_transaction(); + my %changes = (); + + foreach my $flag_type_id (@flag_type_ids) { + my ($flagtype, $can_fully_edit) + = $user->check_can_admin_flagtype($flag_type_id); + + if ($can_fully_edit) { + $flagtype->set_all($params); + } + elsif (scalar keys %$params) { + ThrowUserError('flag_type_not_editable', {flagtype => $flagtype}); } - @flag_type_ids = uniq @flag_type_ids; - - # We delete names and ids to keep only new values to set. - delete $params->{names}; - delete $params->{ids}; - - # Process group inclusions and exclusions - # We removed them from $params because these are handled differently - my $inclusions = _process_lists(delete $params->{inclusions}) if defined $params->{inclusions}; - my $exclusions = _process_lists(delete $params->{exclusions}) if defined $params->{exclusions}; - - $dbh->bz_start_transaction(); - my %changes = (); - foreach my $flag_type_id (@flag_type_ids) { - my ($flagtype, $can_fully_edit) = $user->check_can_admin_flagtype($flag_type_id); + # Process the clusions + foreach my $type ('inclusions', 'exclusions') { + my $clusions = $type eq 'inclusions' ? $inclusions : $exclusions; + next if not defined $clusions; - if ($can_fully_edit) { - $flagtype->set_all($params); - } - elsif (scalar keys %$params) { - ThrowUserError('flag_type_not_editable', { flagtype => $flagtype }); - } + my @extra_clusions = (); + if (!$user->in_group('editcomponents')) { + my $products = $user->get_products_by_permission('editcomponents'); - # Process the clusions - foreach my $type ('inclusions', 'exclusions') { - my $clusions = $type eq 'inclusions' ? $inclusions : $exclusions; - next if not defined $clusions; - - my @extra_clusions = (); - if (!$user->in_group('editcomponents')) { - my $products = $user->get_products_by_permission('editcomponents'); - # Bring back the products the user cannot edit. - foreach my $item (values %{$flagtype->$type}) { - my ($prod_id, $comp_id) = split(':', $item); - push(@extra_clusions, $item) unless grep { $_->id == $prod_id } @$products; - } - } - - $flagtype->set_clusions({ - $type => [@$clusions, @extra_clusions], - }); + # Bring back the products the user cannot edit. + foreach my $item (values %{$flagtype->$type}) { + my ($prod_id, $comp_id) = split(':', $item); + push(@extra_clusions, $item) unless grep { $_->id == $prod_id } @$products; } + } - my $returned_changes = $flagtype->update(); - $changes{$flagtype->id} = { - name => $flagtype->name, - changes => $returned_changes, - }; + $flagtype->set_clusions({$type => [@$clusions, @extra_clusions],}); } - $dbh->bz_commit_transaction(); - - my @result; - foreach my $flag_type_id (keys %changes) { - my %hash = ( - id => $self->type('int', $flag_type_id), - name => $self->type('string', $changes{$flag_type_id}{name}), - changes => {}, - ); - - foreach my $field (keys %{ $changes{$flag_type_id}{changes} }) { - my $change = $changes{$flag_type_id}{changes}{$field}; - $hash{changes}{$field} = { - removed => $self->type('string', $change->[0]), - added => $self->type('string', $change->[1]) - }; - } - push(@result, \%hash); + my $returned_changes = $flagtype->update(); + $changes{$flagtype->id} + = {name => $flagtype->name, changes => $returned_changes,}; + } + $dbh->bz_commit_transaction(); + + my @result; + foreach my $flag_type_id (keys %changes) { + my %hash = ( + id => $self->type('int', $flag_type_id), + name => $self->type('string', $changes{$flag_type_id}{name}), + changes => {}, + ); + + foreach my $field (keys %{$changes{$flag_type_id}{changes}}) { + my $change = $changes{$flag_type_id}{changes}{$field}; + $hash{changes}{$field} = { + removed => $self->type('string', $change->[0]), + added => $self->type('string', $change->[1]) + }; } - return { flagtypes => \@result }; + push(@result, \%hash); + } + + return {flagtypes => \@result}; } sub _flagtype_to_hash { - my ($self, $flagtype, $product) = @_; - my $user = Bugzilla->user; - - my @values = ('X'); - push(@values, '?') if ($flagtype->is_requestable && $user->can_request_flag($flagtype)); - push(@values, '+', '-') if $user->can_set_flag($flagtype); - - my $item = { - id => $self->type('int' , $flagtype->id), - name => $self->type('string' , $flagtype->name), - description => $self->type('string' , $flagtype->description), - type => $self->type('string' , $flagtype->target_type), - values => \@values, - is_active => $self->type('boolean', $flagtype->is_active), - is_requesteeble => $self->type('boolean', $flagtype->is_requesteeble), - is_multiplicable => $self->type('boolean', $flagtype->is_multiplicable) - }; - - if ($product) { - my $inclusions = $self->_flagtype_clusions_to_hash($flagtype->inclusions, $product->id); - my $exclusions = $self->_flagtype_clusions_to_hash($flagtype->exclusions, $product->id); - # if we have both inclusions and exclusions, the exclusions are redundant - $exclusions = [] if @$inclusions && @$exclusions; - # no need to return anything if there's just "any component" - $item->{inclusions} = $inclusions if @$inclusions && $inclusions->[0] ne ''; - $item->{exclusions} = $exclusions if @$exclusions && $exclusions->[0] ne ''; - } - - return $item; + my ($self, $flagtype, $product) = @_; + my $user = Bugzilla->user; + + my @values = ('X'); + push(@values, '?') + if ($flagtype->is_requestable && $user->can_request_flag($flagtype)); + push(@values, '+', '-') if $user->can_set_flag($flagtype); + + my $item = { + id => $self->type('int', $flagtype->id), + name => $self->type('string', $flagtype->name), + description => $self->type('string', $flagtype->description), + type => $self->type('string', $flagtype->target_type), + values => \@values, + is_active => $self->type('boolean', $flagtype->is_active), + is_requesteeble => $self->type('boolean', $flagtype->is_requesteeble), + is_multiplicable => $self->type('boolean', $flagtype->is_multiplicable) + }; + + if ($product) { + my $inclusions + = $self->_flagtype_clusions_to_hash($flagtype->inclusions, $product->id); + my $exclusions + = $self->_flagtype_clusions_to_hash($flagtype->exclusions, $product->id); + + # if we have both inclusions and exclusions, the exclusions are redundant + $exclusions = [] if @$inclusions && @$exclusions; + + # no need to return anything if there's just "any component" + $item->{inclusions} = $inclusions if @$inclusions && $inclusions->[0] ne ''; + $item->{exclusions} = $exclusions if @$exclusions && $exclusions->[0] ne ''; + } + + return $item; } sub _flagtype_clusions_to_hash { - my ($self, $clusions, $product_id) = @_; - my $result = []; - foreach my $key (keys %$clusions) { - my ($prod_id, $comp_id) = split(/:/, $clusions->{$key}, 2); - if ($prod_id == 0 || $prod_id == $product_id) { - if ($comp_id) { - my $component = Bugzilla::Component->new({ id => $comp_id, cache => 1 }); - push @$result, $component->name; - } - else { - return [ '' ]; - } - } + my ($self, $clusions, $product_id) = @_; + my $result = []; + foreach my $key (keys %$clusions) { + my ($prod_id, $comp_id) = split(/:/, $clusions->{$key}, 2); + if ($prod_id == 0 || $prod_id == $product_id) { + if ($comp_id) { + my $component = Bugzilla::Component->new({id => $comp_id, cache => 1}); + push @$result, $component->name; + } + else { + return ['']; + } } - return $result; + } + return $result; } sub _process_lists { - my $list = shift; - my $user = Bugzilla->user; - - my @products; - if ($user->in_group('editcomponents')) { - @products = Bugzilla::Product->get_all; - } - else { - @products = @{$user->get_products_by_permission('editcomponents')}; - } - - my @component_list; - - foreach my $item (@$list) { - # A hash with products as the key and component names as the values - if(ref($item) eq 'HASH') { - while (my ($product_name, $component_names) = each %$item) { - my $product = Bugzilla::Product->check({name => $product_name}); - unless (grep { $product->name eq $_->name } @products) { - ThrowUserError('product_access_denied', { name => $product_name }); - } - my @component_ids; - - foreach my $comp_name (@$component_names) { - my $component = Bugzilla::Component->check({product => $product, name => $comp_name}); - ThrowCodeError('param_invalid', { param => $comp_name}) unless defined $component; - push @component_list, $product->id . ':' . $component->id; - } - } + my $list = shift; + my $user = Bugzilla->user; + + my @products; + if ($user->in_group('editcomponents')) { + @products = Bugzilla::Product->get_all; + } + else { + @products = @{$user->get_products_by_permission('editcomponents')}; + } + + my @component_list; + + foreach my $item (@$list) { + + # A hash with products as the key and component names as the values + if (ref($item) eq 'HASH') { + while (my ($product_name, $component_names) = each %$item) { + my $product = Bugzilla::Product->check({name => $product_name}); + unless (grep { $product->name eq $_->name } @products) { + ThrowUserError('product_access_denied', {name => $product_name}); } - elsif(!ref($item)) { - # These are whole products - my $product = Bugzilla::Product->check({name => $item}); - unless (grep { $product->name eq $_->name } @products) { - ThrowUserError('product_access_denied', { name => $item }); - } - push @component_list, $product->id . ':0'; - } - else { - # The user has passed something invalid - ThrowCodeError('param_invalid', { param => $item }); + my @component_ids; + + foreach my $comp_name (@$component_names) { + my $component + = Bugzilla::Component->check({product => $product, name => $comp_name}); + ThrowCodeError('param_invalid', {param => $comp_name}) + unless defined $component; + push @component_list, $product->id . ':' . $component->id; } + } + } + elsif (!ref($item)) { + + # These are whole products + my $product = Bugzilla::Product->check({name => $item}); + unless (grep { $product->name eq $_->name } @products) { + ThrowUserError('product_access_denied', {name => $item}); + } + push @component_list, $product->id . ':0'; + } + else { + # The user has passed something invalid + ThrowCodeError('param_invalid', {param => $item}); } + } - return \@component_list; + return \@component_list; } 1; diff --git a/Bugzilla/WebService/Group.pm b/Bugzilla/WebService/Group.pm index a9539f83d7..04aa6e9a01 100644 --- a/Bugzilla/WebService/Group.pm +++ b/Bugzilla/WebService/Group.pm @@ -17,207 +17,210 @@ use Bugzilla::Error; use Bugzilla::WebService::Util qw(validate translate params_to_objects); use constant PUBLIC_METHODS => qw( - create - get - update + create + get + update ); -use constant MAPPED_RETURNS => { - userregexp => 'user_regexp', - isactive => 'is_active' -}; +use constant MAPPED_RETURNS => + {userregexp => 'user_regexp', isactive => 'is_active'}; sub create { - my ($self, $params) = @_; - - Bugzilla->login(LOGIN_REQUIRED); - Bugzilla->user->in_group('creategroups') - || ThrowUserError("auth_failure", { group => "creategroups", - action => "add", - object => "group"}); - # Create group - my $group = Bugzilla::Group->create({ - name => $params->{name}, - description => $params->{description}, - userregexp => $params->{user_regexp}, - isactive => $params->{is_active}, - isbuggroup => 1, - icon_url => $params->{icon_url} - }); - return { id => $self->type('int', $group->id) }; + my ($self, $params) = @_; + + Bugzilla->login(LOGIN_REQUIRED); + Bugzilla->user->in_group('creategroups') + || ThrowUserError("auth_failure", + {group => "creategroups", action => "add", object => "group"}); + + # Create group + my $group = Bugzilla::Group->create({ + name => $params->{name}, + description => $params->{description}, + userregexp => $params->{user_regexp}, + isactive => $params->{is_active}, + isbuggroup => 1, + icon_url => $params->{icon_url} + }); + return {id => $self->type('int', $group->id)}; } sub update { - my ($self, $params) = @_; + my ($self, $params) = @_; + + my $dbh = Bugzilla->dbh; + + Bugzilla->login(LOGIN_REQUIRED); + Bugzilla->user->in_group('creategroups') + || ThrowUserError("auth_failure", + {group => "creategroups", action => "edit", object => "group"}); + + defined($params->{names}) + || defined($params->{ids}) + || ThrowCodeError('params_required', + {function => 'Group.update', params => ['ids', 'names']}); + + my $group_objects = params_to_objects($params, 'Bugzilla::Group'); + + my %values = %$params; + + # We delete names and ids to keep only new values to set. + delete $values{names}; + delete $values{ids}; + + $dbh->bz_start_transaction(); + foreach my $group (@$group_objects) { + $group->set_all(\%values); + } + + my %changes; + foreach my $group (@$group_objects) { + my $returned_changes = $group->update(); + $changes{$group->id} = translate($returned_changes, MAPPED_RETURNS); + } + $dbh->bz_commit_transaction(); + + my @result; + foreach my $group (@$group_objects) { + my %hash = (id => $group->id, changes => {},); + foreach my $field (keys %{$changes{$group->id}}) { + my $change = $changes{$group->id}->{$field}; + $hash{changes}{$field} = { + removed => $self->type('string', $change->[0]), + added => $self->type('string', $change->[1]) + }; + } + push(@result, \%hash); + } - my $dbh = Bugzilla->dbh; + return {groups => \@result}; +} - Bugzilla->login(LOGIN_REQUIRED); - Bugzilla->user->in_group('creategroups') - || ThrowUserError("auth_failure", { group => "creategroups", - action => "edit", - object => "group" }); +sub get { + my ($self, $params) = validate(@_, 'ids', 'names', 'type'); - defined($params->{names}) || defined($params->{ids}) - || ThrowCodeError('params_required', - { function => 'Group.update', params => ['ids', 'names'] }); + Bugzilla->login(LOGIN_REQUIRED); - my $group_objects = params_to_objects($params, 'Bugzilla::Group'); + # Reject access if there is no sense in continuing. + my $user = Bugzilla->user; + my $all_groups + = $user->in_group('editusers') || $user->in_group('creategroups'); + if (!$all_groups && !$user->can_bless) { + ThrowUserError('group_cannot_view'); + } - my %values = %$params; - - # We delete names and ids to keep only new values to set. - delete $values{names}; - delete $values{ids}; + Bugzilla->switch_to_shadow_db(); - $dbh->bz_start_transaction(); - foreach my $group (@$group_objects) { - $group->set_all(\%values); - } + my $groups = []; - my %changes; - foreach my $group (@$group_objects) { - my $returned_changes = $group->update(); - $changes{$group->id} = translate($returned_changes, MAPPED_RETURNS); - } - $dbh->bz_commit_transaction(); - - my @result; - foreach my $group (@$group_objects) { - my %hash = ( - id => $group->id, - changes => {}, - ); - foreach my $field (keys %{ $changes{$group->id} }) { - my $change = $changes{$group->id}->{$field}; - $hash{changes}{$field} = { - removed => $self->type('string', $change->[0]), - added => $self->type('string', $change->[1]) - }; - } - push(@result, \%hash); - } + if (defined $params->{ids}) { - return { groups => \@result }; -} - -sub get { - my ($self, $params) = validate(@_, 'ids', 'names', 'type'); + # Get the groups by id + $groups = Bugzilla::Group->new_from_list($params->{ids}); + } - Bugzilla->login(LOGIN_REQUIRED); - - # Reject access if there is no sense in continuing. - my $user = Bugzilla->user; - my $all_groups = $user->in_group('editusers') || $user->in_group('creategroups'); - if (!$all_groups && !$user->can_bless) { - ThrowUserError('group_cannot_view'); - } + if (defined $params->{names}) { - Bugzilla->switch_to_shadow_db(); + # Get the groups by name. Check will throw an error if a bad name is given + foreach my $name (@{$params->{names}}) { - my $groups = []; + # Skip if we got this from params->{id} + next if grep { $_->name eq $name } @$groups; - if (defined $params->{ids}) { - # Get the groups by id - $groups = Bugzilla::Group->new_from_list($params->{ids}); + push @$groups, Bugzilla::Group->check({name => $name}); } + } - if (defined $params->{names}) { - # Get the groups by name. Check will throw an error if a bad name is given - foreach my $name (@{$params->{names}}) { - # Skip if we got this from params->{id} - next if grep { $_->name eq $name } @$groups; - - push @$groups, Bugzilla::Group->check({ name => $name }); - } + if (!defined $params->{ids} && !defined $params->{names}) { + if ($all_groups) { + @$groups = Bugzilla::Group->get_all; } - - if (!defined $params->{ids} && !defined $params->{names}) { - if ($all_groups) { - @$groups = Bugzilla::Group->get_all; - } - else { - # Get only groups the user has bless groups too - $groups = $user->bless_groups; - } + else { + # Get only groups the user has bless groups too + $groups = $user->bless_groups; } + } - # Now create a result entry for each. - my @groups = map { $self->_group_to_hash($params, $_) } @$groups; - return { groups => \@groups }; + # Now create a result entry for each. + my @groups = map { $self->_group_to_hash($params, $_) } @$groups; + return {groups => \@groups}; } sub _group_to_hash { - my ($self, $params, $group) = @_; - my $user = Bugzilla->user; - - my $field_data = { - id => $self->type('int', $group->id), - name => $self->type('string', $group->name), - description => $self->type('string', $group->description), - }; - - if ($user->in_group('creategroups')) { - $field_data->{is_active} = $self->type('boolean', $group->is_active); - $field_data->{is_bug_group} = $self->type('boolean', $group->is_bug_group); - $field_data->{user_regexp} = $self->type('string', $group->user_regexp); - } - - if ($params->{membership}) { - $field_data->{membership} = $self->_get_group_membership($group, $params); - } - return $field_data; + my ($self, $params, $group) = @_; + my $user = Bugzilla->user; + + my $field_data = { + id => $self->type('int', $group->id), + name => $self->type('string', $group->name), + description => $self->type('string', $group->description), + }; + + if ($user->in_group('creategroups')) { + $field_data->{is_active} = $self->type('boolean', $group->is_active); + $field_data->{is_bug_group} = $self->type('boolean', $group->is_bug_group); + $field_data->{user_regexp} = $self->type('string', $group->user_regexp); + } + + if ($params->{membership}) { + $field_data->{membership} = $self->_get_group_membership($group, $params); + } + return $field_data; } sub _get_group_membership { - my ($self, $group, $params) = @_; - my $user = Bugzilla->user; + my ($self, $group, $params) = @_; + my $user = Bugzilla->user; - my %users_only; - my $dbh = Bugzilla->dbh; - my $editusers = $user->in_group('editusers'); + my %users_only; + my $dbh = Bugzilla->dbh; + my $editusers = $user->in_group('editusers'); - my $query = 'SELECT userid FROM profiles'; - my $visibleGroups; + my $query = 'SELECT userid FROM profiles'; + my $visibleGroups; - if (!$editusers && Bugzilla->params->{'usevisibilitygroups'}) { - # Show only users in visible groups. - $visibleGroups = $user->visible_groups_inherited; + if (!$editusers && Bugzilla->params->{'usevisibilitygroups'}) { - if (scalar @$visibleGroups) { - $query .= qq{, user_group_map AS ugm + # Show only users in visible groups. + $visibleGroups = $user->visible_groups_inherited; + + if (scalar @$visibleGroups) { + $query .= qq{, user_group_map AS ugm WHERE ugm.user_id = profiles.userid AND ugm.isbless = 0 AND } . $dbh->sql_in('ugm.group_id', $visibleGroups); - } - } elsif ($editusers || $user->can_bless($group->id) || $user->in_group('creategroups')) { - $visibleGroups = 1; - $query .= qq{, user_group_map AS ugm + } + } + elsif ($editusers + || $user->can_bless($group->id) + || $user->in_group('creategroups')) + { + $visibleGroups = 1; + $query .= qq{, user_group_map AS ugm WHERE ugm.user_id = profiles.userid AND ugm.isbless = 0 }; - } - if (!$visibleGroups) { - ThrowUserError('group_not_visible', { group => $group }); - } - - my $grouplist = Bugzilla::Group->flatten_group_membership($group->id); - $query .= ' AND ' . $dbh->sql_in('ugm.group_id', $grouplist); - - my $userids = $dbh->selectcol_arrayref($query); - my $user_objects = Bugzilla::User->new_from_list($userids); - my @users = - map {{ - id => $self->type('int', $_->id), - real_name => $self->type('string', $_->name), - name => $self->type('login', $_->login), - email => $self->type('email', $_->email), - can_login => $self->type('boolean', $_->is_enabled), - email_enabled => $self->type('boolean', $_->email_enabled), - login_denied_text => $self->type('string', $_->disabledtext), - }} @$user_objects; - - return \@users; + } + if (!$visibleGroups) { + ThrowUserError('group_not_visible', {group => $group}); + } + + my $grouplist = Bugzilla::Group->flatten_group_membership($group->id); + $query .= ' AND ' . $dbh->sql_in('ugm.group_id', $grouplist); + + my $userids = $dbh->selectcol_arrayref($query); + my $user_objects = Bugzilla::User->new_from_list($userids); + my @users = map { { + id => $self->type('int', $_->id), + real_name => $self->type('string', $_->name), + name => $self->type('login', $_->login), + email => $self->type('email', $_->email), + can_login => $self->type('boolean', $_->is_enabled), + email_enabled => $self->type('boolean', $_->email_enabled), + login_denied_text => $self->type('string', $_->disabledtext), + } } @$user_objects; + + return \@users; } 1; diff --git a/Bugzilla/WebService/Product.pm b/Bugzilla/WebService/Product.pm index 66bd32808e..e7eb0bf8f5 100644 --- a/Bugzilla/WebService/Product.pm +++ b/Bugzilla/WebService/Product.pm @@ -17,39 +17,36 @@ use Bugzilla::User; use Bugzilla::Error; use Bugzilla::Constants; use Bugzilla::WebService::Constants; -use Bugzilla::WebService::Util qw(validate filter filter_wants translate params_to_objects); +use Bugzilla::WebService::Util + qw(validate filter filter_wants translate params_to_objects); use constant READ_ONLY => qw( - get - get_accessible_products - get_enterable_products - get_selectable_products + get + get_accessible_products + get_enterable_products + get_selectable_products ); use constant PUBLIC_METHODS => qw( - create - get - get_accessible_products - get_enterable_products - get_selectable_products - update + create + get + get_accessible_products + get_enterable_products + get_selectable_products + update ); -use constant MAPPED_FIELDS => { - has_unconfirmed => 'allows_unconfirmed', - is_open => 'is_active', -}; +use constant MAPPED_FIELDS => + {has_unconfirmed => 'allows_unconfirmed', is_open => 'is_active',}; use constant MAPPED_RETURNS => { - allows_unconfirmed => 'has_unconfirmed', - defaultmilestone => 'default_milestone', - isactive => 'is_open', + allows_unconfirmed => 'has_unconfirmed', + defaultmilestone => 'default_milestone', + isactive => 'is_open', }; -use constant FIELD_MAP => { - has_unconfirmed => 'allows_unconfirmed', - is_open => 'isactive', -}; +use constant FIELD_MAP => + {has_unconfirmed => 'allows_unconfirmed', is_open => 'isactive',}; ################################################## # Add aliases here for method name compatibility # @@ -57,300 +54,277 @@ use constant FIELD_MAP => { # Get the ids of the products the user can search sub get_selectable_products { - Bugzilla->switch_to_shadow_db(); - return {ids => [map {$_->id} @{Bugzilla->user->get_selectable_products}]}; + Bugzilla->switch_to_shadow_db(); + return {ids => [map { $_->id } @{Bugzilla->user->get_selectable_products}]}; } # Get the ids of the products the user can enter bugs against sub get_enterable_products { - Bugzilla->switch_to_shadow_db(); - return {ids => [map {$_->id} @{Bugzilla->user->get_enterable_products}]}; + Bugzilla->switch_to_shadow_db(); + return {ids => [map { $_->id } @{Bugzilla->user->get_enterable_products}]}; } # Get the union of the products the user can search and enter bugs against. sub get_accessible_products { - Bugzilla->switch_to_shadow_db(); - return {ids => [map {$_->id} @{Bugzilla->user->get_accessible_products}]}; + Bugzilla->switch_to_shadow_db(); + return {ids => [map { $_->id } @{Bugzilla->user->get_accessible_products}]}; } # Get a list of actual products, based on list of ids or names sub get { - my ($self, $params) = validate(@_, 'ids', 'names', 'type'); - my $user = Bugzilla->user; - - defined $params->{ids} || defined $params->{names} || defined $params->{type} - || ThrowCodeError("params_required", { function => "Product.get", - params => ['ids', 'names', 'type'] }); - Bugzilla->switch_to_shadow_db(); - - my $products = []; - if (defined $params->{type}) { - my %product_hash; - foreach my $type (@{ $params->{type} }) { - my $result = []; - if ($type eq 'accessible') { - $result = $user->get_accessible_products(); - } - elsif ($type eq 'enterable') { - $result = $user->get_enterable_products(); - } - elsif ($type eq 'selectable') { - $result = $user->get_selectable_products(); - } - else { - ThrowUserError('get_products_invalid_type', - { type => $type }); - } - map { $product_hash{$_->id} = $_ } @$result; - } - $products = [ values %product_hash ]; - } - else { - $products = $user->get_accessible_products; + my ($self, $params) = validate(@_, 'ids', 'names', 'type'); + my $user = Bugzilla->user; + + defined $params->{ids} + || defined $params->{names} + || defined $params->{type} + || ThrowCodeError("params_required", + {function => "Product.get", params => ['ids', 'names', 'type']}); + Bugzilla->switch_to_shadow_db(); + + my $products = []; + if (defined $params->{type}) { + my %product_hash; + foreach my $type (@{$params->{type}}) { + my $result = []; + if ($type eq 'accessible') { + $result = $user->get_accessible_products(); + } + elsif ($type eq 'enterable') { + $result = $user->get_enterable_products(); + } + elsif ($type eq 'selectable') { + $result = $user->get_selectable_products(); + } + else { + ThrowUserError('get_products_invalid_type', {type => $type}); + } + map { $product_hash{$_->id} = $_ } @$result; } + $products = [values %product_hash]; + } + else { + $products = $user->get_accessible_products; + } - my @requested_products; + my @requested_products; - if (defined $params->{ids}) { - # Create a hash with the ids the user wants - my %ids = map { $_ => 1 } @{$params->{ids}}; + if (defined $params->{ids}) { - # Return the intersection of this, by grepping the ids from $products. - push(@requested_products, - grep { $ids{$_->id} } @$products); - } + # Create a hash with the ids the user wants + my %ids = map { $_ => 1 } @{$params->{ids}}; - if (defined $params->{names}) { - # Create a hash with the names the user wants - my %names = map { lc($_) => 1 } @{$params->{names}}; - - # Return the intersection of this, by grepping the names - # from $products, union'ed with products found by ID to - # avoid duplicates - foreach my $product (grep { $names{lc $_->name} } - @$products) { - next if grep { $_->id == $product->id } - @requested_products; - push @requested_products, $product; - } - } + # Return the intersection of this, by grepping the ids from $products. + push(@requested_products, grep { $ids{$_->id} } @$products); + } + + if (defined $params->{names}) { - # If we just requested a specific type of products without - # specifying ids or names, then return the entire list. - if (!defined $params->{ids} && !defined $params->{names}) { - @requested_products = @$products; + # Create a hash with the names the user wants + my %names = map { lc($_) => 1 } @{$params->{names}}; + + # Return the intersection of this, by grepping the names + # from $products, union'ed with products found by ID to + # avoid duplicates + foreach my $product (grep { $names{lc $_->name} } @$products) { + next if grep { $_->id == $product->id } @requested_products; + push @requested_products, $product; } + } + + # If we just requested a specific type of products without + # specifying ids or names, then return the entire list. + if (!defined $params->{ids} && !defined $params->{names}) { + @requested_products = @$products; + } - # Now create a result entry for each. - my @products = map { $self->_product_to_hash($params, $_) } - @requested_products; - return { products => \@products }; + # Now create a result entry for each. + my @products = map { $self->_product_to_hash($params, $_) } @requested_products; + return {products => \@products}; } sub create { - my ($self, $params) = @_; - - Bugzilla->login(LOGIN_REQUIRED); - Bugzilla->user->in_group('editcomponents') - || ThrowUserError("auth_failure", { group => "editcomponents", - action => "add", - object => "products"}); - # Create product - my $args = { - name => $params->{name}, - description => $params->{description}, - version => $params->{version}, - defaultmilestone => $params->{default_milestone}, - # create_series has no default value. - create_series => defined $params->{create_series} ? - $params->{create_series} : 1 - }; - foreach my $field (qw(has_unconfirmed is_open classification)) { - if (defined $params->{$field}) { - my $name = FIELD_MAP->{$field} || $field; - $args->{$name} = $params->{$field}; - } + my ($self, $params) = @_; + + Bugzilla->login(LOGIN_REQUIRED); + Bugzilla->user->in_group('editcomponents') + || ThrowUserError("auth_failure", + {group => "editcomponents", action => "add", object => "products"}); + + # Create product + my $args = { + name => $params->{name}, + description => $params->{description}, + version => $params->{version}, + defaultmilestone => $params->{default_milestone}, + + # create_series has no default value. + create_series => defined $params->{create_series} + ? $params->{create_series} + : 1 + }; + foreach my $field (qw(has_unconfirmed is_open classification)) { + if (defined $params->{$field}) { + my $name = FIELD_MAP->{$field} || $field; + $args->{$name} = $params->{$field}; } - my $product = Bugzilla::Product->create($args); - return { id => $self->type('int', $product->id) }; + } + my $product = Bugzilla::Product->create($args); + return {id => $self->type('int', $product->id)}; } sub update { - my ($self, $params) = @_; - - my $dbh = Bugzilla->dbh; - - Bugzilla->login(LOGIN_REQUIRED); - Bugzilla->user->in_group('editcomponents') - || ThrowUserError("auth_failure", { group => "editcomponents", - action => "edit", - object => "products" }); - - defined($params->{names}) || defined($params->{ids}) - || ThrowCodeError('params_required', - { function => 'Product.update', params => ['ids', 'names'] }); - - my $product_objects = params_to_objects($params, 'Bugzilla::Product'); - - my $values = translate($params, MAPPED_FIELDS); - - # We delete names and ids to keep only new values to set. - delete $values->{names}; - delete $values->{ids}; - - $dbh->bz_start_transaction(); - foreach my $product (@$product_objects) { - $product->set_all($values); + my ($self, $params) = @_; + + my $dbh = Bugzilla->dbh; + + Bugzilla->login(LOGIN_REQUIRED); + Bugzilla->user->in_group('editcomponents') + || ThrowUserError("auth_failure", + {group => "editcomponents", action => "edit", object => "products"}); + + defined($params->{names}) + || defined($params->{ids}) + || ThrowCodeError('params_required', + {function => 'Product.update', params => ['ids', 'names']}); + + my $product_objects = params_to_objects($params, 'Bugzilla::Product'); + + my $values = translate($params, MAPPED_FIELDS); + + # We delete names and ids to keep only new values to set. + delete $values->{names}; + delete $values->{ids}; + + $dbh->bz_start_transaction(); + foreach my $product (@$product_objects) { + $product->set_all($values); + } + + my %changes; + foreach my $product (@$product_objects) { + my $returned_changes = $product->update(); + $changes{$product->id} = translate($returned_changes, MAPPED_RETURNS); + } + $dbh->bz_commit_transaction(); + + my @result; + foreach my $product (@$product_objects) { + my %hash = (id => $product->id, changes => {},); + + foreach my $field (keys %{$changes{$product->id}}) { + my $change = $changes{$product->id}->{$field}; + $hash{changes}{$field} = { + removed => $self->type('string', $change->[0]), + added => $self->type('string', $change->[1]) + }; } - my %changes; - foreach my $product (@$product_objects) { - my $returned_changes = $product->update(); - $changes{$product->id} = translate($returned_changes, MAPPED_RETURNS); - } - $dbh->bz_commit_transaction(); - - my @result; - foreach my $product (@$product_objects) { - my %hash = ( - id => $product->id, - changes => {}, - ); - - foreach my $field (keys %{ $changes{$product->id} }) { - my $change = $changes{$product->id}->{$field}; - $hash{changes}{$field} = { - removed => $self->type('string', $change->[0]), - added => $self->type('string', $change->[1]) - }; - } - - push(@result, \%hash); - } + push(@result, \%hash); + } - return { products => \@result }; + return {products => \@result}; } sub _product_to_hash { - my ($self, $params, $product) = @_; - - my $field_data = { - id => $self->type('int', $product->id), - name => $self->type('string', $product->name), - description => $self->type('string', $product->description), - is_active => $self->type('boolean', $product->is_active), - default_milestone => $self->type('string', $product->default_milestone), - has_unconfirmed => $self->type('boolean', $product->allows_unconfirmed), - classification => $self->type('string', $product->classification->name), - }; - if (filter_wants($params, 'components')) { - $field_data->{components} = [map { - $self->_component_to_hash($_, $params) - } @{$product->components}]; - } - if (filter_wants($params, 'versions')) { - $field_data->{versions} = [map { - $self->_version_to_hash($_, $params) - } @{$product->versions}]; - } - if (filter_wants($params, 'milestones')) { - $field_data->{milestones} = [map { - $self->_milestone_to_hash($_, $params) - } @{$product->milestones}]; - } - return filter($params, $field_data); + my ($self, $params, $product) = @_; + + my $field_data = { + id => $self->type('int', $product->id), + name => $self->type('string', $product->name), + description => $self->type('string', $product->description), + is_active => $self->type('boolean', $product->is_active), + default_milestone => $self->type('string', $product->default_milestone), + has_unconfirmed => $self->type('boolean', $product->allows_unconfirmed), + classification => $self->type('string', $product->classification->name), + }; + if (filter_wants($params, 'components')) { + $field_data->{components} + = [map { $self->_component_to_hash($_, $params) } @{$product->components}]; + } + if (filter_wants($params, 'versions')) { + $field_data->{versions} + = [map { $self->_version_to_hash($_, $params) } @{$product->versions}]; + } + if (filter_wants($params, 'milestones')) { + $field_data->{milestones} + = [map { $self->_milestone_to_hash($_, $params) } @{$product->milestones}]; + } + return filter($params, $field_data); } sub _component_to_hash { - my ($self, $component, $params) = @_; - my $field_data = filter $params, { - id => - $self->type('int', $component->id), - name => - $self->type('string', $component->name), - description => - $self->type('string' , $component->description), - default_assigned_to => - $self->type('login', $component->default_assignee->login), - default_qa_contact => - $self->type('login', $component->default_qa_contact ? - $component->default_qa_contact->login : ""), - sort_key => # sort_key is returned to match Bug.fields - 0, - is_active => - $self->type('boolean', $component->is_active), - }, undef, 'components'; - - if (filter_wants($params, 'flag_types', undef, 'components')) { - $field_data->{flag_types} = { - bug => - [map { - $self->_flag_type_to_hash($_) - } @{$component->flag_types->{'bug'}}], - attachment => - [map { - $self->_flag_type_to_hash($_) - } @{$component->flag_types->{'attachment'}}], - }; - } + my ($self, $component, $params) = @_; + my $field_data = filter $params, { + id => $self->type('int', $component->id), + name => $self->type('string', $component->name), + description => $self->type('string', $component->description), + default_assigned_to => + $self->type('login', $component->default_assignee->login), + default_qa_contact => $self->type( + 'login', + $component->default_qa_contact ? $component->default_qa_contact->login : "" + ), + sort_key => # sort_key is returned to match Bug.fields + 0, + is_active => $self->type('boolean', $component->is_active), + }, + undef, 'components'; + + if (filter_wants($params, 'flag_types', undef, 'components')) { + $field_data->{flag_types} = { + bug => + [map { $self->_flag_type_to_hash($_) } @{$component->flag_types->{'bug'}}], + attachment => [ + map { $self->_flag_type_to_hash($_) } @{$component->flag_types->{'attachment'}} + ], + }; + } - return $field_data; + return $field_data; } sub _flag_type_to_hash { - my ($self, $flag_type, $params) = @_; - return filter $params, { - id => - $self->type('int', $flag_type->id), - name => - $self->type('string', $flag_type->name), - description => - $self->type('string', $flag_type->description), - cc_list => - $self->type('string', $flag_type->cc_list), - sort_key => - $self->type('int', $flag_type->sortkey), - is_active => - $self->type('boolean', $flag_type->is_active), - is_requestable => - $self->type('boolean', $flag_type->is_requestable), - is_requesteeble => - $self->type('boolean', $flag_type->is_requesteeble), - is_multiplicable => - $self->type('boolean', $flag_type->is_multiplicable), - grant_group => - $self->type('int', $flag_type->grant_group_id), - request_group => - $self->type('int', $flag_type->request_group_id), - }, undef, 'flag_types'; + my ($self, $flag_type, $params) = @_; + return filter $params, + { + id => $self->type('int', $flag_type->id), + name => $self->type('string', $flag_type->name), + description => $self->type('string', $flag_type->description), + cc_list => $self->type('string', $flag_type->cc_list), + sort_key => $self->type('int', $flag_type->sortkey), + is_active => $self->type('boolean', $flag_type->is_active), + is_requestable => $self->type('boolean', $flag_type->is_requestable), + is_requesteeble => $self->type('boolean', $flag_type->is_requesteeble), + is_multiplicable => $self->type('boolean', $flag_type->is_multiplicable), + grant_group => $self->type('int', $flag_type->grant_group_id), + request_group => $self->type('int', $flag_type->request_group_id), + }, + undef, 'flag_types'; } sub _version_to_hash { - my ($self, $version, $params) = @_; - return filter $params, { - id => - $self->type('int', $version->id), - name => - $self->type('string', $version->name), - sort_key => # sort_key is returened to match Bug.fields - 0, - is_active => - $self->type('boolean', $version->is_active), - }, undef, 'versions'; + my ($self, $version, $params) = @_; + return filter $params, { + id => $self->type('int', $version->id), + name => $self->type('string', $version->name), + sort_key => # sort_key is returened to match Bug.fields + 0, + is_active => $self->type('boolean', $version->is_active), + }, + undef, 'versions'; } sub _milestone_to_hash { - my ($self, $milestone, $params) = @_; - return filter $params, { - id => - $self->type('int', $milestone->id), - name => - $self->type('string', $milestone->name), - sort_key => - $self->type('int', $milestone->sortkey), - is_active => - $self->type('boolean', $milestone->is_active), - }, undef, 'milestones'; + my ($self, $milestone, $params) = @_; + return filter $params, + { + id => $self->type('int', $milestone->id), + name => $self->type('string', $milestone->name), + sort_key => $self->type('int', $milestone->sortkey), + is_active => $self->type('boolean', $milestone->is_active), + }, + undef, 'milestones'; } 1; diff --git a/Bugzilla/WebService/Server.pm b/Bugzilla/WebService/Server.pm index da92b4d7fc..190c3d5695 100644 --- a/Bugzilla/WebService/Server.pm +++ b/Bugzilla/WebService/Server.pm @@ -20,72 +20,77 @@ use Digest::MD5 qw(md5_base64); use Storable qw(freeze); sub handle_login { - my ($self, $class, $method, $full_method) = @_; - # Throw error if the supplied class does not exist or the method is private - ThrowCodeError('unknown_method', {method => $full_method}) if (!$class or $method =~ /^_/); - - eval "require $class"; - ThrowCodeError('unknown_method', {method => $full_method}) if $@; - return if ($class->login_exempt($method) - and !defined Bugzilla->input_params->{Bugzilla_login}); - Bugzilla->login(); - - Bugzilla::Hook::process( - 'webservice_before_call', - { 'method' => $method, full_method => $full_method }); + my ($self, $class, $method, $full_method) = @_; + + # Throw error if the supplied class does not exist or the method is private + ThrowCodeError('unknown_method', {method => $full_method}) + if (!$class or $method =~ /^_/); + + eval "require $class"; + ThrowCodeError('unknown_method', {method => $full_method}) if $@; + return + if ($class->login_exempt($method) + and !defined Bugzilla->input_params->{Bugzilla_login}); + Bugzilla->login(); + + Bugzilla::Hook::process('webservice_before_call', + {'method' => $method, full_method => $full_method}); } sub datetime_format_inbound { - my ($self, $time) = @_; - - my $converted = datetime_from($time, Bugzilla->local_timezone); - if (!defined $converted) { - ThrowUserError('illegal_date', { date => $time }); - } - $time = $converted->ymd() . ' ' . $converted->hms(); - return $time + my ($self, $time) = @_; + + my $converted = datetime_from($time, Bugzilla->local_timezone); + if (!defined $converted) { + ThrowUserError('illegal_date', {date => $time}); + } + $time = $converted->ymd() . ' ' . $converted->hms(); + return $time; } sub datetime_format_outbound { - my ($self, $date) = @_; - - return undef if (!defined $date or $date eq ''); - - my $time = $date; - if (blessed($date)) { - # We expect this to mean we were sent a datetime object - $time->set_time_zone('UTC'); - } else { - # We always send our time in UTC, for consistency. - # passed in value is likely a string, create a datetime object - $time = datetime_from($date, 'UTC'); - } - return $time->iso8601(); + my ($self, $date) = @_; + + return undef if (!defined $date or $date eq ''); + + my $time = $date; + if (blessed($date)) { + + # We expect this to mean we were sent a datetime object + $time->set_time_zone('UTC'); + } + else { + # We always send our time in UTC, for consistency. + # passed in value is likely a string, create a datetime object + $time = datetime_from($date, 'UTC'); + } + return $time->iso8601(); } # ETag support sub bz_etag { - my ($self, $data) = @_; - my $cache = Bugzilla->request_cache; - if (defined $data) { - # Serialize the data if passed a reference - local $Storable::canonical = 1; - $data = freeze($data) if ref $data; - - # Wide characters cause md5_base64() to die. - utf8::encode($data) if utf8::is_utf8($data); - - # Append content_type to the end of the data - # string as we want the etag to be unique to - # the content_type. We do not need this for - # XMLRPC as text/xml is always returned. - if (blessed($self) && $self->can('content_type')) { - $data .= $self->content_type if $self->content_type; - } - - $cache->{'bz_etag'} = md5_base64($data); + my ($self, $data) = @_; + my $cache = Bugzilla->request_cache; + if (defined $data) { + + # Serialize the data if passed a reference + local $Storable::canonical = 1; + $data = freeze($data) if ref $data; + + # Wide characters cause md5_base64() to die. + utf8::encode($data) if utf8::is_utf8($data); + + # Append content_type to the end of the data + # string as we want the etag to be unique to + # the content_type. We do not need this for + # XMLRPC as text/xml is always returned. + if (blessed($self) && $self->can('content_type')) { + $data .= $self->content_type if $self->content_type; } - return $cache->{'bz_etag'}; + + $cache->{'bz_etag'} = md5_base64($data); + } + return $cache->{'bz_etag'}; } 1; diff --git a/Bugzilla/WebService/Server/JSONRPC.pm b/Bugzilla/WebService/Server/JSONRPC.pm index b488c8de5f..2aebc4cea3 100644 --- a/Bugzilla/WebService/Server/JSONRPC.pm +++ b/Bugzilla/WebService/Server/JSONRPC.pm @@ -12,16 +12,17 @@ use strict; use warnings; use Bugzilla::WebService::Server; -BEGIN { - our @ISA = qw(Bugzilla::WebService::Server); - if (eval { require JSON::RPC::Server::CGI }) { - unshift(@ISA, 'JSON::RPC::Server::CGI'); - } - else { - require JSON::RPC::Legacy::Server::CGI; - unshift(@ISA, 'JSON::RPC::Legacy::Server::CGI'); - } +BEGIN { + our @ISA = qw(Bugzilla::WebService::Server); + + if (eval { require JSON::RPC::Server::CGI }) { + unshift(@ISA, 'JSON::RPC::Server::CGI'); + } + else { + require JSON::RPC::Legacy::Server::CGI; + unshift(@ISA, 'JSON::RPC::Legacy::Server::CGI'); + } } use Bugzilla::Error; @@ -38,79 +39,83 @@ use List::MoreUtils qw(none); ##################################### sub new { - my $class = shift; - my $self = $class->SUPER::new(@_); - Bugzilla->_json_server($self); - $self->dispatch(WS_DISPATCH); - $self->return_die_message(1); - return $self; + my $class = shift; + my $self = $class->SUPER::new(@_); + Bugzilla->_json_server($self); + $self->dispatch(WS_DISPATCH); + $self->return_die_message(1); + return $self; } sub create_json_coder { - my $self = shift; - my $json = $self->SUPER::create_json_coder(@_); - $json->allow_blessed(1); - $json->convert_blessed(1); - # This may seem a little backwards, but what this really means is - # "don't convert our utf8 into byte strings, just leave it as a - # utf8 string." - $json->utf8(0); - return $json; + my $self = shift; + my $json = $self->SUPER::create_json_coder(@_); + $json->allow_blessed(1); + $json->convert_blessed(1); + + # This may seem a little backwards, but what this really means is + # "don't convert our utf8 into byte strings, just leave it as a + # utf8 string." + $json->utf8(0); + return $json; } # Override the JSON::RPC method to return our CGI object instead of theirs. sub cgi { return Bugzilla->cgi; } sub response_header { - my $self = shift; - # The HTTP body needs to be bytes (not a utf8 string) for recent - # versions of HTTP::Message, but JSON::RPC::Server doesn't handle this - # properly. $_[1] is the HTTP body content we're going to be sending. - if (utf8::is_utf8($_[1])) { - utf8::encode($_[1]); - # Since we're going to just be sending raw bytes, we need to - # set STDOUT to not expect utf8. - disable_utf8(); - } - return $self->SUPER::response_header(@_); + my $self = shift; + + # The HTTP body needs to be bytes (not a utf8 string) for recent + # versions of HTTP::Message, but JSON::RPC::Server doesn't handle this + # properly. $_[1] is the HTTP body content we're going to be sending. + if (utf8::is_utf8($_[1])) { + utf8::encode($_[1]); + + # Since we're going to just be sending raw bytes, we need to + # set STDOUT to not expect utf8. + disable_utf8(); + } + return $self->SUPER::response_header(@_); } sub response { - my ($self, $response) = @_; - my $cgi = $self->cgi; - - # Implement JSONP. - if (my $callback = $self->_bz_callback) { - my $content = $response->content; - # Prepend the JSONP response with /**/ in order to protect - # against possible encoding attacks (e.g., affecting Flash). - $response->content("/**/$callback($content)"); - } - - # Use $cgi->header properly instead of just printing text directly. - # This fixes various problems, including sending Bugzilla's cookies - # properly. - my $headers = $response->headers; - my @header_args; - foreach my $name ($headers->header_field_names) { - my @values = $headers->header($name); - $name =~ s/-/_/g; - foreach my $value (@values) { - push(@header_args, "-$name", $value); - } - } - - # ETag support - my $etag = $self->bz_etag; - if ($etag && $cgi->check_etag($etag)) { - push(@header_args, "-ETag", $etag); - print $cgi->header(-status => '304 Not Modified', @header_args); - } - else { - push(@header_args, "-ETag", $etag) if $etag; - print $cgi->header(-status => $response->code, @header_args); - print $response->content; - } + my ($self, $response) = @_; + my $cgi = $self->cgi; + + # Implement JSONP. + if (my $callback = $self->_bz_callback) { + my $content = $response->content; + + # Prepend the JSONP response with /**/ in order to protect + # against possible encoding attacks (e.g., affecting Flash). + $response->content("/**/$callback($content)"); + } + + # Use $cgi->header properly instead of just printing text directly. + # This fixes various problems, including sending Bugzilla's cookies + # properly. + my $headers = $response->headers; + my @header_args; + foreach my $name ($headers->header_field_names) { + my @values = $headers->header($name); + $name =~ s/-/_/g; + foreach my $value (@values) { + push(@header_args, "-$name", $value); + } + } + + # ETag support + my $etag = $self->bz_etag; + if ($etag && $cgi->check_etag($etag)) { + push(@header_args, "-ETag", $etag); + print $cgi->header(-status => '304 Not Modified', @header_args); + } + else { + push(@header_args, "-ETag", $etag) if $etag; + print $cgi->header(-status => $response->code, @header_args); + print $response->content; + } } # The JSON-RPC 1.1 GET specification is not so great--you can't specify @@ -122,70 +127,69 @@ sub response { # Base64 encoded, because that is ridiculous and obnoxious for JavaScript # clients. sub retrieve_json_from_get { - my $self = shift; - my $cgi = $self->cgi; - - my %input; - - # Both version and id must be set before any errors are thrown. - if ($cgi->param('version')) { - $self->version(scalar $cgi->param('version')); - $input{version} = $cgi->param('version'); - } - else { - $self->version('1.0'); - } - - # The JSON-RPC 2.0 spec says that any request that omits an id doesn't - # want a response. However, in an HTTP GET situation, it's stupid to - # expect all clients to specify some id parameter just to get a response, - # so we don't require it. - my $id; - if (defined $cgi->param('id')) { - $id = $cgi->param('id'); - } - # However, JSON::RPC does require that an id exist in most cases, in - # order to throw proper errors. We use the installation's urlbase as - # the id, in this case. - else { - $id = correct_urlbase(); - } - # Setting _bz_request_id here is required in case we throw errors early, - # before _handle. - $self->{_bz_request_id} = $input{id} = $id; - - # _bz_callback can throw an error, so we have to set it here, after we're - # ready to throw errors. - $self->_bz_callback(scalar $cgi->param('callback')); - - if (!$cgi->param('method')) { - ThrowUserError('json_rpc_get_method_required'); - } - $input{method} = $cgi->param('method'); - - my $params; - if (defined $cgi->param('params')) { - local $@; - $params = eval { - $self->json->decode(scalar $cgi->param('params')) - }; - if ($@) { - ThrowUserError('json_rpc_invalid_params', - { params => scalar $cgi->param('params'), - err_msg => $@ }); - } - } - elsif (!$self->version or $self->version ne '1.1') { - $params = []; - } - else { - $params = {}; - } - - $input{params} = $params; - - my $json = $self->json->encode(\%input); - return $json; + my $self = shift; + my $cgi = $self->cgi; + + my %input; + + # Both version and id must be set before any errors are thrown. + if ($cgi->param('version')) { + $self->version(scalar $cgi->param('version')); + $input{version} = $cgi->param('version'); + } + else { + $self->version('1.0'); + } + + # The JSON-RPC 2.0 spec says that any request that omits an id doesn't + # want a response. However, in an HTTP GET situation, it's stupid to + # expect all clients to specify some id parameter just to get a response, + # so we don't require it. + my $id; + if (defined $cgi->param('id')) { + $id = $cgi->param('id'); + } + + # However, JSON::RPC does require that an id exist in most cases, in + # order to throw proper errors. We use the installation's urlbase as + # the id, in this case. + else { + $id = correct_urlbase(); + } + + # Setting _bz_request_id here is required in case we throw errors early, + # before _handle. + $self->{_bz_request_id} = $input{id} = $id; + + # _bz_callback can throw an error, so we have to set it here, after we're + # ready to throw errors. + $self->_bz_callback(scalar $cgi->param('callback')); + + if (!$cgi->param('method')) { + ThrowUserError('json_rpc_get_method_required'); + } + $input{method} = $cgi->param('method'); + + my $params; + if (defined $cgi->param('params')) { + local $@; + $params = eval { $self->json->decode(scalar $cgi->param('params')) }; + if ($@) { + ThrowUserError('json_rpc_invalid_params', + {params => scalar $cgi->param('params'), err_msg => $@}); + } + } + elsif (!$self->version or $self->version ne '1.1') { + $params = []; + } + else { + $params = {}; + } + + $input{params} = $params; + + my $json = $self->json->encode(\%input); + return $json; } ####################################### @@ -193,75 +197,82 @@ sub retrieve_json_from_get { ####################################### sub type { - my ($self, $type, $value) = @_; - - # This is the only type that does something special with undef. - if ($type eq 'boolean') { - return $value ? JSON::true : JSON::false; - } - - return JSON::null if !defined $value; - - my $retval = $value; - - if ($type eq 'int') { - $retval = int($value); - } - if ($type eq 'double') { - $retval = 0.0 + $value; - } - elsif ($type eq 'string') { - # Forces string context, so that JSON will make it a string. - $retval = "$value"; - } - elsif ($type eq 'dateTime') { - # ISO-8601 "YYYYMMDDTHH:MM:SS" with a literal T - $retval = $self->datetime_format_outbound($value); - } - elsif ($type eq 'base64') { - utf8::encode($value) if utf8::is_utf8($value); - $retval = encode_base64($value, ''); - } - elsif ($type eq 'login') { - $retval = Bugzilla->params->{'use_email_as_login'} ? email_filter($retval) : "$retval"; - } - elsif ($type eq 'email') { - $retval = Bugzilla->user->in_group('editusers') ? "$retval" : ''; - } - - return $retval; + my ($self, $type, $value) = @_; + + # This is the only type that does something special with undef. + if ($type eq 'boolean') { + return $value ? JSON::true : JSON::false; + } + + return JSON::null if !defined $value; + + my $retval = $value; + + if ($type eq 'int') { + $retval = int($value); + } + if ($type eq 'double') { + $retval = 0.0 + $value; + } + elsif ($type eq 'string') { + + # Forces string context, so that JSON will make it a string. + $retval = "$value"; + } + elsif ($type eq 'dateTime') { + + # ISO-8601 "YYYYMMDDTHH:MM:SS" with a literal T + $retval = $self->datetime_format_outbound($value); + } + elsif ($type eq 'base64') { + utf8::encode($value) if utf8::is_utf8($value); + $retval = encode_base64($value, ''); + } + elsif ($type eq 'login') { + $retval + = Bugzilla->params->{'use_email_as_login'} + ? email_filter($retval) + : "$retval"; + } + elsif ($type eq 'email') { + $retval = Bugzilla->user->in_group('editusers') ? "$retval" : ''; + } + + return $retval; } sub datetime_format_outbound { - my $self = shift; - # YUI expects ISO8601 in UTC time; including TZ specifier - return $self->SUPER::datetime_format_outbound(@_) . 'Z'; + my $self = shift; + + # YUI expects ISO8601 in UTC time; including TZ specifier + return $self->SUPER::datetime_format_outbound(@_) . 'Z'; } sub handle_login { - my $self = shift; - - # If we're being called using GET, we don't allow cookie-based or Env - # login, because GET requests can be done cross-domain, and we don't - # want private data showing up on another site unless the user - # explicitly gives that site their username and password. (This is - # particularly important for JSONP, which would allow a remote site - # to use private data without the user's knowledge, unless we had this - # protection in place.) - if ($self->request->method ne 'POST') { - # XXX There's no particularly good way for us to get a parameter - # to Bugzilla->login at this point, so we pass this information - # around using request_cache, which is a bit of a hack. The - # implementation of it is in Bugzilla::Auth::Login::Stack. - Bugzilla->request_cache->{auth_no_automatic_login} = 1; - } - - my $path = $self->path_info; - my $class = $self->{dispatch_path}->{$path}; - my $full_method = $self->_bz_method_name; - $full_method =~ /^\S+\.(\S+)/; - my $method = $1; - $self->SUPER::handle_login($class, $method, $full_method); + my $self = shift; + + # If we're being called using GET, we don't allow cookie-based or Env + # login, because GET requests can be done cross-domain, and we don't + # want private data showing up on another site unless the user + # explicitly gives that site their username and password. (This is + # particularly important for JSONP, which would allow a remote site + # to use private data without the user's knowledge, unless we had this + # protection in place.) + if ($self->request->method ne 'POST') { + + # XXX There's no particularly good way for us to get a parameter + # to Bugzilla->login at this point, so we pass this information + # around using request_cache, which is a bit of a hack. The + # implementation of it is in Bugzilla::Auth::Login::Stack. + Bugzilla->request_cache->{auth_no_automatic_login} = 1; + } + + my $path = $self->path_info; + my $class = $self->{dispatch_path}->{$path}; + my $full_method = $self->_bz_method_name; + $full_method =~ /^\S+\.(\S+)/; + my $method = $1; + $self->SUPER::handle_login($class, $method, $full_method); } ###################################### @@ -270,171 +281,171 @@ sub handle_login { # Store the ID of the current call, because Bugzilla::Error will need it. sub _handle { - my $self = shift; - my ($obj) = @_; - $self->{_bz_request_id} = $obj->{id}; + my $self = shift; + my ($obj) = @_; + $self->{_bz_request_id} = $obj->{id}; - my $result = $self->SUPER::_handle(@_); + my $result = $self->SUPER::_handle(@_); - # Reset in_eval to so we get normal exceptions from here - Bugzilla->request_cache->{in_eval} = 0; + # Reset in_eval to so we get normal exceptions from here + Bugzilla->request_cache->{in_eval} = 0; - # Set the ETag if not already set in the webservice methods. - my $etag = $self->bz_etag; - if (!$etag && ref $result) { - my $data = $self->json->decode($result)->{'result'}; - $self->bz_etag($data); - } + # Set the ETag if not already set in the webservice methods. + my $etag = $self->bz_etag; + if (!$etag && ref $result) { + my $data = $self->json->decode($result)->{'result'}; + $self->bz_etag($data); + } - return $result; + return $result; } # Make all error messages returned by JSON::RPC go into the 100000 # range, and bring down all our errors into the normal range. sub _error { - my ($self, $id, $code) = (shift, shift, shift); - # All JSON::RPC errors are less than 1000. - if ($code < 1000) { - $code += 100000; - } - # Bugzilla::Error adds 100,000 to all *our* errors, so - # we know they came from us. - elsif ($code > 100000) { - $code -= 100000; - } - - # We can't just set $_[1] because it's not always settable, - # in JSON::RPC::Server. - unshift(@_, $id, $code); - my $json = $self->SUPER::_error(@_); - - # We want to always send the JSON-RPC 1.1 error format, although - # If we're not in JSON-RPC 1.1, we don't need the silly "name" parameter. - if (!$self->version or $self->version ne '1.1') { - my $object = $self->json->decode($json); - my $message = $object->{error}; - # Just assure that future versions of JSON::RPC don't change the - # JSON-RPC 1.0 error format. - if (!ref $message) { - $object->{error} = { - code => $code, - message => $message, - }; - $json = $self->json->encode($object); - } - } - return $json; + my ($self, $id, $code) = (shift, shift, shift); + + # All JSON::RPC errors are less than 1000. + if ($code < 1000) { + $code += 100000; + } + + # Bugzilla::Error adds 100,000 to all *our* errors, so + # we know they came from us. + elsif ($code > 100000) { + $code -= 100000; + } + + # We can't just set $_[1] because it's not always settable, + # in JSON::RPC::Server. + unshift(@_, $id, $code); + my $json = $self->SUPER::_error(@_); + + # We want to always send the JSON-RPC 1.1 error format, although + # If we're not in JSON-RPC 1.1, we don't need the silly "name" parameter. + if (!$self->version or $self->version ne '1.1') { + my $object = $self->json->decode($json); + my $message = $object->{error}; + + # Just assure that future versions of JSON::RPC don't change the + # JSON-RPC 1.0 error format. + if (!ref $message) { + $object->{error} = {code => $code, message => $message,}; + $json = $self->json->encode($object); + } + } + return $json; } # This handles dispatching our calls to the appropriate class based on # the name of the method. sub _find_procedure { - my $self = shift; + my $self = shift; - my $method = shift; - $self->{_bz_method_name} = $method; + my $method = shift; + $self->{_bz_method_name} = $method; - # This tricks SUPER::_find_procedure into finding the right class. - $method =~ /^(\S+)\.(\S+)$/; - $self->path_info($1); - unshift(@_, $2); + # This tricks SUPER::_find_procedure into finding the right class. + $method =~ /^(\S+)\.(\S+)$/; + $self->path_info($1); + unshift(@_, $2); - return $self->SUPER::_find_procedure(@_); + return $self->SUPER::_find_procedure(@_); } # This is a hacky way to do something right before methods are called. # This is the last thing that JSON::RPC::Server::_handle calls right before # the method is actually called. sub _argument_type_check { - my $self = shift; - my $params = $self->SUPER::_argument_type_check(@_); - - # JSON-RPC 1.0 requires all parameters to be passed as an array, so - # we just pull out the first item and assume it's an object. - my $params_is_array; - if (ref $params eq 'ARRAY') { - $params = $params->[0]; - $params_is_array = 1; - } - - taint_data($params); - - # Now, convert dateTime fields on input. - $self->_bz_method_name =~ /^(\S+)\.(\S+)$/; - my ($class, $method) = ($1, $2); - my $pkg = $self->{dispatch_path}->{$class}; - my @date_fields = @{ $pkg->DATE_FIELDS->{$method} || [] }; - foreach my $field (@date_fields) { - if (defined $params->{$field}) { - my $value = $params->{$field}; - if (ref $value eq 'ARRAY') { - $params->{$field} = - [ map { $self->datetime_format_inbound($_) } @$value ]; - } - else { - $params->{$field} = $self->datetime_format_inbound($value); - } - } - } - my @base64_fields = @{ $pkg->BASE64_FIELDS->{$method} || [] }; - foreach my $field (@base64_fields) { - if (defined $params->{$field}) { - $params->{$field} = decode_base64($params->{$field}); - } - } - - # Update the params to allow for several convenience key/values - # use for authentication - fix_credentials($params, $self->cgi); - - Bugzilla->input_params($params); - - if ($self->request->method eq 'POST') { - # CSRF is possible via XMLHttpRequest when the Content-Type header - # is not application/json (for example: text/plain or - # application/x-www-form-urlencoded). - # application/json is the single official MIME type, per RFC 4627. - my $content_type = $self->cgi->content_type; - # The charset can be appended to the content type, so we use a regexp. - if ($content_type !~ m{^application/json(-rpc)?(;.*)?$}i) { - ThrowUserError('json_rpc_illegal_content_type', - { content_type => $content_type }); - } - } - else { - # When being called using GET, we don't allow calling - # methods that can change data. This protects us against cross-site - # request forgeries. - if (!grep($_ eq $method, $pkg->READ_ONLY)) { - ThrowUserError('json_rpc_post_only', - { method => $self->_bz_method_name }); - } - } - - # Only allowed methods to be used from our whitelist - if (none { $_ eq $method} $pkg->PUBLIC_METHODS) { - ThrowCodeError('unknown_method', { method => $self->_bz_method_name }); - } - - # This is the best time to do login checks. - $self->handle_login(); - - # Bugzilla::WebService packages call internal methods like - # $self->_some_private_method. So we have to inherit from - # that class as well as this Server class. - my $new_class = ref($self) . '::' . $pkg; - my $isa_string = 'our @ISA = qw(' . ref($self) . " $pkg)"; - eval "package $new_class;$isa_string;"; - bless $self, $new_class; - - if ($params_is_array) { - $params = [$params]; - } - - # Let Bugzilla::Error know we are inside an eval() after this point - Bugzilla->request_cache->{in_eval} = 1; - - return $params; + my $self = shift; + my $params = $self->SUPER::_argument_type_check(@_); + + # JSON-RPC 1.0 requires all parameters to be passed as an array, so + # we just pull out the first item and assume it's an object. + my $params_is_array; + if (ref $params eq 'ARRAY') { + $params = $params->[0]; + $params_is_array = 1; + } + + taint_data($params); + + # Now, convert dateTime fields on input. + $self->_bz_method_name =~ /^(\S+)\.(\S+)$/; + my ($class, $method) = ($1, $2); + my $pkg = $self->{dispatch_path}->{$class}; + my @date_fields = @{$pkg->DATE_FIELDS->{$method} || []}; + foreach my $field (@date_fields) { + if (defined $params->{$field}) { + my $value = $params->{$field}; + if (ref $value eq 'ARRAY') { + $params->{$field} = [map { $self->datetime_format_inbound($_) } @$value]; + } + else { + $params->{$field} = $self->datetime_format_inbound($value); + } + } + } + my @base64_fields = @{$pkg->BASE64_FIELDS->{$method} || []}; + foreach my $field (@base64_fields) { + if (defined $params->{$field}) { + $params->{$field} = decode_base64($params->{$field}); + } + } + + # Update the params to allow for several convenience key/values + # use for authentication + fix_credentials($params, $self->cgi); + + Bugzilla->input_params($params); + + if ($self->request->method eq 'POST') { + + # CSRF is possible via XMLHttpRequest when the Content-Type header + # is not application/json (for example: text/plain or + # application/x-www-form-urlencoded). + # application/json is the single official MIME type, per RFC 4627. + my $content_type = $self->cgi->content_type; + + # The charset can be appended to the content type, so we use a regexp. + if ($content_type !~ m{^application/json(-rpc)?(;.*)?$}i) { + ThrowUserError('json_rpc_illegal_content_type', + {content_type => $content_type}); + } + } + else { + # When being called using GET, we don't allow calling + # methods that can change data. This protects us against cross-site + # request forgeries. + if (!grep($_ eq $method, $pkg->READ_ONLY)) { + ThrowUserError('json_rpc_post_only', {method => $self->_bz_method_name}); + } + } + + # Only allowed methods to be used from our whitelist + if (none { $_ eq $method } $pkg->PUBLIC_METHODS) { + ThrowCodeError('unknown_method', {method => $self->_bz_method_name}); + } + + # This is the best time to do login checks. + $self->handle_login(); + + # Bugzilla::WebService packages call internal methods like + # $self->_some_private_method. So we have to inherit from + # that class as well as this Server class. + my $new_class = ref($self) . '::' . $pkg; + my $isa_string = 'our @ISA = qw(' . ref($self) . " $pkg)"; + eval "package $new_class;$isa_string;"; + bless $self, $new_class; + + if ($params_is_array) { + $params = [$params]; + } + + # Let Bugzilla::Error know we are inside an eval() after this point + Bugzilla->request_cache->{in_eval} = 1; + + return $params; } ########################## @@ -443,22 +454,24 @@ sub _argument_type_check { # _bz_method_name is stored by _find_procedure for later use. sub _bz_method_name { - return $_[0]->{_bz_method_name}; + return $_[0]->{_bz_method_name}; } sub _bz_callback { - my ($self, $value) = @_; - if (defined $value) { - $value = trim($value); - # We don't use \w because we don't want to allow Unicode here. - if ($value !~ /^[A-Za-z0-9_\.\[\]]+$/) { - ThrowUserError('json_rpc_invalid_callback', { callback => $value }); - } - $self->{_bz_callback} = $value; - # JSONP needs to be parsed by a JS parser, not by a JSON parser. - $self->content_type('text/javascript'); + my ($self, $value) = @_; + if (defined $value) { + $value = trim($value); + + # We don't use \w because we don't want to allow Unicode here. + if ($value !~ /^[A-Za-z0-9_\.\[\]]+$/) { + ThrowUserError('json_rpc_invalid_callback', {callback => $value}); } - return $self->{_bz_callback}; + $self->{_bz_callback} = $value; + + # JSONP needs to be parsed by a JS parser, not by a JSON parser. + $self->content_type('text/javascript'); + } + return $self->{_bz_callback}; } 1; diff --git a/Bugzilla/WebService/Server/XMLRPC.pm b/Bugzilla/WebService/Server/XMLRPC.pm index c6461abd68..00dadbadf0 100644 --- a/Bugzilla/WebService/Server/XMLRPC.pm +++ b/Bugzilla/WebService/Server/XMLRPC.pm @@ -14,9 +14,10 @@ use warnings; use XMLRPC::Transport::HTTP; use Bugzilla::WebService::Server; if ($ENV{MOD_PERL}) { - our @ISA = qw(XMLRPC::Transport::HTTP::Apache Bugzilla::WebService::Server); -} else { - our @ISA = qw(XMLRPC::Transport::HTTP::CGI Bugzilla::WebService::Server); + our @ISA = qw(XMLRPC::Transport::HTTP::Apache Bugzilla::WebService::Server); +} +else { + our @ISA = qw(XMLRPC::Transport::HTTP::CGI Bugzilla::WebService::Server); } use Bugzilla::WebService::Constants; @@ -26,102 +27,105 @@ use Bugzilla::Util; use List::MoreUtils qw(none); BEGIN { - # Allow WebService methods to call XMLRPC::Lite's type method directly - *Bugzilla::WebService::type = sub { - my ($self, $type, $value) = @_; - if ($type eq 'dateTime') { - # This is the XML-RPC implementation, see the README in Bugzilla/WebService/. - # Our "base" implementation is in Bugzilla::WebService::Server. - $value = Bugzilla::WebService::Server->datetime_format_outbound($value); - $value =~ s/-//g; - } - elsif ($type eq 'login') { - $type = 'string'; - $value = email_filter($value) if Bugzilla->params->{'use_email_as_login'}; - } - elsif ($type eq 'email') { - $type = 'string'; - $value = '' unless Bugzilla->user->in_group('editusers'); - } - return XMLRPC::Data->type($type)->value($value); - }; - - # Add support for ETags into XMLRPC WebServices - *Bugzilla::WebService::bz_etag = sub { - return Bugzilla::WebService::Server->bz_etag($_[1]); - }; + # Allow WebService methods to call XMLRPC::Lite's type method directly + *Bugzilla::WebService::type = sub { + my ($self, $type, $value) = @_; + if ($type eq 'dateTime') { + + # This is the XML-RPC implementation, see the README in Bugzilla/WebService/. + # Our "base" implementation is in Bugzilla::WebService::Server. + $value = Bugzilla::WebService::Server->datetime_format_outbound($value); + $value =~ s/-//g; + } + elsif ($type eq 'login') { + $type = 'string'; + $value = email_filter($value) if Bugzilla->params->{'use_email_as_login'}; + } + elsif ($type eq 'email') { + $type = 'string'; + $value = '' unless Bugzilla->user->in_group('editusers'); + } + return XMLRPC::Data->type($type)->value($value); + }; + + # Add support for ETags into XMLRPC WebServices + *Bugzilla::WebService::bz_etag = sub { + return Bugzilla::WebService::Server->bz_etag($_[1]); + }; } sub initialize { - my $self = shift; - my %retval = $self->SUPER::initialize(@_); - $retval{'serializer'} = Bugzilla::XMLRPC::Serializer->new; - $retval{'deserializer'} = Bugzilla::XMLRPC::Deserializer->new; - $retval{'dispatch_with'} = WS_DISPATCH; - return %retval; + my $self = shift; + my %retval = $self->SUPER::initialize(@_); + $retval{'serializer'} = Bugzilla::XMLRPC::Serializer->new; + $retval{'deserializer'} = Bugzilla::XMLRPC::Deserializer->new; + $retval{'dispatch_with'} = WS_DISPATCH; + return %retval; } sub make_response { - my $self = shift; - my $cgi = Bugzilla->cgi; - - # Fix various problems with IIS. - if ($ENV{'SERVER_SOFTWARE'} =~ /IIS/) { - $ENV{CONTENT_LENGTH} = 0; - binmode(STDOUT, ':bytes'); - } - - $self->SUPER::make_response(@_); - - # XMLRPC::Transport::HTTP::CGI doesn't know about Bugzilla carrying around - # its cookies in Bugzilla::CGI, so we need to copy them over. - foreach my $cookie (@{$cgi->{'Bugzilla_cookie_list'}}) { - $self->response->headers->push_header('Set-Cookie', $cookie); - } - - # Copy across security related headers from Bugzilla::CGI - foreach my $header (split(/[\r\n]+/, $cgi->header)) { - my ($name, $value) = $header =~ /^([^:]+): (.*)/; - if (!$self->response->headers->header($name)) { - $self->response->headers->header($name => $value); - } - } - - # ETag support - my $etag = $self->bz_etag; - if (!$etag) { - my $data = $self->response->as_string; - $etag = $self->bz_etag($data); - } - - if ($etag && $cgi->check_etag($etag)) { - $self->response->headers->push_header('ETag', $etag); - $self->response->headers->push_header('status', '304 Not Modified'); - } - elsif ($etag) { - $self->response->headers->push_header('ETag', $etag); + my $self = shift; + my $cgi = Bugzilla->cgi; + + # Fix various problems with IIS. + if ($ENV{'SERVER_SOFTWARE'} =~ /IIS/) { + $ENV{CONTENT_LENGTH} = 0; + binmode(STDOUT, ':bytes'); + } + + $self->SUPER::make_response(@_); + + # XMLRPC::Transport::HTTP::CGI doesn't know about Bugzilla carrying around + # its cookies in Bugzilla::CGI, so we need to copy them over. + foreach my $cookie (@{$cgi->{'Bugzilla_cookie_list'}}) { + $self->response->headers->push_header('Set-Cookie', $cookie); + } + + # Copy across security related headers from Bugzilla::CGI + foreach my $header (split(/[\r\n]+/, $cgi->header)) { + my ($name, $value) = $header =~ /^([^:]+): (.*)/; + if (!$self->response->headers->header($name)) { + $self->response->headers->header($name => $value); } + } + + # ETag support + my $etag = $self->bz_etag; + if (!$etag) { + my $data = $self->response->as_string; + $etag = $self->bz_etag($data); + } + + if ($etag && $cgi->check_etag($etag)) { + $self->response->headers->push_header('ETag', $etag); + $self->response->headers->push_header('status', '304 Not Modified'); + } + elsif ($etag) { + $self->response->headers->push_header('ETag', $etag); + } } sub handle_login { - my ($self, $classes, $action, $uri, $method) = @_; - my $class = $classes->{$uri}; - if (!$class) { - ThrowCodeError('unknown_method', { method => $method eq 'methodName' ? '' : '.' . $method }); - } - my $full_method = $uri . "." . $method; - # Only allowed methods to be used from the module's whitelist - my $file = $class; - $file =~ s{::}{/}g; - $file .= ".pm"; - require $file; - if (none { $_ eq $method } $class->PUBLIC_METHODS) { - ThrowCodeError('unknown_method', { method => $full_method }); - } - - $ENV{CONTENT_LENGTH} = 0 if $ENV{'SERVER_SOFTWARE'} =~ /IIS/; - $self->SUPER::handle_login($class, $method, $full_method); - return; + my ($self, $classes, $action, $uri, $method) = @_; + my $class = $classes->{$uri}; + if (!$class) { + ThrowCodeError('unknown_method', + {method => $method eq 'methodName' ? '' : '.' . $method}); + } + my $full_method = $uri . "." . $method; + + # Only allowed methods to be used from the module's whitelist + my $file = $class; + $file =~ s{::}{/}g; + $file .= ".pm"; + require $file; + if (none { $_ eq $method } $class->PUBLIC_METHODS) { + ThrowCodeError('unknown_method', {method => $full_method}); + } + + $ENV{CONTENT_LENGTH} = 0 if $ENV{'SERVER_SOFTWARE'} =~ /IIS/; + $self->SUPER::handle_login($class, $method, $full_method); + return; } 1; @@ -145,100 +149,111 @@ use Bugzilla::WebService::Util qw(fix_credentials); use Scalar::Util qw(tainted); sub new { - my $self = shift->SUPER::new(@_); - # Initialise XML::Parser to not expand references to entities, to prevent DoS - require XML::Parser; - my $parser = XML::Parser->new( NoExpand => 1, Handlers => { Default => sub {} } ); - $self->{_parser}->parser($parser, $parser); - return $self; + my $self = shift->SUPER::new(@_); + + # Initialise XML::Parser to not expand references to entities, to prevent DoS + require XML::Parser; + my $parser = XML::Parser->new( + NoExpand => 1, + Handlers => { + Default => sub { } + } + ); + $self->{_parser}->parser($parser, $parser); + return $self; } sub deserialize { - my $self = shift; - - # Only allow certain content types to protect against CSRF attacks - my $content_type = lc($ENV{'CONTENT_TYPE'}); - # Remove charset, etc, if provided - $content_type =~ s/^([^;]+);.*/$1/; - if (!grep($_ eq $content_type, XMLRPC_CONTENT_TYPE_WHITELIST)) { - ThrowUserError('xmlrpc_illegal_content_type', - { content_type => $ENV{'CONTENT_TYPE'} }); - } + my $self = shift; - my ($xml) = @_; - my $som = $self->SUPER::deserialize(@_); - if (tainted($xml)) { - $som->{_bz_do_taint} = 1; - } - bless $som, 'Bugzilla::XMLRPC::SOM'; - my $params = $som->paramsin; - # This allows positional parameters for Testopia. - $params = {} if ref $params ne 'HASH'; + # Only allow certain content types to protect against CSRF attacks + my $content_type = lc($ENV{'CONTENT_TYPE'}); + + # Remove charset, etc, if provided + $content_type =~ s/^([^;]+);.*/$1/; + if (!grep($_ eq $content_type, XMLRPC_CONTENT_TYPE_WHITELIST)) { + ThrowUserError('xmlrpc_illegal_content_type', + {content_type => $ENV{'CONTENT_TYPE'}}); + } - # Update the params to allow for several convenience key/values - # use for authentication - fix_credentials($params); + my ($xml) = @_; + my $som = $self->SUPER::deserialize(@_); + if (tainted($xml)) { + $som->{_bz_do_taint} = 1; + } + bless $som, 'Bugzilla::XMLRPC::SOM'; + my $params = $som->paramsin; - Bugzilla->input_params($params); + # This allows positional parameters for Testopia. + $params = {} if ref $params ne 'HASH'; - return $som; + # Update the params to allow for several convenience key/values + # use for authentication + fix_credentials($params); + + Bugzilla->input_params($params); + + return $som; } # Some method arguments need to be converted in some way, when they are input. sub decode_value { - my $self = shift; - my ($type) = @{ $_[0] }; - my $value = $self->SUPER::decode_value(@_); - - # We only validate/convert certain types here. - return $value if $type !~ /^(?:int|i4|boolean|double|dateTime\.iso8601)$/; - - # Though the XML-RPC standard doesn't allow an empty, - # ,or , we do, and we just say - # "that's undef". - if (grep($type eq $_, qw(int double dateTime))) { - return undef if $value eq ''; - } - - my $validator = $self->_validation_subs->{$type}; - if (!$validator->($value)) { - ThrowUserError('xmlrpc_invalid_value', - { type => $type, value => $value }); - } - - # We convert dateTimes to a DB-friendly date format. - if ($type eq 'dateTime.iso8601') { - if ($value !~ /T.*[\-+Z]/i) { - # The caller did not specify a timezone, so we assume UTC. - # pass 'Z' specifier to datetime_from to force it - $value = $value . 'Z'; - } - $value = Bugzilla::WebService::Server::XMLRPC->datetime_format_inbound($value); + my $self = shift; + my ($type) = @{$_[0]}; + my $value = $self->SUPER::decode_value(@_); + + # We only validate/convert certain types here. + return $value if $type !~ /^(?:int|i4|boolean|double|dateTime\.iso8601)$/; + + # Though the XML-RPC standard doesn't allow an empty , + # ,or , we do, and we just say + # "that's undef". + if (grep($type eq $_, qw(int double dateTime))) { + return undef if $value eq ''; + } + + my $validator = $self->_validation_subs->{$type}; + if (!$validator->($value)) { + ThrowUserError('xmlrpc_invalid_value', {type => $type, value => $value}); + } + + # We convert dateTimes to a DB-friendly date format. + if ($type eq 'dateTime.iso8601') { + if ($value !~ /T.*[\-+Z]/i) { + + # The caller did not specify a timezone, so we assume UTC. + # pass 'Z' specifier to datetime_from to force it + $value = $value . 'Z'; } + $value = Bugzilla::WebService::Server::XMLRPC->datetime_format_inbound($value); + } - return $value; + return $value; } sub _validation_subs { - my $self = shift; - return $self->{_validation_subs} if $self->{_validation_subs}; - # The only place that XMLRPC::Lite stores any sort of validation - # regex is in XMLRPC::Serializer. We want to re-use those regexes here. - my $lookup = Bugzilla::XMLRPC::Serializer->new->typelookup; - - # $lookup is a hash whose values are arrayrefs, and whose keys are the - # names of types. The second item of each arrayref is a subroutine - # that will do our validation for us. - my %validators = map { $_ => $lookup->{$_}->[1] } (keys %$lookup); - # Add a boolean validator - $validators{'boolean'} = sub {$_[0] =~ /^[01]$/}; - # Some types have multiple names, or have a different name in - # XMLRPC::Serializer than their standard XML-RPC name. - $validators{'dateTime.iso8601'} = $validators{'dateTime'}; - $validators{'i4'} = $validators{'int'}; - - $self->{_validation_subs} = \%validators; - return \%validators; + my $self = shift; + return $self->{_validation_subs} if $self->{_validation_subs}; + + # The only place that XMLRPC::Lite stores any sort of validation + # regex is in XMLRPC::Serializer. We want to re-use those regexes here. + my $lookup = Bugzilla::XMLRPC::Serializer->new->typelookup; + + # $lookup is a hash whose values are arrayrefs, and whose keys are the + # names of types. The second item of each arrayref is a subroutine + # that will do our validation for us. + my %validators = map { $_ => $lookup->{$_}->[1] } (keys %$lookup); + + # Add a boolean validator + $validators{'boolean'} = sub { $_[0] =~ /^[01]$/ }; + + # Some types have multiple names, or have a different name in + # XMLRPC::Serializer than their standard XML-RPC name. + $validators{'dateTime.iso8601'} = $validators{'dateTime'}; + $validators{'i4'} = $validators{'int'}; + + $self->{_validation_subs} = \%validators; + return \%validators; } 1; @@ -254,16 +269,16 @@ our @ISA = qw(XMLRPC::SOM); use Bugzilla::WebService::Util qw(taint_data); sub paramsin { - my $self = shift; - if (!$self->{bz_params_in}) { - my @params = $self->SUPER::paramsin(@_); - if ($self->{_bz_do_taint}) { - taint_data(@params); - } - $self->{bz_params_in} = \@params; + my $self = shift; + if (!$self->{bz_params_in}) { + my @params = $self->SUPER::paramsin(@_); + if ($self->{_bz_do_taint}) { + taint_data(@params); } - my $params = $self->{bz_params_in}; - return wantarray ? @$params : $params->[0]; + $self->{bz_params_in} = \@params; + } + my $params = $self->{bz_params_in}; + return wantarray ? @$params : $params->[0]; } 1; @@ -277,43 +292,46 @@ use strict; use warnings; use Scalar::Util qw(blessed reftype); + # We can't use "use parent" because XMLRPC::Serializer doesn't return # a true value. use XMLRPC::Lite; our @ISA = qw(XMLRPC::Serializer); sub new { - my $class = shift; - my $self = $class->SUPER::new(@_); - # This fixes UTF-8. - $self->{'_typelookup'}->{'base64'} = - [10, sub { !utf8::is_utf8($_[0]) && $_[0] =~ /[^\x09\x0a\x0d\x20-\x7f]/}, - 'as_base64']; - # This makes arrays work right even though we're a subclass. - # (See http://rt.cpan.org//Ticket/Display.html?id=34514) - $self->{'_encodingStyle'} = ''; - return $self; + my $class = shift; + my $self = $class->SUPER::new(@_); + + # This fixes UTF-8. + $self->{'_typelookup'}->{'base64'} = [ + 10, sub { !utf8::is_utf8($_[0]) && $_[0] =~ /[^\x09\x0a\x0d\x20-\x7f]/ }, + 'as_base64' + ]; + + # This makes arrays work right even though we're a subclass. + # (See http://rt.cpan.org//Ticket/Display.html?id=34514) + $self->{'_encodingStyle'} = ''; + return $self; } # Here the XMLRPC::Serializer is extended to use the XMLRPC nil extension. sub encode_object { - my $self = shift; - my @encoded = $self->SUPER::encode_object(@_); + my $self = shift; + my @encoded = $self->SUPER::encode_object(@_); - return $encoded[0]->[0] eq 'nil' - ? ['value', {}, [@encoded]] - : @encoded; + return $encoded[0]->[0] eq 'nil' ? ['value', {}, [@encoded]] : @encoded; } # Removes undefined values so they do not produce invalid XMLRPC. sub envelope { - my $self = shift; - my ($type, $method, $data) = @_; - # If the type isn't a successful response we don't want to change the values. - if ($type eq 'response') { - _strip_undefs($data); - } - return $self->SUPER::envelope($type, $method, $data); + my $self = shift; + my ($type, $method, $data) = @_; + + # If the type isn't a successful response we don't want to change the values. + if ($type eq 'response') { + _strip_undefs($data); + } + return $self->SUPER::envelope($type, $method, $data); } # In an XMLRPC response we have to handle hashes of arrays, hashes, scalars, @@ -321,58 +339,58 @@ sub envelope { # The whole XMLRPC::Data object must be removed if its value key is undefined # so it cannot be recursed like the other hash type objects. sub _strip_undefs { - my ($initial) = @_; - my $type = reftype($initial) or return; - - if ($type eq "HASH") { - while (my ($key, $value) = each(%$initial)) { - if ( !defined $value - || (blessed $value && $value->isa('XMLRPC::Data') && !defined $value->value) ) - { - # If the value is undefined remove it from the hash. - delete $initial->{$key}; - } - else { - _strip_undefs($value); - } - } + my ($initial) = @_; + my $type = reftype($initial) or return; + + if ($type eq "HASH") { + while (my ($key, $value) = each(%$initial)) { + if (!defined $value + || (blessed $value && $value->isa('XMLRPC::Data') && !defined $value->value)) + { + # If the value is undefined remove it from the hash. + delete $initial->{$key}; + } + else { + _strip_undefs($value); + } } - elsif ($type eq "ARRAY") { - for (my $count = 0; $count < scalar @{$initial}; $count++) { - my $value = $initial->[$count]; - if ( !defined $value - || (blessed $value && $value->isa('XMLRPC::Data') && !defined $value->value) ) - { - # If the value is undefined remove it from the array. - splice(@$initial, $count, 1); - $count--; - } - else { - _strip_undefs($value); - } - } + } + elsif ($type eq "ARRAY") { + for (my $count = 0; $count < scalar @{$initial}; $count++) { + my $value = $initial->[$count]; + if (!defined $value + || (blessed $value && $value->isa('XMLRPC::Data') && !defined $value->value)) + { + # If the value is undefined remove it from the array. + splice(@$initial, $count, 1); + $count--; + } + else { + _strip_undefs($value); + } } + } } sub BEGIN { - no strict 'refs'; - for my $type (qw(double i4 int dateTime)) { - my $method = 'as_' . $type; - *$method = sub { - my ($self, $value) = @_; - if (!defined($value)) { - return as_nil(); - } - else { - my $super_method = "SUPER::$method"; - return $self->$super_method($value); - } - } - } + no strict 'refs'; + for my $type (qw(double i4 int dateTime)) { + my $method = 'as_' . $type; + *$method = sub { + my ($self, $value) = @_; + if (!defined($value)) { + return as_nil(); + } + else { + my $super_method = "SUPER::$method"; + return $self->$super_method($value); + } + } + } } sub as_nil { - return ['nil', {}]; + return ['nil', {}]; } 1; diff --git a/Bugzilla/WebService/User.pm b/Bugzilla/WebService/User.pm index 98323d4e18..1175767b0b 100644 --- a/Bugzilla/WebService/User.pm +++ b/Bugzilla/WebService/User.pm @@ -18,78 +18,72 @@ use Bugzilla::Error; use Bugzilla::Group; use Bugzilla::User; use Bugzilla::Util qw(trim detaint_natural); -use Bugzilla::WebService::Util qw(filter filter_wants validate translate params_to_objects); +use Bugzilla::WebService::Util + qw(filter filter_wants validate translate params_to_objects); use List::Util qw(first min); # Don't need auth to login -use constant LOGIN_EXEMPT => { - login => 1, - offer_account_by_email => 1, -}; +use constant LOGIN_EXEMPT => {login => 1, offer_account_by_email => 1,}; use constant READ_ONLY => qw( - get + get ); use constant PUBLIC_METHODS => qw( - create - get - login - logout - offer_account_by_email - update - valid_login - whoami + create + get + login + logout + offer_account_by_email + update + valid_login + whoami ); -use constant MAPPED_FIELDS => { - full_name => 'name', - login_denied_text => 'disabledtext', -}; +use constant MAPPED_FIELDS => + {full_name => 'name', login_denied_text => 'disabledtext',}; -use constant MAPPED_RETURNS => { - realname => 'full_name', - disabledtext => 'login_denied_text', -}; +use constant MAPPED_RETURNS => + {realname => 'full_name', disabledtext => 'login_denied_text',}; ############## # User Login # ############## sub login { - my ($self, $params) = @_; + my ($self, $params) = @_; - # Check to see if we are already logged in - my $user = Bugzilla->user; - if ($user->id) { - return $self->_login_to_hash($user); - } + # Check to see if we are already logged in + my $user = Bugzilla->user; + if ($user->id) { + return $self->_login_to_hash($user); + } - # Login name and password params are required - foreach my $param ("login", "password") { - (defined $params->{$param} || defined $params->{'Bugzilla_' . $param}) - || ThrowCodeError('param_required', { param => $param }); - } + # Login name and password params are required + foreach my $param ("login", "password") { + (defined $params->{$param} || defined $params->{'Bugzilla_' . $param}) + || ThrowCodeError('param_required', {param => $param}); + } - $user = Bugzilla->login(); - return $self->_login_to_hash($user); + $user = Bugzilla->login(); + return $self->_login_to_hash($user); } sub logout { - my $self = shift; - Bugzilla->logout; + my $self = shift; + Bugzilla->logout; } sub valid_login { - my ($self, $params) = @_; - defined $params->{login} - || ThrowCodeError('param_required', { param => 'login' }); - Bugzilla->login(); - if (Bugzilla->user->id && Bugzilla->user->login eq $params->{login}) { - return $self->type('boolean', 1); - } - return $self->type('boolean', 0); + my ($self, $params) = @_; + defined $params->{login} + || ThrowCodeError('param_required', {param => 'login'}); + Bugzilla->login(); + if (Bugzilla->user->id && Bugzilla->user->login eq $params->{login}) { + return $self->type('boolean', 1); + } + return $self->type('boolean', 0); } ################# @@ -97,176 +91,182 @@ sub valid_login { ################# sub offer_account_by_email { - my $self = shift; - my ($params) = @_; - my $email = trim($params->{email}) - || ThrowCodeError('param_required', { param => 'email' }); - - my $login = Bugzilla->params->{use_email_as_login} ? $email : trim($params->{login}); - $login or ThrowCodeError('param_required', { param => 'login' }); - - Bugzilla->user->check_account_creation_enabled; - Bugzilla->user->check_and_send_account_creation_confirmation($login, $email); - return undef; + my $self = shift; + my ($params) = @_; + my $email = trim($params->{email}) + || ThrowCodeError('param_required', {param => 'email'}); + + my $login + = Bugzilla->params->{use_email_as_login} ? $email : trim($params->{login}); + $login or ThrowCodeError('param_required', {param => 'login'}); + + Bugzilla->user->check_account_creation_enabled; + Bugzilla->user->check_and_send_account_creation_confirmation($login, $email); + return undef; } sub create { - my $self = shift; - my ($params) = @_; + my $self = shift; + my ($params) = @_; - Bugzilla->user->in_group('editusers') - || ThrowUserError("auth_failure", { group => "editusers", - action => "add", - object => "users"}); + Bugzilla->user->in_group('editusers') + || ThrowUserError("auth_failure", + {group => "editusers", action => "add", object => "users"}); - my $email = trim($params->{email}) - || ThrowCodeError('param_required', { param => 'email' }); + my $email = trim($params->{email}) + || ThrowCodeError('param_required', {param => 'email'}); - my $login = Bugzilla->params->{use_email_as_login} ? $email : trim($params->{login}); - $login or ThrowCodeError('param_required', { param => 'login' }); + my $login + = Bugzilla->params->{use_email_as_login} ? $email : trim($params->{login}); + $login or ThrowCodeError('param_required', {param => 'login'}); - my $realname = trim($params->{full_name}); - my $password = trim($params->{password}) || '*'; + my $realname = trim($params->{full_name}); + my $password = trim($params->{password}) || '*'; - my $user = Bugzilla::User->create({ - login_name => $login, - email => $email, - realname => $realname, - cryptpassword => $password - }); + my $user = Bugzilla::User->create({ + login_name => $login, + email => $email, + realname => $realname, + cryptpassword => $password + }); - return { id => $self->type('int', $user->id) }; + return {id => $self->type('int', $user->id)}; } -# function to return user information by passing either user ids or +# function to return user information by passing either user ids or # login names or both together: -# $call = $rpc->call( 'User.get', { ids => [1,2,3], +# $call = $rpc->call( 'User.get', { ids => [1,2,3], # names => ['testusera@redhat.com', 'testuserb@redhat.com'] }); sub get { - my ($self, $params) = validate(@_, 'names', 'ids', 'match', 'group_ids', 'groups'); - - Bugzilla->switch_to_shadow_db(); - - defined($params->{names}) || defined($params->{ids}) - || defined($params->{match}) - || ThrowCodeError('params_required', - { function => 'User.get', params => ['ids', 'names', 'match'] }); - - my @user_objects; - @user_objects = map { Bugzilla::User->check($_) } @{ $params->{names} } - if $params->{names}; - - # start filtering to remove duplicate user ids - my %unique_users = map { $_->id => $_ } @user_objects; - @user_objects = values %unique_users; - - my @users; - - # If the user is not logged in: Return an error if they passed any user ids. - # Otherwise, return a limited amount of information based on login names. - if (!Bugzilla->user->id){ - if ($params->{ids}){ - ThrowUserError("user_access_by_id_denied"); - } - if ($params->{match}) { - ThrowUserError('user_access_by_match_denied'); - } - my $in_group = $self->_filter_users_by_group( - \@user_objects, $params); - @users = map { filter $params, { - id => $self->type('int', $_->id), - real_name => $self->type('string', $_->name), - name => $self->type('login', $_->login), - } } @$in_group; - - return { users => \@users }; - } + my ($self, $params) + = validate(@_, 'names', 'ids', 'match', 'group_ids', 'groups'); - my $obj_by_ids; - $obj_by_ids = Bugzilla::User->new_from_list($params->{ids}) if $params->{ids}; - - # obj_by_ids are only visible to the user if they can see - # the otheruser, for non visible otheruser throw an error - foreach my $obj (@$obj_by_ids) { - if (Bugzilla->user->can_see_user($obj)){ - if (!$unique_users{$obj->id}) { - push (@user_objects, $obj); - $unique_users{$obj->id} = $obj; - } - } - else { - ThrowUserError('auth_failure', {reason => "not_visible", - action => "access", - object => "user", - userid => $obj->id}); - } - } + Bugzilla->switch_to_shadow_db(); - # User Matching - my $limit = Bugzilla->params->{maxusermatches}; - if ($params->{limit}) { - detaint_natural($params->{limit}) - || ThrowCodeError('param_must_be_numeric', - { function => 'Bugzilla::WebService::User::match', - param => 'limit' }); - $limit = $limit ? min($params->{limit}, $limit) : $params->{limit}; - } + defined($params->{names}) + || defined($params->{ids}) + || defined($params->{match}) + || ThrowCodeError('params_required', + {function => 'User.get', params => ['ids', 'names', 'match']}); - my $exclude_disabled = $params->{'include_disabled'} ? 0 : 1; - foreach my $match_string (@{ $params->{'match'} || [] }) { - my $matched = Bugzilla::User::match($match_string, $limit, $exclude_disabled); - foreach my $user (@$matched) { - if (!$unique_users{$user->id}) { - push(@user_objects, $user); - $unique_users{$user->id} = $user; - } - } - } + my @user_objects; + @user_objects = map { Bugzilla::User->check($_) } @{$params->{names}} + if $params->{names}; + + # start filtering to remove duplicate user ids + my %unique_users = map { $_->id => $_ } @user_objects; + @user_objects = values %unique_users; + + my @users; + # If the user is not logged in: Return an error if they passed any user ids. + # Otherwise, return a limited amount of information based on login names. + if (!Bugzilla->user->id) { + if ($params->{ids}) { + ThrowUserError("user_access_by_id_denied"); + } + if ($params->{match}) { + ThrowUserError('user_access_by_match_denied'); + } my $in_group = $self->_filter_users_by_group(\@user_objects, $params); - foreach my $user (@$in_group) { - my $user_info = filter $params, { - id => $self->type('int', $user->id), - real_name => $self->type('string', $user->name), - name => $self->type('login', $user->login), - can_login => $self->type('boolean', $user->is_enabled ? 1 : 0), - }; - - if (Bugzilla->user->in_group('editusers')) { - $user_info->{email} = $self->type('email', $user->email), - $user_info->{email_enabled} = $self->type('boolean', $user->email_enabled); - $user_info->{login_denied_text} = $self->type('string', $user->disabledtext); + @users = map { + filter $params, + { + id => $self->type('int', $_->id), + real_name => $self->type('string', $_->name), + name => $self->type('login', $_->login), } - - if (Bugzilla->user->id == $user->id) { - if (filter_wants($params, 'saved_searches')) { - $user_info->{saved_searches} = [ - map { $self->_query_to_hash($_) } @{ $user->queries } - ]; - } - if (filter_wants($params, 'saved_reports')) { - $user_info->{saved_reports} = [ - map { $self->_report_to_hash($_) } @{ $user->reports } - ]; - } + } @$in_group; + + return {users => \@users}; + } + + my $obj_by_ids; + $obj_by_ids = Bugzilla::User->new_from_list($params->{ids}) if $params->{ids}; + + # obj_by_ids are only visible to the user if they can see + # the otheruser, for non visible otheruser throw an error + foreach my $obj (@$obj_by_ids) { + if (Bugzilla->user->can_see_user($obj)) { + if (!$unique_users{$obj->id}) { + push(@user_objects, $obj); + $unique_users{$obj->id} = $obj; + } + } + else { + ThrowUserError( + 'auth_failure', + { + reason => "not_visible", + action => "access", + object => "user", + userid => $obj->id } + ); + } + } + + # User Matching + my $limit = Bugzilla->params->{maxusermatches}; + if ($params->{limit}) { + detaint_natural($params->{limit}) + || ThrowCodeError('param_must_be_numeric', + {function => 'Bugzilla::WebService::User::match', param => 'limit'}); + $limit = $limit ? min($params->{limit}, $limit) : $params->{limit}; + } + + my $exclude_disabled = $params->{'include_disabled'} ? 0 : 1; + foreach my $match_string (@{$params->{'match'} || []}) { + my $matched = Bugzilla::User::match($match_string, $limit, $exclude_disabled); + foreach my $user (@$matched) { + if (!$unique_users{$user->id}) { + push(@user_objects, $user); + $unique_users{$user->id} = $user; + } + } + } + + my $in_group = $self->_filter_users_by_group(\@user_objects, $params); + foreach my $user (@$in_group) { + my $user_info = filter $params, + { + id => $self->type('int', $user->id), + real_name => $self->type('string', $user->name), + name => $self->type('login', $user->login), + can_login => $self->type('boolean', $user->is_enabled ? 1 : 0), + }; + + if (Bugzilla->user->in_group('editusers')) { + $user_info->{email} = $self->type('email', $user->email), + $user_info->{email_enabled} = $self->type('boolean', $user->email_enabled); + $user_info->{login_denied_text} = $self->type('string', $user->disabledtext); + } - if (filter_wants($params, 'groups')) { - if (Bugzilla->user->id == $user->id || Bugzilla->user->in_group('editusers')) { - $user_info->{groups} = [ - map { $self->_group_to_hash($_) } @{ $user->groups } - ]; - } - else { - $user_info->{groups} = $self->_filter_bless_groups($user->groups); - } - } + if (Bugzilla->user->id == $user->id) { + if (filter_wants($params, 'saved_searches')) { + $user_info->{saved_searches} + = [map { $self->_query_to_hash($_) } @{$user->queries}]; + } + if (filter_wants($params, 'saved_reports')) { + $user_info->{saved_reports} + = [map { $self->_report_to_hash($_) } @{$user->reports}]; + } + } - push(@users, $user_info); + if (filter_wants($params, 'groups')) { + if (Bugzilla->user->id == $user->id || Bugzilla->user->in_group('editusers')) { + $user_info->{groups} = [map { $self->_group_to_hash($_) } @{$user->groups}]; + } + else { + $user_info->{groups} = $self->_filter_bless_groups($user->groups); + } } - return { users => \@users }; + push(@users, $user_info); + } + + return {users => \@users}; } ############### @@ -274,168 +274,170 @@ sub get { ############### sub update { - my ($self, $params) = @_; - - my $dbh = Bugzilla->dbh; + my ($self, $params) = @_; - my $user = Bugzilla->login(LOGIN_REQUIRED); + my $dbh = Bugzilla->dbh; - # Reject access if there is no sense in continuing. - $user->in_group('editusers') - || $user->can_bless() - || ThrowUserError("auth_failure", {group => "editusers", - action => "edit", - object => "users"}); + my $user = Bugzilla->login(LOGIN_REQUIRED); - defined($params->{names}) || defined($params->{ids}) - || ThrowCodeError('params_required', - { function => 'User.update', params => ['ids', 'names'] }); + # Reject access if there is no sense in continuing. + $user->in_group('editusers') + || $user->can_bless() + || ThrowUserError("auth_failure", + {group => "editusers", action => "edit", object => "users"}); - my $user_objects = params_to_objects($params, 'Bugzilla::User'); + defined($params->{names}) + || defined($params->{ids}) + || ThrowCodeError('params_required', + {function => 'User.update', params => ['ids', 'names']}); - my $values = translate($params, MAPPED_FIELDS); + my $user_objects = params_to_objects($params, 'Bugzilla::User'); - # We delete names and ids to keep only new values to set. - delete $values->{names}; - delete $values->{ids}; + my $values = translate($params, MAPPED_FIELDS); - $dbh->bz_start_transaction(); + # We delete names and ids to keep only new values to set. + delete $values->{names}; + delete $values->{ids}; - $values = { groups => $values->{groups} } unless $user->in_group('editusers'); - foreach my $user (@$user_objects){ - $user->set_all($values); - } + $dbh->bz_start_transaction(); - my %changes; - foreach my $user (@$user_objects){ - my $returned_changes = $user->update(); - $changes{$user->id} = translate($returned_changes, MAPPED_RETURNS); - } - $dbh->bz_commit_transaction(); - - my @result; - foreach my $user (@$user_objects) { - my %hash = ( - id => $user->id, - changes => {}, - ); - - foreach my $field (keys %{ $changes{$user->id} }) { - my $change = $changes{$user->id}->{$field}; - # We normalize undef to an empty string, so that the API - # stays consistent for things that can become empty. - $change->[0] = '' if !defined $change->[0]; - $change->[1] = '' if !defined $change->[1]; - # We also flatten arrays (used by groups and blessed_groups) - $change->[0] = join(',', @{$change->[0]}) if ref $change->[0]; - $change->[1] = join(',', @{$change->[1]}) if ref $change->[1]; - - $hash{changes}{$field} = { - removed => $self->type('string', $change->[0]), - added => $self->type('string', $change->[1]) - }; - } + $values = {groups => $values->{groups}} unless $user->in_group('editusers'); + foreach my $user (@$user_objects) { + $user->set_all($values); + } - push(@result, \%hash); - } + my %changes; + foreach my $user (@$user_objects) { + my $returned_changes = $user->update(); + $changes{$user->id} = translate($returned_changes, MAPPED_RETURNS); + } + $dbh->bz_commit_transaction(); - return { users => \@result }; -} + my @result; + foreach my $user (@$user_objects) { + my %hash = (id => $user->id, changes => {},); -sub _filter_users_by_group { - my ($self, $users, $params) = @_; - my ($group_ids, $group_names) = @$params{qw(group_ids groups)}; + foreach my $field (keys %{$changes{$user->id}}) { + my $change = $changes{$user->id}->{$field}; - # If no groups are specified, we return all users. - return $users if (!$group_ids and !$group_names); + # We normalize undef to an empty string, so that the API + # stays consistent for things that can become empty. + $change->[0] = '' if !defined $change->[0]; + $change->[1] = '' if !defined $change->[1]; - my $user = Bugzilla->user; - my (@groups, %groups); + # We also flatten arrays (used by groups and blessed_groups) + $change->[0] = join(',', @{$change->[0]}) if ref $change->[0]; + $change->[1] = join(',', @{$change->[1]}) if ref $change->[1]; - if ($group_ids) { - @groups = map { Bugzilla::Group->check({ id => $_ }) } @$group_ids; - $groups{$_->id} = $_ foreach @groups; + $hash{changes}{$field} = { + removed => $self->type('string', $change->[0]), + added => $self->type('string', $change->[1]) + }; } - if ($group_names) { - foreach my $name (@$group_names) { - my $group = Bugzilla::Group->check({ name => $name, _error => 'invalid_group_name' }); - $user->in_group($group) || ThrowUserError('invalid_group_name', { name => $name }); - $groups{$group->id} = $group; - } + + push(@result, \%hash); + } + + return {users => \@result}; +} + +sub _filter_users_by_group { + my ($self, $users, $params) = @_; + my ($group_ids, $group_names) = @$params{qw(group_ids groups)}; + + # If no groups are specified, we return all users. + return $users if (!$group_ids and !$group_names); + + my $user = Bugzilla->user; + my (@groups, %groups); + + if ($group_ids) { + @groups = map { Bugzilla::Group->check({id => $_}) } @$group_ids; + $groups{$_->id} = $_ foreach @groups; + } + if ($group_names) { + foreach my $name (@$group_names) { + my $group + = Bugzilla::Group->check({name => $name, _error => 'invalid_group_name'}); + $user->in_group($group) + || ThrowUserError('invalid_group_name', {name => $name}); + $groups{$group->id} = $group; } - @groups = values %groups; + } + @groups = values %groups; - my @in_group = grep { $self->_user_in_any_group($_, \@groups) } @$users; - return \@in_group; + my @in_group = grep { $self->_user_in_any_group($_, \@groups) } @$users; + return \@in_group; } sub _user_in_any_group { - my ($self, $user, $groups) = @_; - foreach my $group (@$groups) { - return 1 if $user->in_group($group); - } - return 0; + my ($self, $user, $groups) = @_; + foreach my $group (@$groups) { + return 1 if $user->in_group($group); + } + return 0; } sub _filter_bless_groups { - my ($self, $groups) = @_; - my $user = Bugzilla->user; + my ($self, $groups) = @_; + my $user = Bugzilla->user; - my @filtered_groups; - foreach my $group (@$groups) { - next unless $user->can_bless($group->id); - push(@filtered_groups, $self->_group_to_hash($group)); - } + my @filtered_groups; + foreach my $group (@$groups) { + next unless $user->can_bless($group->id); + push(@filtered_groups, $self->_group_to_hash($group)); + } - return \@filtered_groups; + return \@filtered_groups; } sub _group_to_hash { - my ($self, $group) = @_; - my $item = { - id => $self->type('int', $group->id), - name => $self->type('string', $group->name), - description => $self->type('string', $group->description), - }; - return $item; + my ($self, $group) = @_; + my $item = { + id => $self->type('int', $group->id), + name => $self->type('string', $group->name), + description => $self->type('string', $group->description), + }; + return $item; } sub _query_to_hash { - my ($self, $query) = @_; - my $item = { - id => $self->type('int', $query->id), - name => $self->type('string', $query->name), - query => $self->type('string', $query->url), - }; - return $item; + my ($self, $query) = @_; + my $item = { + id => $self->type('int', $query->id), + name => $self->type('string', $query->name), + query => $self->type('string', $query->url), + }; + return $item; } sub _report_to_hash { - my ($self, $report) = @_; - my $item = { - id => $self->type('int', $report->id), - name => $self->type('string', $report->name), - query => $self->type('string', $report->query), - }; - return $item; + my ($self, $report) = @_; + my $item = { + id => $self->type('int', $report->id), + name => $self->type('string', $report->name), + query => $self->type('string', $report->query), + }; + return $item; } sub _login_to_hash { - my ($self, $user) = @_; - my $item = { id => $self->type('int', $user->id) }; - if ($user->{_login_token}) { - $item->{'token'} = $user->id . "-" . $user->{_login_token}; - } - return $item; + my ($self, $user) = @_; + my $item = {id => $self->type('int', $user->id)}; + if ($user->{_login_token}) { + $item->{'token'} = $user->id . "-" . $user->{_login_token}; + } + return $item; } sub whoami { - my ($self, $params) = @_; - my $user = Bugzilla->login(LOGIN_REQUIRED); - return filter $params, { - id => $self->type('int', $user->id), - real_name => $self->type('string', $user->name), - name => $self->type('email', $user->login), + my ($self, $params) = @_; + my $user = Bugzilla->login(LOGIN_REQUIRED); + return filter $params, + { + id => $self->type('int', $user->id), + real_name => $self->type('string', $user->name), + name => $self->type('email', $user->login), }; } diff --git a/Bugzilla/WebService/Util.pm b/Bugzilla/WebService/Util.pm index d4da2f5751..dacf2b1254 100644 --- a/Bugzilla/WebService/Util.pm +++ b/Bugzilla/WebService/Util.pm @@ -27,278 +27,287 @@ use parent qw(Exporter); require Test::Taint; our @EXPORT_OK = qw( - extract_flags - filter - filter_wants - taint_data - validate - translate - params_to_objects - fix_credentials + extract_flags + filter + filter_wants + taint_data + validate + translate + params_to_objects + fix_credentials ); sub extract_flags { - my ($flags, $flag_types, $current_flags) = @_; - my (@new_flags, @old_flags); + my ($flags, $flag_types, $current_flags) = @_; + my (@new_flags, @old_flags); - $current_flags //= []; + $current_flags //= []; - # Copy the user provided $flags as we may call extract_flags more than - # once when editing multiple bugs or attachments. - my $flags_copy = dclone($flags); + # Copy the user provided $flags as we may call extract_flags more than + # once when editing multiple bugs or attachments. + my $flags_copy = dclone($flags); - foreach my $flag (@$flags_copy) { - my $id = $flag->{id}; - my $type_id = $flag->{type_id}; + foreach my $flag (@$flags_copy) { + my $id = $flag->{id}; + my $type_id = $flag->{type_id}; - my $new = delete $flag->{new}; - my $name = delete $flag->{name}; + my $new = delete $flag->{new}; + my $name = delete $flag->{name}; - if ($id) { - my $flag_obj = grep($id == $_->id, @$current_flags); - $flag_obj || ThrowUserError('object_does_not_exist', - { class => 'Bugzilla::Flag', id => $id }); - } - elsif ($type_id) { - my $type_obj = grep($type_id == $_->id, @$flag_types); - $type_obj || ThrowUserError('object_does_not_exist', - { class => 'Bugzilla::FlagType', id => $type_id }); - if (!$new) { - my @flag_matches = grep($type_id == $_->type->id, @$current_flags); - @flag_matches > 1 && ThrowUserError('flag_not_unique', - { value => $type_id }); - if (!@flag_matches) { - delete $flag->{id}; - } - else { - delete $flag->{type_id}; - $flag->{id} = $flag_matches[0]->id; - } - } + if ($id) { + my $flag_obj = grep($id == $_->id, @$current_flags); + $flag_obj + || ThrowUserError('object_does_not_exist', + {class => 'Bugzilla::Flag', id => $id}); + } + elsif ($type_id) { + my $type_obj = grep($type_id == $_->id, @$flag_types); + $type_obj + || ThrowUserError('object_does_not_exist', + {class => 'Bugzilla::FlagType', id => $type_id}); + if (!$new) { + my @flag_matches = grep($type_id == $_->type->id, @$current_flags); + @flag_matches > 1 && ThrowUserError('flag_not_unique', {value => $type_id}); + if (!@flag_matches) { + delete $flag->{id}; } - elsif ($name) { - my @type_matches = grep($name eq $_->name, @$flag_types); - @type_matches > 1 && ThrowUserError('flag_type_not_unique', - { value => $name }); - @type_matches || ThrowUserError('object_does_not_exist', - { class => 'Bugzilla::FlagType', name => $name }); - if ($new) { - delete $flag->{id}; - $flag->{type_id} = $type_matches[0]->id; - } - else { - my @flag_matches = grep($name eq $_->type->name, @$current_flags); - @flag_matches > 1 && ThrowUserError('flag_not_unique', { value => $name }); - if (@flag_matches) { - $flag->{id} = $flag_matches[0]->id; - } - else { - delete $flag->{id}; - $flag->{type_id} = $type_matches[0]->id; - } - } + else { + delete $flag->{type_id}; + $flag->{id} = $flag_matches[0]->id; } - - if ($flag->{id}) { - push(@old_flags, $flag); + } + } + elsif ($name) { + my @type_matches = grep($name eq $_->name, @$flag_types); + @type_matches > 1 && ThrowUserError('flag_type_not_unique', {value => $name}); + @type_matches + || ThrowUserError('object_does_not_exist', + {class => 'Bugzilla::FlagType', name => $name}); + if ($new) { + delete $flag->{id}; + $flag->{type_id} = $type_matches[0]->id; + } + else { + my @flag_matches = grep($name eq $_->type->name, @$current_flags); + @flag_matches > 1 && ThrowUserError('flag_not_unique', {value => $name}); + if (@flag_matches) { + $flag->{id} = $flag_matches[0]->id; } else { - push(@new_flags, $flag); + delete $flag->{id}; + $flag->{type_id} = $type_matches[0]->id; } + } } - return (\@old_flags, \@new_flags); + if ($flag->{id}) { + push(@old_flags, $flag); + } + else { + push(@new_flags, $flag); + } + } + + return (\@old_flags, \@new_flags); } sub filter($$;$$) { - my ($params, $hash, $types, $prefix) = @_; - my %newhash = %$hash; + my ($params, $hash, $types, $prefix) = @_; + my %newhash = %$hash; - foreach my $key (keys %$hash) { - delete $newhash{$key} if !filter_wants($params, $key, $types, $prefix); - } + foreach my $key (keys %$hash) { + delete $newhash{$key} if !filter_wants($params, $key, $types, $prefix); + } - return \%newhash; + return \%newhash; } sub filter_wants($$;$$) { - my ($params, $field, $types, $prefix) = @_; - - # Since this is operation is resource intensive, we will cache the results - # This assumes that $params->{*_fields} doesn't change between calls - my $cache = Bugzilla->request_cache->{filter_wants} ||= {}; - $field = "${prefix}.${field}" if $prefix; - - if (exists $cache->{$field}) { - return $cache->{$field}; - } - - # Mimic old behavior if no types provided - my %field_types = map { $_ => 1 } (ref $types ? @$types : ($types || 'default')); - - my %include = map { $_ => 1 } @{ $params->{'include_fields'} || [] }; - my %exclude = map { $_ => 1 } @{ $params->{'exclude_fields'} || [] }; - - my %include_types; - my %exclude_types; - - # Only return default fields if nothing is specified - $include_types{default} = 1 if !%include; - - # Look for any field types requested - foreach my $key (keys %include) { - next if $key !~ /^_(.*)$/; - $include_types{$1} = 1; - delete $include{$key}; - } - foreach my $key (keys %exclude) { - next if $key !~ /^_(.*)$/; - $exclude_types{$1} = 1; - delete $exclude{$key}; - } - - # Explicit inclusion/exclusion - return $cache->{$field} = 0 if $exclude{$field}; - return $cache->{$field} = 1 if $include{$field}; - - # If the user has asked to include all or exclude all - return $cache->{$field} = 0 if $exclude_types{'all'}; - return $cache->{$field} = 1 if $include_types{'all'}; - - # If the user has not asked for any fields specifically or if the user has asked - # for one or more of the field's types (and not excluded them) - foreach my $type (keys %field_types) { - return $cache->{$field} = 0 if $exclude_types{$type}; - return $cache->{$field} = 1 if $include_types{$type}; - } - - my $wants = 0; - if ($prefix) { - # Include the field if the parent is include (and this one is not excluded) - $wants = 1 if $include{$prefix}; - } - else { - # We want to include this if one of the sub keys is included - my $key = $field . '.'; - my $len = length($key); - $wants = 1 if grep { substr($_, 0, $len) eq $key } keys %include; - } - - return $cache->{$field} = $wants; + my ($params, $field, $types, $prefix) = @_; + + # Since this is operation is resource intensive, we will cache the results + # This assumes that $params->{*_fields} doesn't change between calls + my $cache = Bugzilla->request_cache->{filter_wants} ||= {}; + $field = "${prefix}.${field}" if $prefix; + + if (exists $cache->{$field}) { + return $cache->{$field}; + } + + # Mimic old behavior if no types provided + my %field_types + = map { $_ => 1 } (ref $types ? @$types : ($types || 'default')); + + my %include = map { $_ => 1 } @{$params->{'include_fields'} || []}; + my %exclude = map { $_ => 1 } @{$params->{'exclude_fields'} || []}; + + my %include_types; + my %exclude_types; + + # Only return default fields if nothing is specified + $include_types{default} = 1 if !%include; + + # Look for any field types requested + foreach my $key (keys %include) { + next if $key !~ /^_(.*)$/; + $include_types{$1} = 1; + delete $include{$key}; + } + foreach my $key (keys %exclude) { + next if $key !~ /^_(.*)$/; + $exclude_types{$1} = 1; + delete $exclude{$key}; + } + + # Explicit inclusion/exclusion + return $cache->{$field} = 0 if $exclude{$field}; + return $cache->{$field} = 1 if $include{$field}; + + # If the user has asked to include all or exclude all + return $cache->{$field} = 0 if $exclude_types{'all'}; + return $cache->{$field} = 1 if $include_types{'all'}; + + # If the user has not asked for any fields specifically or if the user has asked + # for one or more of the field's types (and not excluded them) + foreach my $type (keys %field_types) { + return $cache->{$field} = 0 if $exclude_types{$type}; + return $cache->{$field} = 1 if $include_types{$type}; + } + + my $wants = 0; + if ($prefix) { + + # Include the field if the parent is include (and this one is not excluded) + $wants = 1 if $include{$prefix}; + } + else { + # We want to include this if one of the sub keys is included + my $key = $field . '.'; + my $len = length($key); + $wants = 1 if grep { substr($_, 0, $len) eq $key } keys %include; + } + + return $cache->{$field} = $wants; } sub taint_data { - my @params = @_; - return if !@params; - # Though this is a private function, it hasn't changed since 2004 and - # should be safe to use, and prevents us from having to write it ourselves - # or require another module to do it. - Test::Taint::_deeply_traverse(\&_delete_bad_keys, \@params); - Test::Taint::taint_deeply(\@params); + my @params = @_; + return if !@params; + + # Though this is a private function, it hasn't changed since 2004 and + # should be safe to use, and prevents us from having to write it ourselves + # or require another module to do it. + Test::Taint::_deeply_traverse(\&_delete_bad_keys, \@params); + Test::Taint::taint_deeply(\@params); } sub _delete_bad_keys { - foreach my $item (@_) { - next if ref $item ne 'HASH'; - foreach my $key (keys %$item) { - # Making something a hash key always untaints it, in Perl. - # However, we need to validate our argument names in some way. - # We know that all hash keys passed in to the WebService will - # match \w+, contain '.' or '-', so we delete any key that - # doesn't match that. - if ($key !~ /^[\w\.\-]+$/) { - delete $item->{$key}; - } - } + foreach my $item (@_) { + next if ref $item ne 'HASH'; + foreach my $key (keys %$item) { + + # Making something a hash key always untaints it, in Perl. + # However, we need to validate our argument names in some way. + # We know that all hash keys passed in to the WebService will + # match \w+, contain '.' or '-', so we delete any key that + # doesn't match that. + if ($key !~ /^[\w\.\-]+$/) { + delete $item->{$key}; + } } - return @_; + } + return @_; } -sub validate { - my ($self, $params, @keys) = @_; - - # If $params is defined but not a reference, then we weren't - # sent any parameters at all, and we're getting @keys where - # $params should be. - return ($self, undef) if (defined $params and !ref $params); - - my @id_params = qw(ids comment_ids); - # If @keys is not empty then we convert any named - # parameters that have scalar values to arrayrefs - # that match. - foreach my $key (@keys) { - if (exists $params->{$key}) { - $params->{$key} = [ $params->{$key} ] unless ref $params->{$key}; - - if (any { $key eq $_ } @id_params) { - my $ids = $params->{$key}; - ThrowCodeError('param_scalar_array_required', { param => $key }) - unless ref($ids) eq 'ARRAY' && none { ref $_ } @$ids; - } - } +sub validate { + my ($self, $params, @keys) = @_; + + # If $params is defined but not a reference, then we weren't + # sent any parameters at all, and we're getting @keys where + # $params should be. + return ($self, undef) if (defined $params and !ref $params); + + my @id_params = qw(ids comment_ids); + + # If @keys is not empty then we convert any named + # parameters that have scalar values to arrayrefs + # that match. + foreach my $key (@keys) { + if (exists $params->{$key}) { + $params->{$key} = [$params->{$key}] unless ref $params->{$key}; + + if (any { $key eq $_ } @id_params) { + my $ids = $params->{$key}; + ThrowCodeError('param_scalar_array_required', {param => $key}) + unless ref($ids) eq 'ARRAY' && none { ref $_ } @$ids; + } } + } - return ($self, $params); + return ($self, $params); } sub translate { - my ($params, $mapped) = @_; - my %changes; - while (my ($key,$value) = each (%$params)) { - my $new_field = $mapped->{$key} || $key; - $changes{$new_field} = $value; - } - return \%changes; + my ($params, $mapped) = @_; + my %changes; + while (my ($key, $value) = each(%$params)) { + my $new_field = $mapped->{$key} || $key; + $changes{$new_field} = $value; + } + return \%changes; } sub params_to_objects { - my ($params, $class) = @_; - my (@objects, @objects_by_ids); + my ($params, $class) = @_; + my (@objects, @objects_by_ids); - @objects = map { $class->check($_) } - @{ $params->{names} } if $params->{names}; + @objects = map { $class->check($_) } @{$params->{names}} if $params->{names}; - @objects_by_ids = map { $class->check({ id => $_ }) } - @{ $params->{ids} } if $params->{ids}; + @objects_by_ids = map { $class->check({id => $_}) } @{$params->{ids}} + if $params->{ids}; - push(@objects, @objects_by_ids); - my %seen; - @objects = grep { !$seen{$_->id}++ } @objects; - return \@objects; + push(@objects, @objects_by_ids); + my %seen; + @objects = grep { !$seen{$_->id}++ } @objects; + return \@objects; } sub fix_credentials { - my ($params, $cgi) = @_; - - # Allow user to pass in authentication details in X-Headers - # This allows callers to keep credentials out of GET request query-strings - if ($cgi) { - foreach my $field (keys %{ API_AUTH_HEADERS() }) { - next if exists $params->{API_AUTH_HEADERS->{$field}} || ($cgi->http($field) // '') eq ''; - $params->{API_AUTH_HEADERS->{$field}} = uri_unescape($cgi->http($field)); - } + my ($params, $cgi) = @_; + + # Allow user to pass in authentication details in X-Headers + # This allows callers to keep credentials out of GET request query-strings + if ($cgi) { + foreach my $field (keys %{API_AUTH_HEADERS()}) { + next + if exists $params->{API_AUTH_HEADERS->{$field}} + || ($cgi->http($field) // '') eq ''; + $params->{API_AUTH_HEADERS->{$field}} = uri_unescape($cgi->http($field)); } - - # Allow user to pass in login=foo&password=bar as a convenience - # even if not calling GET /login. We also do not delete them as - # GET /login requires "login" and "password". - if (exists $params->{'login'} && exists $params->{'password'}) { - $params->{'Bugzilla_login'} = delete $params->{'login'}; - $params->{'Bugzilla_password'} = delete $params->{'password'}; - } - # Allow user to pass api_key=12345678 as a convenience which becomes - # "Bugzilla_api_key" which is what the auth code looks for. - if (exists $params->{api_key}) { - $params->{Bugzilla_api_key} = delete $params->{api_key}; - } - # Allow user to pass token=12345678 as a convenience which becomes - # "Bugzilla_token" which is what the auth code looks for. - if (exists $params->{'token'}) { - $params->{'Bugzilla_token'} = delete $params->{'token'}; - } - - # Allow extensions to modify the credential data before login - Bugzilla::Hook::process('webservice_fix_credentials', { params => $params }); + } + + # Allow user to pass in login=foo&password=bar as a convenience + # even if not calling GET /login. We also do not delete them as + # GET /login requires "login" and "password". + if (exists $params->{'login'} && exists $params->{'password'}) { + $params->{'Bugzilla_login'} = delete $params->{'login'}; + $params->{'Bugzilla_password'} = delete $params->{'password'}; + } + + # Allow user to pass api_key=12345678 as a convenience which becomes + # "Bugzilla_api_key" which is what the auth code looks for. + if (exists $params->{api_key}) { + $params->{Bugzilla_api_key} = delete $params->{api_key}; + } + + # Allow user to pass token=12345678 as a convenience which becomes + # "Bugzilla_token" which is what the auth code looks for. + if (exists $params->{'token'}) { + $params->{'Bugzilla_token'} = delete $params->{'token'}; + } + + # Allow extensions to modify the credential data before login + Bugzilla::Hook::process('webservice_fix_credentials', {params => $params}); } __END__ diff --git a/Bugzilla/Whine.pm b/Bugzilla/Whine.pm index 6f322fe96c..6170df7514 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 a691fd2ff6..a4c2f36e8f 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 8417affb16..b55fd8b10a 100644 --- a/Bugzilla/Whine/Schedule.pm +++ b/Bugzilla/Whine/Schedule.pm @@ -22,22 +22,22 @@ use Bugzilla::Constants; use constant DB_TABLE => 'whine_schedules'; use constant DB_COLUMNS => qw( - id - eventid - run_day - run_time - run_next - mailto - mailto_type + id + eventid + run_day + run_time + run_next + mailto + mailto_type ); use constant UPDATE_COLUMNS => qw( - eventid - run_day - run_time - run_next - mailto - mailto_type + eventid + run_day + run_time + run_next + mailto + mailto_type ); use constant NAME_FIELD => 'id'; use constant LIST_ORDER => 'id'; @@ -45,36 +45,38 @@ use constant LIST_ORDER => 'id'; #################### # Simple Accessors # #################### -sub eventid { return $_[0]->{'eventid'}; } -sub run_day { return $_[0]->{'run_day'}; } -sub run_time { return $_[0]->{'run_time'}; } +sub eventid { return $_[0]->{'eventid'}; } +sub run_day { return $_[0]->{'run_day'}; } +sub run_time { return $_[0]->{'run_time'}; } sub mailto_is_group { return $_[0]->{'mailto_type'}; } sub mailto { - my $self = shift; - - return $self->{mailto_object} if exists $self->{mailto_object}; - my $id = $self->{'mailto'}; - - if ($self->mailto_is_group) { - $self->{mailto_object} = Bugzilla::Group->new($id); - } else { - $self->{mailto_object} = Bugzilla::User->new($id); - } - return $self->{mailto_object}; + my $self = shift; + + return $self->{mailto_object} if exists $self->{mailto_object}; + my $id = $self->{'mailto'}; + + if ($self->mailto_is_group) { + $self->{mailto_object} = Bugzilla::Group->new($id); + } + else { + $self->{mailto_object} = Bugzilla::User->new($id); + } + return $self->{mailto_object}; } -sub mailto_users { - my $self = shift; - return $self->{mailto_users} if exists $self->{mailto_users}; - my $object = $self->mailto; - - if ($self->mailto_is_group) { - $self->{mailto_users} = $object->members_non_inherited if $object->is_active; - } else { - $self->{mailto_users} = $object; - } - return $self->{mailto_users}; +sub mailto_users { + my $self = shift; + return $self->{mailto_users} if exists $self->{mailto_users}; + my $object = $self->mailto; + + if ($self->mailto_is_group) { + $self->{mailto_users} = $object->members_non_inherited if $object->is_active; + } + else { + $self->{mailto_users} = $object; + } + return $self->{mailto_users}; } 1; diff --git a/admin.cgi b/admin.cgi index f84d863b0d..de1f30529a 100755 --- a/admin.cgi +++ b/admin.cgi @@ -16,14 +16,15 @@ use Bugzilla; use Bugzilla::Constants; use Bugzilla::Error; -my $cgi = Bugzilla->cgi; +my $cgi = Bugzilla->cgi; my $template = Bugzilla->template; -my $user = Bugzilla->login(LOGIN_REQUIRED); +my $user = Bugzilla->login(LOGIN_REQUIRED); print $cgi->header(); $user->can_administer - || ThrowUserError('auth_failure', {action => 'access', object => 'administrative_pages'}); + || ThrowUserError('auth_failure', + {action => 'access', object => 'administrative_pages'}); $template->process('admin/admin.html.tmpl') || ThrowTemplateError($template->error()); diff --git a/attachment.cgi b/attachment.cgi index 59c07c84dd..cf5c6a6432 100755 --- a/attachment.cgi +++ b/attachment.cgi @@ -16,8 +16,8 @@ use Bugzilla; use Bugzilla::BugMail; use Bugzilla::Constants; use Bugzilla::Error; -use Bugzilla::Flag; -use Bugzilla::FlagType; +use Bugzilla::Flag; +use Bugzilla::FlagType; use Bugzilla::User; use Bugzilla::Util; use Bugzilla::Bug; @@ -33,9 +33,9 @@ use Cwd qw(realpath); # when preparing Bugzilla for mod_perl, this script used these # variables in so many subroutines that it was easier to just # make them globals. -local our $cgi = Bugzilla->cgi; -local our $template = Bugzilla->template; -local our $vars = {}; +local our $cgi = Bugzilla->cgi; +local our $template = Bugzilla->template; +local our $vars = {}; local $Bugzilla::CGI::ALLOW_UNSAFE_RESPONSE = 1; # All calls to this script should contain an "action" variable whose @@ -50,57 +50,48 @@ my $format = $cgi->param('format') || ''; # You must use the appropriate urlbase/sslbase param when doing anything # but viewing an attachment, or a raw diff. if ($action ne 'view' - && (($action !~ /^(?:interdiff|diff)$/) || $format ne 'raw')) + && (($action !~ /^(?:interdiff|diff)$/) || $format ne 'raw')) { - do_ssl_redirect_if_required(); - if ($cgi->url_is_attachment_base) { - $cgi->redirect_to_urlbase; - } - Bugzilla->login(); + do_ssl_redirect_if_required(); + if ($cgi->url_is_attachment_base) { + $cgi->redirect_to_urlbase; + } + Bugzilla->login(); } # When viewing an attachment, do not request credentials if we are on # the alternate host. Let view() decide when to call Bugzilla->login. -if ($action eq "view") -{ - view(); +if ($action eq "view") { + view(); } -elsif ($action eq "interdiff") -{ - interdiff(); +elsif ($action eq "interdiff") { + interdiff(); } -elsif ($action eq "diff") -{ - diff(); +elsif ($action eq "diff") { + diff(); } -elsif ($action eq "viewall") -{ - viewall(); +elsif ($action eq "viewall") { + viewall(); } -elsif ($action eq "enter") -{ - Bugzilla->login(LOGIN_REQUIRED); - enter(); +elsif ($action eq "enter") { + Bugzilla->login(LOGIN_REQUIRED); + enter(); } -elsif ($action eq "insert") -{ - Bugzilla->login(LOGIN_REQUIRED); - insert(); +elsif ($action eq "insert") { + Bugzilla->login(LOGIN_REQUIRED); + insert(); } -elsif ($action eq "edit") -{ - edit(); +elsif ($action eq "edit") { + edit(); } -elsif ($action eq "update") -{ - Bugzilla->login(LOGIN_REQUIRED); - update(); +elsif ($action eq "update") { + Bugzilla->login(LOGIN_REQUIRED); + update(); } elsif ($action eq "delete") { - delete_attachment(); + delete_attachment(); } -else -{ +else { ThrowUserError('unknown_action', {action => $action}); } @@ -122,72 +113,73 @@ exit; # Returns an attachment object. sub validateID { - my($param, $dont_validate_access) = @_; - $param ||= 'id'; + my ($param, $dont_validate_access) = @_; + $param ||= 'id'; - # If we're not doing interdiffs, check if id wasn't specified and - # prompt them with a page that allows them to choose an attachment. - # Happens when calling plain attachment.cgi from the urlbar directly - if ($param eq 'id' && !$cgi->param('id')) { - print $cgi->header(); - $template->process("attachment/choose.html.tmpl", $vars) || - ThrowTemplateError($template->error()); - exit; - } - - my $attach_id = $cgi->param($param); - - # Validate the specified attachment id. detaint kills $attach_id if - # non-natural, so use the original value from $cgi in our exception - # message here. - detaint_natural($attach_id) - || ThrowUserError("invalid_attach_id", - { attach_id => scalar $cgi->param($param) }); - - # Make sure the attachment exists in the database. - my $attachment = new Bugzilla::Attachment({ id => $attach_id, cache => 1 }) - || ThrowUserError("invalid_attach_id", { attach_id => $attach_id }); - - return $attachment if ($dont_validate_access || check_can_access($attachment)); + # If we're not doing interdiffs, check if id wasn't specified and + # prompt them with a page that allows them to choose an attachment. + # Happens when calling plain attachment.cgi from the urlbar directly + if ($param eq 'id' && !$cgi->param('id')) { + print $cgi->header(); + $template->process("attachment/choose.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + exit; + } + + my $attach_id = $cgi->param($param); + + # Validate the specified attachment id. detaint kills $attach_id if + # non-natural, so use the original value from $cgi in our exception + # message here. + detaint_natural($attach_id) + || ThrowUserError("invalid_attach_id", + {attach_id => scalar $cgi->param($param)}); + + # Make sure the attachment exists in the database. + my $attachment = new Bugzilla::Attachment({id => $attach_id, cache => 1}) + || ThrowUserError("invalid_attach_id", {attach_id => $attach_id}); + + return $attachment if ($dont_validate_access || check_can_access($attachment)); } # Make sure the current user has access to the specified attachment. sub check_can_access { - my $attachment = shift; - my $user = Bugzilla->user; - - # Make sure the user is authorized to access this attachment's bug. - Bugzilla::Bug->check({ id => $attachment->bug_id, cache => 1 }); - if ($attachment->isprivate && $user->id != $attachment->attacher->id - && !$user->is_insider) - { - ThrowUserError('auth_failure', {action => 'access', - object => 'attachment', - attach_id => $attachment->id}); - } - return 1; + my $attachment = shift; + my $user = Bugzilla->user; + + # Make sure the user is authorized to access this attachment's bug. + Bugzilla::Bug->check({id => $attachment->bug_id, cache => 1}); + if ( $attachment->isprivate + && $user->id != $attachment->attacher->id + && !$user->is_insider) + { + ThrowUserError('auth_failure', + {action => 'access', object => 'attachment', attach_id => $attachment->id}); + } + return 1; } # Determines if the attachment is public -- that is, if users who are # not logged in have access to the attachment sub attachmentIsPublic { - my $attachment = shift; + my $attachment = shift; - return 0 if Bugzilla->params->{'requirelogin'}; - return 0 if $attachment->isprivate; + return 0 if Bugzilla->params->{'requirelogin'}; + return 0 if $attachment->isprivate; - my $anon_user = new Bugzilla::User; - return $anon_user->can_see_bug($attachment->bug_id); + my $anon_user = new Bugzilla::User; + return $anon_user->can_see_bug($attachment->bug_id); } # Validates format of a diff/interdiff. Takes a list as an parameter, which # defines the valid format values. Will throw an error if the format is not # in the list. Returns either the user selected or default format. sub validateFormat { + # receives a list of legal formats; first item is a default my $format = $cgi->param('format') || $_[0]; if (not grep($_ eq $format, @_)) { - ThrowUserError("invalid_format", { format => $format, formats => \@_ }); + ThrowUserError("invalid_format", {format => $format, formats => \@_}); } return $format; @@ -196,125 +188,139 @@ sub validateFormat { # Gets the attachment object(s) generated by validateID, while ensuring # attachbase and token authentication is used when required. sub get_attachment { - my @field_names = @_ ? @_ : qw(id); - - my %attachments; - - if (use_attachbase()) { - # Load each attachment, and ensure they are all from the same bug - my $bug_id = 0; + my @field_names = @_ ? @_ : qw(id); + + my %attachments; + + if (use_attachbase()) { + + # Load each attachment, and ensure they are all from the same bug + my $bug_id = 0; + foreach my $field_name (@field_names) { + my $attachment = validateID($field_name, 1); + if (!$bug_id) { + $bug_id = $attachment->bug_id; + } + elsif ($attachment->bug_id != $bug_id) { + ThrowUserError('attachment_bug_id_mismatch'); + } + $attachments{$field_name} = $attachment; + } + my @args = map { $_ . '=' . $attachments{$_}->id } @field_names; + my $cgi_params = $cgi->canonicalise_query(@field_names, 't', 'Bugzilla_login', + 'Bugzilla_password'); + push(@args, $cgi_params) if $cgi_params; + my $path = 'attachment.cgi?' . join('&', @args); + + # Make sure the attachment is served from the correct server. + if ($cgi->url_is_attachment_base($bug_id)) { + + # No need to validate the token for public attachments. We cannot request + # credentials as we are on the alternate host. + if (!all_attachments_are_public(\%attachments)) { + my $token = $cgi->param('t'); + my ($userid, undef, $token_data) = Bugzilla::Token::GetTokenData($token); + my %token_data = unpack_token_data($token_data); + my $valid_token = 1; foreach my $field_name (@field_names) { - my $attachment = validateID($field_name, 1); - if (!$bug_id) { - $bug_id = $attachment->bug_id; - } elsif ($attachment->bug_id != $bug_id) { - ThrowUserError('attachment_bug_id_mismatch'); - } - $attachments{$field_name} = $attachment; - } - my @args = map { $_ . '=' . $attachments{$_}->id } @field_names; - my $cgi_params = $cgi->canonicalise_query(@field_names, 't', - 'Bugzilla_login', 'Bugzilla_password'); - push(@args, $cgi_params) if $cgi_params; - my $path = 'attachment.cgi?' . join('&', @args); - - # Make sure the attachment is served from the correct server. - if ($cgi->url_is_attachment_base($bug_id)) { - # No need to validate the token for public attachments. We cannot request - # credentials as we are on the alternate host. - if (!all_attachments_are_public(\%attachments)) { - my $token = $cgi->param('t'); - my ($userid, undef, $token_data) = Bugzilla::Token::GetTokenData($token); - my %token_data = unpack_token_data($token_data); - my $valid_token = 1; - foreach my $field_name (@field_names) { - my $token_id = $token_data{$field_name}; - if (!$token_id - || !detaint_natural($token_id) - || $attachments{$field_name}->id != $token_id) - { - $valid_token = 0; - last; - } - } - unless ($userid && $valid_token) { - # Not a valid token. - print $cgi->redirect('-location' => correct_urlbase() . $path); - exit; - } - # Change current user without creating cookies. - Bugzilla->set_user(new Bugzilla::User($userid)); - # Tokens are single use only, delete it. - delete_token($token); - } + my $token_id = $token_data{$field_name}; + if ( !$token_id + || !detaint_natural($token_id) + || $attachments{$field_name}->id != $token_id) + { + $valid_token = 0; + last; + } } - elsif ($cgi->url_is_attachment_base) { - # If we come here, this means that each bug has its own host - # for attachments, and that we are trying to view one attachment - # using another bug's host. That's not desired. - $cgi->redirect_to_urlbase; - } - else { - # We couldn't call Bugzilla->login earlier as we first had to - # make sure we were not going to request credentials on the - # alternate host. - Bugzilla->login(); - my $attachbase = Bugzilla->params->{'attachment_base'}; - # Replace %bugid% by the ID of the bug the attachment - # belongs to, if present. - $attachbase =~ s/\%bugid\%/$bug_id/; - if (all_attachments_are_public(\%attachments)) { - # No need for a token; redirect to attachment base. - print $cgi->redirect(-location => $attachbase . $path); - exit; - } else { - # Make sure the user can view the attachment. - foreach my $field_name (@field_names) { - check_can_access($attachments{$field_name}); - } - # Create a token and redirect. - my $token = url_quote(issue_session_token(pack_token_data(\%attachments))); - print $cgi->redirect(-location => $attachbase . "$path&t=$token"); - exit; - } + unless ($userid && $valid_token) { + + # Not a valid token. + print $cgi->redirect('-location' => correct_urlbase() . $path); + exit; } - } else { - do_ssl_redirect_if_required(); - # No alternate host is used. Request credentials if required. - Bugzilla->login(); + + # Change current user without creating cookies. + Bugzilla->set_user(new Bugzilla::User($userid)); + + # Tokens are single use only, delete it. + delete_token($token); + } + } + elsif ($cgi->url_is_attachment_base) { + + # If we come here, this means that each bug has its own host + # for attachments, and that we are trying to view one attachment + # using another bug's host. That's not desired. + $cgi->redirect_to_urlbase; + } + else { + # We couldn't call Bugzilla->login earlier as we first had to + # make sure we were not going to request credentials on the + # alternate host. + Bugzilla->login(); + my $attachbase = Bugzilla->params->{'attachment_base'}; + + # Replace %bugid% by the ID of the bug the attachment + # belongs to, if present. + $attachbase =~ s/\%bugid\%/$bug_id/; + if (all_attachments_are_public(\%attachments)) { + + # No need for a token; redirect to attachment base. + print $cgi->redirect(-location => $attachbase . $path); + exit; + } + else { + # Make sure the user can view the attachment. foreach my $field_name (@field_names) { - $attachments{$field_name} = validateID($field_name); + check_can_access($attachments{$field_name}); } + + # Create a token and redirect. + my $token = url_quote(issue_session_token(pack_token_data(\%attachments))); + print $cgi->redirect(-location => $attachbase . "$path&t=$token"); + exit; + } } + } + else { + do_ssl_redirect_if_required(); + + # No alternate host is used. Request credentials if required. + Bugzilla->login(); + foreach my $field_name (@field_names) { + $attachments{$field_name} = validateID($field_name); + } + } - return wantarray - ? map { $attachments{$_} } @field_names - : $attachments{$field_names[0]}; + return + wantarray + ? map { $attachments{$_} } @field_names + : $attachments{$field_names[0]}; } sub all_attachments_are_public { - my $attachments = shift; - foreach my $field_name (keys %$attachments) { - if (!attachmentIsPublic($attachments->{$field_name})) { - return 0; - } + my $attachments = shift; + foreach my $field_name (keys %$attachments) { + if (!attachmentIsPublic($attachments->{$field_name})) { + return 0; } - return 1; + } + return 1; } sub pack_token_data { - my $attachments = shift; - return join(' ', map { $_ . '=' . $attachments->{$_}->id } keys %$attachments); + my $attachments = shift; + return join(' ', map { $_ . '=' . $attachments->{$_}->id } keys %$attachments); } sub unpack_token_data { - my @token_data = split(/ /, shift || ''); - my %data; - foreach my $token (@token_data) { - my ($field_name, $attach_id) = split('=', $token); - $data{$field_name} = $attach_id; - } - return %data; + my @token_data = split(/ /, shift || ''); + my %data; + foreach my $token (@token_data) { + my ($field_name, $attach_id) = split('=', $token); + $data{$field_name} = $attach_id; + } + return %data; } ################################################################################ @@ -323,286 +329,311 @@ sub unpack_token_data { # Display an attachment. sub view { - my $attachment = get_attachment(); + my $attachment = get_attachment(); - # At this point, Bugzilla->login has been called if it had to. - my $contenttype = $attachment->contenttype; - my $filename = $attachment->filename; + # At this point, Bugzilla->login has been called if it had to. + my $contenttype = $attachment->contenttype; + my $filename = $attachment->filename; - # Bug 111522: allow overriding content-type manually in the posted form - # params. - if (my $content_type = $cgi->param('content_type')) { - $contenttype = $attachment->_check_content_type($content_type); - } + # Bug 111522: allow overriding content-type manually in the posted form + # params. + if (my $content_type = $cgi->param('content_type')) { + $contenttype = $attachment->_check_content_type($content_type); + } - # Return the appropriate HTTP response headers. - $attachment->datasize || ThrowUserError("attachment_removed"); - - $filename =~ s/^.*[\/\\]//; - # escape quotes and backslashes in the filename, per RFCs 2045/822 - $filename =~ s/\\/\\\\/g; # escape backslashes - $filename =~ s/"/\\"/g; # escape quotes - # Follow RFC 6266 section 4.1 (which itself points to RFC 5987 section 3.2) - $filename = uri_escape_utf8($filename); - - my $disposition = (Bugzilla->params->{'allow_attachment_display'} - && $attachment->is_viewable) ? 'inline' : 'attachment'; - - # Don't send a charset header with attachments--they might not be UTF-8. - # However, we do allow people to explicitly specify a charset if they - # want. - if ($contenttype !~ /\bcharset=/i) { - # In order to prevent Apache from adding a charset, we have to send a - # charset that's a single space. - $cgi->charset(' '); - if (Bugzilla->feature('detect_charset') && $contenttype =~ /^text\//) { - my $encoding = detect_encoding($attachment->data); - if ($encoding) { - $cgi->charset(find_encoding($encoding)->mime_name); - } - } - } - my $sendfile_header = {}; - my $sendfile_param = Bugzilla->params->{'xsendfile_header'}; - if ($attachment->is_on_filesystem && $sendfile_param ne 'off') { - # attachment is on filesystem and Admin turned on feature. - # This means we can let webserver handle the request and stream the file - # for us. This is called the X-Sendfile feature. see bug 1073264. - my $attachment_path = realpath($attachment->_get_local_filename()); - $sendfile_header->{$sendfile_param} = $attachment_path; - } - # IE8 and older do not support RFC 6266. So for these old browsers - # we still pass the old 'filename' attribute. Modern browsers will - # automatically pick the new 'filename*' attribute. - print $cgi->header(-type=> $contenttype, - -content_disposition=> "$disposition; filename=\"$filename\"; filename*=UTF-8''$filename", - -content_length => $attachment->datasize, - %$sendfile_header); - if ($attachment->is_on_filesystem && $sendfile_param ne 'off') { - # in case of X-Sendfile, we do not print the data. - # that is handled directly by the webserver. - return; + # Return the appropriate HTTP response headers. + $attachment->datasize || ThrowUserError("attachment_removed"); + + $filename =~ s/^.*[\/\\]//; + + # escape quotes and backslashes in the filename, per RFCs 2045/822 + $filename =~ s/\\/\\\\/g; # escape backslashes + $filename =~ s/"/\\"/g; # escape quotes + # Follow RFC 6266 section 4.1 (which itself points to RFC 5987 section 3.2) + $filename = uri_escape_utf8($filename); + + my $disposition + = (Bugzilla->params->{'allow_attachment_display'} && $attachment->is_viewable) + ? 'inline' + : 'attachment'; + + # Don't send a charset header with attachments--they might not be UTF-8. + # However, we do allow people to explicitly specify a charset if they + # want. + if ($contenttype !~ /\bcharset=/i) { + + # In order to prevent Apache from adding a charset, we have to send a + # charset that's a single space. + $cgi->charset(' '); + if (Bugzilla->feature('detect_charset') && $contenttype =~ /^text\//) { + my $encoding = detect_encoding($attachment->data); + if ($encoding) { + $cgi->charset(find_encoding($encoding)->mime_name); + } } - disable_utf8(); - print $attachment->data; + } + my $sendfile_header = {}; + my $sendfile_param = Bugzilla->params->{'xsendfile_header'}; + if ($attachment->is_on_filesystem && $sendfile_param ne 'off') { + + # attachment is on filesystem and Admin turned on feature. + # This means we can let webserver handle the request and stream the file + # for us. This is called the X-Sendfile feature. see bug 1073264. + my $attachment_path = realpath($attachment->_get_local_filename()); + $sendfile_header->{$sendfile_param} = $attachment_path; + } + + # IE8 and older do not support RFC 6266. So for these old browsers + # we still pass the old 'filename' attribute. Modern browsers will + # automatically pick the new 'filename*' attribute. + print $cgi->header( + -type => $contenttype, + -content_disposition => + "$disposition; filename=\"$filename\"; filename*=UTF-8''$filename", + -content_length => $attachment->datasize, + %$sendfile_header + ); + if ($attachment->is_on_filesystem && $sendfile_param ne 'off') { + + # in case of X-Sendfile, we do not print the data. + # that is handled directly by the webserver. + return; + } + disable_utf8(); + print $attachment->data; } sub interdiff { - # Retrieve and validate parameters - my $format = validateFormat('html', 'raw'); - my($old_attachment, $new_attachment); - if ($format eq 'raw') { - ($old_attachment, $new_attachment) = get_attachment('oldid', 'newid'); - } else { - $old_attachment = validateID('oldid'); - $new_attachment = validateID('newid'); - } - Bugzilla::Attachment::PatchReader::process_interdiff( - $old_attachment, $new_attachment, $format); + # Retrieve and validate parameters + my $format = validateFormat('html', 'raw'); + my ($old_attachment, $new_attachment); + if ($format eq 'raw') { + ($old_attachment, $new_attachment) = get_attachment('oldid', 'newid'); + } + else { + $old_attachment = validateID('oldid'); + $new_attachment = validateID('newid'); + } + + Bugzilla::Attachment::PatchReader::process_interdiff($old_attachment, + $new_attachment, $format); } sub diff { - # Retrieve and validate parameters - my $format = validateFormat('html', 'raw'); - my $attachment = $format eq 'raw' ? get_attachment() : validateID(); - - # If it is not a patch, view normally. - if (!$attachment->ispatch) { - view(); - return; - } - Bugzilla::Attachment::PatchReader::process_diff($attachment, $format); + # Retrieve and validate parameters + my $format = validateFormat('html', 'raw'); + my $attachment = $format eq 'raw' ? get_attachment() : validateID(); + + # If it is not a patch, view normally. + if (!$attachment->ispatch) { + view(); + return; + } + + Bugzilla::Attachment::PatchReader::process_diff($attachment, $format); } # Display all attachments for a given bug in a series of IFRAMEs within one # HTML page. sub viewall { - # Retrieve and validate parameters - my $bug = Bugzilla::Bug->check({ id => scalar $cgi->param('bugid'), cache => 1 }); - my $attachments = Bugzilla::Attachment->get_attachments_by_bug($bug); - # Ignore deleted attachments. - @$attachments = grep { $_->datasize } @$attachments; + # Retrieve and validate parameters + my $bug = Bugzilla::Bug->check({id => scalar $cgi->param('bugid'), cache => 1}); - if ($cgi->param('hide_obsolete')) { - @$attachments = grep { !$_->isobsolete } @$attachments; - $vars->{'hide_obsolete'} = 1; - } + my $attachments = Bugzilla::Attachment->get_attachments_by_bug($bug); - # Define the variables and functions that will be passed to the UI template. - $vars->{'bug'} = $bug; - $vars->{'attachments'} = $attachments; + # Ignore deleted attachments. + @$attachments = grep { $_->datasize } @$attachments; - print $cgi->header(); + if ($cgi->param('hide_obsolete')) { + @$attachments = grep { !$_->isobsolete } @$attachments; + $vars->{'hide_obsolete'} = 1; + } - # Generate and return the UI (HTML page) from the appropriate template. - $template->process("attachment/show-multiple.html.tmpl", $vars) - || ThrowTemplateError($template->error()); + # Define the variables and functions that will be passed to the UI template. + $vars->{'bug'} = $bug; + $vars->{'attachments'} = $attachments; + + print $cgi->header(); + + # Generate and return the UI (HTML page) from the appropriate template. + $template->process("attachment/show-multiple.html.tmpl", $vars) + || ThrowTemplateError($template->error()); } # Display a form for entering a new attachment. sub enter { - # Retrieve and validate parameters - my $bug = Bugzilla::Bug->check(scalar $cgi->param('bugid')); - my $bugid = $bug->id; - Bugzilla::Attachment->_check_bug($bug); - my $dbh = Bugzilla->dbh; - my $user = Bugzilla->user; - - # Retrieve the attachments the user can edit from the database and write - # them into an array of hashes where each hash represents one attachment. - - my ($can_edit, $not_private) = ('', ''); - if (!$user->in_group('editbugs', $bug->product_id)) { - $can_edit = "AND submitter_id = " . $user->id; - } - if (!$user->is_insider) { - $not_private = "AND isprivate = 0"; - } - my $attach_ids = $dbh->selectcol_arrayref( - "SELECT attach_id + + # Retrieve and validate parameters + my $bug = Bugzilla::Bug->check(scalar $cgi->param('bugid')); + my $bugid = $bug->id; + Bugzilla::Attachment->_check_bug($bug); + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->user; + + # Retrieve the attachments the user can edit from the database and write + # them into an array of hashes where each hash represents one attachment. + + my ($can_edit, $not_private) = ('', ''); + if (!$user->in_group('editbugs', $bug->product_id)) { + $can_edit = "AND submitter_id = " . $user->id; + } + if (!$user->is_insider) { + $not_private = "AND isprivate = 0"; + } + my $attach_ids = $dbh->selectcol_arrayref( + "SELECT attach_id FROM attachments WHERE bug_id = ? AND isobsolete = 0 $can_edit $not_private - ORDER BY attach_id", - undef, $bugid); - - # Define the variables and functions that will be passed to the UI template. - $vars->{'bug'} = $bug; - $vars->{'attachments'} = Bugzilla::Attachment->new_from_list($attach_ids); - - my $flag_types = Bugzilla::FlagType::match({ - 'target_type' => 'attachment', - 'product_id' => $bug->product_id, - 'component_id' => $bug->component_id - }); - $vars->{'flag_types'} = $flag_types; - $vars->{'any_flags_requesteeble'} = - grep { $_->is_requestable && $_->is_requesteeble } @$flag_types; - $vars->{'token'} = issue_session_token('create_attachment'); - - print $cgi->header(); - - # Generate and return the UI (HTML page) from the appropriate template. - $template->process("attachment/create.html.tmpl", $vars) - || ThrowTemplateError($template->error()); + ORDER BY attach_id", undef, $bugid + ); + + # Define the variables and functions that will be passed to the UI template. + $vars->{'bug'} = $bug; + $vars->{'attachments'} = Bugzilla::Attachment->new_from_list($attach_ids); + + my $flag_types = Bugzilla::FlagType::match({ + 'target_type' => 'attachment', + 'product_id' => $bug->product_id, + 'component_id' => $bug->component_id + }); + $vars->{'flag_types'} = $flag_types; + $vars->{'any_flags_requesteeble'} + = grep { $_->is_requestable && $_->is_requesteeble } @$flag_types; + $vars->{'token'} = issue_session_token('create_attachment'); + + print $cgi->header(); + + # Generate and return the UI (HTML page) from the appropriate template. + $template->process("attachment/create.html.tmpl", $vars) + || ThrowTemplateError($template->error()); } # Insert a new attachment into the database. sub insert { - my $dbh = Bugzilla->dbh; - my $user = Bugzilla->user; - - $dbh->bz_start_transaction; - - # Retrieve and validate parameters - my $bug = Bugzilla::Bug->check(scalar $cgi->param('bugid')); - my $bugid = $bug->id; - my ($timestamp) = $dbh->selectrow_array("SELECT NOW()"); + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->user; + + $dbh->bz_start_transaction; + + # Retrieve and validate parameters + my $bug = Bugzilla::Bug->check(scalar $cgi->param('bugid')); + my $bugid = $bug->id; + my ($timestamp) = $dbh->selectrow_array("SELECT NOW()"); + + # Detect if the user already used the same form to submit an attachment + my $token = trim(scalar $cgi->param('token')); + check_token_data($token, 'create_attachment', 'index.cgi'); + + # Check attachments the user tries to mark as obsolete. + my @obsolete_attachments; + if ($cgi->param('obsolete')) { + my @obsolete = $cgi->multi_param('obsolete'); + @obsolete_attachments + = Bugzilla::Attachment->validate_obsolete($bug, \@obsolete); + } - # Detect if the user already used the same form to submit an attachment - my $token = trim(scalar $cgi->param('token')); - check_token_data($token, 'create_attachment', 'index.cgi'); + my $minor_update = $cgi->param('minor_update') ? 1 : 0; + $minor_update = $bug->has_unsent_changes ? 0 : $minor_update; + + # Must be called before create() as it may alter $cgi->param('ispatch'). + my $content_type = Bugzilla::Attachment::get_content_type(); + + # Get the filehandle of the attachment. + my $data_fh = $cgi->upload('data'); + my $attach_text = $cgi->param('attach_text'); + + my $attachment = Bugzilla::Attachment->create({ + bug => $bug, + creation_ts => $timestamp, + data => $attach_text || $data_fh, + description => scalar $cgi->param('description'), + filename => $attach_text ? "file_$bugid.txt" : $data_fh, + ispatch => scalar $cgi->param('ispatch'), + isprivate => scalar $cgi->param('isprivate'), + mimetype => $content_type, + }); + + # Delete the token used to create this attachment. + delete_token($token); + + foreach my $obsolete_attachment (@obsolete_attachments) { + $obsolete_attachment->set_is_obsolete(1); + $obsolete_attachment->update($timestamp); + } - # Check attachments the user tries to mark as obsolete. - my @obsolete_attachments; - if ($cgi->param('obsolete')) { - my @obsolete = $cgi->multi_param('obsolete'); - @obsolete_attachments = Bugzilla::Attachment->validate_obsolete($bug, \@obsolete); + my ($flags, $new_flags) + = Bugzilla::Flag->extract_flags_from_cgi($vars, SKIP_REQUESTEE_ON_ERROR, + {bug => $bug, attachment => $attachment}); + $attachment->set_flags($flags, $new_flags); + + # Insert a comment about the new attachment into the database. + my $comment = $cgi->param('comment'); + $comment = '' unless defined $comment; + my $is_markdown = ($user->use_markdown && $cgi->param('use_markdown')) ? 1 : 0; + $bug->add_comment( + $comment, + { + isprivate => $attachment->isprivate, + type => CMT_ATTACHMENT_CREATED, + extra_data => $attachment->id, + is_markdown => $is_markdown } + ); - my $minor_update = $cgi->param('minor_update') ? 1 : 0; - $minor_update = $bug->has_unsent_changes ? 0 : $minor_update; - - # Must be called before create() as it may alter $cgi->param('ispatch'). - my $content_type = Bugzilla::Attachment::get_content_type(); - - # Get the filehandle of the attachment. - my $data_fh = $cgi->upload('data'); - my $attach_text = $cgi->param('attach_text'); + # Assign the bug to the user, if they are allowed to take it + my $owner = ""; + if ($cgi->param('takebug') && $user->in_group('editbugs', $bug->product_id)) { - my $attachment = Bugzilla::Attachment->create( - {bug => $bug, - creation_ts => $timestamp, - data => $attach_text || $data_fh, - description => scalar $cgi->param('description'), - filename => $attach_text ? "file_$bugid.txt" : $data_fh, - ispatch => scalar $cgi->param('ispatch'), - isprivate => scalar $cgi->param('isprivate'), - mimetype => $content_type, - }); - - # Delete the token used to create this attachment. - delete_token($token); + # When taking a bug, we have to follow the workflow. + my $bug_status = $cgi->param('bug_status') || ''; + ($bug_status) = grep { $_->name eq $bug_status } @{$bug->status->can_change_to}; - foreach my $obsolete_attachment (@obsolete_attachments) { - $obsolete_attachment->set_is_obsolete(1); - $obsolete_attachment->update($timestamp); + if ( $bug_status + && $bug_status->is_open + && ($bug_status->name ne 'UNCONFIRMED' || $bug->product_obj->allows_unconfirmed) + ) + { + $bug->set_bug_status($bug_status->name); + $bug->clear_resolution(); } - my ($flags, $new_flags) = Bugzilla::Flag->extract_flags_from_cgi( - $vars, SKIP_REQUESTEE_ON_ERROR, - { bug => $bug, attachment => $attachment }); - $attachment->set_flags($flags, $new_flags); + # Make sure the person we are taking the bug from gets mail. + $owner = $bug->assigned_to->login; + $bug->set_assigned_to($user); + } - # Insert a comment about the new attachment into the database. - my $comment = $cgi->param('comment'); - $comment = '' unless defined $comment; - my $is_markdown = ($user->use_markdown - && $cgi->param('use_markdown')) ? 1 : 0; - $bug->add_comment($comment, { isprivate => $attachment->isprivate, - type => CMT_ATTACHMENT_CREATED, - extra_data => $attachment->id, - is_markdown => $is_markdown}); - - # Assign the bug to the user, if they are allowed to take it - my $owner = ""; - if ($cgi->param('takebug') && $user->in_group('editbugs', $bug->product_id)) { - # When taking a bug, we have to follow the workflow. - my $bug_status = $cgi->param('bug_status') || ''; - ($bug_status) = grep { $_->name eq $bug_status } - @{ $bug->status->can_change_to }; - - if ($bug_status && $bug_status->is_open - && ($bug_status->name ne 'UNCONFIRMED' - || $bug->product_obj->allows_unconfirmed)) - { - $bug->set_bug_status($bug_status->name); - $bug->clear_resolution(); - } - # Make sure the person we are taking the bug from gets mail. - $owner = $bug->assigned_to->login; - $bug->set_assigned_to($user); - } + $bug->add_cc($user) if $cgi->param('addselfcc'); + $bug->update($timestamp); - $bug->add_cc($user) if $cgi->param('addselfcc'); - $bug->update($timestamp); + # We have to update the attachment after updating the bug, to ensure new + # comments are available. + $attachment->update($timestamp); - # We have to update the attachment after updating the bug, to ensure new - # comments are available. - $attachment->update($timestamp); + $dbh->bz_commit_transaction; - $dbh->bz_commit_transaction; + # Define the variables and functions that will be passed to the UI template. + $vars->{'attachment'} = $attachment; - # Define the variables and functions that will be passed to the UI template. - $vars->{'attachment'} = $attachment; - # We cannot reuse the $bug object as delta_ts has eventually been updated - # since the object was created. - $vars->{'bugs'} = [new Bugzilla::Bug($bugid)]; - $vars->{'header_done'} = 1; - $vars->{'contenttypemethod'} = $cgi->param('contenttypemethod'); + # We cannot reuse the $bug object as delta_ts has eventually been updated + # since the object was created. + $vars->{'bugs'} = [new Bugzilla::Bug($bugid)]; + $vars->{'header_done'} = 1; + $vars->{'contenttypemethod'} = $cgi->param('contenttypemethod'); - my $recipients = { 'changer' => $user, 'owner' => $owner }; - my $params = { 'minor_update' => $minor_update }; - $vars->{'sent_bugmail'} = Bugzilla::BugMail::Send($bugid, $recipients, $params); + my $recipients = {'changer' => $user, 'owner' => $owner}; + my $params = {'minor_update' => $minor_update}; + $vars->{'sent_bugmail'} = Bugzilla::BugMail::Send($bugid, $recipients, $params); - print $cgi->header(); - # Generate and return the UI (HTML page) from the appropriate template. - $template->process("attachment/created.html.tmpl", $vars) - || ThrowTemplateError($template->error()); + print $cgi->header(); + + # Generate and return the UI (HTML page) from the appropriate template. + $template->process("attachment/created.html.tmpl", $vars) + || ThrowTemplateError($template->error()); } # Displays a form for editing attachment properties. @@ -610,231 +641,244 @@ sub insert { # is private and the user does not belong to the insider group. # Validations are done later when the user submits changes. sub edit { - my $attachment = validateID(); + my $attachment = validateID(); - my $bugattachments = - Bugzilla::Attachment->get_attachments_by_bug($attachment->bug); + my $bugattachments + = Bugzilla::Attachment->get_attachments_by_bug($attachment->bug); - my $any_flags_requesteeble = grep { $_->is_requestable && $_->is_requesteeble } - @{ $attachment->flag_types }; - # Useful in case a flagtype is no longer requestable but a requestee - # has been set before we turned off that bit. - $any_flags_requesteeble ||= grep { $_->requestee_id } @{ $attachment->flags }; - $vars->{'any_flags_requesteeble'} = $any_flags_requesteeble; - $vars->{'attachment'} = $attachment; - $vars->{'attachments'} = $bugattachments; + my $any_flags_requesteeble = grep { $_->is_requestable && $_->is_requesteeble } + @{$attachment->flag_types}; - print $cgi->header(); + # Useful in case a flagtype is no longer requestable but a requestee + # has been set before we turned off that bit. + $any_flags_requesteeble ||= grep { $_->requestee_id } @{$attachment->flags}; + $vars->{'any_flags_requesteeble'} = $any_flags_requesteeble; + $vars->{'attachment'} = $attachment; + $vars->{'attachments'} = $bugattachments; + + print $cgi->header(); - # Generate and return the UI (HTML page) from the appropriate template. - $template->process("attachment/edit.html.tmpl", $vars) - || ThrowTemplateError($template->error()); + # Generate and return the UI (HTML page) from the appropriate template. + $template->process("attachment/edit.html.tmpl", $vars) + || ThrowTemplateError($template->error()); } # Updates an attachment record. Only users with "editbugs" privileges, # (or the original attachment's submitter) can edit the attachment. # Users cannot edit the content of the attachment itself. sub update { - my $user = Bugzilla->user; - my $dbh = Bugzilla->dbh; - - # Start a transaction in preparation for updating the attachment. - $dbh->bz_start_transaction(); - - # Retrieve and validate parameters - my $attachment = validateID(); - my $bug = $attachment->bug; - $attachment->_check_bug; - my $can_edit = $attachment->validate_can_edit; - - if ($can_edit) { - $attachment->set_description(scalar $cgi->param('description')); - $attachment->set_is_patch(scalar $cgi->param('ispatch')); - $attachment->set_content_type(scalar $cgi->param('contenttypeentry')); - $attachment->set_is_obsolete(scalar $cgi->param('isobsolete')); - $attachment->set_is_private(scalar $cgi->param('isprivate')); - $attachment->set_filename(scalar $cgi->param('filename')); - - # Now make sure the attachment has not been edited since we loaded the page. - my $delta_ts = $cgi->param('delta_ts'); - my $modification_time = $attachment->modification_time; - - if ($delta_ts && $delta_ts ne $modification_time) { - datetime_from($delta_ts) - or ThrowCodeError('invalid_timestamp', { timestamp => $delta_ts }); - ($vars->{'operations'}) = $bug->get_activity($attachment->id, $delta_ts); - - # If the modification date changed but there is no entry in - # the activity table, this means someone commented only. - # In this case, there is no reason to midair. - if (scalar(@{$vars->{'operations'}})) { - $cgi->param('delta_ts', $modification_time); - # The token contains the old modification_time. We need a new one. - $cgi->param('token', issue_hash_token([$attachment->id, $modification_time])); - - $vars->{'attachment'} = $attachment; - - print $cgi->header(); - # Warn the user about the mid-air collision and ask them what to do. - $template->process("attachment/midair.html.tmpl", $vars) - || ThrowTemplateError($template->error()); - exit; - } - } - } + my $user = Bugzilla->user; + my $dbh = Bugzilla->dbh; + + # Start a transaction in preparation for updating the attachment. + $dbh->bz_start_transaction(); + + # Retrieve and validate parameters + my $attachment = validateID(); + my $bug = $attachment->bug; + $attachment->_check_bug; + my $can_edit = $attachment->validate_can_edit; + + if ($can_edit) { + $attachment->set_description(scalar $cgi->param('description')); + $attachment->set_is_patch(scalar $cgi->param('ispatch')); + $attachment->set_content_type(scalar $cgi->param('contenttypeentry')); + $attachment->set_is_obsolete(scalar $cgi->param('isobsolete')); + $attachment->set_is_private(scalar $cgi->param('isprivate')); + $attachment->set_filename(scalar $cgi->param('filename')); + + # Now make sure the attachment has not been edited since we loaded the page. + my $delta_ts = $cgi->param('delta_ts'); + my $modification_time = $attachment->modification_time; + + if ($delta_ts && $delta_ts ne $modification_time) { + datetime_from($delta_ts) + or ThrowCodeError('invalid_timestamp', {timestamp => $delta_ts}); + ($vars->{'operations'}) = $bug->get_activity($attachment->id, $delta_ts); + + # If the modification date changed but there is no entry in + # the activity table, this means someone commented only. + # In this case, there is no reason to midair. + if (scalar(@{$vars->{'operations'}})) { + $cgi->param('delta_ts', $modification_time); + + # The token contains the old modification_time. We need a new one. + $cgi->param('token', issue_hash_token([$attachment->id, $modification_time])); - # We couldn't do this check earlier as we first had to validate attachment ID - # and display the mid-air collision page if modification_time changed. - my $token = $cgi->param('token'); - check_hash_token($token, [$attachment->id, $attachment->modification_time]); - - my $minor_update = $cgi->param('minor_update') ? 1 : 0; - $minor_update = $bug->has_unsent_changes ? 0 : $minor_update; - - # If the user submitted a comment while editing the attachment, - # add the comment to the bug. Do this after having validated isprivate! - my $comment = $cgi->param('comment'); - if (defined $comment && trim($comment) ne '') { - $bug->add_comment($comment, { isprivate => $attachment->isprivate, - type => CMT_ATTACHMENT_UPDATED, - extra_data => $attachment->id }); + $vars->{'attachment'} = $attachment; + + print $cgi->header(); + + # Warn the user about the mid-air collision and ask them what to do. + $template->process("attachment/midair.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + exit; + } } + } + + # We couldn't do this check earlier as we first had to validate attachment ID + # and display the mid-air collision page if modification_time changed. + my $token = $cgi->param('token'); + check_hash_token($token, [$attachment->id, $attachment->modification_time]); + + my $minor_update = $cgi->param('minor_update') ? 1 : 0; + $minor_update = $bug->has_unsent_changes ? 0 : $minor_update; + + # If the user submitted a comment while editing the attachment, + # add the comment to the bug. Do this after having validated isprivate! + my $comment = $cgi->param('comment'); + if (defined $comment && trim($comment) ne '') { + $bug->add_comment( + $comment, + { + isprivate => $attachment->isprivate, + type => CMT_ATTACHMENT_UPDATED, + extra_data => $attachment->id + } + ); + } + + $bug->add_cc($user) if $cgi->param('addselfcc'); - $bug->add_cc($user) if $cgi->param('addselfcc'); + my ($flags, $new_flags) + = Bugzilla::Flag->extract_flags_from_cgi($vars, undef, + {bug => $bug, attachment => $attachment}); - my ($flags, $new_flags) = - Bugzilla::Flag->extract_flags_from_cgi($vars, undef, { bug => $bug, attachment => $attachment }); + if ($can_edit) { + $attachment->set_flags($flags, $new_flags); + } - if ($can_edit) { - $attachment->set_flags($flags, $new_flags); + # Requestees can set flags targetted to them, even if they cannot + # edit the attachment. Flag setters can edit their own flags too. + elsif (scalar @$flags) { + my %flag_list = map { $_->{id} => $_ } @$flags; + my $flag_objs = Bugzilla::Flag->new_from_list([keys %flag_list]); + + my @editable_flags; + foreach my $flag_obj (@$flag_objs) { + if ($flag_obj->setter_id == $user->id + || ($flag_obj->requestee_id && $flag_obj->requestee_id == $user->id)) + { + push(@editable_flags, $flag_list{$flag_obj->id}); + } } - # Requestees can set flags targetted to them, even if they cannot - # edit the attachment. Flag setters can edit their own flags too. - elsif (scalar @$flags) { - my %flag_list = map { $_->{id} => $_ } @$flags; - my $flag_objs = Bugzilla::Flag->new_from_list([keys %flag_list]); - - my @editable_flags; - foreach my $flag_obj (@$flag_objs) { - if ($flag_obj->setter_id == $user->id - || ($flag_obj->requestee_id && $flag_obj->requestee_id == $user->id)) - { - push(@editable_flags, $flag_list{$flag_obj->id}); - } - } - if (scalar @editable_flags) { - $attachment->set_flags(\@editable_flags, []); - # Flag changes must be committed. - $can_edit = 1; - } + if (scalar @editable_flags) { + $attachment->set_flags(\@editable_flags, []); + + # Flag changes must be committed. + $can_edit = 1; } + } - # Figure out when the changes were made. - my $timestamp = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); + # Figure out when the changes were made. + my $timestamp = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); - # Commit the comment, if any. - # This has to happen before updating the attachment, to ensure new comments - # are available to $attachment->update. - $bug->update($timestamp); + # Commit the comment, if any. + # This has to happen before updating the attachment, to ensure new comments + # are available to $attachment->update. + $bug->update($timestamp); - if ($can_edit) { - my $changes = $attachment->update($timestamp); - # If there are changes, we updated delta_ts in the DB. We have to - # reflect this change in the bug object. - $bug->{delta_ts} = $timestamp if scalar(keys %$changes); - } + if ($can_edit) { + my $changes = $attachment->update($timestamp); - # Commit the transaction now that we are finished updating the database. - $dbh->bz_commit_transaction(); + # If there are changes, we updated delta_ts in the DB. We have to + # reflect this change in the bug object. + $bug->{delta_ts} = $timestamp if scalar(keys %$changes); + } - # Define the variables and functions that will be passed to the UI template. - $vars->{'attachment'} = $attachment; - $vars->{'bugs'} = [$bug]; - $vars->{'header_done'} = 1; - $vars->{'sent_bugmail'} = - Bugzilla::BugMail::Send($bug->id, { 'changer' => $user }, - {'minor_update' => $minor_update }); + # Commit the transaction now that we are finished updating the database. + $dbh->bz_commit_transaction(); - print $cgi->header(); + # Define the variables and functions that will be passed to the UI template. + $vars->{'attachment'} = $attachment; + $vars->{'bugs'} = [$bug]; + $vars->{'header_done'} = 1; + $vars->{'sent_bugmail'} = Bugzilla::BugMail::Send( + $bug->id, + {'changer' => $user}, + {'minor_update' => $minor_update} + ); - # Generate and return the UI (HTML page) from the appropriate template. - $template->process("attachment/updated.html.tmpl", $vars) - || ThrowTemplateError($template->error()); + print $cgi->header(); + + # Generate and return the UI (HTML page) from the appropriate template. + $template->process("attachment/updated.html.tmpl", $vars) + || ThrowTemplateError($template->error()); } # Only administrators can delete attachments. sub delete_attachment { - my $user = Bugzilla->login(LOGIN_REQUIRED); - my $dbh = Bugzilla->dbh; + my $user = Bugzilla->login(LOGIN_REQUIRED); + my $dbh = Bugzilla->dbh; - print $cgi->header(); + print $cgi->header(); - $user->in_group('admin') - || ThrowUserError('auth_failure', {group => 'admin', - action => 'delete', - object => 'attachment'}); - - Bugzilla->params->{'allow_attachment_deletion'} - || ThrowUserError('attachment_deletion_disabled'); - - # Make sure the administrator is allowed to edit this attachment. - my $attachment = validateID(); - Bugzilla::Attachment->_check_bug($attachment->bug); - - $attachment->datasize || ThrowUserError('attachment_removed'); - - # We don't want to let a malicious URL accidentally delete an attachment. - my $token = trim(scalar $cgi->param('token')); - if ($token) { - my ($creator_id, $date, $event) = Bugzilla::Token::GetTokenData($token); - unless ($creator_id - && ($creator_id == $user->id) - && ($event eq 'delete_attachment' . $attachment->id)) - { - # The token is invalid. - ThrowUserError('token_does_not_exist'); - } + $user->in_group('admin') + || ThrowUserError('auth_failure', + {group => 'admin', action => 'delete', object => 'attachment'}); - my $bug = new Bugzilla::Bug($attachment->bug_id); + Bugzilla->params->{'allow_attachment_deletion'} + || ThrowUserError('attachment_deletion_disabled'); - # The token is valid. Delete the content of the attachment. - my $msg; - $vars->{'attachment'} = $attachment; - $vars->{'reason'} = clean_text($cgi->param('reason') || ''); + # Make sure the administrator is allowed to edit this attachment. + my $attachment = validateID(); + Bugzilla::Attachment->_check_bug($attachment->bug); - $template->process("attachment/delete_reason.txt.tmpl", $vars, \$msg) - || ThrowTemplateError($template->error()); + $attachment->datasize || ThrowUserError('attachment_removed'); - # Paste the reason provided by the admin into a comment. - $bug->add_comment($msg); + # We don't want to let a malicious URL accidentally delete an attachment. + my $token = trim(scalar $cgi->param('token')); + if ($token) { + my ($creator_id, $date, $event) = Bugzilla::Token::GetTokenData($token); + unless ($creator_id + && ($creator_id == $user->id) + && ($event eq 'delete_attachment' . $attachment->id)) + { + # The token is invalid. + ThrowUserError('token_does_not_exist'); + } - $attachment->remove_from_db(); + my $bug = new Bugzilla::Bug($attachment->bug_id); - # Now delete the token. - delete_token($token); + # The token is valid. Delete the content of the attachment. + my $msg; + $vars->{'attachment'} = $attachment; + $vars->{'reason'} = clean_text($cgi->param('reason') || ''); - # Insert the comment. - $bug->update(); + $template->process("attachment/delete_reason.txt.tmpl", $vars, \$msg) + || ThrowTemplateError($template->error()); - # Required to display the bug the deleted attachment belongs to. - $vars->{'bugs'} = [$bug]; - $vars->{'header_done'} = 1; + # Paste the reason provided by the admin into a comment. + $bug->add_comment($msg); - $vars->{'sent_bugmail'} = - Bugzilla::BugMail::Send($bug->id, { 'changer' => $user }); + $attachment->remove_from_db(); - $template->process("attachment/updated.html.tmpl", $vars) - || ThrowTemplateError($template->error()); - } - else { - # Create a token. - $token = issue_session_token('delete_attachment' . $attachment->id); + # Now delete the token. + delete_token($token); - $vars->{'a'} = $attachment; - $vars->{'token'} = $token; + # Insert the comment. + $bug->update(); - $template->process("attachment/confirm-delete.html.tmpl", $vars) - || ThrowTemplateError($template->error()); - } + # Required to display the bug the deleted attachment belongs to. + $vars->{'bugs'} = [$bug]; + $vars->{'header_done'} = 1; + + $vars->{'sent_bugmail'} + = Bugzilla::BugMail::Send($bug->id, {'changer' => $user}); + + $template->process("attachment/updated.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + } + else { + # Create a token. + $token = issue_session_token('delete_attachment' . $attachment->id); + + $vars->{'a'} = $attachment; + $vars->{'token'} = $token; + + $template->process("attachment/confirm-delete.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + } } diff --git a/auth.cgi b/auth.cgi index 3f45909535..6ef3e2b282 100755 --- a/auth.cgi +++ b/auth.cgi @@ -28,108 +28,115 @@ use JSON qw(decode_json encode_json); Bugzilla->login(LOGIN_REQUIRED); -ThrowUserError('auth_delegation_disabled') unless Bugzilla->params->{auth_delegation}; +ThrowUserError('auth_delegation_disabled') + unless Bugzilla->params->{auth_delegation}; -my $cgi = Bugzilla->cgi; -my $template = Bugzilla->template; -my $user = Bugzilla->user; -my $callback = $cgi->param('callback') or ThrowUserError("auth_delegation_missing_callback"); -my $description = $cgi->param('description') or ThrowUserError("auth_delegation_missing_description"); +my $cgi = Bugzilla->cgi; +my $template = Bugzilla->template; +my $user = Bugzilla->user; +my $callback = $cgi->param('callback') + or ThrowUserError("auth_delegation_missing_callback"); +my $description = $cgi->param('description') + or ThrowUserError("auth_delegation_missing_description"); trick_taint($callback); trick_taint($description); -my $callback_uri = URI->new($callback); +my $callback_uri = URI->new($callback); $callback_uri->scheme =~ /^https?$/ - or ThrowUserError('auth_delegation_illegal_protocol', { protocol => $callback_uri->scheme }); + or ThrowUserError('auth_delegation_illegal_protocol', + {protocol => $callback_uri->scheme}); my $callback_base = $callback_uri->clone; $callback_base->query(undef); my $skip_confirmation = 0; -my %args = ( skip_confirmation => \$skip_confirmation, - callback => $callback_uri, - description => $description, - callback_base => $callback_base ); +my %args = ( + skip_confirmation => \$skip_confirmation, + callback => $callback_uri, + description => $description, + callback_base => $callback_base +); Bugzilla::Hook::process('auth_delegation_confirm', \%args); my $confirmed = lc($cgi->request_method) eq 'post' && $cgi->param('confirm'); if ($confirmed || $skip_confirmation) { - my $token = $cgi->param('token'); - unless ($skip_confirmation) { - ThrowUserError("auth_delegation_missing_token") unless $token; - trick_taint($token); - - unless (check_auth_delegation_token($token, $callback)) { - ThrowUserError('auth_delegation_invalid_token', - { token => $token, callback => $callback }); - } + my $token = $cgi->param('token'); + unless ($skip_confirmation) { + ThrowUserError("auth_delegation_missing_token") unless $token; + trick_taint($token); + + unless (check_auth_delegation_token($token, $callback)) { + ThrowUserError('auth_delegation_invalid_token', + {token => $token, callback => $callback}); } - my $app_id = sha256_hex($callback_uri, $description); - my $keys = Bugzilla::User::APIKey->match({ - user_id => $user->id, - app_id => $app_id, - revoked => 0, + } + my $app_id = sha256_hex($callback_uri, $description); + my $keys + = Bugzilla::User::APIKey->match({ + user_id => $user->id, app_id => $app_id, revoked => 0, }); - my $api_key; - if (@$keys) { - $api_key = $keys->[0]; - } - else { - $api_key = Bugzilla::User::APIKey->create({ - user_id => $user->id, - description => $description, - app_id => $app_id, - }); - my $template = Bugzilla->template_inner($user->setting('lang')); - my $vars = { user => $user, new_key => $api_key }; - my $message; - $template->process('email/new-api-key.txt.tmpl', $vars, \$message) - or ThrowTemplateError($template->error()); - - MessageToMTA($message); - } + my $api_key; + if (@$keys) { + $api_key = $keys->[0]; + } + else { + $api_key + = Bugzilla::User::APIKey->create({ + user_id => $user->id, description => $description, app_id => $app_id, + }); + my $template = Bugzilla->template_inner($user->setting('lang')); + my $vars = {user => $user, new_key => $api_key}; + my $message; + $template->process('email/new-api-key.txt.tmpl', $vars, \$message) + or ThrowTemplateError($template->error()); - my $ua = LWP::UserAgent->new(); - $ua->timeout(2); - $ua->protocols_allowed(['http', 'https']); - # If the URL of the proxy is given, use it, else get this information - # from the environment variable. - my $proxy_url = Bugzilla->params->{'proxy_url'}; - if ($proxy_url) { - $ua->proxy(['http', 'https'], $proxy_url); + MessageToMTA($message); + } + + my $ua = LWP::UserAgent->new(); + $ua->timeout(2); + $ua->protocols_allowed(['http', 'https']); + + # If the URL of the proxy is given, use it, else get this information + # from the environment variable. + my $proxy_url = Bugzilla->params->{'proxy_url'}; + if ($proxy_url) { + $ua->proxy(['http', 'https'], $proxy_url); + } + else { + $ua->env_proxy; + } + my $content = encode_json( + {client_api_key => $api_key->api_key, client_api_login => $user->login}); + my $resp = $ua->post( + $callback_uri, + 'Content-Type' => 'application/json', + Content => $content + ); + if ($resp->code == 200) { + $callback_uri->query_param(client_api_login => $user->login); + eval { + my $data = decode_json($resp->content); + $callback_uri->query_param(callback_result => $data->{result}); + }; + if ($@) { + ThrowUserError('auth_delegation_json_error', {json_text => $resp->content}); } else { - $ua->env_proxy; - } - my $content = encode_json({ client_api_key => $api_key->api_key, - client_api_login => $user->login }); - my $resp = $ua->post($callback_uri, - 'Content-Type' => 'application/json', - Content => $content); - if ($resp->code == 200) { - $callback_uri->query_param(client_api_login => $user->login); - eval { - my $data = decode_json($resp->content); - $callback_uri->query_param(callback_result => $data->{result}); - }; - if ($@) { - ThrowUserError('auth_delegation_json_error', { json_text => $resp->content }); - } - else { - print $cgi->redirect($callback_uri); - } - } - else { - ThrowUserError('auth_delegation_post_error', { code => $resp->code }); + print $cgi->redirect($callback_uri); } + } + else { + ThrowUserError('auth_delegation_post_error', {code => $resp->code}); + } } else { - $args{token} = issue_auth_delegation_token($callback); + $args{token} = issue_auth_delegation_token($callback); - print $cgi->header(); - $template->process("account/auth/delegation.html.tmpl", \%args) - or ThrowTemplateError($template->error()); + print $cgi->header(); + $template->process("account/auth/delegation.html.tmpl", \%args) + or ThrowTemplateError($template->error()); } diff --git a/buglist.cgi b/buglist.cgi index d046373530..1b92210e67 100755 --- a/buglist.cgi +++ b/buglist.cgi @@ -29,10 +29,10 @@ use Bugzilla::Hook; use Date::Parse; -my $cgi = Bugzilla->cgi; -my $dbh = Bugzilla->dbh; +my $cgi = Bugzilla->cgi; +my $dbh = Bugzilla->dbh; my $template = Bugzilla->template; -my $vars = {}; +my $vars = {}; # We have to check the login here to get the correct footer if an error is # thrown and to prevent a logged out user to use QuickSearch if 'requirelogin' @@ -43,26 +43,28 @@ $cgi->redirect_search_url(); my $buffer = $cgi->query_string(); if (length($buffer) == 0) { - ThrowUserError("buglist_parameters_required"); + ThrowUserError("buglist_parameters_required"); } # Determine whether this is a quicksearch query. my $searchstring = $cgi->param('quicksearch'); if (defined($searchstring)) { - $buffer = quicksearch($searchstring); - # Quicksearch may do a redirect, in which case it does not return. - # If it does return, it has modified $cgi->params so we can use them here - # as if this had been a normal query from the beginning. + $buffer = quicksearch($searchstring); + + # Quicksearch may do a redirect, in which case it does not return. + # If it does return, it has modified $cgi->params so we can use them here + # as if this had been a normal query from the beginning. } # If configured to not allow empty words, reject empty searches from the # Find a Specific Bug search form, including words being a single or # several consecutive whitespaces only. -if (!Bugzilla->params->{'search_allow_no_criteria'} - && defined($cgi->param('content')) && $cgi->param('content') =~ /^\s*$/) +if ( !Bugzilla->params->{'search_allow_no_criteria'} + && defined($cgi->param('content')) + && $cgi->param('content') =~ /^\s*$/) { - ThrowUserError("buglist_parameters_required"); + ThrowUserError("buglist_parameters_required"); } ################################################################################ @@ -74,19 +76,21 @@ my $dotweak = $cgi->param('tweak') ? 1 : 0; # Log the user in if ($dotweak) { - Bugzilla->login(LOGIN_REQUIRED); + Bugzilla->login(LOGIN_REQUIRED); } # Hack to support legacy applications that think the RDF ctype is at format=rdf. -if (defined $cgi->param('format') && $cgi->param('format') eq "rdf" - && !defined $cgi->param('ctype')) { - $cgi->param('ctype', "rdf"); - $cgi->delete('format'); +if ( defined $cgi->param('format') + && $cgi->param('format') eq "rdf" + && !defined $cgi->param('ctype')) +{ + $cgi->param('ctype', "rdf"); + $cgi->delete('format'); } # Treat requests for ctype=rss as requests for ctype=atom if (defined $cgi->param('ctype') && $cgi->param('ctype') eq "rss") { - $cgi->param('ctype', "atom"); + $cgi->param('ctype', "atom"); } my $order = $cgi->param('order') || ""; @@ -97,25 +101,26 @@ my $params; # If the user is retrieving the last bug list they looked at, hack the buffer # storing the query string so that it looks like a query retrieving those bugs. if (my $last_list = $cgi->param('regetlastlist')) { - my $bug_ids; - - # Logged-out users use the old cookie method for storing the last search. - if (!$user->id or $last_list eq 'cookie') { - $bug_ids = $cgi->cookie('BUGLIST') or ThrowUserError("missing_cookie"); - $bug_ids =~ s/[:-]/,/g; - $order ||= "reuse last sort"; - } - # But logged in users store the last X searches in the DB so they can - # have multiple bug lists available. - else { - my $last_search = Bugzilla::Search::Recent->check( - { id => $last_list }); - $bug_ids = join(',', @{ $last_search->bug_list }); - $order ||= $last_search->list_order; - } - # set up the params for this new query - $params = new Bugzilla::CGI({ bug_id => $bug_ids, order => $order }); - $params->param('list_id', $last_list); + my $bug_ids; + + # Logged-out users use the old cookie method for storing the last search. + if (!$user->id or $last_list eq 'cookie') { + $bug_ids = $cgi->cookie('BUGLIST') or ThrowUserError("missing_cookie"); + $bug_ids =~ s/[:-]/,/g; + $order ||= "reuse last sort"; + } + + # But logged in users store the last X searches in the DB so they can + # have multiple bug lists available. + else { + my $last_search = Bugzilla::Search::Recent->check({id => $last_list}); + $bug_ids = join(',', @{$last_search->bug_list}); + $order ||= $last_search->list_order; + } + + # set up the params for this new query + $params = new Bugzilla::CGI({bug_id => $bug_ids, order => $order}); + $params->param('list_id', $last_list); } # Figure out whether or not the user is doing a fulltext search. If not, @@ -125,10 +130,10 @@ my $fulltext = 0; if ($cgi->param('content')) { $fulltext = 1 } my @charts = map { /^field(\d-\d-\d)$/ ? $1 : () } $cgi->multi_param(); foreach my $chart (@charts) { - if ($cgi->param("field$chart") eq 'content' && $cgi->param("value$chart")) { - $fulltext = 1; - last; - } + if ($cgi->param("field$chart") eq 'content' && $cgi->param("value$chart")) { + $fulltext = 1; + last; + } } ################################################################################ @@ -136,34 +141,35 @@ foreach my $chart (@charts) { ################################################################################ sub DiffDate { - my ($datestr) = @_; - my $date = str2time($datestr); - my $age = time() - $date; - - if( $age < 18*60*60 ) { - $date = format_time($datestr, '%H:%M:%S'); - } elsif( $age < 6*24*60*60 ) { - $date = format_time($datestr, '%a %H:%M'); - } else { - $date = format_time($datestr, '%Y-%m-%d'); - } - return $date; + my ($datestr) = @_; + my $date = str2time($datestr); + my $age = time() - $date; + + if ($age < 18 * 60 * 60) { + $date = format_time($datestr, '%H:%M:%S'); + } + elsif ($age < 6 * 24 * 60 * 60) { + $date = format_time($datestr, '%a %H:%M'); + } + else { + $date = format_time($datestr, '%Y-%m-%d'); + } + return $date; } sub LookupNamedQuery { - my ($name, $sharer_id) = @_; + my ($name, $sharer_id) = @_; - Bugzilla->login(LOGIN_REQUIRED); + Bugzilla->login(LOGIN_REQUIRED); - my $query = Bugzilla::Search::Saved->check( - { user => $sharer_id, name => $name, _error => 'missing_query' }); + my $query = Bugzilla::Search::Saved->check( + {user => $sharer_id, name => $name, _error => 'missing_query'}); - $query->url - || ThrowUserError("buglist_parameters_required"); + $query->url || ThrowUserError("buglist_parameters_required"); - # Detaint $sharer_id. - $sharer_id = $query->user->id if $sharer_id; - return wantarray ? ($query->url, $query->id, $sharer_id) : $query->url; + # Detaint $sharer_id. + $sharer_id = $query->user->id if $sharer_id; + return wantarray ? ($query->url, $query->id, $sharer_id) : $query->url; } # Inserts a Named Query (a "Saved Search") into the database, or @@ -185,109 +191,111 @@ sub LookupNamedQuery { # Returns: A boolean true value if the query existed in the database # before, and we updated it. A boolean false value otherwise. sub InsertNamedQuery { - my ($query_name, $query, $link_in_footer) = @_; - my $dbh = Bugzilla->dbh; - - $query_name = trim($query_name); - my ($query_obj) = grep {lc($_->name) eq lc($query_name)} @{Bugzilla->user->queries}; - - if ($query_obj) { - $query_obj->set_name($query_name); - $query_obj->set_url($query); - $query_obj->update(); - } else { - Bugzilla::Search::Saved->create({ - name => $query_name, - query => $query, - link_in_footer => $link_in_footer - }); - } - - return $query_obj ? 1 : 0; + my ($query_name, $query, $link_in_footer) = @_; + my $dbh = Bugzilla->dbh; + + $query_name = trim($query_name); + my ($query_obj) + = grep { lc($_->name) eq lc($query_name) } @{Bugzilla->user->queries}; + + if ($query_obj) { + $query_obj->set_name($query_name); + $query_obj->set_url($query); + $query_obj->update(); + } + else { + Bugzilla::Search::Saved->create({ + name => $query_name, query => $query, link_in_footer => $link_in_footer + }); + } + + return $query_obj ? 1 : 0; } sub LookupSeries { - my ($series_id) = @_; - detaint_natural($series_id) || ThrowCodeError("invalid_series_id"); - - my $dbh = Bugzilla->dbh; - my $result = $dbh->selectrow_array("SELECT query FROM series " . - "WHERE series_id = ?" - , undef, ($series_id)); - $result - || ThrowCodeError("invalid_series_id", {'series_id' => $series_id}); - return $result; + my ($series_id) = @_; + detaint_natural($series_id) || ThrowCodeError("invalid_series_id"); + + my $dbh = Bugzilla->dbh; + my $result + = $dbh->selectrow_array("SELECT query FROM series " . "WHERE series_id = ?", + undef, ($series_id)); + $result || ThrowCodeError("invalid_series_id", {'series_id' => $series_id}); + return $result; } sub GetQuip { - my $dbh = Bugzilla->dbh; - # COUNT is quick because it is cached for MySQL. We may want to revisit - # this when we support other databases. - my $count = $dbh->selectrow_array("SELECT COUNT(quip)" - . " FROM quips WHERE approved = 1"); - my $random = int(rand($count)); - my $quip = - $dbh->selectrow_array("SELECT quip FROM quips WHERE approved = 1 " . - $dbh->sql_limit(1, $random)); - return $quip; + my $dbh = Bugzilla->dbh; + + # COUNT is quick because it is cached for MySQL. We may want to revisit + # this when we support other databases. + my $count = $dbh->selectrow_array( + "SELECT COUNT(quip)" . " FROM quips WHERE approved = 1"); + my $random = int(rand($count)); + my $quip = $dbh->selectrow_array( + "SELECT quip FROM quips WHERE approved = 1 " . $dbh->sql_limit(1, $random)); + return $quip; } # Return groups available for at least one product of the buglist. sub GetGroups { - my $product_names = shift; - my $user = Bugzilla->user; - my %legal_groups; + my $product_names = shift; + my $user = Bugzilla->user; + my %legal_groups; - foreach my $product_name (@$product_names) { - my $product = Bugzilla::Product->new({name => $product_name, cache => 1}); + foreach my $product_name (@$product_names) { + my $product = Bugzilla::Product->new({name => $product_name, cache => 1}); - foreach my $gid (keys %{$product->group_controls}) { - # The user can only edit groups they belong to. - next unless $user->in_group_id($gid); + foreach my $gid (keys %{$product->group_controls}) { - # The user has no control on groups marked as NA or MANDATORY. - my $group = $product->group_controls->{$gid}; - next if ($group->{membercontrol} == CONTROLMAPMANDATORY - || $group->{membercontrol} == CONTROLMAPNA); + # The user can only edit groups they belong to. + next unless $user->in_group_id($gid); - # It's fine to include inactive groups. Those will be marked - # as "remove only" when editing several bugs at once. - $legal_groups{$gid} ||= $group->{group}; - } + # The user has no control on groups marked as NA or MANDATORY. + my $group = $product->group_controls->{$gid}; + next + if ($group->{membercontrol} == CONTROLMAPMANDATORY + || $group->{membercontrol} == CONTROLMAPNA); + + # It's fine to include inactive groups. Those will be marked + # as "remove only" when editing several bugs at once. + $legal_groups{$gid} ||= $group->{group}; } - # Return a list of group objects. - return [values %legal_groups]; + } + + # Return a list of group objects. + return [values %legal_groups]; } sub _get_common_flag_types { - my $component_ids = shift; - my $user = Bugzilla->user; - - # Get all the different components in the bug list - my $components = Bugzilla::Component->new_from_list($component_ids); - my %flag_types; - my @flag_types_ids; - foreach my $component (@$components) { - foreach my $flag_type (@{$component->flag_types->{'bug'}}) { - push @flag_types_ids, $flag_type->id; - $flag_types{$flag_type->id} = $flag_type; - } - } - - # We only want flags that appear in all components - my %common_flag_types; - foreach my $id (keys %flag_types) { - my $flag_type_count = scalar grep { $_ == $id } @flag_types_ids; - $common_flag_types{$id} = $flag_types{$id} - if $flag_type_count == scalar @$components; + my $component_ids = shift; + my $user = Bugzilla->user; + + # Get all the different components in the bug list + my $components = Bugzilla::Component->new_from_list($component_ids); + my %flag_types; + my @flag_types_ids; + foreach my $component (@$components) { + foreach my $flag_type (@{$component->flag_types->{'bug'}}) { + push @flag_types_ids, $flag_type->id; + $flag_types{$flag_type->id} = $flag_type; } - - # We only show flags that a user can request. - my @show_flag_types - = grep { $user->can_request_flag($_) } values %common_flag_types; - my $any_flags_requesteeble = grep { $_->is_requesteeble } @show_flag_types; - - return(\@show_flag_types, $any_flags_requesteeble); + } + + # We only want flags that appear in all components + my %common_flag_types; + foreach my $id (keys %flag_types) { + my $flag_type_count = scalar grep { $_ == $id } @flag_types_ids; + $common_flag_types{$id} = $flag_types{$id} + if $flag_type_count == scalar @$components; + } + + # We only show flags that a user can request. + my @show_flag_types + = grep { $user->can_request_flag($_) } values %common_flag_types; + my $any_flags_requesteeble = grep { $_->is_requesteeble } @show_flag_types; + + return (\@show_flag_types, $any_flags_requesteeble); } ################################################################################ @@ -301,8 +309,8 @@ my $sharer_id; # Backwards-compatibility - the old interface had cmdtype="runnamed" to run # a named command, and we can't break this because it's in bookmarks. if ($cmdtype eq "runnamed") { - $cmdtype = "dorem"; - $remaction = "run"; + $cmdtype = "dorem"; + $remaction = "run"; } # Now we're going to be running, so ensure that the params object is set up, @@ -314,41 +322,42 @@ $params ||= new Bugzilla::CGI($cgi); # Take appropriate action based on user's request. if ($cmdtype eq "dorem") { - if ($remaction eq "run") { - my $query_id; - ($buffer, $query_id, $sharer_id) = - LookupNamedQuery(scalar $cgi->param("namedcmd"), - scalar $cgi->param('sharer_id')); - # If this is the user's own query, remember information about it - # so that it can be modified easily. - $vars->{'searchname'} = $cgi->param('namedcmd'); - if (!$cgi->param('sharer_id') || - $cgi->param('sharer_id') == $user->id) { - $vars->{'searchtype'} = "saved"; - $vars->{'search_id'} = $query_id; - } - $params = new Bugzilla::CGI($buffer); - $order = $params->param('order') || $order; - - } - elsif ($remaction eq "runseries") { - $buffer = LookupSeries(scalar $cgi->param("series_id")); - $vars->{'searchname'} = $cgi->param('namedcmd'); - $vars->{'searchtype'} = "series"; - $params = new Bugzilla::CGI($buffer); - $order = $params->param('order') || $order; + if ($remaction eq "run") { + my $query_id; + ($buffer, $query_id, $sharer_id) + = LookupNamedQuery(scalar $cgi->param("namedcmd"), + scalar $cgi->param('sharer_id')); + + # If this is the user's own query, remember information about it + # so that it can be modified easily. + $vars->{'searchname'} = $cgi->param('namedcmd'); + if (!$cgi->param('sharer_id') || $cgi->param('sharer_id') == $user->id) { + $vars->{'searchtype'} = "saved"; + $vars->{'search_id'} = $query_id; } - elsif ($remaction eq "forget") { - $user = Bugzilla->login(LOGIN_REQUIRED); - # Copy the name into a variable, so that we can trick_taint it for - # the DB. We know it's safe, because we're using placeholders in - # the SQL, and the SQL is only a DELETE. - my $qname = $cgi->param('namedcmd'); - trick_taint($qname); - - # Do not forget the saved search if it is being used in a whine - my $whines_in_use = - $dbh->selectcol_arrayref('SELECT DISTINCT whine_events.subject + $params = new Bugzilla::CGI($buffer); + $order = $params->param('order') || $order; + + } + elsif ($remaction eq "runseries") { + $buffer = LookupSeries(scalar $cgi->param("series_id")); + $vars->{'searchname'} = $cgi->param('namedcmd'); + $vars->{'searchtype'} = "series"; + $params = new Bugzilla::CGI($buffer); + $order = $params->param('order') || $order; + } + elsif ($remaction eq "forget") { + $user = Bugzilla->login(LOGIN_REQUIRED); + + # Copy the name into a variable, so that we can trick_taint it for + # the DB. We know it's safe, because we're using placeholders in + # the SQL, and the SQL is only a DELETE. + my $qname = $cgi->param('namedcmd'); + trick_taint($qname); + + # Do not forget the saved search if it is being used in a whine + my $whines_in_use = $dbh->selectcol_arrayref( + 'SELECT DISTINCT whine_events.subject FROM whine_events INNER JOIN whine_queries ON whine_queries.eventid @@ -357,99 +366,110 @@ if ($cmdtype eq "dorem") { = ? AND whine_queries.query_name = ? - ', undef, $user->id, $qname); - if (scalar(@$whines_in_use)) { - ThrowUserError('saved_search_used_by_whines', - { subjects => join(',', @$whines_in_use), - search_name => $qname } - ); - } - - # If we are here, then we can safely remove the saved search - my $query_id; - ($buffer, $query_id) = LookupNamedQuery(scalar $cgi->param("namedcmd"), - $user->id); - if ($query_id) { - # Make sure the user really wants to delete their saved search. - my $token = $cgi->param('token'); - check_hash_token($token, [$query_id, $qname]); - - $dbh->do('DELETE FROM namedqueries - WHERE id = ?', - undef, $query_id); - $dbh->do('DELETE FROM namedqueries_link_in_footer - WHERE namedquery_id = ?', - undef, $query_id); - $dbh->do('DELETE FROM namedquery_group_map - WHERE namedquery_id = ?', - undef, $query_id); - Bugzilla->memcached->clear({ table => 'namedqueries', id => $query_id }); - } + ', undef, $user->id, $qname + ); + if (scalar(@$whines_in_use)) { + ThrowUserError('saved_search_used_by_whines', + {subjects => join(',', @$whines_in_use), search_name => $qname}); + } - # Now reset the cached queries - $user->flush_queries_cache(); - - print $cgi->header(); - # Generate and return the UI (HTML page) from the appropriate template. - $vars->{'message'} = "buglist_query_gone"; - $vars->{'namedcmd'} = $qname; - $vars->{'url'} = "buglist.cgi?newquery=" . url_quote($buffer) - . "&cmdtype=doit&remtype=asnamed&newqueryname=" . url_quote($qname) - . "&token=" . url_quote(issue_hash_token(['savedsearch'])); - $template->process("global/message.html.tmpl", $vars) - || ThrowTemplateError($template->error()); - exit; + # If we are here, then we can safely remove the saved search + my $query_id; + ($buffer, $query_id) + = LookupNamedQuery(scalar $cgi->param("namedcmd"), $user->id); + if ($query_id) { + + # Make sure the user really wants to delete their saved search. + my $token = $cgi->param('token'); + check_hash_token($token, [$query_id, $qname]); + + $dbh->do( + 'DELETE FROM namedqueries + WHERE id = ?', undef, $query_id + ); + $dbh->do( + 'DELETE FROM namedqueries_link_in_footer + WHERE namedquery_id = ?', undef, $query_id + ); + $dbh->do( + 'DELETE FROM namedquery_group_map + WHERE namedquery_id = ?', undef, $query_id + ); + Bugzilla->memcached->clear({table => 'namedqueries', id => $query_id}); } + + # Now reset the cached queries + $user->flush_queries_cache(); + + print $cgi->header(); + + # Generate and return the UI (HTML page) from the appropriate template. + $vars->{'message'} = "buglist_query_gone"; + $vars->{'namedcmd'} = $qname; + $vars->{'url'} + = "buglist.cgi?newquery=" + . url_quote($buffer) + . "&cmdtype=doit&remtype=asnamed&newqueryname=" + . url_quote($qname) + . "&token=" + . url_quote(issue_hash_token(['savedsearch'])); + $template->process("global/message.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + exit; + } } elsif (($cmdtype eq "doit") && defined $cgi->param('remtype')) { - if ($cgi->param('remtype') eq "asdefault") { - $user = Bugzilla->login(LOGIN_REQUIRED); - my $token = $cgi->param('token'); - check_hash_token($token, ['searchknob']); - $buffer = $params->canonicalise_query('cmdtype', 'remtype', - 'query_based_on', 'token'); - InsertNamedQuery(DEFAULT_QUERY_NAME, $buffer); - $vars->{'message'} = "buglist_new_default_query"; + if ($cgi->param('remtype') eq "asdefault") { + $user = Bugzilla->login(LOGIN_REQUIRED); + my $token = $cgi->param('token'); + check_hash_token($token, ['searchknob']); + $buffer = $params->canonicalise_query('cmdtype', 'remtype', 'query_based_on', + 'token'); + InsertNamedQuery(DEFAULT_QUERY_NAME, $buffer); + $vars->{'message'} = "buglist_new_default_query"; + } + elsif ($cgi->param('remtype') eq "asnamed") { + $user = Bugzilla->login(LOGIN_REQUIRED); + my $query_name = $cgi->param('newqueryname'); + my $new_query = $cgi->param('newquery'); + my $token = $cgi->param('token'); + check_hash_token($token, ['savedsearch']); + my $existed_before = InsertNamedQuery($query_name, $new_query, 1); + if ($existed_before) { + $vars->{'message'} = "buglist_updated_named_query"; } - elsif ($cgi->param('remtype') eq "asnamed") { - $user = Bugzilla->login(LOGIN_REQUIRED); - my $query_name = $cgi->param('newqueryname'); - my $new_query = $cgi->param('newquery'); - my $token = $cgi->param('token'); - check_hash_token($token, ['savedsearch']); - my $existed_before = InsertNamedQuery($query_name, $new_query, 1); - if ($existed_before) { - $vars->{'message'} = "buglist_updated_named_query"; - } - else { - $vars->{'message'} = "buglist_new_named_query"; - } - $vars->{'queryname'} = $query_name; + else { + $vars->{'message'} = "buglist_new_named_query"; + } + $vars->{'queryname'} = $query_name; - # Make sure to invalidate any cached query data, so that the footer is - # correctly displayed - $user->flush_queries_cache(); + # Make sure to invalidate any cached query data, so that the footer is + # correctly displayed + $user->flush_queries_cache(); - print $cgi->header(); - $template->process("global/message.html.tmpl", $vars) - || ThrowTemplateError($template->error()); - exit; - } + print $cgi->header(); + $template->process("global/message.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + exit; + } } # backward compatibility hack: if the saved query doesn't say which # form was used to create it, assume it was on the advanced query # form - see bug 252295 if (!$params->param('query_format')) { - $params->param('query_format', 'advanced'); - $buffer = $params->query_string; + $params->param('query_format', 'advanced'); + $buffer = $params->query_string; } # Determine the format in which the user would like to receive the output. # Uses the default format if the user did not specify an output format; # otherwise validates the user's choice against the list of available formats. -my $format = $template->get_format("list/list", scalar $params->param('format'), - scalar $params->param('ctype')); +my $format = $template->get_format( + "list/list", + scalar $params->param('format'), + scalar $params->param('ctype') +); # Use server push to display a "Please wait..." message for the user while # executing their query if their browser supports it and they are viewing @@ -459,14 +479,13 @@ my $format = $template->get_format("list/list", scalar $params->param('format'), # Server push is compatible with Gecko-based browsers and Opera, but not with # MSIE, Lynx or Safari (bug 441496). -my $serverpush = - $format->{'extension'} eq "html" - && exists $ENV{'HTTP_USER_AGENT'} - && $ENV{'HTTP_USER_AGENT'} =~ /(Mozilla.[3-9]|Opera)/ - && $ENV{'HTTP_USER_AGENT'} !~ /compatible/i - && $ENV{'HTTP_USER_AGENT'} !~ /(?:WebKit|Trident|KHTML)/ - && !defined($cgi->param('serverpush')) - || $cgi->param('serverpush'); +my $serverpush + = $format->{'extension'} eq "html" + && exists $ENV{'HTTP_USER_AGENT'} + && $ENV{'HTTP_USER_AGENT'} =~ /(Mozilla.[3-9]|Opera)/ + && $ENV{'HTTP_USER_AGENT'} !~ /compatible/i + && $ENV{'HTTP_USER_AGENT'} !~ /(?:WebKit|Trident|KHTML)/ + && !defined($cgi->param('serverpush')) || $cgi->param('serverpush'); # Generate a reasonable filename for the user agent to suggest to the user @@ -476,7 +495,7 @@ my $serverpush = # forgotten in the process of retrieving it. my $disp_prefix = "bugs"; if ($cmdtype eq "dorem" && $remaction =~ /^run/) { - $disp_prefix = $cgi->param('namedcmd'); + $disp_prefix = $cgi->param('namedcmd'); } ################################################################################ @@ -493,32 +512,34 @@ my $columns = Bugzilla::Search::COLUMNS; # columnlist CGI parameter, the user's preferences, or the default. my @displaycolumns = (); if (defined $params->param('columnlist')) { - if ($params->param('columnlist') eq "all") { - # If the value of the CGI parameter is "all", display all columns, - # but remove the redundant "short_desc" column. - @displaycolumns = grep($_ ne 'short_desc', keys(%$columns)); - } - else { - @displaycolumns = split(/[ ,]+/, $params->param('columnlist')); - } + if ($params->param('columnlist') eq "all") { + + # If the value of the CGI parameter is "all", display all columns, + # but remove the redundant "short_desc" column. + @displaycolumns = grep($_ ne 'short_desc', keys(%$columns)); + } + else { + @displaycolumns = split(/[ ,]+/, $params->param('columnlist')); + } } elsif (defined $cgi->cookie('COLUMNLIST')) { - # 2002-10-31 Rename column names (see bug 176461) - my $columnlist = $cgi->cookie('COLUMNLIST'); - $columnlist =~ s/\bowner\b/assigned_to/; - $columnlist =~ s/\bowner_realname\b/assigned_to_realname/; - $columnlist =~ s/\bplatform\b/rep_platform/; - $columnlist =~ s/\bseverity\b/bug_severity/; - $columnlist =~ s/\bstatus\b/bug_status/; - $columnlist =~ s/\bsummaryfull\b/short_desc/; - $columnlist =~ s/\bsummary\b/short_short_desc/; - - # Use the columns listed in the user's preferences. - @displaycolumns = split(/ /, $columnlist); + + # 2002-10-31 Rename column names (see bug 176461) + my $columnlist = $cgi->cookie('COLUMNLIST'); + $columnlist =~ s/\bowner\b/assigned_to/; + $columnlist =~ s/\bowner_realname\b/assigned_to_realname/; + $columnlist =~ s/\bplatform\b/rep_platform/; + $columnlist =~ s/\bseverity\b/bug_severity/; + $columnlist =~ s/\bstatus\b/bug_status/; + $columnlist =~ s/\bsummaryfull\b/short_desc/; + $columnlist =~ s/\bsummary\b/short_short_desc/; + + # Use the columns listed in the user's preferences. + @displaycolumns = split(/ /, $columnlist); } else { - # Use the default list of columns. - @displaycolumns = DEFAULT_COLUMN_LIST; + # Use the default list of columns. + @displaycolumns = DEFAULT_COLUMN_LIST; } # Weed out columns that don't actually exist to prevent the user @@ -533,14 +554,14 @@ else { # Remove the timetracking columns if they are not a part of the group # (happens if a user had access to time tracking and it was revoked/disabled) if (!$user->is_timetracker) { - foreach my $tt_field (TIMETRACKING_FIELDS) { - @displaycolumns = grep($_ ne $tt_field, @displaycolumns); - } + foreach my $tt_field (TIMETRACKING_FIELDS) { + @displaycolumns = grep($_ ne $tt_field, @displaycolumns); + } } # Remove the relevance column if the user is not doing a fulltext search. if (grep('relevance', @displaycolumns) && !$fulltext) { - @displaycolumns = grep($_ ne 'relevance', @displaycolumns); + @displaycolumns = grep($_ ne 'relevance', @displaycolumns); } ################################################################################ @@ -552,13 +573,14 @@ if (grep('relevance', @displaycolumns) && !$fulltext) { # The bug ID is always selected because bug IDs are always displayed. # Severity, priority, resolution and status are required for buglist # CSS classes. -my @selectcolumns = ("bug_id", "bug_severity", "priority", "bug_status", - "resolution", "product"); +my @selectcolumns + = ("bug_id", "bug_severity", "priority", "bug_status", "resolution", + "product"); # remaining and actual_time are required for percentage_complete calculation: if (grep { $_ eq "percentage_complete" } @displaycolumns) { - push (@selectcolumns, "remaining_time"); - push (@selectcolumns, "actual_time"); + push(@selectcolumns, "remaining_time"); + push(@selectcolumns, "actual_time"); } # Make sure that the login_name version of a field is always also @@ -566,58 +588,51 @@ if (grep { $_ eq "percentage_complete" } @displaycolumns) { # display the login name when the realname is empty. my @realname_fields = grep(/_realname$/, @displaycolumns); foreach my $item (@realname_fields) { - my $login_field = $item; - $login_field =~ s/_realname$//; - if (!grep($_ eq $login_field, @selectcolumns)) { - push(@selectcolumns, $login_field); - } + my $login_field = $item; + $login_field =~ s/_realname$//; + if (!grep($_ eq $login_field, @selectcolumns)) { + push(@selectcolumns, $login_field); + } } # Display columns are selected because otherwise we could not display them. foreach my $col (@displaycolumns) { - push (@selectcolumns, $col) if !grep($_ eq $col, @selectcolumns); + push(@selectcolumns, $col) if !grep($_ eq $col, @selectcolumns); } # If the user is editing multiple bugs, we also make sure to select the # status, because the values of that field determines what options the user # has for modifying the bugs. if ($dotweak) { - push(@selectcolumns, "bug_status") if !grep($_ eq 'bug_status', @selectcolumns); - push(@selectcolumns, "bugs.component_id"); + push(@selectcolumns, "bug_status") if !grep($_ eq 'bug_status', @selectcolumns); + push(@selectcolumns, "bugs.component_id"); } if ($format->{'extension'} eq 'ics') { - push(@selectcolumns, "opendate") if !grep($_ eq 'opendate', @selectcolumns); - if (Bugzilla->params->{'timetrackinggroup'}) { - push(@selectcolumns, "deadline") if !grep($_ eq 'deadline', @selectcolumns); - } + push(@selectcolumns, "opendate") if !grep($_ eq 'opendate', @selectcolumns); + if (Bugzilla->params->{'timetrackinggroup'}) { + push(@selectcolumns, "deadline") if !grep($_ eq 'deadline', @selectcolumns); + } } if ($format->{'extension'} eq 'atom') { - # The title of the Atom feed will be the same one as for the bug list. - $vars->{'title'} = $cgi->param('title'); - - # This is the list of fields that are needed by the Atom filter. - my @required_atom_columns = ( - 'short_desc', - 'opendate', - 'changeddate', - 'reporter', - 'reporter_realname', - 'priority', - 'bug_severity', - 'assigned_to', - 'assigned_to_realname', - 'bug_status', - 'product', - 'component', - 'resolution' - ); - push(@required_atom_columns, 'target_milestone') if Bugzilla->params->{'usetargetmilestone'}; - foreach my $required (@required_atom_columns) { - push(@selectcolumns, $required) if !grep($_ eq $required,@selectcolumns); - } + # The title of the Atom feed will be the same one as for the bug list. + $vars->{'title'} = $cgi->param('title'); + + # This is the list of fields that are needed by the Atom filter. + my @required_atom_columns = ( + 'short_desc', 'opendate', 'changeddate', 'reporter', + 'reporter_realname', 'priority', 'bug_severity', 'assigned_to', + 'assigned_to_realname', 'bug_status', 'product', 'component', + 'resolution' + ); + push(@required_atom_columns, 'target_milestone') + if Bugzilla->params->{'usetargetmilestone'}; + + foreach my $required (@required_atom_columns) { + push(@selectcolumns, $required) if !grep($_ eq $required, @selectcolumns); + } } ################################################################################ @@ -629,66 +644,70 @@ if ($format->{'extension'} eq 'atom') { # First check if we'll want to reuse the last sorting order; that happens if # the order is not defined or its value is "reuse last sort" if (!$order || $order =~ /^reuse/i) { - if ($cgi->cookie('LASTORDER')) { - $order = $cgi->cookie('LASTORDER'); - - # Cookies from early versions of Specific Search included this text, - # which is now invalid. - $order =~ s/ LIMIT 200//; - } - else { - $order = ''; # Remove possible "reuse" identifier as unnecessary - } + if ($cgi->cookie('LASTORDER')) { + $order = $cgi->cookie('LASTORDER'); + + # Cookies from early versions of Specific Search included this text, + # which is now invalid. + $order =~ s/ LIMIT 200//; + } + else { + $order = ''; # Remove possible "reuse" identifier as unnecessary + } } my @order_columns; if ($order) { - # Convert the value of the "order" form field into a list of columns - # by which to sort the results. - my $descending = $params->param('descending') ? " DESC" : ""; - my %order_types = ( - "Bug Number" => [ "bug_id$descending" ], - "Importance" => [ "priority$descending", "bug_severity" ], - "Assignee" => [ "assigned_to$descending", "bug_status", "priority", "bug_id" ], - "Last Changed" => [ "changeddate$descending", "bug_status", "priority", - "assigned_to", "bug_id" ], - ); - if ($order_types{$order}) { - @order_columns = @{ $order_types{$order} }; - } - else { - @order_columns = split(/\s*,\s*/, $order); - } + + # Convert the value of the "order" form field into a list of columns + # by which to sort the results. + my $descending = $params->param('descending') ? " DESC" : ""; + my %order_types = ( + "Bug Number" => ["bug_id$descending"], + "Importance" => ["priority$descending", "bug_severity"], + "Assignee" => ["assigned_to$descending", "bug_status", "priority", "bug_id"], + "Last Changed" => + ["changeddate$descending", "bug_status", "priority", "assigned_to", "bug_id"], + ); + if ($order_types{$order}) { + @order_columns = @{$order_types{$order}}; + } + else { + @order_columns = split(/\s*,\s*/, $order); + } } if (!scalar @order_columns) { - # DEFAULT - @order_columns = ("bug_status", "priority", "assigned_to", "bug_id"); + + # DEFAULT + @order_columns = ("bug_status", "priority", "assigned_to", "bug_id"); } # In the HTML interface, by default, we limit the returned results, # which speeds up quite a few searches where people are really only looking # for the top results. if ($format->{'extension'} eq 'html' && !defined $params->param('limit')) { - $params->param('limit', Bugzilla->params->{'default_search_limit'}); - $vars->{'default_limited'} = 1; + $params->param('limit', Bugzilla->params->{'default_search_limit'}); + $vars->{'default_limited'} = 1; } # Generate the basic SQL query that will be used to generate the bug list. -my $search = new Bugzilla::Search('fields' => \@selectcolumns, - 'params' => scalar $params->Vars, - 'order' => \@order_columns, - 'sharer' => $sharer_id); +my $search = new Bugzilla::Search( + 'fields' => \@selectcolumns, + 'params' => scalar $params->Vars, + 'order' => \@order_columns, + 'sharer' => $sharer_id +); $order = join(',', $search->order); if (scalar @{$search->invalid_order_columns}) { - $vars->{'message'} = 'invalid_column_name'; - $vars->{'invalid_fragments'} = $search->invalid_order_columns; + $vars->{'message'} = 'invalid_column_name'; + $vars->{'invalid_fragments'} = $search->invalid_order_columns; } -if ($fulltext and grep { /^relevance/ } $search->order) { - $vars->{'message'} = 'buglist_sorted_by_relevance' +if ($fulltext and grep {/^relevance/} $search->order) { + $vars->{'message'} = 'buglist_sorted_by_relevance'; } # We don't want saved searches and other buglist things to save @@ -702,22 +721,22 @@ $params->delete('limit') if $vars->{'default_limited'}; # Time to use server push to display an interim message to the user until # the query completes and we can display the bug list. if ($serverpush) { - print $cgi->multipart_init(); - print $cgi->multipart_start(-type => 'text/html'); - - # Generate and return the UI (HTML page) from the appropriate template. - $template->process("list/server-push.html.tmpl", $vars) - || ThrowTemplateError($template->error()); - - # Under mod_perl, flush stdout so that the page actually shows up. - if ($ENV{MOD_PERL}) { - require Apache2::RequestUtil; - Apache2::RequestUtil->request->rflush(); - } - - # Don't do multipart_end() until we're ready to display the replacement - # page, otherwise any errors that happen before then (like SQL errors) - # will result in a blank page being shown to the user instead of the error. + print $cgi->multipart_init(); + print $cgi->multipart_start(-type => 'text/html'); + + # Generate and return the UI (HTML page) from the appropriate template. + $template->process("list/server-push.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + + # Under mod_perl, flush stdout so that the page actually shows up. + if ($ENV{MOD_PERL}) { + require Apache2::RequestUtil; + Apache2::RequestUtil->request->rflush(); + } + + # Don't do multipart_end() until we're ready to display the replacement + # page, otherwise any errors that happen before then (like SQL errors) + # will result in a blank page being shown to the user instead of the error. } # Connect to the shadow database if this installation is using one to improve @@ -734,24 +753,25 @@ $::SIG{PIPE} = 'DEFAULT'; my ($data, $extra_data) = $search->data; $vars->{'search_description'} = $search->search_description; -if ($cgi->param('debug') - && Bugzilla->params->{debug_group} - && $user->in_group(Bugzilla->params->{debug_group}) -) { - $vars->{'debug'} = 1; - $vars->{'queries'} = $extra_data; - my $query_time = 0; - $query_time += $_->{'time'} foreach @$extra_data; - $vars->{'query_time'} = $query_time; - # Explains are limited to admins because you could use them to figure - # out how many hidden bugs are in a particular product (by doing - # searches and looking at the number of rows the explain says it's - # examining). - if ($user->in_group('admin')) { - foreach my $query (@$extra_data) { - $query->{explain} = $dbh->bz_explain($query->{sql}); - } +if ( $cgi->param('debug') + && Bugzilla->params->{debug_group} + && $user->in_group(Bugzilla->params->{debug_group})) +{ + $vars->{'debug'} = 1; + $vars->{'queries'} = $extra_data; + my $query_time = 0; + $query_time += $_->{'time'} foreach @$extra_data; + $vars->{'query_time'} = $query_time; + + # Explains are limited to admins because you could use them to figure + # out how many hidden bugs are in a particular product (by doing + # searches and looking at the number of rows the explain says it's + # examining). + if ($user->in_group('admin')) { + foreach my $query (@$extra_data) { + $query->{explain} = $dbh->bz_explain($query->{sql}); } + } } ################################################################################ @@ -763,72 +783,74 @@ if ($cgi->param('debug') # If we're doing time tracking, then keep totals for all bugs. my $percentage_complete = grep($_ eq 'percentage_complete', @displaycolumns); -my $estimated_time = grep($_ eq 'estimated_time', @displaycolumns); -my $remaining_time = grep($_ eq 'remaining_time', @displaycolumns) - || $percentage_complete; -my $actual_time = grep($_ eq 'actual_time', @displaycolumns) - || $percentage_complete; - -my $time_info = { 'estimated_time' => 0, - 'remaining_time' => 0, - 'actual_time' => 0, - 'percentage_complete' => 0, - 'time_present' => ($estimated_time || $remaining_time || - $actual_time || $percentage_complete), - }; - -my $bugowners = {}; -my $bugproducts = {}; +my $estimated_time = grep($_ eq 'estimated_time', @displaycolumns); +my $remaining_time + = grep($_ eq 'remaining_time', @displaycolumns) || $percentage_complete; +my $actual_time + = grep($_ eq 'actual_time', @displaycolumns) || $percentage_complete; + +my $time_info = { + 'estimated_time' => 0, + 'remaining_time' => 0, + 'actual_time' => 0, + 'percentage_complete' => 0, + 'time_present' => + ($estimated_time || $remaining_time || $actual_time || $percentage_complete), +}; + +my $bugowners = {}; +my $bugproducts = {}; my $bugcomponentids = {}; -my $bugcomponents = {}; -my $bugstatuses = {}; +my $bugcomponents = {}; +my $bugstatuses = {}; my @bugidlist; -my @bugs; # the list of records +my @bugs; # the list of records foreach my $row (@$data) { - my $bug = {}; # a record - - # Slurp the row of data into the record. - # The second from last column in the record is the number of groups - # to which the bug is restricted. - foreach my $column (@selectcolumns) { - $bug->{$column} = shift @$row; - } - - # Process certain values further (i.e. date format conversion). - if ($bug->{'changeddate'}) { - $bug->{'changeddate'} =~ - s/^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})$/$1-$2-$3 $4:$5:$6/; - - $bug->{'changedtime'} = $bug->{'changeddate'}; # for iCalendar and Atom - $bug->{'changeddate'} = DiffDate($bug->{'changeddate'}); - } - - if ($bug->{'opendate'}) { - $bug->{'opentime'} = $bug->{'opendate'}; # for iCalendar - $bug->{'opendate'} = DiffDate($bug->{'opendate'}); - } - - # Record the assignee, product, and status in the big hashes of those things. - $bugowners->{$bug->{'assigned_to'}} = 1 if $bug->{'assigned_to'}; - $bugproducts->{$bug->{'product'}} = 1 if $bug->{'product'}; - $bugcomponentids->{$bug->{'bugs.component_id'}} = 1 if $bug->{'bugs.component_id'}; - $bugcomponents->{$bug->{'component'}} = 1 if $bug->{'component'}; - $bugstatuses->{$bug->{'bug_status'}} = 1 if $bug->{'bug_status'}; - - $bug->{'secure_mode'} = undef; - - # Add the record to the list. - push(@bugs, $bug); - - # Add id to list for checking for bug privacy later - push(@bugidlist, $bug->{'bug_id'}); - - # Compute time tracking info. - $time_info->{'estimated_time'} += $bug->{'estimated_time'} if ($estimated_time); - $time_info->{'remaining_time'} += $bug->{'remaining_time'} if ($remaining_time); - $time_info->{'actual_time'} += $bug->{'actual_time'} if ($actual_time); + my $bug = {}; # a record + + # Slurp the row of data into the record. + # The second from last column in the record is the number of groups + # to which the bug is restricted. + foreach my $column (@selectcolumns) { + $bug->{$column} = shift @$row; + } + + # Process certain values further (i.e. date format conversion). + if ($bug->{'changeddate'}) { + $bug->{'changeddate'} + =~ s/^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})$/$1-$2-$3 $4:$5:$6/; + + $bug->{'changedtime'} = $bug->{'changeddate'}; # for iCalendar and Atom + $bug->{'changeddate'} = DiffDate($bug->{'changeddate'}); + } + + if ($bug->{'opendate'}) { + $bug->{'opentime'} = $bug->{'opendate'}; # for iCalendar + $bug->{'opendate'} = DiffDate($bug->{'opendate'}); + } + + # Record the assignee, product, and status in the big hashes of those things. + $bugowners->{$bug->{'assigned_to'}} = 1 if $bug->{'assigned_to'}; + $bugproducts->{$bug->{'product'}} = 1 if $bug->{'product'}; + $bugcomponentids->{$bug->{'bugs.component_id'}} = 1 + if $bug->{'bugs.component_id'}; + $bugcomponents->{$bug->{'component'}} = 1 if $bug->{'component'}; + $bugstatuses->{$bug->{'bug_status'}} = 1 if $bug->{'bug_status'}; + + $bug->{'secure_mode'} = undef; + + # Add the record to the list. + push(@bugs, $bug); + + # Add id to list for checking for bug privacy later + push(@bugidlist, $bug->{'bug_id'}); + + # Compute time tracking info. + $time_info->{'estimated_time'} += $bug->{'estimated_time'} if ($estimated_time); + $time_info->{'remaining_time'} += $bug->{'remaining_time'} if ($remaining_time); + $time_info->{'actual_time'} += $bug->{'actual_time'} if ($actual_time); } # Check for bug privacy and set $bug->{'secure_mode'} to 'implied' or 'manual' @@ -836,38 +858,40 @@ foreach my $row (@$data) { # or because of human choice my %min_membercontrol; if (@bugidlist) { - my $sth = $dbh->prepare( - "SELECT DISTINCT bugs.bug_id, MIN(group_control_map.membercontrol) " . - "FROM bugs " . - "INNER JOIN bug_group_map " . - "ON bugs.bug_id = bug_group_map.bug_id " . - "LEFT JOIN group_control_map " . - "ON group_control_map.product_id = bugs.product_id " . - "AND group_control_map.group_id = bug_group_map.group_id " . - "WHERE " . $dbh->sql_in('bugs.bug_id', \@bugidlist) . - $dbh->sql_group_by('bugs.bug_id')); - $sth->execute(); - while (my ($bug_id, $min_membercontrol) = $sth->fetchrow_array()) { - $min_membercontrol{$bug_id} = $min_membercontrol || CONTROLMAPNA; + my $sth + = $dbh->prepare( + "SELECT DISTINCT bugs.bug_id, MIN(group_control_map.membercontrol) " + . "FROM bugs " + . "INNER JOIN bug_group_map " + . "ON bugs.bug_id = bug_group_map.bug_id " + . "LEFT JOIN group_control_map " + . "ON group_control_map.product_id = bugs.product_id " + . "AND group_control_map.group_id = bug_group_map.group_id " + . "WHERE " + . $dbh->sql_in('bugs.bug_id', \@bugidlist) + . $dbh->sql_group_by('bugs.bug_id')); + $sth->execute(); + while (my ($bug_id, $min_membercontrol) = $sth->fetchrow_array()) { + $min_membercontrol{$bug_id} = $min_membercontrol || CONTROLMAPNA; + } + foreach my $bug (@bugs) { + next unless defined($min_membercontrol{$bug->{'bug_id'}}); + if ($min_membercontrol{$bug->{'bug_id'}} == CONTROLMAPMANDATORY) { + $bug->{'secure_mode'} = 'implied'; } - foreach my $bug (@bugs) { - next unless defined($min_membercontrol{$bug->{'bug_id'}}); - if ($min_membercontrol{$bug->{'bug_id'}} == CONTROLMAPMANDATORY) { - $bug->{'secure_mode'} = 'implied'; - } - else { - $bug->{'secure_mode'} = 'manual'; - } + else { + $bug->{'secure_mode'} = 'manual'; } + } } # Compute percentage complete without rounding. -my $sum = $time_info->{'actual_time'}+$time_info->{'remaining_time'}; +my $sum = $time_info->{'actual_time'} + $time_info->{'remaining_time'}; if ($sum > 0) { - $time_info->{'percentage_complete'} = 100*$time_info->{'actual_time'}/$sum; + $time_info->{'percentage_complete'} = 100 * $time_info->{'actual_time'} / $sum; } -else { # remaining_time <= 0 - $time_info->{'percentage_complete'} = 0 +else { # remaining_time <= 0 + $time_info->{'percentage_complete'} = 0; } ################################################################################ @@ -876,45 +900,45 @@ else { # remaining_time <= 0 # Define the variables and functions that will be passed to the UI template. -$vars->{'bugs'} = \@bugs; -$vars->{'buglist'} = \@bugidlist; -$vars->{'columns'} = $columns; +$vars->{'bugs'} = \@bugs; +$vars->{'buglist'} = \@bugidlist; +$vars->{'columns'} = $columns; $vars->{'displaycolumns'} = \@displaycolumns; $vars->{'openstates'} = [BUG_STATE_OPEN]; -$vars->{'closedstates'} = [map {$_->name} closed_bug_statuses()]; +$vars->{'closedstates'} = [map { $_->name } closed_bug_statuses()]; # The iCal file needs priorities ordered from 1 to 9 (highest to lowest) # If there are more than 9 values, just make all the lower ones 9 if ($format->{'extension'} eq 'ics') { - my $n = 1; - $vars->{'ics_priorities'} = {}; - my $priorities = get_legal_field_values('priority'); - foreach my $p (@$priorities) { - $vars->{'ics_priorities'}->{$p} = ($n > 9) ? 9 : $n++; - } + my $n = 1; + $vars->{'ics_priorities'} = {}; + my $priorities = get_legal_field_values('priority'); + foreach my $p (@$priorities) { + $vars->{'ics_priorities'}->{$p} = ($n > 9) ? 9 : $n++; + } } -$vars->{'order'} = $order; +$vars->{'order'} = $order; $vars->{'caneditbugs'} = 1; -$vars->{'time_info'} = $time_info; +$vars->{'time_info'} = $time_info; if (!$user->in_group('editbugs')) { - foreach my $product (keys %$bugproducts) { - my $prod = Bugzilla::Product->new({name => $product, cache => 1}); - if (!$user->in_group('editbugs', $prod->id)) { - $vars->{'caneditbugs'} = 0; - last; - } + foreach my $product (keys %$bugproducts) { + my $prod = Bugzilla::Product->new({name => $product, cache => 1}); + if (!$user->in_group('editbugs', $prod->id)) { + $vars->{'caneditbugs'} = 0; + last; } + } } my @bugowners = keys %$bugowners; -if (scalar(@bugowners) > 1 - && $user->in_group('editbugs') - && Bugzilla->params->{'use_email_as_login'}) +if ( scalar(@bugowners) > 1 + && $user->in_group('editbugs') + && Bugzilla->params->{'use_email_as_login'}) { - $vars->{'bugowners'} = join(",", @bugowners); + $vars->{'bugowners'} = join(",", @bugowners); } # Whether or not to split the column titles across two rows to make @@ -922,7 +946,7 @@ if (scalar(@bugowners) > 1 $vars->{'splitheader'} = $cgi->cookie('SPLITHEADER') ? 1 : 0; if ($user->settings->{'display_quips'}->{'value'} eq 'on') { - $vars->{'quip'} = GetQuip(); + $vars->{'quip'} = GetQuip(); } $vars->{'currenttime'} = localtime(time()); @@ -932,18 +956,20 @@ $vars->{'currenttime'} = localtime(time()); my @products = keys %$bugproducts; my $one_product; if (scalar(@products) == 1) { - $one_product = Bugzilla::Product->new({ name => $products[0], cache => 1 }); + $one_product = Bugzilla::Product->new({name => $products[0], cache => 1}); } + # This is used in the "Zarroo Boogs" case. elsif (my @product_input = $cgi->multi_param('product')) { - if (scalar(@product_input) == 1 and $product_input[0] ne '') { - $one_product = Bugzilla::Product->new({ name => $product_input[0], cache => 1 }); - } + if (scalar(@product_input) == 1 and $product_input[0] ne '') { + $one_product = Bugzilla::Product->new({name => $product_input[0], cache => 1}); + } } + # We only want the template to use it if the user can actually # enter bugs against it. if ($one_product && $user->can_enter_product($one_product)) { - $vars->{'one_product'} = $one_product; + $vars->{'one_product'} = $one_product; } # See if there's only one component in all the results (or only one component @@ -951,50 +977,50 @@ if ($one_product && $user->can_enter_product($one_product)) { my @components = keys %$bugcomponents; my $one_component; if (scalar(@components) == 1) { - $vars->{one_component} = $components[0]; + $vars->{one_component} = $components[0]; } + # This is used in the "Zarroo Boogs" case. elsif (my @component_input = $cgi->multi_param('component')) { - if (scalar(@component_input) == 1 and $component_input[0] ne '') { - $vars->{one_component}= $cgi->param('component'); - } + if (scalar(@component_input) == 1 and $component_input[0] ne '') { + $vars->{one_component} = $cgi->param('component'); + } } # The following variables are used when the user is making changes to multiple bugs. if ($dotweak && scalar @bugs) { - if (!$vars->{'caneditbugs'}) { - ThrowUserError('auth_failure', {group => 'editbugs', - action => 'modify', - object => 'multiple_bugs'}); - } - $vars->{'dotweak'} = 1; - - # issue_session_token needs to write to the master DB. - Bugzilla->switch_to_main_db(); - $vars->{'token'} = issue_session_token('buglist_mass_change'); - Bugzilla->switch_to_shadow_db(); - - $vars->{'products'} = $user->get_enterable_products; - $vars->{'platforms'} = get_legal_field_values('rep_platform'); - $vars->{'op_sys'} = get_legal_field_values('op_sys'); - $vars->{'priorities'} = get_legal_field_values('priority'); - $vars->{'severities'} = get_legal_field_values('bug_severity'); - $vars->{'resolutions'} = get_legal_field_values('resolution'); - - ($vars->{'flag_types'}, $vars->{any_flags_requesteeble}) - = _get_common_flag_types([keys %$bugcomponentids]); - - # Convert bug statuses to their ID. - my @bug_statuses = map {$dbh->quote($_)} keys %$bugstatuses; - my $bug_status_ids = - $dbh->selectcol_arrayref('SELECT id FROM bug_status - WHERE ' . $dbh->sql_in('value', \@bug_statuses)); - - # This query collects new statuses which are common to all current bug statuses. - # It also accepts transitions where the bug status doesn't change. - $bug_status_ids = - $dbh->selectcol_arrayref( - 'SELECT DISTINCT sw1.new_status + if (!$vars->{'caneditbugs'}) { + ThrowUserError('auth_failure', + {group => 'editbugs', action => 'modify', object => 'multiple_bugs'}); + } + $vars->{'dotweak'} = 1; + + # issue_session_token needs to write to the master DB. + Bugzilla->switch_to_main_db(); + $vars->{'token'} = issue_session_token('buglist_mass_change'); + Bugzilla->switch_to_shadow_db(); + + $vars->{'products'} = $user->get_enterable_products; + $vars->{'platforms'} = get_legal_field_values('rep_platform'); + $vars->{'op_sys'} = get_legal_field_values('op_sys'); + $vars->{'priorities'} = get_legal_field_values('priority'); + $vars->{'severities'} = get_legal_field_values('bug_severity'); + $vars->{'resolutions'} = get_legal_field_values('resolution'); + + ($vars->{'flag_types'}, $vars->{any_flags_requesteeble}) + = _get_common_flag_types([keys %$bugcomponentids]); + + # Convert bug statuses to their ID. + my @bug_statuses = map { $dbh->quote($_) } keys %$bugstatuses; + my $bug_status_ids = $dbh->selectcol_arrayref( + 'SELECT id FROM bug_status + WHERE ' . $dbh->sql_in('value', \@bug_statuses) + ); + + # This query collects new statuses which are common to all current bug statuses. + # It also accepts transitions where the bug status doesn't change. + $bug_status_ids = $dbh->selectcol_arrayref( + 'SELECT DISTINCT sw1.new_status FROM status_workflow sw1 INNER JOIN bug_status ON bug_status.id = sw1.new_status @@ -1003,67 +1029,71 @@ if ($dotweak && scalar @bugs) { (SELECT * FROM status_workflow sw2 WHERE sw2.old_status != sw1.new_status AND ' - . $dbh->sql_in('sw2.old_status', $bug_status_ids) - . ' AND NOT EXISTS + . $dbh->sql_in('sw2.old_status', $bug_status_ids) . ' AND NOT EXISTS (SELECT * FROM status_workflow sw3 WHERE sw3.new_status = sw1.new_status - AND sw3.old_status = sw2.old_status))'); - - $vars->{'current_bug_statuses'} = [keys %$bugstatuses]; - $vars->{'new_bug_statuses'} = Bugzilla::Status->new_from_list($bug_status_ids); - - # The groups the user belongs to and which are editable for the given buglist. - $vars->{'groups'} = GetGroups(\@products); - - # If all bugs being changed are in the same product, the user can change - # their version and component, so generate a list of products, a list of - # versions for the product (if there is only one product on the list of - # products), and a list of components for the product. - if ($one_product) { - $vars->{'versions'} = [map($_->name, grep($_->is_active, @{ $one_product->versions }))]; - $vars->{'components'} = [map($_->name, grep($_->is_active, @{ $one_product->components }))]; - if (Bugzilla->params->{'usetargetmilestone'}) { - $vars->{'milestones'} = [map($_->name, grep($_->is_active, - @{ $one_product->milestones }))]; - } + AND sw3.old_status = sw2.old_status))' + ); + + $vars->{'current_bug_statuses'} = [keys %$bugstatuses]; + $vars->{'new_bug_statuses'} = Bugzilla::Status->new_from_list($bug_status_ids); + + # The groups the user belongs to and which are editable for the given buglist. + $vars->{'groups'} = GetGroups(\@products); + + # If all bugs being changed are in the same product, the user can change + # their version and component, so generate a list of products, a list of + # versions for the product (if there is only one product on the list of + # products), and a list of components for the product. + if ($one_product) { + $vars->{'versions'} + = [map($_->name, grep($_->is_active, @{$one_product->versions}))]; + $vars->{'components'} + = [map($_->name, grep($_->is_active, @{$one_product->components}))]; + if (Bugzilla->params->{'usetargetmilestone'}) { + $vars->{'milestones'} + = [map($_->name, grep($_->is_active, @{$one_product->milestones}))]; + } + } + else { + # We will only show the values at are active in all products. + my %values = (); + my @fields = ('components', 'versions'); + if (Bugzilla->params->{'usetargetmilestone'}) { + push @fields, 'milestones'; } - else { - # We will only show the values at are active in all products. - my %values = (); - my @fields = ('components', 'versions'); - if (Bugzilla->params->{'usetargetmilestone'}) { - push @fields, 'milestones'; - } - # Go through each product and count the number of times each field - # is used - foreach my $product_name (@products) { - my $product = Bugzilla::Product->new({name => $product_name, cache => 1}); - foreach my $field (@fields) { - my $list = $product->$field; - foreach my $item (@$list) { - ++$values{$field}{$item->name} if $item->is_active; - } - } + # Go through each product and count the number of times each field + # is used + foreach my $product_name (@products) { + my $product = Bugzilla::Product->new({name => $product_name, cache => 1}); + foreach my $field (@fields) { + my $list = $product->$field; + foreach my $item (@$list) { + ++$values{$field}{$item->name} if $item->is_active; } + } + } - # Now we get the list of each field and see which values have - # $product_count (i.e. appears in every product) - my $product_count = scalar(@products); - foreach my $field (@fields) { - my @values = grep { $values{$field}{$_} == $product_count } keys %{$values{$field}}; - if (scalar @values) { - @{$vars->{$field}} = $field eq 'version' - ? sort { vers_cmp(lc($a), lc($b)) } @values - : sort { lc($a) cmp lc($b) } @values - } - - # Do we need to show a warning about limited visiblity? - if (@values != scalar keys %{$values{$field}}) { - $vars->{excluded_values} = 1; - } - } + # Now we get the list of each field and see which values have + # $product_count (i.e. appears in every product) + my $product_count = scalar(@products); + foreach my $field (@fields) { + my @values + = grep { $values{$field}{$_} == $product_count } keys %{$values{$field}}; + if (scalar @values) { + @{$vars->{$field}} + = $field eq 'version' + ? sort { vers_cmp(lc($a), lc($b)) } @values + : sort { lc($a) cmp lc($b) } @values; + } + + # Do we need to show a warning about limited visiblity? + if (@values != scalar keys %{$values{$field}}) { + $vars->{excluded_values} = 1; + } } + } } # If we're editing a stored query, use the existing query name as default for @@ -1084,40 +1114,40 @@ my $contenttype; my $disposition = "inline"; if ($format->{'extension'} eq "html") { - my $list_id = $cgi->param('list_id') || $cgi->param('regetlastlist'); - my $search = $user->save_last_search( - { bugs => \@bugidlist, order => $order, vars => $vars, list_id => $list_id }); - $cgi->param('list_id', $search->id) if $search; - $contenttype = "text/html"; + my $list_id = $cgi->param('list_id') || $cgi->param('regetlastlist'); + my $search = $user->save_last_search( + {bugs => \@bugidlist, order => $order, vars => $vars, list_id => $list_id}); + $cgi->param('list_id', $search->id) if $search; + $contenttype = "text/html"; } else { - $contenttype = $format->{'ctype'}; + $contenttype = $format->{'ctype'}; } # Set 'urlquerypart' once the buglist ID is known. -$vars->{'urlquerypart'} = $params->canonicalise_query('order', 'cmdtype', - 'query_based_on', - 'token'); +$vars->{'urlquerypart'} + = $params->canonicalise_query('order', 'cmdtype', 'query_based_on', 'token'); if ($format->{'extension'} eq "csv") { - # We set CSV files to be downloaded, as they are designed for importing - # into other programs. - $disposition = "attachment"; - # If the user clicked the CSV link in the search results, - # They should get the Field Description, not the column name in the db - $vars->{'human'} = $cgi->param('human'); + # We set CSV files to be downloaded, as they are designed for importing + # into other programs. + $disposition = "attachment"; + + # If the user clicked the CSV link in the search results, + # They should get the Field Description, not the column name in the db + $vars->{'human'} = $cgi->param('human'); } -$cgi->close_standby_message($contenttype, $disposition, $disp_prefix, $format->{'extension'}); +$cgi->close_standby_message($contenttype, $disposition, $disp_prefix, + $format->{'extension'}); ################################################################################ # Content Generation ################################################################################ -Bugzilla::Hook::process("buglist_format", {'vars' => $vars, - 'format' => $format, - 'params' => $params}); +Bugzilla::Hook::process("buglist_format", + {'vars' => $vars, 'format' => $format, 'params' => $params}); # Generate and return the UI (HTML page) from the appropriate template. $template->process($format->{'template'}, $vars) diff --git a/chart.cgi b/chart.cgi index b3a52245cb..5fa6a2dfb7 100755 --- a/chart.cgi +++ b/chart.cgi @@ -46,26 +46,27 @@ use Bugzilla::Token; # when preparing Bugzilla for mod_perl, this script used these # variables in so many subroutines that it was easier to just # make them globals. -local our $cgi = Bugzilla->cgi; +local our $cgi = Bugzilla->cgi; local our $template = Bugzilla->template; -local our $vars = {}; +local our $vars = {}; my $dbh = Bugzilla->dbh; my $user = Bugzilla->login(LOGIN_REQUIRED); if (!Bugzilla->feature('new_charts')) { - ThrowUserError('feature_disabled', { feature => 'new_charts' }); + ThrowUserError('feature_disabled', {feature => 'new_charts'}); } # Go back to query.cgi if we are adding a boolean chart parameter. if (grep(/^cmd-/, $cgi->multi_param())) { - my $params = $cgi->canonicalise_query("format", "ctype", "action"); - print $cgi->redirect("query.cgi?format=" . $cgi->param('query_format') . - ($params ? "&$params" : "")); - exit; + my $params = $cgi->canonicalise_query("format", "ctype", "action"); + print $cgi->redirect("query.cgi?format=" + . $cgi->param('query_format') + . ($params ? "&$params" : "")); + exit; } -my $action = $cgi->param('action'); +my $action = $cgi->param('action'); my $series_id = $cgi->param('series_id'); $vars->{'doc_section'} = 'using/reports-and-charts.html#charts'; @@ -75,283 +76,296 @@ $vars->{'doc_section'} = 'using/reports-and-charts.html#charts'; # series_id they apply to (e.g. subscribe, unsubscribe). my @actions = grep(/^action-/, $cgi->multi_param()); if ($actions[0] && $actions[0] =~ /^action-([^\d]+)(\d*)$/) { - $action = $1; - $series_id = $2 if $2; + $action = $1; + $series_id = $2 if $2; } $action ||= "assemble"; # Go to buglist.cgi if we are doing a search. if ($action eq "search") { - my $params = $cgi->canonicalise_query("format", "ctype", "action"); - print $cgi->redirect("buglist.cgi" . ($params ? "?$params" : "")); - exit; + my $params = $cgi->canonicalise_query("format", "ctype", "action"); + print $cgi->redirect("buglist.cgi" . ($params ? "?$params" : "")); + exit; } -$user->in_group(Bugzilla->params->{"chartgroup"}) - || ThrowUserError("auth_failure", {group => Bugzilla->params->{"chartgroup"}, - action => "use", - object => "charts"}); +$user->in_group(Bugzilla->params->{"chartgroup"}) || ThrowUserError( + "auth_failure", + { + group => Bugzilla->params->{"chartgroup"}, + action => "use", + object => "charts" + } +); # Only admins may create public queries $user->in_group('admin') || $cgi->delete('public'); # All these actions relate to chart construction. if ($action =~ /^(assemble|add|remove|sum|subscribe|unsubscribe)$/) { - # These two need to be done before the creation of the Chart object, so - # that the changes they make will be reflected in it. - if ($action =~ /^subscribe|unsubscribe$/) { - detaint_natural($series_id) || ThrowCodeError("invalid_series_id"); - my $series = new Bugzilla::Series($series_id); - $series->$action($user->id); - } - - my $chart = new Bugzilla::Chart($cgi); - if ($action =~ /^remove|sum$/) { - $chart->$action(getSelectedLines()); - } - elsif ($action eq "add") { - my @series_ids = getAndValidateSeriesIDs(); - $chart->add(@series_ids); - } - - view($chart); + # These two need to be done before the creation of the Chart object, so + # that the changes they make will be reflected in it. + if ($action =~ /^subscribe|unsubscribe$/) { + detaint_natural($series_id) || ThrowCodeError("invalid_series_id"); + my $series = new Bugzilla::Series($series_id); + $series->$action($user->id); + } + + my $chart = new Bugzilla::Chart($cgi); + + if ($action =~ /^remove|sum$/) { + $chart->$action(getSelectedLines()); + } + elsif ($action eq "add") { + my @series_ids = getAndValidateSeriesIDs(); + $chart->add(@series_ids); + } + + view($chart); } elsif ($action eq "plot") { - plot(); + plot(); } elsif ($action eq "wrap") { - # For CSV "wrap", we go straight to "plot". - if ($cgi->param('ctype') && $cgi->param('ctype') eq "csv") { - plot(); - } - else { - wrap(); - } + + # For CSV "wrap", we go straight to "plot". + if ($cgi->param('ctype') && $cgi->param('ctype') eq "csv") { + plot(); + } + else { + wrap(); + } } elsif ($action eq "create") { - assertCanCreate($cgi); - my $token = $cgi->param('token'); - check_hash_token($token, ['create-series']); - - my $series = new Bugzilla::Series($cgi); + assertCanCreate($cgi); + my $token = $cgi->param('token'); + check_hash_token($token, ['create-series']); - ThrowUserError("series_already_exists", {'series' => $series}) - if $series->existsInDatabase; + my $series = new Bugzilla::Series($cgi); - $series->writeToDatabase(); - $vars->{'message'} = "series_created"; - $vars->{'series'} = $series; + ThrowUserError("series_already_exists", {'series' => $series}) + if $series->existsInDatabase; - my $chart = new Bugzilla::Chart($cgi); - view($chart); + $series->writeToDatabase(); + $vars->{'message'} = "series_created"; + $vars->{'series'} = $series; + + my $chart = new Bugzilla::Chart($cgi); + view($chart); } elsif ($action eq "edit") { - my $series = assertCanEdit($series_id); - edit($series); + my $series = assertCanEdit($series_id); + edit($series); } elsif ($action eq "alter") { - my $series = assertCanEdit($series_id); - my $token = $cgi->param('token'); - check_hash_token($token, [$series->id, $series->name]); - # XXX - This should be replaced by $series->set_foo() methods. - $series = new Bugzilla::Series($cgi); - - # We need to check if there is _another_ series in the database with - # our (potentially new) name. So we call existsInDatabase() to see if - # the return value is us or some other series we need to avoid stomping - # on. - my $id_of_series_in_db = $series->existsInDatabase(); - if (defined($id_of_series_in_db) && - $id_of_series_in_db != $series->{'series_id'}) - { - ThrowUserError("series_already_exists", {'series' => $series}); - } - - $series->writeToDatabase(); - $vars->{'changes_saved'} = 1; - - edit($series); + my $series = assertCanEdit($series_id); + my $token = $cgi->param('token'); + check_hash_token($token, [$series->id, $series->name]); + + # XXX - This should be replaced by $series->set_foo() methods. + $series = new Bugzilla::Series($cgi); + + # We need to check if there is _another_ series in the database with + # our (potentially new) name. So we call existsInDatabase() to see if + # the return value is us or some other series we need to avoid stomping + # on. + my $id_of_series_in_db = $series->existsInDatabase(); + if (defined($id_of_series_in_db) + && $id_of_series_in_db != $series->{'series_id'}) + { + ThrowUserError("series_already_exists", {'series' => $series}); + } + + $series->writeToDatabase(); + $vars->{'changes_saved'} = 1; + + edit($series); } elsif ($action eq "confirm-delete") { - $vars->{'series'} = assertCanEdit($series_id); + $vars->{'series'} = assertCanEdit($series_id); - print $cgi->header(); - $template->process("reports/delete-series.html.tmpl", $vars) - || ThrowTemplateError($template->error()); + print $cgi->header(); + $template->process("reports/delete-series.html.tmpl", $vars) + || ThrowTemplateError($template->error()); } elsif ($action eq "delete") { - my $series = assertCanEdit($series_id); - my $token = $cgi->param('token'); - check_hash_token($token, [$series->id, $series->name]); - - $dbh->bz_start_transaction(); - - $series->remove_from_db(); - # Remove (sub)categories which no longer have any series. - foreach my $cat (qw(category subcategory)) { - my $is_used = $dbh->selectrow_array("SELECT COUNT(*) FROM series WHERE $cat = ?", - undef, $series->{"${cat}_id"}); - if (!$is_used) { - $dbh->do('DELETE FROM series_categories WHERE id = ?', - undef, $series->{"${cat}_id"}); - } + my $series = assertCanEdit($series_id); + my $token = $cgi->param('token'); + check_hash_token($token, [$series->id, $series->name]); + + $dbh->bz_start_transaction(); + + $series->remove_from_db(); + + # Remove (sub)categories which no longer have any series. + foreach my $cat (qw(category subcategory)) { + my $is_used + = $dbh->selectrow_array("SELECT COUNT(*) FROM series WHERE $cat = ?", + undef, $series->{"${cat}_id"}); + if (!$is_used) { + $dbh->do('DELETE FROM series_categories WHERE id = ?', + undef, $series->{"${cat}_id"}); } - $dbh->bz_commit_transaction(); + } + $dbh->bz_commit_transaction(); - $vars->{'message'} = "series_deleted"; - $vars->{'series'} = $series; - view(); + $vars->{'message'} = "series_deleted"; + $vars->{'series'} = $series; + view(); } elsif ($action eq "convert_search") { - my $saved_search = $cgi->param('series_from_search') || ''; - my ($query) = grep { $_->name eq $saved_search } @{ $user->queries }; - my $url = ''; - if ($query) { - my $params = new Bugzilla::CGI($query->edit_link); - # These two parameters conflict with the one below. - $url = $params->canonicalise_query('format', 'query_format'); - $url = '&' . html_quote($url); - } - print $cgi->redirect(-location => correct_urlbase() . "query.cgi?format=create-series$url"); + my $saved_search = $cgi->param('series_from_search') || ''; + my ($query) = grep { $_->name eq $saved_search } @{$user->queries}; + my $url = ''; + if ($query) { + my $params = new Bugzilla::CGI($query->edit_link); + + # These two parameters conflict with the one below. + $url = $params->canonicalise_query('format', 'query_format'); + $url = '&' . html_quote($url); + } + print $cgi->redirect( + -location => correct_urlbase() . "query.cgi?format=create-series$url"); } else { - ThrowUserError('unknown_action', {action => $action}); + ThrowUserError('unknown_action', {action => $action}); } exit; # Find any selected series and return either the first or all of them. sub getAndValidateSeriesIDs { - my @series_ids = grep(/^\d+$/, $cgi->multi_param("name")); + my @series_ids = grep(/^\d+$/, $cgi->multi_param("name")); - return wantarray ? @series_ids : $series_ids[0]; + return wantarray ? @series_ids : $series_ids[0]; } # Return a list of IDs of all the lines selected in the UI. sub getSelectedLines { - my @ids = map { /^select(\d+)$/a ? $1 : () } $cgi->multi_param(); + my @ids = map { /^select(\d+)$/a ? $1 : () } $cgi->multi_param(); - return @ids; + return @ids; } -# Check if the user is the owner of series_id or is an admin. +# Check if the user is the owner of series_id or is an admin. sub assertCanEdit { - my $series_id = shift; - my $user = Bugzilla->user; + my $series_id = shift; + my $user = Bugzilla->user; - my $series = new Bugzilla::Series($series_id) - || ThrowCodeError('invalid_series_id'); + my $series + = new Bugzilla::Series($series_id) || ThrowCodeError('invalid_series_id'); - if (!$user->in_group('admin') && $series->{creator_id} != $user->id) { - ThrowUserError('illegal_series_edit'); - } + if (!$user->in_group('admin') && $series->{creator_id} != $user->id) { + ThrowUserError('illegal_series_edit'); + } - return $series; + return $series; } # Check if the user is permitted to create this series with these parameters. sub assertCanCreate { - my ($cgi) = shift; - my $user = Bugzilla->user; + my ($cgi) = shift; + my $user = Bugzilla->user; - $user->in_group("editbugs") || ThrowUserError("illegal_series_creation"); + $user->in_group("editbugs") || ThrowUserError("illegal_series_creation"); - # Check permission for frequency - my $min_freq = 7; - if ($cgi->param('frequency') < $min_freq && !$user->in_group("admin")) { - ThrowUserError("illegal_frequency", { 'minimum' => $min_freq }); - } + # Check permission for frequency + my $min_freq = 7; + if ($cgi->param('frequency') < $min_freq && !$user->in_group("admin")) { + ThrowUserError("illegal_frequency", {'minimum' => $min_freq}); + } } sub validateWidthAndHeight { - $vars->{'width'} = $cgi->param('width'); - $vars->{'height'} = $cgi->param('height'); - - if (defined($vars->{'width'})) { - (detaint_natural($vars->{'width'}) && $vars->{'width'} > 0) - || ThrowUserError("invalid_dimensions"); - } - - if (defined($vars->{'height'})) { - (detaint_natural($vars->{'height'}) && $vars->{'height'} > 0) - || ThrowUserError("invalid_dimensions"); - } - - # The equivalent of 2000 square seems like a very reasonable maximum size. - # This is merely meant to prevent accidental or deliberate DOS, and should - # have no effect in practice. - if ($vars->{'width'} && $vars->{'height'}) { - (($vars->{'width'} * $vars->{'height'}) <= 4000000) - || ThrowUserError("chart_too_large"); - } + $vars->{'width'} = $cgi->param('width'); + $vars->{'height'} = $cgi->param('height'); + + if (defined($vars->{'width'})) { + (detaint_natural($vars->{'width'}) && $vars->{'width'} > 0) + || ThrowUserError("invalid_dimensions"); + } + + if (defined($vars->{'height'})) { + (detaint_natural($vars->{'height'}) && $vars->{'height'} > 0) + || ThrowUserError("invalid_dimensions"); + } + + # The equivalent of 2000 square seems like a very reasonable maximum size. + # This is merely meant to prevent accidental or deliberate DOS, and should + # have no effect in practice. + if ($vars->{'width'} && $vars->{'height'}) { + (($vars->{'width'} * $vars->{'height'}) <= 4000000) + || ThrowUserError("chart_too_large"); + } } sub edit { - my $series = shift; + my $series = shift; - $vars->{'category'} = Bugzilla::Chart::getVisibleSeries(); - $vars->{'default'} = $series; - $vars->{'message'} = 'series_updated' if $vars->{'changes_saved'}; + $vars->{'category'} = Bugzilla::Chart::getVisibleSeries(); + $vars->{'default'} = $series; + $vars->{'message'} = 'series_updated' if $vars->{'changes_saved'}; - print $cgi->header(); - $template->process("reports/edit-series.html.tmpl", $vars) - || ThrowTemplateError($template->error()); + print $cgi->header(); + $template->process("reports/edit-series.html.tmpl", $vars) + || ThrowTemplateError($template->error()); } sub plot { - validateWidthAndHeight(); - $vars->{'chart'} = new Bugzilla::Chart($cgi); + validateWidthAndHeight(); + $vars->{'chart'} = new Bugzilla::Chart($cgi); - my $format = $template->get_format("reports/chart", "", scalar($cgi->param('ctype'))); - $format->{'ctype'} = 'text/html' if $cgi->param('debug'); + my $format + = $template->get_format("reports/chart", "", scalar($cgi->param('ctype'))); + $format->{'ctype'} = 'text/html' if $cgi->param('debug'); - $cgi->set_dated_content_disp('inline', 'chart', $format->{extension}); - print $cgi->header($format->{'ctype'}); - disable_utf8() if ($format->{'ctype'} =~ /^image\//); + $cgi->set_dated_content_disp('inline', 'chart', $format->{extension}); + print $cgi->header($format->{'ctype'}); + disable_utf8() if ($format->{'ctype'} =~ /^image\//); - # Debugging PNGs is a pain; we need to be able to see the error messages - $vars->{'chart'}->dump() if $cgi->param('debug'); + # Debugging PNGs is a pain; we need to be able to see the error messages + $vars->{'chart'}->dump() if $cgi->param('debug'); - $template->process($format->{'template'}, $vars) - || ThrowTemplateError($template->error()); + $template->process($format->{'template'}, $vars) + || ThrowTemplateError($template->error()); } sub wrap { - validateWidthAndHeight(); - - # We create a Chart object so we can validate the parameters - my $chart = new Bugzilla::Chart($cgi); - - $vars->{'time'} = localtime(time()); - - $vars->{'imagebase'} = $cgi->canonicalise_query( - "action", "action-wrap", "ctype", "format", "width", "height"); - - print $cgi->header(); - $template->process("reports/chart.html.tmpl", $vars) - || ThrowTemplateError($template->error()); + validateWidthAndHeight(); + + # We create a Chart object so we can validate the parameters + my $chart = new Bugzilla::Chart($cgi); + + $vars->{'time'} = localtime(time()); + + $vars->{'imagebase'} + = $cgi->canonicalise_query("action", "action-wrap", "ctype", "format", + "width", "height"); + + print $cgi->header(); + $template->process("reports/chart.html.tmpl", $vars) + || ThrowTemplateError($template->error()); } sub view { - my $chart = shift; + my $chart = shift; - # Set defaults - foreach my $field ('category', 'subcategory', 'name', 'ctype') { - $vars->{'default'}{$field} = $cgi->param($field) || 0; - } + # Set defaults + foreach my $field ('category', 'subcategory', 'name', 'ctype') { + $vars->{'default'}{$field} = $cgi->param($field) || 0; + } - # Pass the state object to the display UI. - $vars->{'chart'} = $chart; - $vars->{'category'} = Bugzilla::Chart::getVisibleSeries(); + # Pass the state object to the display UI. + $vars->{'chart'} = $chart; + $vars->{'category'} = Bugzilla::Chart::getVisibleSeries(); - print $cgi->header(); + print $cgi->header(); - # If we have having problems with bad data, we can set debug=1 to dump - # the data structure. - $chart->dump() if $cgi->param('debug'); + # If we have having problems with bad data, we can set debug=1 to dump + # the data structure. + $chart->dump() if $cgi->param('debug'); - $template->process("reports/create-chart.html.tmpl", $vars) - || ThrowTemplateError($template->error()); + $template->process("reports/create-chart.html.tmpl", $vars) + || ThrowTemplateError($template->error()); } diff --git a/checksetup.pl b/checksetup.pl index 7ce9673ced..a63e53dce5 100755 --- a/checksetup.pl +++ b/checksetup.pl @@ -25,14 +25,15 @@ our @BUGZILLA_INC = grep { !/checksetup_lib/ } @INC; use Getopt::Long qw(:config bundling); use Pod::Usage; + # Bug 1270550 - Tie::Hash::NamedCapture must be loaded before Safe. use Tie::Hash::NamedCapture; use Safe; use Bugzilla::Constants; use Bugzilla::Install::Requirements; -use Bugzilla::Install::Util qw(install_string get_version_and_os - init_console success); +use Bugzilla::Install::Util qw(install_string get_version_and_os + init_console success); ###################################################################### # Live Code @@ -46,16 +47,18 @@ Bugzilla::Install::Util::no_checksetup_from_cgi() if $ENV{'SERVER_SOFTWARE'}; init_console(); my %switch; -GetOptions(\%switch, 'help|h|?', - 'no-templates|t', 'verbose|v|no-silent', - 'cpanm:s', 'check-modules', - 'make-admin=s', 'reset-password=s', 'version|V', - 'no-permissions|p'); +GetOptions( + \%switch, 'help|h|?', + 'no-templates|t', 'verbose|v|no-silent', + 'cpanm:s', 'check-modules', + 'make-admin=s', 'reset-password=s', + 'version|V', 'no-permissions|p' +); # Print the help message if that switch was selected. pod2usage({-verbose => 1, -exitval => 1}) if $switch{'help'}; -# Read in the "answers" file if it exists, for running in +# Read in the "answers" file if it exists, for running in # non-interactive mode. my $answers_file = $ARGV[0]; my $silent = $answers_file && !$switch{'verbose'}; @@ -63,32 +66,37 @@ print(install_string('header', get_version_and_os()) . "\n") unless $silent; exit 0 if $switch{'version'}; if (defined $switch{cpanm}) { - my $default = join(' ', qw( - all notest -oracle -mysql -pg -mod_perl -old_charts -new_charts - -graphical_reports -detect_charset -auth_radius -auth_ldap - )); - my @features = split(/\s+/, $switch{cpanm} || $default); - my @cpanm_args = ('-l', 'local', '--installdeps'); - while (my $feature = shift @features) { - if ($feature eq 'all') { - push @cpanm_args, '--with-all-features'; - } - elsif ($feature eq 'default') { - unshift @features, split(/\s+/, $default); - } - elsif ($feature eq 'notest' || $feature eq 'skip-satisfied' || $feature eq 'quiet') { - push @cpanm_args, "--$feature"; - } - elsif ($feature =~ /^-(.+)$/) { - push @cpanm_args, "--without-feature=$1"; - } - else { - push @cpanm_args, "--with-feature=$feature"; - } + my $default = join( + ' ', qw( + all notest -oracle -mysql -pg -mod_perl -old_charts -new_charts + -graphical_reports -detect_charset -auth_radius -auth_ldap + ) + ); + my @features = split(/\s+/, $switch{cpanm} || $default); + my @cpanm_args = ('-l', 'local', '--installdeps'); + while (my $feature = shift @features) { + if ($feature eq 'all') { + push @cpanm_args, '--with-all-features'; + } + elsif ($feature eq 'default') { + unshift @features, split(/\s+/, $default); + } + elsif ($feature eq 'notest' + || $feature eq 'skip-satisfied' + || $feature eq 'quiet') + { + push @cpanm_args, "--$feature"; + } + elsif ($feature =~ /^-(.+)$/) { + push @cpanm_args, "--without-feature=$1"; } - print "cpanm @cpanm_args \".\"\n" if !$silent; - my $rv = system('cpanm', @cpanm_args, '.'); - exit 1 if $rv != 0; + else { + push @cpanm_args, "--with-feature=$feature"; + } + } + print "cpanm @cpanm_args \".\"\n" if !$silent; + my $rv = system('cpanm', @cpanm_args, '.'); + exit 1 if $rv != 0; } $ENV{PERL_MM_USE_DEFAULT} = 1; @@ -97,10 +105,10 @@ system($^X, "Makefile.PL"); my $meta = load_cpan_meta(); if (keys %{$meta->{optional_features}} < 1) { - warn "Your version of ExtUtils::MakeMaker is probably too old\n"; - warn "Falling back to static (and wrong) META.json\n"; - unlink('MYMETA.json'); - $meta = load_cpan_meta(); + warn "Your version of ExtUtils::MakeMaker is probably too old\n"; + warn "Falling back to static (and wrong) META.json\n"; + unlink('MYMETA.json'); + $meta = load_cpan_meta(); } my $requirements = check_cpan_requirements($meta, \@BUGZILLA_INC, !$silent); @@ -132,7 +140,7 @@ import Bugzilla::Install::Localconfig qw(update_localconfig); require Bugzilla::Install::Filesystem; import Bugzilla::Install::Filesystem qw(update_filesystem create_htaccess - fix_all_file_permissions); + fix_all_file_permissions); require Bugzilla::Install::DB; require Bugzilla::DB; require Bugzilla::Template; @@ -146,8 +154,8 @@ Bugzilla->installation_answers($answers_file); # Check and update --LOCAL-- configuration ########################################################################### -print "Reading " . bz_locations()->{'localconfig'} . "...\n" unless $silent; -update_localconfig({ output => !$silent }); +print "Reading " . bz_locations()->{'localconfig'} . "...\n" unless $silent; +update_localconfig({output => !$silent}); my $lc_hash = Bugzilla->localconfig; ########################################################################### @@ -163,8 +171,10 @@ Bugzilla::DB::bz_create_database() if $lc_hash->{'db_check'}; # now get a handle to the database: my $dbh = Bugzilla->dbh; + # Create the tables, and do any database-specific schema changes. $dbh->bz_setup_database(); + # Populate the tables that hold the values for the