From 70780e40e5586c6882e33dd65a3dc3f31031a321 Mon Sep 17 00:00:00 2001
From: "Robin H. Johnson" Bugzilla::Chart object:";
- print html_quote(Data::Dumper::Dumper($self));
- print "
";
+ my $self = shift;
+
+ # Make sure we've read in our data
+ my $data = $self->data;
+
+ require Data::Dumper;
+ say "Bugzilla::Chart object:";
+ print html_quote(Data::Dumper::Dumper($self));
+ print "
";
}
1;
diff --git a/Bugzilla/Classification.pm b/Bugzilla/Classification.pm
index 09f71baaf..1ea86f592 100644
--- a/Bugzilla/Classification.pm
+++ b/Bugzilla/Classification.pm
@@ -26,26 +26,26 @@ use parent qw(Bugzilla::Field::ChoiceInterface Bugzilla::Object Exporter);
use constant IS_CONFIG => 1;
-use constant DB_TABLE => 'classifications';
+use constant DB_TABLE => 'classifications';
use constant LIST_ORDER => 'sortkey, name';
use constant DB_COLUMNS => qw(
- id
- name
- description
- sortkey
+ id
+ name
+ description
+ sortkey
);
use constant UPDATE_COLUMNS => qw(
- name
- description
- sortkey
+ name
+ description
+ sortkey
);
use constant VALIDATORS => {
- name => \&_check_name,
- description => \&_check_description,
- sortkey => \&_check_sortkey,
+ name => \&_check_name,
+ description => \&_check_description,
+ sortkey => \&_check_sortkey,
};
###############################
@@ -53,29 +53,31 @@ use constant VALIDATORS => {
###############################
sub remove_from_db {
- my $self = shift;
- my $dbh = Bugzilla->dbh;
+ my $self = shift;
+ my $dbh = Bugzilla->dbh;
- ThrowUserError("classification_not_deletable") if ($self->id == 1);
+ ThrowUserError("classification_not_deletable") if ($self->id == 1);
- $dbh->bz_start_transaction();
+ $dbh->bz_start_transaction();
- # Reclassify products to the default classification, if needed.
- my $product_ids = $dbh->selectcol_arrayref(
- 'SELECT id FROM products WHERE classification_id = ?', undef, $self->id);
-
- if (@$product_ids) {
- $dbh->do('UPDATE products SET classification_id = 1 WHERE '
- . $dbh->sql_in('id', $product_ids));
- foreach my $id (@$product_ids) {
- Bugzilla->memcached->clear({ table => 'products', id => $id });
- }
- Bugzilla->memcached->clear_config();
+ # Reclassify products to the default classification, if needed.
+ my $product_ids
+ = $dbh->selectcol_arrayref(
+ 'SELECT id FROM products WHERE classification_id = ?',
+ undef, $self->id);
+
+ if (@$product_ids) {
+ $dbh->do('UPDATE products SET classification_id = 1 WHERE '
+ . $dbh->sql_in('id', $product_ids));
+ foreach my $id (@$product_ids) {
+ Bugzilla->memcached->clear({table => 'products', id => $id});
}
+ Bugzilla->memcached->clear_config();
+ }
- $self->SUPER::remove_from_db();
+ $self->SUPER::remove_from_db();
- $dbh->bz_commit_transaction();
+ $dbh->bz_commit_transaction();
}
@@ -84,38 +86,41 @@ sub remove_from_db {
###############################
sub _check_name {
- my ($invocant, $name) = @_;
-
- $name = trim($name);
- $name || ThrowUserError('classification_not_specified');
-
- if (length($name) > MAX_CLASSIFICATION_SIZE) {
- ThrowUserError('classification_name_too_long', {'name' => $name});
- }
-
- my $classification = new Bugzilla::Classification({name => $name});
- if ($classification && (!ref $invocant || $classification->id != $invocant->id)) {
- ThrowUserError("classification_already_exists", { name => $classification->name });
- }
- return $name;
+ my ($invocant, $name) = @_;
+
+ $name = trim($name);
+ $name || ThrowUserError('classification_not_specified');
+
+ if (length($name) > MAX_CLASSIFICATION_SIZE) {
+ ThrowUserError('classification_name_too_long', {'name' => $name});
+ }
+
+ my $classification = new Bugzilla::Classification({name => $name});
+ if ($classification && (!ref $invocant || $classification->id != $invocant->id))
+ {
+ ThrowUserError("classification_already_exists",
+ {name => $classification->name});
+ }
+ return $name;
}
sub _check_description {
- my ($invocant, $description) = @_;
+ my ($invocant, $description) = @_;
- $description = trim($description || '');
- return $description;
+ $description = trim($description || '');
+ return $description;
}
sub _check_sortkey {
- my ($invocant, $sortkey) = @_;
-
- $sortkey ||= 0;
- my $stored_sortkey = $sortkey;
- if (!detaint_natural($sortkey) || $sortkey > MAX_SMALLINT) {
- ThrowUserError('classification_invalid_sortkey', { 'sortkey' => $stored_sortkey });
- }
- return $sortkey;
+ my ($invocant, $sortkey) = @_;
+
+ $sortkey ||= 0;
+ my $stored_sortkey = $sortkey;
+ if (!detaint_natural($sortkey) || $sortkey > MAX_SMALLINT) {
+ ThrowUserError('classification_invalid_sortkey',
+ {'sortkey' => $stored_sortkey});
+ }
+ return $sortkey;
}
#####################################
@@ -124,41 +129,45 @@ sub _check_sortkey {
use constant FIELD_NAME => 'classification';
use constant is_default => 0;
-use constant is_active => 1;
+use constant is_active => 1;
###############################
#### Methods ####
###############################
-sub set_name { $_[0]->set('name', $_[1]); }
+sub set_name { $_[0]->set('name', $_[1]); }
sub set_description { $_[0]->set('description', $_[1]); }
-sub set_sortkey { $_[0]->set('sortkey', $_[1]); }
+sub set_sortkey { $_[0]->set('sortkey', $_[1]); }
sub product_count {
- my $self = shift;
- my $dbh = Bugzilla->dbh;
+ my $self = shift;
+ my $dbh = Bugzilla->dbh;
- if (!defined $self->{'product_count'}) {
- $self->{'product_count'} = $dbh->selectrow_array(q{
+ if (!defined $self->{'product_count'}) {
+ $self->{'product_count'} = $dbh->selectrow_array(
+ q{
SELECT COUNT(*) FROM products
- WHERE classification_id = ?}, undef, $self->id) || 0;
- }
- return $self->{'product_count'};
+ WHERE classification_id = ?}, undef, $self->id
+ ) || 0;
+ }
+ return $self->{'product_count'};
}
sub products {
- my $self = shift;
- my $dbh = Bugzilla->dbh;
+ my $self = shift;
+ my $dbh = Bugzilla->dbh;
- if (!$self->{'products'}) {
- my $product_ids = $dbh->selectcol_arrayref(q{
+ if (!$self->{'products'}) {
+ my $product_ids = $dbh->selectcol_arrayref(
+ q{
SELECT id FROM products
WHERE classification_id = ?
- ORDER BY name}, undef, $self->id);
+ ORDER BY name}, undef, $self->id
+ );
- $self->{'products'} = Bugzilla::Product->new_from_list($product_ids);
- }
- return $self->{'products'};
+ $self->{'products'} = Bugzilla::Product->new_from_list($product_ids);
+ }
+ return $self->{'products'};
}
###############################
@@ -166,7 +175,7 @@ sub products {
###############################
sub description { return $_[0]->{'description'}; }
-sub sortkey { return $_[0]->{'sortkey'}; }
+sub sortkey { return $_[0]->{'sortkey'}; }
###############################
@@ -177,27 +186,32 @@ sub sortkey { return $_[0]->{'sortkey'}; }
# in global/choose-product.html.tmpl.
sub sort_products_by_classification {
- my $products = shift;
- my $list;
-
- if (Bugzilla->params->{'useclassification'}) {
- my $class = {};
- # Get all classifications with at least one product.
- foreach my $product (@$products) {
- $class->{$product->classification_id}->{'object'} ||=
- new Bugzilla::Classification($product->classification_id);
- # Nice way to group products per classification, without querying
- # the DB again.
- push(@{$class->{$product->classification_id}->{'products'}}, $product);
- }
- $list = [sort {$a->{'object'}->sortkey <=> $b->{'object'}->sortkey
- || lc($a->{'object'}->name) cmp lc($b->{'object'}->name)}
- (values %$class)];
- }
- else {
- $list = [{object => undef, products => $products}];
+ my $products = shift;
+ my $list;
+
+ if (Bugzilla->params->{'useclassification'}) {
+ my $class = {};
+
+ # Get all classifications with at least one product.
+ foreach my $product (@$products) {
+ $class->{$product->classification_id}->{'object'}
+ ||= new Bugzilla::Classification($product->classification_id);
+
+ # Nice way to group products per classification, without querying
+ # the DB again.
+ push(@{$class->{$product->classification_id}->{'products'}}, $product);
}
- return $list;
+ $list = [
+ sort {
+ $a->{'object'}->sortkey <=> $b->{'object'}->sortkey
+ || lc($a->{'object'}->name) cmp lc($b->{'object'}->name)
+ } (values %$class)
+ ];
+ }
+ else {
+ $list = [{object => undef, products => $products}];
+ }
+ return $list;
}
1;
diff --git a/Bugzilla/Comment.pm b/Bugzilla/Comment.pm
index b036907d7..02a044ff5 100644
--- a/Bugzilla/Comment.pm
+++ b/Bugzilla/Comment.pm
@@ -33,47 +33,48 @@ use constant AUDIT_CREATES => 0;
use constant AUDIT_UPDATES => 0;
use constant DB_COLUMNS => qw(
- comment_id
- bug_id
- who
- bug_when
- work_time
- thetext
- isprivate
- already_wrapped
- type
- extra_data
+ comment_id
+ bug_id
+ who
+ bug_when
+ work_time
+ thetext
+ isprivate
+ already_wrapped
+ type
+ extra_data
);
use constant UPDATE_COLUMNS => qw(
- isprivate
- type
- extra_data
+ isprivate
+ type
+ extra_data
);
use constant DB_TABLE => 'longdescs';
use constant ID_FIELD => 'comment_id';
+
# In some rare cases, two comments can have identical timestamps. If
# this happens, we want to be sure that the comment added later shows up
# later in the sequence.
use constant LIST_ORDER => 'bug_when, comment_id';
use constant VALIDATORS => {
- bug_id => \&_check_bug_id,
- who => \&_check_who,
- bug_when => \&_check_bug_when,
- work_time => \&_check_work_time,
- thetext => \&_check_thetext,
- isprivate => \&_check_isprivate,
- extra_data => \&_check_extra_data,
- type => \&_check_type,
+ bug_id => \&_check_bug_id,
+ who => \&_check_who,
+ bug_when => \&_check_bug_when,
+ work_time => \&_check_work_time,
+ thetext => \&_check_thetext,
+ isprivate => \&_check_isprivate,
+ extra_data => \&_check_extra_data,
+ type => \&_check_type,
};
use constant VALIDATOR_DEPENDENCIES => {
- extra_data => ['type'],
- bug_id => ['who'],
- work_time => ['who', 'bug_id'],
- isprivate => ['who'],
+ extra_data => ['type'],
+ bug_id => ['who'],
+ work_time => ['who', 'bug_id'],
+ isprivate => ['who'],
};
#########################
@@ -81,95 +82,100 @@ use constant VALIDATOR_DEPENDENCIES => {
#########################
sub update {
- my $self = shift;
- my ($changes, $old_comment) = $self->SUPER::update(@_);
-
- if (exists $changes->{'thetext'} || exists $changes->{'isprivate'}) {
- $self->bug->_sync_fulltext( update_comments => 1);
- }
-
- my @old_tags = @{ $old_comment->tags };
- my @new_tags = @{ $self->tags };
- my ($removed_tags, $added_tags) = diff_arrays(\@old_tags, \@new_tags);
-
- if (@$removed_tags || @$added_tags) {
- my $dbh = Bugzilla->dbh;
- my $when = $dbh->selectrow_array("SELECT LOCALTIMESTAMP(0)");
- my $sth_delete = $dbh->prepare(
- "DELETE FROM longdescs_tags WHERE comment_id = ? AND tag = ?"
- );
- my $sth_insert = $dbh->prepare(
- "INSERT INTO longdescs_tags(comment_id, tag) VALUES (?, ?)"
- );
- my $sth_activity = $dbh->prepare(
- "INSERT INTO longdescs_tags_activity
+ my $self = shift;
+ my ($changes, $old_comment) = $self->SUPER::update(@_);
+
+ if (exists $changes->{'thetext'} || exists $changes->{'isprivate'}) {
+ $self->bug->_sync_fulltext(update_comments => 1);
+ }
+
+ my @old_tags = @{$old_comment->tags};
+ my @new_tags = @{$self->tags};
+ my ($removed_tags, $added_tags) = diff_arrays(\@old_tags, \@new_tags);
+
+ if (@$removed_tags || @$added_tags) {
+ my $dbh = Bugzilla->dbh;
+ my $when = $dbh->selectrow_array("SELECT LOCALTIMESTAMP(0)");
+ my $sth_delete = $dbh->prepare(
+ "DELETE FROM longdescs_tags WHERE comment_id = ? AND tag = ?");
+ my $sth_insert
+ = $dbh->prepare("INSERT INTO longdescs_tags(comment_id, tag) VALUES (?, ?)");
+ my $sth_activity = $dbh->prepare(
+ "INSERT INTO longdescs_tags_activity
(bug_id, comment_id, who, bug_when, added, removed)
VALUES (?, ?, ?, ?, ?, ?)"
- );
-
- foreach my $tag (@$removed_tags) {
- my $weighted = Bugzilla::Comment::TagWeights->new({ name => $tag });
- if ($weighted) {
- if ($weighted->weight == 1) {
- $weighted->remove_from_db();
- } else {
- $weighted->set_weight($weighted->weight - 1);
- $weighted->update();
- }
- }
- trick_taint($tag);
- $sth_delete->execute($self->id, $tag);
- $sth_activity->execute(
- $self->bug_id, $self->id, Bugzilla->user->id, $when, '', $tag);
- }
+ );
- foreach my $tag (@$added_tags) {
- my $weighted = Bugzilla::Comment::TagWeights->new({ name => $tag });
- if ($weighted) {
- $weighted->set_weight($weighted->weight + 1);
- $weighted->update();
- } else {
- Bugzilla::Comment::TagWeights->create({ tag => $tag, weight => 1 });
- }
- trick_taint($tag);
- $sth_insert->execute($self->id, $tag);
- $sth_activity->execute(
- $self->bug_id, $self->id, Bugzilla->user->id, $when, $tag, '');
+ foreach my $tag (@$removed_tags) {
+ my $weighted = Bugzilla::Comment::TagWeights->new({name => $tag});
+ if ($weighted) {
+ if ($weighted->weight == 1) {
+ $weighted->remove_from_db();
}
+ else {
+ $weighted->set_weight($weighted->weight - 1);
+ $weighted->update();
+ }
+ }
+ trick_taint($tag);
+ $sth_delete->execute($self->id, $tag);
+ $sth_activity->execute($self->bug_id, $self->id, Bugzilla->user->id, $when, '',
+ $tag);
}
- return $changes;
+ foreach my $tag (@$added_tags) {
+ my $weighted = Bugzilla::Comment::TagWeights->new({name => $tag});
+ if ($weighted) {
+ $weighted->set_weight($weighted->weight + 1);
+ $weighted->update();
+ }
+ else {
+ Bugzilla::Comment::TagWeights->create({tag => $tag, weight => 1});
+ }
+ trick_taint($tag);
+ $sth_insert->execute($self->id, $tag);
+ $sth_activity->execute($self->bug_id, $self->id, Bugzilla->user->id, $when,
+ $tag, '');
+ }
+ }
+
+ return $changes;
}
# Speeds up displays of comment lists by loading all author objects and tags at
# once for a whole list.
sub preload {
- my ($class, $comments) = @_;
- # Author
- my %user_ids = map { $_->{who} => 1 } @$comments;
- my $users = Bugzilla::User->new_from_list([keys %user_ids]);
- my %user_map = map { $_->id => $_ } @$users;
- foreach my $comment (@$comments) {
- $comment->{author} = $user_map{$comment->{who}};
- }
- # Tags
- if (Bugzilla->params->{'comment_taggers_group'}) {
- my $dbh = Bugzilla->dbh;
- my @comment_ids = map { $_->id } @$comments;
- my %comment_map = map { $_->id => $_ } @$comments;
- my $rows = $dbh->selectall_arrayref(
- "SELECT comment_id, " . $dbh->sql_group_concat('tag', "','") . "
+ my ($class, $comments) = @_;
+
+ # Author
+ my %user_ids = map { $_->{who} => 1 } @$comments;
+ my $users = Bugzilla::User->new_from_list([keys %user_ids]);
+ my %user_map = map { $_->id => $_ } @$users;
+ foreach my $comment (@$comments) {
+ $comment->{author} = $user_map{$comment->{who}};
+ }
+
+ # Tags
+ if (Bugzilla->params->{'comment_taggers_group'}) {
+ my $dbh = Bugzilla->dbh;
+ my @comment_ids = map { $_->id } @$comments;
+ my %comment_map = map { $_->id => $_ } @$comments;
+ my $rows = $dbh->selectall_arrayref(
+ "SELECT comment_id, " . $dbh->sql_group_concat('tag', "','") . "
FROM longdescs_tags
- WHERE " . $dbh->sql_in('comment_id', \@comment_ids) . ' ' .
- $dbh->sql_group_by('comment_id'));
- foreach my $row (@$rows) {
- $comment_map{$row->[0]}->{tags} = [ split(/,/, $row->[1]) ];
- }
- # Also sets the 'tags' attribute for comments which have no entry
- # in the longdescs_tags table, else calling $comment->tags will
- # trigger another SQL query again.
- $comment_map{$_}->{tags} ||= [] foreach @comment_ids;
+ WHERE "
+ . $dbh->sql_in('comment_id', \@comment_ids) . ' '
+ . $dbh->sql_group_by('comment_id')
+ );
+ foreach my $row (@$rows) {
+ $comment_map{$row->[0]}->{tags} = [split(/,/, $row->[1])];
}
+
+ # Also sets the 'tags' attribute for comments which have no entry
+ # in the longdescs_tags table, else calling $comment->tags will
+ # trigger another SQL query again.
+ $comment_map{$_}->{tags} ||= [] foreach @comment_ids;
+ }
}
###############################
@@ -177,130 +183,132 @@ sub preload {
###############################
sub already_wrapped { return $_[0]->{'already_wrapped'}; }
-sub body { return $_[0]->{'thetext'}; }
-sub bug_id { return $_[0]->{'bug_id'}; }
-sub creation_ts { return $_[0]->{'bug_when'}; }
-sub is_private { return $_[0]->{'isprivate'}; }
-sub work_time {
- # Work time is returned as a string (see bug 607909)
- return 0 if $_[0]->{'work_time'} + 0 == 0;
- return $_[0]->{'work_time'};
+sub body { return $_[0]->{'thetext'}; }
+sub bug_id { return $_[0]->{'bug_id'}; }
+sub creation_ts { return $_[0]->{'bug_when'}; }
+sub is_private { return $_[0]->{'isprivate'}; }
+
+sub work_time {
+
+ # Work time is returned as a string (see bug 607909)
+ return 0 if $_[0]->{'work_time'} + 0 == 0;
+ return $_[0]->{'work_time'};
}
-sub type { return $_[0]->{'type'}; }
-sub extra_data { return $_[0]->{'extra_data'} }
+sub type { return $_[0]->{'type'}; }
+sub extra_data { return $_[0]->{'extra_data'} }
sub tags {
- my ($self) = @_;
- state $comment_taggers_group = Bugzilla->params->{'comment_taggers_group'};
- return [] unless $comment_taggers_group;
- $self->{'tags'} ||= Bugzilla->dbh->selectcol_arrayref(
- "SELECT tag
+ my ($self) = @_;
+ state $comment_taggers_group = Bugzilla->params->{'comment_taggers_group'};
+ return [] unless $comment_taggers_group;
+ $self->{'tags'} ||= Bugzilla->dbh->selectcol_arrayref(
+ "SELECT tag
FROM longdescs_tags
WHERE comment_id = ?
- ORDER BY tag",
- undef, $self->id);
- return $self->{'tags'};
+ ORDER BY tag", undef, $self->id
+ );
+ return $self->{'tags'};
}
sub collapsed {
- my ($self) = @_;
- state $comment_taggers_group = Bugzilla->params->{'comment_taggers_group'};
- return 0 unless $comment_taggers_group;
- return $self->{collapsed} if exists $self->{collapsed};
-
- state $collapsed_comment_tags = Bugzilla->params->{'collapsed_comment_tags'};
- $self->{collapsed} = 0;
- Bugzilla->request_cache->{comment_tags_collapsed}
- ||= [ split(/\s*,\s*/, $collapsed_comment_tags) ];
- my @collapsed_tags = @{ Bugzilla->request_cache->{comment_tags_collapsed} };
- foreach my $my_tag (@{ $self->tags }) {
- $my_tag = lc($my_tag);
- foreach my $collapsed_tag (@collapsed_tags) {
- if ($my_tag eq lc($collapsed_tag)) {
- $self->{collapsed} = 1;
- last;
- }
- }
- last if $self->{collapsed};
+ my ($self) = @_;
+ state $comment_taggers_group = Bugzilla->params->{'comment_taggers_group'};
+ return 0 unless $comment_taggers_group;
+ return $self->{collapsed} if exists $self->{collapsed};
+
+ state $collapsed_comment_tags = Bugzilla->params->{'collapsed_comment_tags'};
+ $self->{collapsed} = 0;
+ Bugzilla->request_cache->{comment_tags_collapsed}
+ ||= [split(/\s*,\s*/, $collapsed_comment_tags)];
+ my @collapsed_tags = @{Bugzilla->request_cache->{comment_tags_collapsed}};
+ foreach my $my_tag (@{$self->tags}) {
+ $my_tag = lc($my_tag);
+ foreach my $collapsed_tag (@collapsed_tags) {
+ if ($my_tag eq lc($collapsed_tag)) {
+ $self->{collapsed} = 1;
+ last;
+ }
}
- return $self->{collapsed};
+ last if $self->{collapsed};
+ }
+ return $self->{collapsed};
}
sub bug {
- my $self = shift;
- require Bugzilla::Bug;
- $self->{bug} ||= new Bugzilla::Bug($self->bug_id);
- return $self->{bug};
+ my $self = shift;
+ require Bugzilla::Bug;
+ $self->{bug} ||= new Bugzilla::Bug($self->bug_id);
+ return $self->{bug};
}
sub is_about_attachment {
- my ($self) = @_;
- return 1 if ($self->type == CMT_ATTACHMENT_CREATED
- or $self->type == CMT_ATTACHMENT_UPDATED);
- return 0;
+ my ($self) = @_;
+ return 1
+ if ($self->type == CMT_ATTACHMENT_CREATED
+ or $self->type == CMT_ATTACHMENT_UPDATED);
+ return 0;
}
sub attachment {
- my ($self) = @_;
- return undef if not $self->is_about_attachment;
- $self->{attachment} ||=
- new Bugzilla::Attachment({ id => $self->extra_data, cache => 1 });
- return $self->{attachment};
+ my ($self) = @_;
+ return undef if not $self->is_about_attachment;
+ $self->{attachment}
+ ||= new Bugzilla::Attachment({id => $self->extra_data, cache => 1});
+ return $self->{attachment};
}
-sub author {
- my $self = shift;
- $self->{'author'}
- ||= new Bugzilla::User({ id => $self->{'who'}, cache => 1 });
- return $self->{'author'};
+sub author {
+ my $self = shift;
+ $self->{'author'} ||= new Bugzilla::User({id => $self->{'who'}, cache => 1});
+ return $self->{'author'};
}
sub body_full {
- my ($self, $params) = @_;
- $params ||= {};
- my $template = Bugzilla->template_inner;
- my $body;
- if ($self->type) {
- $template->process("bug/format_comment.txt.tmpl",
- { comment => $self, %$params }, \$body)
- || ThrowTemplateError($template->error());
- $body =~ s/^X//;
- }
- else {
- $body = $self->body;
- }
- if ($params->{wrap} and !$self->already_wrapped) {
- $body = wrap_comment($body);
- }
- return $body;
+ my ($self, $params) = @_;
+ $params ||= {};
+ my $template = Bugzilla->template_inner;
+ my $body;
+ if ($self->type) {
+ $template->process("bug/format_comment.txt.tmpl", {comment => $self, %$params},
+ \$body)
+ || ThrowTemplateError($template->error());
+ $body =~ s/^X//;
+ }
+ else {
+ $body = $self->body;
+ }
+ if ($params->{wrap} and !$self->already_wrapped) {
+ $body = wrap_comment($body);
+ }
+ return $body;
}
############
# Mutators #
############
-sub set_is_private { $_[0]->set('isprivate', $_[1]); }
-sub set_type { $_[0]->set('type', $_[1]); }
-sub set_extra_data { $_[0]->set('extra_data', $_[1]); }
+sub set_is_private { $_[0]->set('isprivate', $_[1]); }
+sub set_type { $_[0]->set('type', $_[1]); }
+sub set_extra_data { $_[0]->set('extra_data', $_[1]); }
sub add_tag {
- my ($self, $tag) = @_;
- $tag = $self->_check_tag($tag);
+ my ($self, $tag) = @_;
+ $tag = $self->_check_tag($tag);
- my $tags = $self->tags;
- return if grep { lc($tag) eq lc($_) } @$tags;
- push @$tags, $tag;
- $self->{'tags'} = [ sort @$tags ];
+ my $tags = $self->tags;
+ return if grep { lc($tag) eq lc($_) } @$tags;
+ push @$tags, $tag;
+ $self->{'tags'} = [sort @$tags];
}
sub remove_tag {
- my ($self, $tag) = @_;
- $tag = $self->_check_tag($tag);
+ my ($self, $tag) = @_;
+ $tag = $self->_check_tag($tag);
- my $tags = $self->tags;
- my $index = first { lc($tags->[$_]) eq lc($tag) } 0..scalar(@$tags) - 1;
- return unless defined $index;
- splice(@$tags, $index, 1);
+ my $tags = $self->tags;
+ my $index = first { lc($tags->[$_]) eq lc($tag) } 0 .. scalar(@$tags) - 1;
+ return unless defined $index;
+ splice(@$tags, $index, 1);
}
##############
@@ -308,180 +316,180 @@ sub remove_tag {
##############
sub run_create_validators {
- my $self = shift;
- my $params = $self->SUPER::run_create_validators(@_);
- # Sometimes this run_create_validators is called with parameters that
- # skip bug_id validation, so it might not exist in the resulting hash.
- if (defined $params->{bug_id}) {
- $params->{bug_id} = $params->{bug_id}->id;
- }
- return $params;
+ my $self = shift;
+ my $params = $self->SUPER::run_create_validators(@_);
+
+ # Sometimes this run_create_validators is called with parameters that
+ # skip bug_id validation, so it might not exist in the resulting hash.
+ if (defined $params->{bug_id}) {
+ $params->{bug_id} = $params->{bug_id}->id;
+ }
+ return $params;
}
sub _check_extra_data {
- my ($invocant, $extra_data, undef, $params) = @_;
- my $type = blessed($invocant) ? $invocant->type : $params->{type};
+ my ($invocant, $extra_data, undef, $params) = @_;
+ my $type = blessed($invocant) ? $invocant->type : $params->{type};
- if ($type == CMT_NORMAL) {
- if (defined $extra_data) {
- ThrowCodeError('comment_extra_data_not_allowed',
- { type => $type, extra_data => $extra_data });
- }
+ if ($type == CMT_NORMAL) {
+ if (defined $extra_data) {
+ ThrowCodeError('comment_extra_data_not_allowed',
+ {type => $type, extra_data => $extra_data});
+ }
+ }
+ else {
+ if (!defined $extra_data) {
+ ThrowCodeError('comment_extra_data_required', {type => $type});
+ }
+ elsif ($type == CMT_ATTACHMENT_CREATED or $type == CMT_ATTACHMENT_UPDATED) {
+ my $attachment = Bugzilla::Attachment->check({id => $extra_data});
+ $extra_data = $attachment->id;
}
else {
- if (!defined $extra_data) {
- ThrowCodeError('comment_extra_data_required', { type => $type });
- }
- elsif ($type == CMT_ATTACHMENT_CREATED
- or $type == CMT_ATTACHMENT_UPDATED)
- {
- my $attachment = Bugzilla::Attachment->check({
- id => $extra_data });
- $extra_data = $attachment->id;
- }
- else {
- my $original = $extra_data;
- detaint_natural($extra_data)
- or ThrowCodeError('comment_extra_data_not_numeric',
- { type => $type, extra_data => $original });
- }
+ my $original = $extra_data;
+ detaint_natural($extra_data)
+ or ThrowCodeError('comment_extra_data_not_numeric',
+ {type => $type, extra_data => $original});
}
+ }
- return $extra_data;
+ return $extra_data;
}
sub _check_type {
- my ($invocant, $type) = @_;
- $type ||= CMT_NORMAL;
- my $original = $type;
- detaint_natural($type)
- or ThrowCodeError('comment_type_invalid', { type => $original });
- return $type;
+ my ($invocant, $type) = @_;
+ $type ||= CMT_NORMAL;
+ my $original = $type;
+ detaint_natural($type)
+ or ThrowCodeError('comment_type_invalid', {type => $original});
+ return $type;
}
sub _check_bug_id {
- my ($invocant, $bug_id) = @_;
-
- ThrowCodeError('param_required', {function => 'Bugzilla::Comment->create',
- param => 'bug_id'}) unless $bug_id;
-
- my $bug;
- if (blessed $bug_id) {
- # We got a bug object passed in, use it
- $bug = $bug_id;
- $bug->check_is_visible;
- }
- else {
- # We got a bug id passed in, check it and get the bug object
- $bug = Bugzilla::Bug->check({ id => $bug_id });
- }
-
- # Make sure the user can edit the product
- Bugzilla->user->can_edit_product($bug->{product_id});
-
- # Make sure the user can comment
- my $privs;
- $bug->check_can_change_field('longdesc', 0, 1, \$privs)
- || ThrowUserError('illegal_change',
- { field => 'longdesc', privs => $privs });
- return $bug;
+ my ($invocant, $bug_id) = @_;
+
+ ThrowCodeError('param_required',
+ {function => 'Bugzilla::Comment->create', param => 'bug_id'})
+ unless $bug_id;
+
+ my $bug;
+ if (blessed $bug_id) {
+
+ # We got a bug object passed in, use it
+ $bug = $bug_id;
+ $bug->check_is_visible;
+ }
+ else {
+ # We got a bug id passed in, check it and get the bug object
+ $bug = Bugzilla::Bug->check({id => $bug_id});
+ }
+
+ # Make sure the user can edit the product
+ Bugzilla->user->can_edit_product($bug->{product_id});
+
+ # Make sure the user can comment
+ my $privs;
+ $bug->check_can_change_field('longdesc', 0, 1, \$privs)
+ || ThrowUserError('illegal_change', {field => 'longdesc', privs => $privs});
+ return $bug;
}
sub _check_who {
- my ($invocant, $who) = @_;
- Bugzilla->login(LOGIN_REQUIRED);
- return Bugzilla->user->id;
+ my ($invocant, $who) = @_;
+ Bugzilla->login(LOGIN_REQUIRED);
+ return Bugzilla->user->id;
}
sub _check_bug_when {
- my ($invocant, $when) = @_;
+ my ($invocant, $when) = @_;
- # Make sure the timestamp is defined, default to a timestamp from the db
- if (!defined $when) {
- $when = Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
- }
+ # Make sure the timestamp is defined, default to a timestamp from the db
+ if (!defined $when) {
+ $when = Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
+ }
- # Make sure the timestamp parses
- if (!datetime_from($when)) {
- ThrowCodeError('invalid_timestamp', { timestamp => $when });
- }
+ # Make sure the timestamp parses
+ if (!datetime_from($when)) {
+ ThrowCodeError('invalid_timestamp', {timestamp => $when});
+ }
- return $when;
+ return $when;
}
sub _check_work_time {
- my ($invocant, $value_in, $field, $params) = @_;
-
- # Call down to Bugzilla::Object, letting it know negative
- # values are ok
- my $time = $invocant->check_time($value_in, $field, $params, 1);
- my $privs;
- $params->{bug_id}->check_can_change_field('work_time', 0, $time, \$privs)
- || ThrowUserError('illegal_change',
- { field => 'work_time', privs => $privs });
- return $time;
+ my ($invocant, $value_in, $field, $params) = @_;
+
+ # Call down to Bugzilla::Object, letting it know negative
+ # values are ok
+ my $time = $invocant->check_time($value_in, $field, $params, 1);
+ my $privs;
+ $params->{bug_id}->check_can_change_field('work_time', 0, $time, \$privs)
+ || ThrowUserError('illegal_change', {field => 'work_time', privs => $privs});
+ return $time;
}
sub _check_thetext {
- my ($invocant, $thetext) = @_;
-
- ThrowCodeError('param_required',{function => 'Bugzilla::Comment->create',
- param => 'thetext'}) unless defined $thetext;
-
- # Remove any trailing whitespace. Leading whitespace could be
- # a valid part of the comment.
- $thetext =~ s/\s*$//s;
- $thetext =~ s/\r\n?/\n/g; # Get rid of \r.
-
- # Characters above U+FFFF cannot be stored by MySQL older than 5.5.3 as they
- # require the new utf8mb4 character set. Other DB servers are handling them
- # without any problem. So we need to replace these characters if we use MySQL,
- # else the comment is truncated.
- # XXX - Once we use utf8mb4 for comments, this hack for MySQL can go away.
- state $is_mysql = Bugzilla->dbh->isa('Bugzilla::DB::Mysql') ? 1 : 0;
- if ($is_mysql) {
- # Perl 5.13.8 and older complain about non-characters.
- no warnings 'utf8';
- $thetext =~ s/([\x{10000}-\x{10FFFF}])/"\x{FDD0}[" . uc(sprintf('U+%04x', ord($1))) . "]\x{FDD1}"/eg;
- }
-
- ThrowUserError('comment_too_long') if length($thetext) > MAX_COMMENT_LENGTH;
- return $thetext;
+ my ($invocant, $thetext) = @_;
+
+ ThrowCodeError('param_required',
+ {function => 'Bugzilla::Comment->create', param => 'thetext'})
+ unless defined $thetext;
+
+ # Remove any trailing whitespace. Leading whitespace could be
+ # a valid part of the comment.
+ $thetext =~ s/\s*$//s;
+ $thetext =~ s/\r\n?/\n/g; # Get rid of \r.
+
+ # Characters above U+FFFF cannot be stored by MySQL older than 5.5.3 as they
+ # require the new utf8mb4 character set. Other DB servers are handling them
+ # without any problem. So we need to replace these characters if we use MySQL,
+ # else the comment is truncated.
+ # XXX - Once we use utf8mb4 for comments, this hack for MySQL can go away.
+ state $is_mysql = Bugzilla->dbh->isa('Bugzilla::DB::Mysql') ? 1 : 0;
+ if ($is_mysql) {
+
+ # Perl 5.13.8 and older complain about non-characters.
+ no warnings 'utf8';
+ $thetext
+ =~ s/([\x{10000}-\x{10FFFF}])/"\x{FDD0}[" . uc(sprintf('U+%04x', ord($1))) . "]\x{FDD1}"/eg;
+ }
+
+ ThrowUserError('comment_too_long') if length($thetext) > MAX_COMMENT_LENGTH;
+ return $thetext;
}
sub _check_isprivate {
- my ($invocant, $isprivate) = @_;
- if ($isprivate && !Bugzilla->user->is_insider) {
- ThrowUserError('user_not_insider');
- }
- return $isprivate ? 1 : 0;
+ my ($invocant, $isprivate) = @_;
+ if ($isprivate && !Bugzilla->user->is_insider) {
+ ThrowUserError('user_not_insider');
+ }
+ return $isprivate ? 1 : 0;
}
sub _check_tag {
- my ($invocant, $tag) = @_;
- length($tag) < MIN_COMMENT_TAG_LENGTH
- and ThrowUserError('comment_tag_too_short', { tag => $tag });
- length($tag) > MAX_COMMENT_TAG_LENGTH
- and ThrowUserError('comment_tag_too_long', { tag => $tag });
- $tag =~ /^[\w\d\._-]+$/
- or ThrowUserError('comment_tag_invalid', { tag => $tag });
- return $tag;
+ my ($invocant, $tag) = @_;
+ length($tag) < MIN_COMMENT_TAG_LENGTH
+ and ThrowUserError('comment_tag_too_short', {tag => $tag});
+ length($tag) > MAX_COMMENT_TAG_LENGTH
+ and ThrowUserError('comment_tag_too_long', {tag => $tag});
+ $tag =~ /^[\w\d\._-]+$/ or ThrowUserError('comment_tag_invalid', {tag => $tag});
+ return $tag;
}
sub count {
- my ($self) = @_;
+ my ($self) = @_;
- return $self->{'count'} if defined $self->{'count'};
+ return $self->{'count'} if defined $self->{'count'};
- my $dbh = Bugzilla->dbh;
- ($self->{'count'}) = $dbh->selectrow_array(
- "SELECT COUNT(*)
+ my $dbh = Bugzilla->dbh;
+ ($self->{'count'}) = $dbh->selectrow_array(
+ "SELECT COUNT(*)
FROM longdescs
WHERE bug_id = ?
- AND bug_when <= ?",
- undef, $self->bug_id, $self->creation_ts);
+ AND bug_when <= ?", undef, $self->bug_id, $self->creation_ts
+ );
- return --$self->{'count'};
+ return --$self->{'count'};
}
1;
diff --git a/Bugzilla/Comment/TagWeights.pm b/Bugzilla/Comment/TagWeights.pm
index 7dba53e34..5355cad7f 100644
--- a/Bugzilla/Comment/TagWeights.pm
+++ b/Bugzilla/Comment/TagWeights.pm
@@ -21,20 +21,20 @@ use constant AUDIT_UPDATES => 0;
use constant AUDIT_REMOVES => 0;
use constant DB_COLUMNS => qw(
- id
- tag
- weight
+ id
+ tag
+ weight
);
use constant UPDATE_COLUMNS => qw(
- weight
+ weight
);
use constant DB_TABLE => 'longdescs_tags_weights';
use constant ID_FIELD => 'id';
use constant NAME_FIELD => 'tag';
use constant LIST_ORDER => 'weight DESC';
-use constant VALIDATORS => { };
+use constant VALIDATORS => {};
# There's no gain to caching these objects
use constant USE_MEMCACHED => 0;
diff --git a/Bugzilla/Component.pm b/Bugzilla/Component.pm
index d5a6ece5d..3cdee9d63 100644
--- a/Bugzilla/Component.pm
+++ b/Bugzilla/Component.pm
@@ -27,150 +27,147 @@ use Scalar::Util qw(blessed);
###############################
use constant DB_TABLE => 'components';
+
# This is mostly for the editfields.cgi case where ->get_all is called.
use constant LIST_ORDER => 'product_id, name';
use constant DB_COLUMNS => qw(
- id
- name
- product_id
- initialowner
- initialqacontact
- description
- isactive
+ id
+ name
+ product_id
+ initialowner
+ initialqacontact
+ description
+ isactive
);
use constant UPDATE_COLUMNS => qw(
- name
- initialowner
- initialqacontact
- description
- isactive
+ name
+ initialowner
+ initialqacontact
+ description
+ isactive
);
-use constant REQUIRED_FIELD_MAP => {
- product_id => 'product',
-};
+use constant REQUIRED_FIELD_MAP => {product_id => 'product',};
use constant VALIDATORS => {
- create_series => \&Bugzilla::Object::check_boolean,
- product => \&_check_product,
- initialowner => \&_check_initialowner,
- initialqacontact => \&_check_initialqacontact,
- description => \&_check_description,
- initial_cc => \&_check_cc_list,
- name => \&_check_name,
- isactive => \&Bugzilla::Object::check_boolean,
+ create_series => \&Bugzilla::Object::check_boolean,
+ product => \&_check_product,
+ initialowner => \&_check_initialowner,
+ initialqacontact => \&_check_initialqacontact,
+ description => \&_check_description,
+ initial_cc => \&_check_cc_list,
+ name => \&_check_name,
+ isactive => \&Bugzilla::Object::check_boolean,
};
-use constant VALIDATOR_DEPENDENCIES => {
- name => ['product'],
-};
+use constant VALIDATOR_DEPENDENCIES => {name => ['product'],};
###############################
sub new {
- my $class = shift;
- my $param = shift;
- my $dbh = Bugzilla->dbh;
-
- my $product;
- if (ref $param and !defined $param->{id}) {
- $product = $param->{product};
- my $name = $param->{name};
- if (!defined $product) {
- ThrowCodeError('bad_arg',
- {argument => 'product',
- function => "${class}::new"});
- }
- if (!defined $name) {
- ThrowCodeError('bad_arg',
- {argument => 'name',
- function => "${class}::new"});
- }
-
- my $condition = 'product_id = ? AND name = ?';
- my @values = ($product->id, $name);
- $param = { condition => $condition, values => \@values };
+ my $class = shift;
+ my $param = shift;
+ my $dbh = Bugzilla->dbh;
+
+ my $product;
+ if (ref $param and !defined $param->{id}) {
+ $product = $param->{product};
+ my $name = $param->{name};
+ if (!defined $product) {
+ ThrowCodeError('bad_arg', {argument => 'product', function => "${class}::new"});
}
+ if (!defined $name) {
+ ThrowCodeError('bad_arg', {argument => 'name', function => "${class}::new"});
+ }
+
+ my $condition = 'product_id = ? AND name = ?';
+ my @values = ($product->id, $name);
+ $param = {condition => $condition, values => \@values};
+ }
- unshift @_, $param;
- my $component = $class->SUPER::new(@_);
- # Add the product object as attribute only if the component exists.
- $component->{product} = $product if ($component && $product);
- return $component;
+ unshift @_, $param;
+ my $component = $class->SUPER::new(@_);
+
+ # Add the product object as attribute only if the component exists.
+ $component->{product} = $product if ($component && $product);
+ return $component;
}
sub create {
- my $class = shift;
- my $dbh = Bugzilla->dbh;
+ my $class = shift;
+ my $dbh = Bugzilla->dbh;
- $dbh->bz_start_transaction();
+ $dbh->bz_start_transaction();
- $class->check_required_create_fields(@_);
- my $params = $class->run_create_validators(@_);
- my $cc_list = delete $params->{initial_cc};
- my $create_series = delete $params->{create_series};
- my $product = delete $params->{product};
- $params->{product_id} = $product->id;
+ $class->check_required_create_fields(@_);
+ my $params = $class->run_create_validators(@_);
+ my $cc_list = delete $params->{initial_cc};
+ my $create_series = delete $params->{create_series};
+ my $product = delete $params->{product};
+ $params->{product_id} = $product->id;
- my $component = $class->insert_create_data($params);
- $component->{product} = $product;
+ my $component = $class->insert_create_data($params);
+ $component->{product} = $product;
- # We still have to fill the component_cc table.
- $component->_update_cc_list($cc_list) if $cc_list;
+ # We still have to fill the component_cc table.
+ $component->_update_cc_list($cc_list) if $cc_list;
- # Create series for the new component.
- $component->_create_series() if $create_series;
+ # Create series for the new component.
+ $component->_create_series() if $create_series;
- $dbh->bz_commit_transaction();
- return $component;
+ $dbh->bz_commit_transaction();
+ return $component;
}
sub update {
- my $self = shift;
- my $changes = $self->SUPER::update(@_);
-
- # Update the component_cc table if necessary.
- if (defined $self->{cc_ids}) {
- my $diff = $self->_update_cc_list($self->{cc_ids});
- $changes->{cc_list} = $diff if defined $diff;
- }
- return $changes;
+ my $self = shift;
+ my $changes = $self->SUPER::update(@_);
+
+ # Update the component_cc table if necessary.
+ if (defined $self->{cc_ids}) {
+ my $diff = $self->_update_cc_list($self->{cc_ids});
+ $changes->{cc_list} = $diff if defined $diff;
+ }
+ return $changes;
}
sub remove_from_db {
- my $self = shift;
- my $dbh = Bugzilla->dbh;
+ my $self = shift;
+ my $dbh = Bugzilla->dbh;
- $self->_check_if_controller(); # From ChoiceInterface
+ $self->_check_if_controller(); # From ChoiceInterface
- $dbh->bz_start_transaction();
+ $dbh->bz_start_transaction();
- # Products must have at least one component.
- my @components = @{ $self->product->components };
- if (scalar(@components) == 1) {
- ThrowUserError('component_is_last', { comp => $self });
- }
+ # Products must have at least one component.
+ my @components = @{$self->product->components};
+ if (scalar(@components) == 1) {
+ ThrowUserError('component_is_last', {comp => $self});
+ }
+
+ if ($self->bug_count) {
+ if (Bugzilla->params->{'allowbugdeletion'}) {
+ require Bugzilla::Bug;
+ foreach my $bug_id (@{$self->bug_ids}) {
- if ($self->bug_count) {
- if (Bugzilla->params->{'allowbugdeletion'}) {
- require Bugzilla::Bug;
- foreach my $bug_id (@{$self->bug_ids}) {
- # Note: We allow admins to delete bugs even if they can't
- # see them, as long as they can see the product.
- my $bug = new Bugzilla::Bug($bug_id);
- $bug->remove_from_db();
- }
- } else {
- ThrowUserError('component_has_bugs', {nb => $self->bug_count});
- }
+ # Note: We allow admins to delete bugs even if they can't
+ # see them, as long as they can see the product.
+ my $bug = new Bugzilla::Bug($bug_id);
+ $bug->remove_from_db();
+ }
}
- # Update the list of components in the product object.
- $self->product->{components} = [grep { $_->id != $self->id } @components];
- $self->SUPER::remove_from_db();
+ else {
+ ThrowUserError('component_has_bugs', {nb => $self->bug_count});
+ }
+ }
+
+ # Update the list of components in the product object.
+ $self->product->{components} = [grep { $_->id != $self->id } @components];
+ $self->SUPER::remove_from_db();
- $dbh->bz_commit_transaction();
+ $dbh->bz_commit_transaction();
}
################################
@@ -178,69 +175,70 @@ sub remove_from_db {
################################
sub _check_name {
- my ($invocant, $name, undef, $params) = @_;
- my $product = blessed($invocant) ? $invocant->product : $params->{product};
-
- $name = trim($name);
- $name || ThrowUserError('component_blank_name');
-
- if (length($name) > MAX_COMPONENT_SIZE) {
- ThrowUserError('component_name_too_long', {'name' => $name});
- }
-
- my $component = new Bugzilla::Component({product => $product, name => $name});
- if ($component && (!ref $invocant || $component->id != $invocant->id)) {
- ThrowUserError('component_already_exists', { name => $component->name,
- product => $product });
- }
- return $name;
+ my ($invocant, $name, undef, $params) = @_;
+ my $product = blessed($invocant) ? $invocant->product : $params->{product};
+
+ $name = trim($name);
+ $name || ThrowUserError('component_blank_name');
+
+ if (length($name) > MAX_COMPONENT_SIZE) {
+ ThrowUserError('component_name_too_long', {'name' => $name});
+ }
+
+ my $component = new Bugzilla::Component({product => $product, name => $name});
+ if ($component && (!ref $invocant || $component->id != $invocant->id)) {
+ ThrowUserError('component_already_exists',
+ {name => $component->name, product => $product});
+ }
+ return $name;
}
sub _check_description {
- my ($invocant, $description) = @_;
+ my ($invocant, $description) = @_;
- $description = trim($description);
- $description || ThrowUserError('component_blank_description');
- return $description;
+ $description = trim($description);
+ $description || ThrowUserError('component_blank_description');
+ return $description;
}
sub _check_initialowner {
- my ($invocant, $owner) = @_;
+ my ($invocant, $owner) = @_;
- $owner || ThrowUserError('component_need_initialowner');
- my $owner_id = Bugzilla::User->check($owner)->id;
- return $owner_id;
+ $owner || ThrowUserError('component_need_initialowner');
+ my $owner_id = Bugzilla::User->check($owner)->id;
+ return $owner_id;
}
sub _check_initialqacontact {
- my ($invocant, $qa_contact) = @_;
-
- my $qa_contact_id;
- if (Bugzilla->params->{'useqacontact'}) {
- $qa_contact_id = Bugzilla::User->check($qa_contact)->id if $qa_contact;
- }
- elsif (ref $invocant) {
- $qa_contact_id = $invocant->{initialqacontact};
- }
- return $qa_contact_id;
+ my ($invocant, $qa_contact) = @_;
+
+ my $qa_contact_id;
+ if (Bugzilla->params->{'useqacontact'}) {
+ $qa_contact_id = Bugzilla::User->check($qa_contact)->id if $qa_contact;
+ }
+ elsif (ref $invocant) {
+ $qa_contact_id = $invocant->{initialqacontact};
+ }
+ return $qa_contact_id;
}
sub _check_product {
- my ($invocant, $product) = @_;
- $product || ThrowCodeError('param_required',
- { function => "$invocant->create", param => 'product' });
- return Bugzilla->user->check_can_admin_product($product->name);
+ my ($invocant, $product) = @_;
+ $product
+ || ThrowCodeError('param_required',
+ {function => "$invocant->create", param => 'product'});
+ return Bugzilla->user->check_can_admin_product($product->name);
}
sub _check_cc_list {
- my ($invocant, $cc_list) = @_;
-
- my %cc_ids;
- foreach my $cc (@$cc_list) {
- my $id = login_to_id($cc, THROW_ERROR);
- $cc_ids{$id} = 1;
- }
- return [keys %cc_ids];
+ my ($invocant, $cc_list) = @_;
+
+ my %cc_ids;
+ foreach my $cc (@$cc_list) {
+ my $id = login_to_id($cc, THROW_ERROR);
+ $cc_ids{$id} = 1;
+ }
+ return [keys %cc_ids];
}
###############################
@@ -248,156 +246,176 @@ sub _check_cc_list {
###############################
sub _update_cc_list {
- my ($self, $cc_list) = @_;
- my $dbh = Bugzilla->dbh;
+ my ($self, $cc_list) = @_;
+ my $dbh = Bugzilla->dbh;
- my $old_cc_list =
- $dbh->selectcol_arrayref('SELECT user_id FROM component_cc
- WHERE component_id = ?', undef, $self->id);
+ my $old_cc_list = $dbh->selectcol_arrayref(
+ 'SELECT user_id FROM component_cc
+ WHERE component_id = ?', undef, $self->id
+ );
- my ($removed, $added) = diff_arrays($old_cc_list, $cc_list);
- my $diff;
- if (scalar @$removed || scalar @$added) {
- $diff = [join(', ', @$removed), join(', ', @$added)];
- }
+ my ($removed, $added) = diff_arrays($old_cc_list, $cc_list);
+ my $diff;
+ if (scalar @$removed || scalar @$added) {
+ $diff = [join(', ', @$removed), join(', ', @$added)];
+ }
- $dbh->do('DELETE FROM component_cc WHERE component_id = ?', undef, $self->id);
+ $dbh->do('DELETE FROM component_cc WHERE component_id = ?', undef, $self->id);
- my $sth = $dbh->prepare('INSERT INTO component_cc
- (user_id, component_id) VALUES (?, ?)');
- $sth->execute($_, $self->id) foreach (@$cc_list);
+ my $sth = $dbh->prepare(
+ 'INSERT INTO component_cc
+ (user_id, component_id) VALUES (?, ?)'
+ );
+ $sth->execute($_, $self->id) foreach (@$cc_list);
- return $diff;
+ return $diff;
}
sub _create_series {
- my $self = shift;
-
- # Insert default charting queries for this product.
- # If they aren't using charting, this won't do any harm.
- my $prodcomp = "&product=" . url_quote($self->product->name) .
- "&component=" . url_quote($self->name);
-
- my $open_query = 'field0-0-0=resolution&type0-0-0=notregexp&value0-0-0=.' .
- $prodcomp;
- my $nonopen_query = 'field0-0-0=resolution&type0-0-0=regexp&value0-0-0=.' .
- $prodcomp;
-
- my @series = ([get_text('series_all_open'), $open_query],
- [get_text('series_all_closed'), $nonopen_query]);
-
- foreach my $sdata (@series) {
- my $series = new Bugzilla::Series(undef, $self->product->name,
- $self->name, $sdata->[0],
- Bugzilla->user->id, 1, $sdata->[1], 1);
- $series->writeToDatabase();
- }
+ my $self = shift;
+
+ # Insert default charting queries for this product.
+ # If they aren't using charting, this won't do any harm.
+ my $prodcomp
+ = "&product="
+ . url_quote($self->product->name)
+ . "&component="
+ . url_quote($self->name);
+
+ my $open_query
+ = 'field0-0-0=resolution&type0-0-0=notregexp&value0-0-0=.' . $prodcomp;
+ my $nonopen_query
+ = 'field0-0-0=resolution&type0-0-0=regexp&value0-0-0=.' . $prodcomp;
+
+ my @series = (
+ [get_text('series_all_open'), $open_query],
+ [get_text('series_all_closed'), $nonopen_query]
+ );
+
+ foreach my $sdata (@series) {
+ my $series
+ = new Bugzilla::Series(undef, $self->product->name, $self->name, $sdata->[0],
+ Bugzilla->user->id, 1, $sdata->[1], 1);
+ $series->writeToDatabase();
+ }
}
-sub set_name { $_[0]->set('name', $_[1]); }
+sub set_name { $_[0]->set('name', $_[1]); }
sub set_description { $_[0]->set('description', $_[1]); }
-sub set_is_active { $_[0]->set('isactive', $_[1]); }
+sub set_is_active { $_[0]->set('isactive', $_[1]); }
+
sub set_default_assignee {
- my ($self, $owner) = @_;
+ my ($self, $owner) = @_;
+
+ $self->set('initialowner', $owner);
- $self->set('initialowner', $owner);
- # Reset the default owner object.
- delete $self->{default_assignee};
+ # Reset the default owner object.
+ delete $self->{default_assignee};
}
+
sub set_default_qa_contact {
- my ($self, $qa_contact) = @_;
+ my ($self, $qa_contact) = @_;
+
+ $self->set('initialqacontact', $qa_contact);
- $self->set('initialqacontact', $qa_contact);
- # Reset the default QA contact object.
- delete $self->{default_qa_contact};
+ # Reset the default QA contact object.
+ delete $self->{default_qa_contact};
}
+
sub set_cc_list {
- my ($self, $cc_list) = @_;
+ my ($self, $cc_list) = @_;
+
+ $self->{cc_ids} = $self->_check_cc_list($cc_list);
- $self->{cc_ids} = $self->_check_cc_list($cc_list);
- # Reset the list of CC user objects.
- delete $self->{initial_cc};
+ # Reset the list of CC user objects.
+ delete $self->{initial_cc};
}
sub bug_count {
- my $self = shift;
- my $dbh = Bugzilla->dbh;
+ my $self = shift;
+ my $dbh = Bugzilla->dbh;
- if (!defined $self->{'bug_count'}) {
- $self->{'bug_count'} = $dbh->selectrow_array(q{
+ if (!defined $self->{'bug_count'}) {
+ $self->{'bug_count'} = $dbh->selectrow_array(
+ q{
SELECT COUNT(*) FROM bugs
- WHERE component_id = ?}, undef, $self->id) || 0;
- }
- return $self->{'bug_count'};
+ WHERE component_id = ?}, undef, $self->id
+ ) || 0;
+ }
+ return $self->{'bug_count'};
}
sub bug_ids {
- my $self = shift;
- my $dbh = Bugzilla->dbh;
+ my $self = shift;
+ my $dbh = Bugzilla->dbh;
- if (!defined $self->{'bugs_ids'}) {
- $self->{'bugs_ids'} = $dbh->selectcol_arrayref(q{
+ if (!defined $self->{'bugs_ids'}) {
+ $self->{'bugs_ids'} = $dbh->selectcol_arrayref(
+ q{
SELECT bug_id FROM bugs
- WHERE component_id = ?}, undef, $self->id);
- }
- return $self->{'bugs_ids'};
+ WHERE component_id = ?}, undef, $self->id
+ );
+ }
+ return $self->{'bugs_ids'};
}
sub default_assignee {
- my $self = shift;
+ my $self = shift;
- return $self->{'default_assignee'}
- ||= new Bugzilla::User({ id => $self->{'initialowner'}, cache => 1 });
+ return $self->{'default_assignee'}
+ ||= new Bugzilla::User({id => $self->{'initialowner'}, cache => 1});
}
sub default_qa_contact {
- my $self = shift;
+ my $self = shift;
- return unless $self->{'initialqacontact'};
- return $self->{'default_qa_contact'}
- ||= new Bugzilla::User({id => $self->{'initialqacontact'}, cache => 1 });
+ return unless $self->{'initialqacontact'};
+ return $self->{'default_qa_contact'}
+ ||= new Bugzilla::User({id => $self->{'initialqacontact'}, cache => 1});
}
sub flag_types {
- my $self = shift;
-
- if (!defined $self->{'flag_types'}) {
- my $flagtypes = Bugzilla::FlagType::match({ product_id => $self->product_id,
- component_id => $self->id });
-
- $self->{'flag_types'} = {};
- $self->{'flag_types'}->{'bug'} =
- [grep { $_->target_type eq 'bug' } @$flagtypes];
- $self->{'flag_types'}->{'attachment'} =
- [grep { $_->target_type eq 'attachment' } @$flagtypes];
- }
- return $self->{'flag_types'};
+ my $self = shift;
+
+ if (!defined $self->{'flag_types'}) {
+ my $flagtypes = Bugzilla::FlagType::match(
+ {product_id => $self->product_id, component_id => $self->id});
+
+ $self->{'flag_types'} = {};
+ $self->{'flag_types'}->{'bug'}
+ = [grep { $_->target_type eq 'bug' } @$flagtypes];
+ $self->{'flag_types'}->{'attachment'}
+ = [grep { $_->target_type eq 'attachment' } @$flagtypes];
+ }
+ return $self->{'flag_types'};
}
sub initial_cc {
- my $self = shift;
- my $dbh = Bugzilla->dbh;
-
- if (!defined $self->{'initial_cc'}) {
- # If set_cc_list() has been called but data are not yet written
- # into the DB, we want the new values defined by it.
- my $cc_ids = $self->{cc_ids}
- || $dbh->selectcol_arrayref('SELECT user_id FROM component_cc
- WHERE component_id = ?',
- undef, $self->id);
-
- $self->{'initial_cc'} = Bugzilla::User->new_from_list($cc_ids);
- }
- return $self->{'initial_cc'};
+ my $self = shift;
+ my $dbh = Bugzilla->dbh;
+
+ if (!defined $self->{'initial_cc'}) {
+
+ # If set_cc_list() has been called but data are not yet written
+ # into the DB, we want the new values defined by it.
+ my $cc_ids = $self->{cc_ids} || $dbh->selectcol_arrayref(
+ 'SELECT user_id FROM component_cc
+ WHERE component_id = ?', undef,
+ $self->id
+ );
+
+ $self->{'initial_cc'} = Bugzilla::User->new_from_list($cc_ids);
+ }
+ return $self->{'initial_cc'};
}
sub product {
- my $self = shift;
- if (!defined $self->{'product'}) {
- require Bugzilla::Product; # We cannot |use| it.
- $self->{'product'} = new Bugzilla::Product($self->product_id);
- }
- return $self->{'product'};
+ my $self = shift;
+ if (!defined $self->{'product'}) {
+ require Bugzilla::Product; # We cannot |use| it.
+ $self->{'product'} = new Bugzilla::Product($self->product_id);
+ }
+ return $self->{'product'};
}
###############################
@@ -405,8 +423,8 @@ sub product {
###############################
sub description { return $_[0]->{'description'}; }
-sub product_id { return $_[0]->{'product_id'}; }
-sub is_active { return $_[0]->{'isactive'}; }
+sub product_id { return $_[0]->{'product_id'}; }
+sub is_active { return $_[0]->{'isactive'}; }
##############################################
# Implement Bugzilla::Field::ChoiceInterface #
@@ -416,11 +434,11 @@ use constant FIELD_NAME => 'component';
use constant is_default => 0;
sub is_set_on_bug {
- my ($self, $bug) = @_;
- my $value = blessed($bug) ? $bug->component_id : $bug->{component};
- $value = $value->id if blessed($value);
- return 0 unless $value;
- return $value == $self->id ? 1 : 0;
+ my ($self, $bug) = @_;
+ my $value = blessed($bug) ? $bug->component_id : $bug->{component};
+ $value = $value->id if blessed($value);
+ return 0 unless $value;
+ return $value == $self->id ? 1 : 0;
}
###############################
diff --git a/Bugzilla/Config.pm b/Bugzilla/Config.pm
index 458616701..1aa944985 100644
--- a/Bugzilla/Config.pm
+++ b/Bugzilla/Config.pm
@@ -25,316 +25,323 @@ use File::Basename;
# Don't export localvars by default - people should have to explicitly
# ask for it, as a (probably futile) attempt to stop code using it
# when it shouldn't
-%Bugzilla::Config::EXPORT_TAGS =
- (
- admin => [qw(update_params SetParam write_params)],
- );
+%Bugzilla::Config::EXPORT_TAGS
+ = (admin => [qw(update_params SetParam write_params)],);
Exporter::export_ok_tags('admin');
# INITIALISATION CODE
# Perl throws a warning if we use bz_locations() directly after do.
our %params;
+
# Load in the param definitions
sub _load_params {
- my $panels = param_panels();
- my %hook_panels;
- foreach my $panel (keys %$panels) {
- my $module = $panels->{$panel};
- eval("require $module") || die $@;
- my @new_param_list = $module->get_param_list();
- $hook_panels{lc($panel)} = { params => \@new_param_list };
- }
- # This hook is also called in editparams.cgi. This call here is required
- # to make SetParam work.
- Bugzilla::Hook::process('config_modify_panels',
- { panels => \%hook_panels });
-
- foreach my $panel (keys %hook_panels) {
- foreach my $item (@{$hook_panels{$panel}->{params}}) {
- $params{$item->{'name'}} = $item;
- }
+ my $panels = param_panels();
+ my %hook_panels;
+ foreach my $panel (keys %$panels) {
+ my $module = $panels->{$panel};
+ eval("require $module") || die $@;
+ my @new_param_list = $module->get_param_list();
+ $hook_panels{lc($panel)} = {params => \@new_param_list};
+ }
+
+ # This hook is also called in editparams.cgi. This call here is required
+ # to make SetParam work.
+ Bugzilla::Hook::process('config_modify_panels', {panels => \%hook_panels});
+
+ foreach my $panel (keys %hook_panels) {
+ foreach my $item (@{$hook_panels{$panel}->{params}}) {
+ $params{$item->{'name'}} = $item;
}
+ }
}
+
# END INIT CODE
# Subroutines go here
sub param_panels {
- my $param_panels = {};
- my $libpath = bz_locations()->{'libpath'};
- foreach my $item ((glob "$libpath/Bugzilla/Config/*.pm")) {
- $item =~ m#/([^/]+)\.pm$#;
- my $module = $1;
- $param_panels->{$module} = "Bugzilla::Config::$module" unless $module eq 'Common';
- }
- # Now check for any hooked params
- Bugzilla::Hook::process('config_add_panels',
- { panel_modules => $param_panels });
- return $param_panels;
+ my $param_panels = {};
+ my $libpath = bz_locations()->{'libpath'};
+ foreach my $item ((glob "$libpath/Bugzilla/Config/*.pm")) {
+ $item =~ m#/([^/]+)\.pm$#;
+ my $module = $1;
+ $param_panels->{$module} = "Bugzilla::Config::$module"
+ unless $module eq 'Common';
+ }
+
+ # Now check for any hooked params
+ Bugzilla::Hook::process('config_add_panels', {panel_modules => $param_panels});
+ return $param_panels;
}
sub SetParam {
- my ($name, $value) = @_;
+ my ($name, $value) = @_;
- _load_params unless %params;
- die "Unknown param $name" unless (exists $params{$name});
+ _load_params unless %params;
+ die "Unknown param $name" unless (exists $params{$name});
- my $entry = $params{$name};
+ my $entry = $params{$name};
- # sanity check the value
+ # sanity check the value
- # XXX - This runs the checks. Which would be good, except that
- # check_shadowdb creates the database as a side effect, and so the
- # checker fails the second time around...
- if ($name ne 'shadowdb' && exists $entry->{'checker'}) {
- my $err = $entry->{'checker'}->($value, $entry);
- die "Param $name is not valid: $err" unless $err eq '';
- }
+ # XXX - This runs the checks. Which would be good, except that
+ # check_shadowdb creates the database as a side effect, and so the
+ # checker fails the second time around...
+ if ($name ne 'shadowdb' && exists $entry->{'checker'}) {
+ my $err = $entry->{'checker'}->($value, $entry);
+ die "Param $name is not valid: $err" unless $err eq '';
+ }
- Bugzilla->params->{$name} = $value;
+ Bugzilla->params->{$name} = $value;
}
sub update_params {
- my ($params) = @_;
- my $answer = Bugzilla->installation_answers;
- my $datadir = bz_locations()->{'datadir'};
- my $param;
-
- # If the old data/params file using Data::Dumper output still exists,
- # read it. It will be deleted once the parameters are stored in the new
- # data/params.json file.
- my $old_file = "$datadir/params";
-
- if (-e $old_file) {
- require Safe;
- my $s = new Safe;
-
- $s->rdo($old_file);
- die "Error reading $old_file: $!" if $!;
- die "Error evaluating $old_file: $@" if $@;
-
- # Now read the param back out from the sandbox.
- $param = \%{ $s->varglob('param') };
+ my ($params) = @_;
+ my $answer = Bugzilla->installation_answers;
+ my $datadir = bz_locations()->{'datadir'};
+ my $param;
+
+ # If the old data/params file using Data::Dumper output still exists,
+ # read it. It will be deleted once the parameters are stored in the new
+ # data/params.json file.
+ my $old_file = "$datadir/params";
+
+ if (-e $old_file) {
+ require Safe;
+ my $s = new Safe;
+
+ $s->rdo($old_file);
+ die "Error reading $old_file: $!" if $!;
+ die "Error evaluating $old_file: $@" if $@;
+
+ # Now read the param back out from the sandbox.
+ $param = \%{$s->varglob('param')};
+ }
+ else {
+ # Rename params.js to params.json if checksetup.pl
+ # was executed with an earlier version of this change
+ rename "$old_file.js", "$old_file.json"
+ if -e "$old_file.js" && !-e "$old_file.json";
+
+ # Read the new data/params.json file.
+ $param = read_param_file();
+ }
+
+ my %new_params;
+
+ # If we didn't return any param values, then this is a new installation.
+ my $new_install = !(keys %$param);
+
+ # --- UPDATE OLD PARAMS ---
+
+ # Change from usebrowserinfo to defaultplatform/defaultopsys combo
+ if (exists $param->{'usebrowserinfo'}) {
+ if (!$param->{'usebrowserinfo'}) {
+ if (!exists $param->{'defaultplatform'}) {
+ $new_params{'defaultplatform'} = 'Other';
+ }
+ if (!exists $param->{'defaultopsys'}) {
+ $new_params{'defaultopsys'} = 'Other';
+ }
}
- else {
- # Rename params.js to params.json if checksetup.pl
- # was executed with an earlier version of this change
- rename "$old_file.js", "$old_file.json"
- if -e "$old_file.js" && !-e "$old_file.json";
-
- # Read the new data/params.json file.
- $param = read_param_file();
+ }
+
+ # Change from a boolean for quips to multi-state
+ if (exists $param->{'usequip'} && !exists $param->{'enablequips'}) {
+ $new_params{'enablequips'} = $param->{'usequip'} ? 'on' : 'off';
+ }
+
+ # Change from old product groups to controls for group_control_map
+ # 2002-10-14 bug 147275 bugreport@peshkin.net
+ if (exists $param->{'usebuggroups'} && !exists $param->{'makeproductgroups'}) {
+ $new_params{'makeproductgroups'} = $param->{'usebuggroups'};
+ }
+
+ # Modularise auth code
+ if (exists $param->{'useLDAP'} && !exists $param->{'loginmethod'}) {
+ $new_params{'loginmethod'} = $param->{'useLDAP'} ? "LDAP" : "DB";
+ }
+
+ # set verify method to whatever loginmethod was
+ if (exists $param->{'loginmethod'} && !exists $param->{'user_verify_class'}) {
+ $new_params{'user_verify_class'} = $param->{'loginmethod'};
+ }
+
+ # Remove quip-display control from parameters
+ # and give it to users via User Settings (Bug 41972)
+ if (exists $param->{'enablequips'}
+ && !exists $param->{'quip_list_entry_control'})
+ {
+ my $new_value;
+ ($param->{'enablequips'} eq 'on') && do { $new_value = 'open'; };
+ ($param->{'enablequips'} eq 'approved') && do { $new_value = 'moderated'; };
+ ($param->{'enablequips'} eq 'frozen') && do { $new_value = 'closed'; };
+ ($param->{'enablequips'} eq 'off') && do { $new_value = 'closed'; };
+ $new_params{'quip_list_entry_control'} = $new_value;
+ }
+
+ # Old mail_delivery_method choices contained no uppercase characters
+ my $mta = $param->{'mail_delivery_method'};
+ if ($mta) {
+ if ($mta !~ /[A-Z]/) {
+ my %translation = (
+ 'sendmail' => 'Sendmail',
+ 'smtp' => 'SMTP',
+ 'qmail' => 'Qmail',
+ 'testfile' => 'Test',
+ 'none' => 'None'
+ );
+ $param->{'mail_delivery_method'} = $translation{$mta};
}
- my %new_params;
-
- # If we didn't return any param values, then this is a new installation.
- my $new_install = !(keys %$param);
-
- # --- UPDATE OLD PARAMS ---
-
- # Change from usebrowserinfo to defaultplatform/defaultopsys combo
- if (exists $param->{'usebrowserinfo'}) {
- if (!$param->{'usebrowserinfo'}) {
- if (!exists $param->{'defaultplatform'}) {
- $new_params{'defaultplatform'} = 'Other';
- }
- if (!exists $param->{'defaultopsys'}) {
- $new_params{'defaultopsys'} = 'Other';
- }
- }
+ # This will force the parameter to be reset to its default value.
+ delete $param->{'mail_delivery_method'}
+ if $param->{'mail_delivery_method'} eq 'Qmail';
+ }
+
+ # Convert the old "ssl" parameter to the new "ssl_redirect" parameter.
+ # Both "authenticated sessions" and "always" turn on "ssl_redirect"
+ # when upgrading.
+ if (exists $param->{'ssl'} and $param->{'ssl'} ne 'never') {
+ $new_params{'ssl_redirect'} = 1;
+ }
+
+# "specific_search_allow_empty_words" has been renamed to "search_allow_no_criteria".
+ if (exists $param->{'specific_search_allow_empty_words'}) {
+ $new_params{'search_allow_no_criteria'}
+ = $param->{'specific_search_allow_empty_words'};
+ }
+
+ # --- DEFAULTS FOR NEW PARAMS ---
+
+ _load_params unless %params;
+ foreach my $name (keys %params) {
+ my $item = $params{$name};
+ unless (exists $param->{$name}) {
+ print "New parameter: $name\n" unless $new_install;
+ if (exists $new_params{$name}) {
+ $param->{$name} = $new_params{$name};
+ }
+ elsif (exists $answer->{$name}) {
+ $param->{$name} = $answer->{$name};
+ }
+ else {
+ $param->{$name} = $item->{'default'};
+ }
}
+ }
- # Change from a boolean for quips to multi-state
- if (exists $param->{'usequip'} && !exists $param->{'enablequips'}) {
- $new_params{'enablequips'} = $param->{'usequip'} ? 'on' : 'off';
- }
+ $param->{'utf8'} = 1 if $new_install;
- # Change from old product groups to controls for group_control_map
- # 2002-10-14 bug 147275 bugreport@peshkin.net
- if (exists $param->{'usebuggroups'} &&
- !exists $param->{'makeproductgroups'})
- {
- $new_params{'makeproductgroups'} = $param->{'usebuggroups'};
- }
+ # Bug 452525: OR based groups are on by default for new installations
+ $param->{'or_groups'} = 1 if $new_install;
- # Modularise auth code
- if (exists $param->{'useLDAP'} && !exists $param->{'loginmethod'}) {
- $new_params{'loginmethod'} = $param->{'useLDAP'} ? "LDAP" : "DB";
- }
+ # --- REMOVE OLD PARAMS ---
- # set verify method to whatever loginmethod was
- if (exists $param->{'loginmethod'}
- && !exists $param->{'user_verify_class'})
- {
- $new_params{'user_verify_class'} = $param->{'loginmethod'};
- }
+ my %oldparams;
- # Remove quip-display control from parameters
- # and give it to users via User Settings (Bug 41972)
- if ( exists $param->{'enablequips'}
- && !exists $param->{'quip_list_entry_control'})
- {
- my $new_value;
- ($param->{'enablequips'} eq 'on') && do {$new_value = 'open';};
- ($param->{'enablequips'} eq 'approved') && do {$new_value = 'moderated';};
- ($param->{'enablequips'} eq 'frozen') && do {$new_value = 'closed';};
- ($param->{'enablequips'} eq 'off') && do {$new_value = 'closed';};
- $new_params{'quip_list_entry_control'} = $new_value;
+ # Remove any old params
+ foreach my $item (keys %$param) {
+ if (!exists $params{$item}) {
+ $oldparams{$item} = delete $param->{$item};
}
-
- # Old mail_delivery_method choices contained no uppercase characters
- my $mta = $param->{'mail_delivery_method'};
- if ($mta) {
- if ($mta !~ /[A-Z]/) {
- my %translation = (
- 'sendmail' => 'Sendmail',
- 'smtp' => 'SMTP',
- 'qmail' => 'Qmail',
- 'testfile' => 'Test',
- 'none' => 'None');
- $param->{'mail_delivery_method'} = $translation{$mta};
- }
- # This will force the parameter to be reset to its default value.
- delete $param->{'mail_delivery_method'} if $param->{'mail_delivery_method'} eq 'Qmail';
+ }
+
+ # Write any old parameters to old-params.txt
+ my $old_param_file = "$datadir/old-params.txt";
+ if (scalar(keys %oldparams)) {
+ my $op_file = new IO::File($old_param_file, '>>', 0600)
+ || die "Couldn't create $old_param_file: $!";
+
+ print "The following parameters are no longer used in Bugzilla,",
+ " and so have been\nmoved from your parameters file into",
+ " $old_param_file:\n";
+
+ my $comma = "";
+ foreach my $item (keys %oldparams) {
+ print $op_file "\n\n$item:\n" . $oldparams{$item} . "\n";
+ print "${comma}$item";
+ $comma = ", ";
}
+ print "\n";
+ $op_file->close;
+ }
- # Convert the old "ssl" parameter to the new "ssl_redirect" parameter.
- # Both "authenticated sessions" and "always" turn on "ssl_redirect"
- # when upgrading.
- if (exists $param->{'ssl'} and $param->{'ssl'} ne 'never') {
- $new_params{'ssl_redirect'} = 1;
- }
+ write_params($param);
- # "specific_search_allow_empty_words" has been renamed to "search_allow_no_criteria".
- if (exists $param->{'specific_search_allow_empty_words'}) {
- $new_params{'search_allow_no_criteria'} = $param->{'specific_search_allow_empty_words'};
- }
+ if (-e $old_file) {
+ unlink $old_file;
+ say "$old_file has been converted into $old_file.json, using the JSON format.";
+ }
- # --- DEFAULTS FOR NEW PARAMS ---
-
- _load_params unless %params;
- foreach my $name (keys %params) {
- my $item = $params{$name};
- unless (exists $param->{$name}) {
- print "New parameter: $name\n" unless $new_install;
- if (exists $new_params{$name}) {
- $param->{$name} = $new_params{$name};
- }
- elsif (exists $answer->{$name}) {
- $param->{$name} = $answer->{$name};
- }
- else {
- $param->{$name} = $item->{'default'};
- }
- }
- }
-
- $param->{'utf8'} = 1 if $new_install;
-
- # Bug 452525: OR based groups are on by default for new installations
- $param->{'or_groups'} = 1 if $new_install;
-
- # --- REMOVE OLD PARAMS ---
-
- my %oldparams;
- # Remove any old params
- foreach my $item (keys %$param) {
- if (!exists $params{$item}) {
- $oldparams{$item} = delete $param->{$item};
- }
- }
-
- # Write any old parameters to old-params.txt
- my $old_param_file = "$datadir/old-params.txt";
- if (scalar(keys %oldparams)) {
- my $op_file = new IO::File($old_param_file, '>>', 0600)
- || die "Couldn't create $old_param_file: $!";
-
- print "The following parameters are no longer used in Bugzilla,",
- " and so have been\nmoved from your parameters file into",
- " $old_param_file:\n";
-
- my $comma = "";
- foreach my $item (keys %oldparams) {
- print $op_file "\n\n$item:\n" . $oldparams{$item} . "\n";
- print "${comma}$item";
- $comma = ", ";
- }
- print "\n";
- $op_file->close;
- }
-
- write_params($param);
-
- if (-e $old_file) {
- unlink $old_file;
- say "$old_file has been converted into $old_file.json, using the JSON format.";
- }
-
- # Return deleted params and values so that checksetup.pl has a chance
- # to convert old params to new data.
- return %oldparams;
+ # Return deleted params and values so that checksetup.pl has a chance
+ # to convert old params to new data.
+ return %oldparams;
}
sub write_params {
- my ($param_data) = @_;
- $param_data ||= Bugzilla->params;
- my $param_file = bz_locations()->{'datadir'} . '/params.json';
+ my ($param_data) = @_;
+ $param_data ||= Bugzilla->params;
+ my $param_file = bz_locations()->{'datadir'} . '/params.json';
- my $json_data = JSON::XS->new->canonical->pretty->encode($param_data);
- write_text($param_file, $json_data);
+ my $json_data = JSON::XS->new->canonical->pretty->encode($param_data);
+ write_text($param_file, $json_data);
- # It's not common to edit parameters and loading
- # Bugzilla::Install::Filesystem is slow.
- require Bugzilla::Install::Filesystem;
- Bugzilla::Install::Filesystem::fix_file_permissions($param_file);
+ # It's not common to edit parameters and loading
+ # Bugzilla::Install::Filesystem is slow.
+ require Bugzilla::Install::Filesystem;
+ Bugzilla::Install::Filesystem::fix_file_permissions($param_file);
- # And now we have to reset the params cache so that Bugzilla will re-read
- # them.
- delete Bugzilla->request_cache->{params};
+ # And now we have to reset the params cache so that Bugzilla will re-read
+ # them.
+ delete Bugzilla->request_cache->{params};
}
sub read_param_file {
- my %params;
- my $file = bz_locations()->{'datadir'} . '/params.json';
-
- if (-e $file) {
- my $data = read_text($file);
- trick_taint($data);
-
- # If params.json has been manually edited and e.g. some quotes are
- # missing, we don't want JSON::XS to leak the content of the file
- # to all users in its error message, so we have to eval'uate it.
- %params = eval { %{JSON::XS->new->decode($data)} };
- if ($@) {
- my $error_msg = (basename($0) eq 'checksetup.pl') ?
- $@ : 'run checksetup.pl to see the details.';
- die "Error parsing $file: $error_msg";
- }
- # JSON::XS doesn't detaint data for us.
- foreach my $key (keys %params) {
- if (ref($params{$key}) eq "ARRAY") {
- foreach my $item (@{$params{$key}}) {
- trick_taint($item);
- }
- } else {
- trick_taint($params{$key}) if defined $params{$key};
- }
- }
+ my %params;
+ my $file = bz_locations()->{'datadir'} . '/params.json';
+
+ if (-e $file) {
+ my $data = read_text($file);
+ trick_taint($data);
+
+ # If params.json has been manually edited and e.g. some quotes are
+ # missing, we don't want JSON::XS to leak the content of the file
+ # to all users in its error message, so we have to eval'uate it.
+ %params = eval { %{JSON::XS->new->decode($data)} };
+ if ($@) {
+ my $error_msg
+ = (basename($0) eq 'checksetup.pl')
+ ? $@
+ : 'run checksetup.pl to see the details.';
+ die "Error parsing $file: $error_msg";
}
- elsif ($ENV{'SERVER_SOFTWARE'}) {
- # We're in a CGI, but the params file doesn't exist. We can't
- # Template Toolkit, or even install_string, since checksetup
- # might not have thrown an error. Bugzilla::CGI->new
- # hasn't even been called yet, so we manually use CGI::Carp here
- # so that the user sees the error.
- require CGI::Carp;
- CGI::Carp->import('fatalsToBrowser');
- die "The $file file does not exist."
- . ' You probably need to run checksetup.pl.',
+
+ # JSON::XS doesn't detaint data for us.
+ foreach my $key (keys %params) {
+ if (ref($params{$key}) eq "ARRAY") {
+ foreach my $item (@{$params{$key}}) {
+ trick_taint($item);
+ }
+ }
+ else {
+ trick_taint($params{$key}) if defined $params{$key};
+ }
}
- return \%params;
+ }
+ elsif ($ENV{'SERVER_SOFTWARE'}) {
+
+ # We're in a CGI, but the params file doesn't exist. We can't
+ # Template Toolkit, or even install_string, since checksetup
+ # might not have thrown an error. Bugzilla::CGI->new
+ # hasn't even been called yet, so we manually use CGI::Carp here
+ # so that the user sees the error.
+ require CGI::Carp;
+ CGI::Carp->import('fatalsToBrowser');
+ die "The $file file does not exist."
+ . ' You probably need to run checksetup.pl.',;
+ }
+ return \%params;
}
1;
diff --git a/Bugzilla/Config/Admin.pm b/Bugzilla/Config/Admin.pm
index 41d929298..fe19d7cf0 100644
--- a/Bugzilla/Config/Admin.pm
+++ b/Bugzilla/Config/Admin.pm
@@ -16,32 +16,21 @@ use Bugzilla::Config::Common;
our $sortkey = 200;
sub get_param_list {
- my $class = shift;
+ my $class = shift;
my @param_list = (
- {
- name => 'allowbugdeletion',
- type => 'b',
- default => 0
- },
-
- {
- name => 'allowemailchange',
- type => 'b',
- default => 1
- },
-
- {
- name => 'allowuserdeletion',
- type => 'b',
- default => 0
- },
-
- {
- name => 'last_visit_keep_days',
- type => 't',
- default => 10,
- checker => \&check_numeric
- });
+ {name => 'allowbugdeletion', type => 'b', default => 0},
+
+ {name => 'allowemailchange', type => 'b', default => 1},
+
+ {name => 'allowuserdeletion', type => 'b', default => 0},
+
+ {
+ name => 'last_visit_keep_days',
+ type => 't',
+ default => 10,
+ checker => \&check_numeric
+ }
+ );
return @param_list;
}
diff --git a/Bugzilla/Config/Advanced.pm b/Bugzilla/Config/Advanced.pm
index 8356c3361..043a892d7 100644
--- a/Bugzilla/Config/Advanced.pm
+++ b/Bugzilla/Config/Advanced.pm
@@ -16,31 +16,18 @@ use Bugzilla::Config::Common;
our $sortkey = 1700;
use constant get_param_list => (
- {
- name => 'cookiedomain',
- type => 't',
- default => ''
- },
+ {name => 'cookiedomain', type => 't', default => ''},
- {
- name => 'inbound_proxies',
- type => 't',
- default => '',
- checker => \&check_ip
- },
+ {name => 'inbound_proxies', type => 't', default => '', checker => \&check_ip},
- {
- name => 'proxy_url',
- type => 't',
- default => ''
- },
+ {name => 'proxy_url', type => 't', default => ''},
{
- name => 'strict_transport_security',
- type => 's',
- choices => ['off', 'this_domain_only', 'include_subdomains'],
- default => 'off',
- checker => \&check_multi
+ name => 'strict_transport_security',
+ type => 's',
+ choices => ['off', 'this_domain_only', 'include_subdomains'],
+ default => 'off',
+ checker => \&check_multi
},
);
diff --git a/Bugzilla/Config/Attachment.pm b/Bugzilla/Config/Attachment.pm
index 580ec46d9..0cf4b768a 100644
--- a/Bugzilla/Config/Attachment.pm
+++ b/Bugzilla/Config/Attachment.pm
@@ -16,48 +16,41 @@ use Bugzilla::Config::Common;
our $sortkey = 400;
sub get_param_list {
- my $class = shift;
+ my $class = shift;
my @param_list = (
- {
- name => 'allow_attachment_display',
- type => 'b',
- default => 0
- },
-
- {
- name => 'attachment_base',
- type => 't',
- default => '',
- checker => \&check_urlbase
- },
-
- {
- name => 'allow_attachment_deletion',
- type => 'b',
- default => 0
- },
-
- {
- name => 'maxattachmentsize',
- type => 't',
- default => '1000',
- checker => \&check_maxattachmentsize
- },
-
- # The maximum size (in bytes) for patches and non-patch attachments.
- # The default limit is 1000KB, which is 24KB less than mysql's default
- # maximum packet size (which determines how much data can be sent in a
- # single mysql packet and thus how much data can be inserted into the
- # database) to provide breathing space for the data in other fields of
- # the attachment record as well as any mysql packet overhead (I don't
- # know of any, but I suspect there may be some.)
-
- {
- name => 'maxlocalattachment',
- type => 't',
- default => '0',
- checker => \&check_numeric
- } );
+ {name => 'allow_attachment_display', type => 'b', default => 0},
+
+ {
+ name => 'attachment_base',
+ type => 't',
+ default => '',
+ checker => \&check_urlbase
+ },
+
+ {name => 'allow_attachment_deletion', type => 'b', default => 0},
+
+ {
+ name => 'maxattachmentsize',
+ type => 't',
+ default => '1000',
+ checker => \&check_maxattachmentsize
+ },
+
+ # The maximum size (in bytes) for patches and non-patch attachments.
+ # The default limit is 1000KB, which is 24KB less than mysql's default
+ # maximum packet size (which determines how much data can be sent in a
+ # single mysql packet and thus how much data can be inserted into the
+ # database) to provide breathing space for the data in other fields of
+ # the attachment record as well as any mysql packet overhead (I don't
+ # know of any, but I suspect there may be some.)
+
+ {
+ name => 'maxlocalattachment',
+ type => 't',
+ default => '0',
+ checker => \&check_numeric
+ }
+ );
return @param_list;
}
diff --git a/Bugzilla/Config/Auth.pm b/Bugzilla/Config/Auth.pm
index 78d719b15..09e81339f 100644
--- a/Bugzilla/Config/Auth.pm
+++ b/Bugzilla/Config/Auth.pm
@@ -16,111 +16,85 @@ use Bugzilla::Config::Common;
our $sortkey = 300;
sub get_param_list {
- my $class = shift;
+ my $class = shift;
my @param_list = (
- {
- name => 'auth_env_id',
- type => 't',
- default => '',
- },
-
- {
- name => 'auth_env_email',
- type => 't',
- default => '',
- },
-
- {
- name => 'auth_env_realname',
- type => 't',
- default => '',
- },
-
- # XXX in the future:
- #
- # user_verify_class and user_info_class should have choices gathered from
- # whatever sits in their respective directories
- #
- # rather than comma-separated lists, these two should eventually become
- # arrays, but that requires alterations to editparams first
-
- {
- name => 'user_info_class',
- type => 's',
- choices => [ 'CGI', 'Env', 'Env,CGI' ],
- default => 'CGI',
- checker => \&check_multi
- },
-
- {
- name => 'user_verify_class',
- type => 'o',
- choices => [ 'DB', 'RADIUS', 'LDAP' ],
- default => 'DB',
- checker => \&check_user_verify_class
- },
-
- {
- name => 'rememberlogin',
- type => 's',
- choices => ['on', 'defaulton', 'defaultoff', 'off'],
- default => 'on',
- checker => \&check_multi
- },
-
- {
- name => 'requirelogin',
- type => 'b',
- default => '0'
- },
-
- {
- name => 'webservice_email_filter',
- type => 'b',
- default => 0
- },
-
- {
- name => 'emailregexp',
- type => 't',
- default => q:^[\\w\\.\\+\\-=']+@[\\w\\.\\-]+\\.[\\w\\-]+$:,
- checker => \&check_regexp
- },
-
- {
- name => 'emailregexpdesc',
- type => 'l',
- default => 'A legal address must contain exactly one \'@\', and at least ' .
- 'one \'.\' after the @.'
- },
-
- {
- name => 'emailsuffix',
- type => 't',
- default => ''
- },
-
- {
- name => 'createemailregexp',
- type => 't',
- default => q:.*:,
- checker => \&check_regexp
- },
-
- {
- name => 'password_complexity',
- type => 's',
- choices => [ 'no_constraints', 'mixed_letters', 'letters_numbers',
- 'letters_numbers_specialchars' ],
- default => 'no_constraints',
- checker => \&check_multi
- },
-
- {
- name => 'password_check_on_login',
- type => 'b',
- default => '1'
- },
+ {name => 'auth_env_id', type => 't', default => '',},
+
+ {name => 'auth_env_email', type => 't', default => '',},
+
+ {name => 'auth_env_realname', type => 't', default => '',},
+
+ # XXX in the future:
+ #
+ # user_verify_class and user_info_class should have choices gathered from
+ # whatever sits in their respective directories
+ #
+ # rather than comma-separated lists, these two should eventually become
+ # arrays, but that requires alterations to editparams first
+
+ {
+ name => 'user_info_class',
+ type => 's',
+ choices => ['CGI', 'Env', 'Env,CGI'],
+ default => 'CGI',
+ checker => \&check_multi
+ },
+
+ {
+ name => 'user_verify_class',
+ type => 'o',
+ choices => ['DB', 'RADIUS', 'LDAP'],
+ default => 'DB',
+ checker => \&check_user_verify_class
+ },
+
+ {
+ name => 'rememberlogin',
+ type => 's',
+ choices => ['on', 'defaulton', 'defaultoff', 'off'],
+ default => 'on',
+ checker => \&check_multi
+ },
+
+ {name => 'requirelogin', type => 'b', default => '0'},
+
+ {name => 'webservice_email_filter', type => 'b', default => 0},
+
+ {
+ name => 'emailregexp',
+ type => 't',
+ default => q:^[\\w\\.\\+\\-=']+@[\\w\\.\\-]+\\.[\\w\\-]+$:,
+ checker => \&check_regexp
+ },
+
+ {
+ name => 'emailregexpdesc',
+ type => 'l',
+ default => 'A legal address must contain exactly one \'@\', and at least '
+ . 'one \'.\' after the @.'
+ },
+
+ {name => 'emailsuffix', type => 't', default => ''},
+
+ {
+ name => 'createemailregexp',
+ type => 't',
+ default => q:.*:,
+ checker => \&check_regexp
+ },
+
+ {
+ name => 'password_complexity',
+ type => 's',
+ choices => [
+ 'no_constraints', 'mixed_letters',
+ 'letters_numbers', 'letters_numbers_specialchars'
+ ],
+ default => 'no_constraints',
+ checker => \&check_multi
+ },
+
+ {name => 'password_check_on_login', type => 'b', default => '1'},
);
return @param_list;
}
diff --git a/Bugzilla/Config/BugChange.pm b/Bugzilla/Config/BugChange.pm
index 0acdc0ce4..ad1cafefc 100644
--- a/Bugzilla/Config/BugChange.pm
+++ b/Bugzilla/Config/BugChange.pm
@@ -26,55 +26,33 @@ sub get_param_list {
# and bug_status.is_open is not yet defined (hence the eval), so we use
# the bug statuses above as they are still hardcoded.
eval {
- my @current_closed_states = map {$_->name} closed_bug_statuses();
- # If no closed state was found, use the default list above.
- @closed_bug_statuses = @current_closed_states if scalar(@current_closed_states);
+ my @current_closed_states = map { $_->name } closed_bug_statuses();
+
+ # If no closed state was found, use the default list above.
+ @closed_bug_statuses = @current_closed_states if scalar(@current_closed_states);
};
my @param_list = (
- {
- name => 'duplicate_or_move_bug_status',
- type => 's',
- choices => \@closed_bug_statuses,
- default => $closed_bug_statuses[0],
- checker => \&check_bug_status
- },
-
- {
- name => 'letsubmitterchoosepriority',
- type => 'b',
- default => 1
- },
-
- {
- name => 'letsubmitterchoosemilestone',
- type => 'b',
- default => 1
- },
-
- {
- name => 'musthavemilestoneonaccept',
- type => 'b',
- default => 0
- },
-
- {
- name => 'commentonchange_resolution',
- type => 'b',
- default => 0
- },
-
- {
- name => 'commentonduplicate',
- type => 'b',
- default => 0
- },
-
- {
- name => 'noresolveonopenblockers',
- type => 'b',
- default => 0,
- } );
+ {
+ name => 'duplicate_or_move_bug_status',
+ type => 's',
+ choices => \@closed_bug_statuses,
+ default => $closed_bug_statuses[0],
+ checker => \&check_bug_status
+ },
+
+ {name => 'letsubmitterchoosepriority', type => 'b', default => 1},
+
+ {name => 'letsubmitterchoosemilestone', type => 'b', default => 1},
+
+ {name => 'musthavemilestoneonaccept', type => 'b', default => 0},
+
+ {name => 'commentonchange_resolution', type => 'b', default => 0},
+
+ {name => 'commentonduplicate', type => 'b', default => 0},
+
+ {name => 'noresolveonopenblockers', type => 'b', default => 0,}
+ );
return @param_list;
}
diff --git a/Bugzilla/Config/BugFields.pm b/Bugzilla/Config/BugFields.pm
index ef2faa64b..1659dc66a 100644
--- a/Bugzilla/Config/BugFields.pm
+++ b/Bugzilla/Config/BugFields.pm
@@ -25,73 +25,50 @@ sub get_param_list {
my @legal_OS = @{get_legal_field_values('op_sys')};
my @param_list = (
- {
- name => 'useclassification',
- type => 'b',
- default => 0
- },
-
- {
- name => 'usetargetmilestone',
- type => 'b',
- default => 0
- },
-
- {
- name => 'useqacontact',
- type => 'b',
- default => 0
- },
-
- {
- name => 'usestatuswhiteboard',
- type => 'b',
- default => 0
- },
-
- {
- name => 'use_see_also',
- type => 'b',
- default => 1
- },
-
- {
- name => 'defaultpriority',
- type => 's',
- choices => \@legal_priorities,
- default => $legal_priorities[-1],
- checker => \&check_priority
- },
-
- {
- name => 'defaultseverity',
- type => 's',
- choices => \@legal_severities,
- default => $legal_severities[-1],
- checker => \&check_severity
- },
-
- {
- name => 'defaultplatform',
- type => 's',
- choices => ['', @legal_platforms],
- default => '',
- checker => \&check_platform
- },
-
- {
- name => 'defaultopsys',
- type => 's',
- choices => ['', @legal_OS],
- default => '',
- checker => \&check_opsys
- },
-
- {
- name => 'collapsed_comment_tags',
- type => 't',
- default => 'obsolete, spam',
- });
+ {name => 'useclassification', type => 'b', default => 0},
+
+ {name => 'usetargetmilestone', type => 'b', default => 0},
+
+ {name => 'useqacontact', type => 'b', default => 0},
+
+ {name => 'usestatuswhiteboard', type => 'b', default => 0},
+
+ {name => 'use_see_also', type => 'b', default => 1},
+
+ {
+ name => 'defaultpriority',
+ type => 's',
+ choices => \@legal_priorities,
+ default => $legal_priorities[-1],
+ checker => \&check_priority
+ },
+
+ {
+ name => 'defaultseverity',
+ type => 's',
+ choices => \@legal_severities,
+ default => $legal_severities[-1],
+ checker => \&check_severity
+ },
+
+ {
+ name => 'defaultplatform',
+ type => 's',
+ choices => ['', @legal_platforms],
+ default => '',
+ checker => \&check_platform
+ },
+
+ {
+ name => 'defaultopsys',
+ type => 's',
+ choices => ['', @legal_OS],
+ default => '',
+ checker => \&check_opsys
+ },
+
+ {name => 'collapsed_comment_tags', type => 't', default => 'obsolete, spam',}
+ );
return @param_list;
}
diff --git a/Bugzilla/Config/Common.pm b/Bugzilla/Config/Common.pm
index bd9b0bf84..756dbb0dd 100644
--- a/Bugzilla/Config/Common.pm
+++ b/Bugzilla/Config/Common.pm
@@ -21,392 +21,406 @@ use Bugzilla::Group;
use Bugzilla::Status;
use parent qw(Exporter);
-@Bugzilla::Config::Common::EXPORT =
- qw(check_multi check_numeric check_regexp check_url check_group
- check_sslbase check_priority check_severity check_platform
- check_opsys check_shadowdb check_urlbase check_webdotbase
- check_user_verify_class check_ip check_font_file
- check_mail_delivery_method check_notification check_utf8
- check_bug_status check_smtp_auth check_theschwartz_available
- check_maxattachmentsize check_email check_smtp_ssl
- check_comment_taggers_group check_smtp_server
+@Bugzilla::Config::Common::EXPORT
+ = qw(check_multi check_numeric check_regexp check_url check_group
+ check_sslbase check_priority check_severity check_platform
+ check_opsys check_shadowdb check_urlbase check_webdotbase
+ check_user_verify_class check_ip check_font_file
+ check_mail_delivery_method check_notification check_utf8
+ check_bug_status check_smtp_auth check_theschwartz_available
+ check_maxattachmentsize check_email check_smtp_ssl
+ check_comment_taggers_group check_smtp_server
);
# Checking functions for the various values
sub check_multi {
- my ($value, $param) = (@_);
+ my ($value, $param) = (@_);
- if ($param->{'type'} eq "s") {
- unless (scalar(grep {$_ eq $value} (@{$param->{'choices'}}))) {
- return "Invalid choice '$value' for single-select list param '$param->{'name'}'";
- }
-
- return "";
+ if ($param->{'type'} eq "s") {
+ unless (scalar(grep { $_ eq $value } (@{$param->{'choices'}}))) {
+ return
+ "Invalid choice '$value' for single-select list param '$param->{'name'}'";
}
- elsif ($param->{'type'} eq 'm' || $param->{'type'} eq 'o') {
- if (ref($value) ne "ARRAY") {
- $value = [split(',', $value)]
- }
- foreach my $chkParam (@$value) {
- unless (scalar(grep {$_ eq $chkParam} (@{$param->{'choices'}}))) {
- return "Invalid choice '$chkParam' for multi-select list param '$param->{'name'}'";
- }
- }
-
- return "";
+
+ return "";
+ }
+ elsif ($param->{'type'} eq 'm' || $param->{'type'} eq 'o') {
+ if (ref($value) ne "ARRAY") {
+ $value = [split(',', $value)];
}
- else {
- return "Invalid param type '$param->{'type'}' for check_multi(); " .
- "contact your Bugzilla administrator";
+ foreach my $chkParam (@$value) {
+ unless (scalar(grep { $_ eq $chkParam } (@{$param->{'choices'}}))) {
+ return
+ "Invalid choice '$chkParam' for multi-select list param '$param->{'name'}'";
+ }
}
+
+ return "";
+ }
+ else {
+ return "Invalid param type '$param->{'type'}' for check_multi(); "
+ . "contact your Bugzilla administrator";
+ }
}
sub check_numeric {
- my ($value) = (@_);
- if ($value !~ /^[0-9]+$/) {
- return "must be a numeric value";
- }
- return "";
+ my ($value) = (@_);
+ if ($value !~ /^[0-9]+$/) {
+ return "must be a numeric value";
+ }
+ return "";
}
sub check_regexp {
- my ($value) = (@_);
- eval { qr/$value/ };
- return $@;
+ my ($value) = (@_);
+ eval {qr/$value/};
+ return $@;
}
sub check_email {
- my ($value) = @_;
- if ($value !~ $Email::Address::mailbox) {
- return "must be a valid email address.";
- }
- return "";
+ my ($value) = @_;
+ if ($value !~ $Email::Address::mailbox) {
+ return "must be a valid email address.";
+ }
+ return "";
}
sub check_sslbase {
- my $url = shift;
- if ($url ne '') {
- if ($url !~ m#^https://([^/]+).*/$#) {
- return "must be a legal URL, that starts with https and ends with a slash.";
- }
- my $host = $1;
- # Fall back to port 443 if for some reason getservbyname() fails.
- my $port = getservbyname('https', 'tcp') || 443;
- if ($host =~ /^(.+):(\d+)$/) {
- $host = $1;
- $port = $2;
- }
- local *SOCK;
- my $proto = getprotobyname('tcp');
- socket(SOCK, PF_INET, SOCK_STREAM, $proto);
- my $iaddr = inet_aton($host) || return "The host $host cannot be resolved";
- my $sin = sockaddr_in($port, $iaddr);
- if (!connect(SOCK, $sin)) {
- return "Failed to connect to $host:$port ($!); unable to enable SSL";
- }
- close(SOCK);
- }
- return "";
+ my $url = shift;
+ if ($url ne '') {
+ if ($url !~ m#^https://([^/]+).*/$#) {
+ return "must be a legal URL, that starts with https and ends with a slash.";
+ }
+ my $host = $1;
+
+ # Fall back to port 443 if for some reason getservbyname() fails.
+ my $port = getservbyname('https', 'tcp') || 443;
+ if ($host =~ /^(.+):(\d+)$/) {
+ $host = $1;
+ $port = $2;
+ }
+ local *SOCK;
+ my $proto = getprotobyname('tcp');
+ socket(SOCK, PF_INET, SOCK_STREAM, $proto);
+ my $iaddr = inet_aton($host) || return "The host $host cannot be resolved";
+ my $sin = sockaddr_in($port, $iaddr);
+ if (!connect(SOCK, $sin)) {
+ return "Failed to connect to $host:$port ($!); unable to enable SSL";
+ }
+ close(SOCK);
+ }
+ return "";
}
sub check_ip {
- my $inbound_proxies = shift;
- my @proxies = split(/[\s,]+/, $inbound_proxies);
- foreach my $proxy (@proxies) {
- validate_ip($proxy) || return "$proxy is not a valid IPv4 or IPv6 address";
- }
- return "";
+ my $inbound_proxies = shift;
+ my @proxies = split(/[\s,]+/, $inbound_proxies);
+ foreach my $proxy (@proxies) {
+ validate_ip($proxy) || return "$proxy is not a valid IPv4 or IPv6 address";
+ }
+ return "";
}
sub check_utf8 {
- my $utf8 = shift;
- # You cannot turn off the UTF-8 parameter if you've already converted
- # your tables to utf-8.
- my $dbh = Bugzilla->dbh;
- if ($dbh->isa('Bugzilla::DB::Mysql') && $dbh->bz_db_is_utf8 && !$utf8) {
- return "You cannot disable UTF-8 support, because your MySQL database"
- . " is encoded in UTF-8";
- }
- return "";
+ my $utf8 = shift;
+
+ # You cannot turn off the UTF-8 parameter if you've already converted
+ # your tables to utf-8.
+ my $dbh = Bugzilla->dbh;
+ if ($dbh->isa('Bugzilla::DB::Mysql') && $dbh->bz_db_is_utf8 && !$utf8) {
+ return "You cannot disable UTF-8 support, because your MySQL database"
+ . " is encoded in UTF-8";
+ }
+ return "";
}
sub check_priority {
- my ($value) = (@_);
- my $legal_priorities = get_legal_field_values('priority');
- if (!grep($_ eq $value, @$legal_priorities)) {
- return "Must be a legal priority value: one of " .
- join(", ", @$legal_priorities);
- }
- return "";
+ my ($value) = (@_);
+ my $legal_priorities = get_legal_field_values('priority');
+ if (!grep($_ eq $value, @$legal_priorities)) {
+ return "Must be a legal priority value: one of "
+ . join(", ", @$legal_priorities);
+ }
+ return "";
}
sub check_severity {
- my ($value) = (@_);
- my $legal_severities = get_legal_field_values('bug_severity');
- if (!grep($_ eq $value, @$legal_severities)) {
- return "Must be a legal severity value: one of " .
- join(", ", @$legal_severities);
- }
- return "";
+ my ($value) = (@_);
+ my $legal_severities = get_legal_field_values('bug_severity');
+ if (!grep($_ eq $value, @$legal_severities)) {
+ return "Must be a legal severity value: one of "
+ . join(", ", @$legal_severities);
+ }
+ return "";
}
sub check_platform {
- my ($value) = (@_);
- my $legal_platforms = get_legal_field_values('rep_platform');
- if (!grep($_ eq $value, '', @$legal_platforms)) {
- return "Must be empty or a legal platform value: one of " .
- join(", ", @$legal_platforms);
- }
- return "";
+ my ($value) = (@_);
+ my $legal_platforms = get_legal_field_values('rep_platform');
+ if (!grep($_ eq $value, '', @$legal_platforms)) {
+ return "Must be empty or a legal platform value: one of "
+ . join(", ", @$legal_platforms);
+ }
+ return "";
}
sub check_opsys {
- my ($value) = (@_);
- my $legal_OS = get_legal_field_values('op_sys');
- if (!grep($_ eq $value, '', @$legal_OS)) {
- return "Must be empty or a legal operating system value: one of " .
- join(", ", @$legal_OS);
- }
- return "";
+ my ($value) = (@_);
+ my $legal_OS = get_legal_field_values('op_sys');
+ if (!grep($_ eq $value, '', @$legal_OS)) {
+ return "Must be empty or a legal operating system value: one of "
+ . join(", ", @$legal_OS);
+ }
+ return "";
}
sub check_bug_status {
- my $bug_status = shift;
- my @closed_bug_statuses = map {$_->name} closed_bug_statuses();
- if (!grep($_ eq $bug_status, @closed_bug_statuses)) {
- return "Must be a valid closed status: one of " . join(', ', @closed_bug_statuses);
- }
- return "";
+ my $bug_status = shift;
+ my @closed_bug_statuses = map { $_->name } closed_bug_statuses();
+ if (!grep($_ eq $bug_status, @closed_bug_statuses)) {
+ return "Must be a valid closed status: one of "
+ . join(', ', @closed_bug_statuses);
+ }
+ return "";
}
sub check_group {
- my $group_name = shift;
- return "" unless $group_name;
- my $group = new Bugzilla::Group({'name' => $group_name});
- unless (defined $group) {
- return "Must be an existing group name";
- }
- return "";
+ my $group_name = shift;
+ return "" unless $group_name;
+ my $group = new Bugzilla::Group({'name' => $group_name});
+ unless (defined $group) {
+ return "Must be an existing group name";
+ }
+ return "";
}
sub check_shadowdb {
- my ($value) = (@_);
- $value = trim($value);
- if ($value eq "") {
- return "";
- }
+ my ($value) = (@_);
+ $value = trim($value);
+ if ($value eq "") {
+ return "";
+ }
- if (!Bugzilla->params->{'shadowdbhost'}) {
- return "You need to specify a host when using a shadow database";
- }
+ if (!Bugzilla->params->{'shadowdbhost'}) {
+ return "You need to specify a host when using a shadow database";
+ }
- # Can't test existence of this because ConnectToDatabase uses the param,
- # but we can't set this before testing....
- # This can really only be fixed after we can use the DBI more openly
- return "";
+ # Can't test existence of this because ConnectToDatabase uses the param,
+ # but we can't set this before testing....
+ # This can really only be fixed after we can use the DBI more openly
+ return "";
}
sub check_urlbase {
- my ($url) = (@_);
- if ($url && $url !~ m:^http.*/$:) {
- return "must be a legal URL, that starts with http and ends with a slash.";
- }
- return "";
+ my ($url) = (@_);
+ if ($url && $url !~ m:^http.*/$:) {
+ return "must be a legal URL, that starts with http and ends with a slash.";
+ }
+ return "";
}
sub check_url {
- my ($url) = (@_);
- return '' if $url eq ''; # Allow empty URLs
- if ($url !~ m:/$:) {
- return 'must be a legal URL, absolute or relative, ending with a slash.';
- }
- return '';
+ my ($url) = (@_);
+ return '' if $url eq ''; # Allow empty URLs
+ if ($url !~ m:/$:) {
+ return 'must be a legal URL, absolute or relative, ending with a slash.';
+ }
+ return '';
}
sub check_webdotbase {
- my ($value) = (@_);
- $value = trim($value);
- if ($value eq "") {
- return "";
- }
- if($value !~ /^https?:/) {
- if(! -x $value) {
- return "The file path \"$value\" is not a valid executable. Please specify the complete file path to 'dot' if you intend to generate graphs locally.";
- }
- # Check .htaccess allows access to generated images
- my $webdotdir = bz_locations()->{'webdotdir'};
- if(-e "$webdotdir/.htaccess") {
- open HTACCESS, "<", "$webdotdir/.htaccess";
- if(! grep(/ \\\.png\$/,
Second error: $error2
tags. - return qq|| - . qq|$link_text| - . qq| [details, diff]| - . qq||; - } - else { - # Whitespace matters here because these links are intags. - return qq|| - . qq|$link_text| - . qq| [details]| - . qq||; - } + # Prevent code injection in the title. + $title = html_quote(clean_text($title)); + + $link_text =~ s/ \[details(?:, diff)?\]$//; + 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'; + } + + if ($patchlink) { + + # Whitespace matters here because these links are intags. + return + qq|| + . qq|$link_text| + . qq| [details, diff]| + . qq||; } else { - return qq{$link_text}; + # 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. @@ -354,53 +365,59 @@ sub get_attachment_link { # comment in the bug sub get_bug_link { - my ($bug, $link_text, $options) = @_; - $options ||= {}; - $options->{user} ||= Bugzilla->user; - - if (defined $bug && $bug ne '') { - if (!blessed($bug)) { - require Bugzilla::Bug; - $bug = new Bugzilla::Bug({ id => $bug, cache => 1 }); - } - return $link_text if $bug->{error}; + my ($bug, $link_text, $options) = @_; + $options ||= {}; + $options->{user} ||= Bugzilla->user; + + if (defined $bug && $bug ne '') { + if (!blessed($bug)) { + require Bugzilla::Bug; + $bug = new Bugzilla::Bug({id => $bug, cache => 1}); } - - my $template = Bugzilla->template_inner; - my $linkified; - $template->process('bug/link.html.tmpl', - { bug => $bug, link_text => $link_text, %$options }, \$linkified); - return $linkified; + return $link_text if $bug->{error}; + } + + my $template = Bugzilla->template_inner; + my $linkified; + $template->process('bug/link.html.tmpl', + {bug => $bug, link_text => $link_text, %$options}, + \$linkified); + return $linkified; } # We use this instead of format because format doesn't deal well with # multi-byte languages. sub multiline_sprintf { - my ($format, $args, $sizes) = @_; - my @parts; - my @my_sizes = @$sizes; # Copy this so we don't modify the input array. - foreach my $string (@$args) { - my $size = shift @my_sizes; - my @pieces = split("\n", wrap_hard($string, $size)); - push(@parts, \@pieces); - } - - my $formatted; - while (1) { - # Get the first item of each part. - my @line = map { shift @$_ } @parts; - # If they're all undef, we're done. - last if !grep { defined $_ } @line; - # Make any single undef item into '' - @line = map { defined $_ ? $_ : '' } @line; - # And append a formatted line - $formatted .= sprintf($format, @line); - # Remove trailing spaces, or they become lots of =20's in - # quoted-printable emails. - $formatted =~ s/\s+$//; - $formatted .= "\n"; - } - return $formatted; + my ($format, $args, $sizes) = @_; + my @parts; + my @my_sizes = @$sizes; # Copy this so we don't modify the input array. + foreach my $string (@$args) { + my $size = shift @my_sizes; + my @pieces = split("\n", wrap_hard($string, $size)); + push(@parts, \@pieces); + } + + my $formatted; + while (1) { + + # Get the first item of each part. + my @line = map { shift @$_ } @parts; + + # If they're all undef, we're done. + last if !grep { defined $_ } @line; + + # Make any single undef item into '' + @line = map { defined $_ ? $_ : '' } @line; + + # And append a formatted line + $formatted .= sprintf($format, @line); + + # Remove trailing spaces, or they become lots of =20's in + # quoted-printable emails. + $formatted =~ s/\s+$//; + $formatted .= "\n"; + } + return $formatted; } ##################### @@ -412,17 +429,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: @@ -435,183 +453,186 @@ sub mtime_filter { # 6. Custom Bugzilla stylesheet set sub css_files { - my ($style_urls, $yui, $yui_css) = @_; + my ($style_urls, $yui, $yui_css) = @_; - # global.css goes on every page. - my @requested_css = ('skins/standard/global.css', @$style_urls); + # global.css goes on every page. + my @requested_css = ('skins/standard/global.css', @$style_urls); - my @yui_required_css; - foreach my $yui_name (@$yui) { - next if !$yui_css->{$yui_name}; - push(@yui_required_css, "js/yui/assets/skins/sam/$yui_name.css"); - } - unshift(@requested_css, @yui_required_css); - - my @css_sets = map { _css_link_set($_) } @requested_css; - - my %by_type = (standard => [], skin => [], custom => []); - foreach my $set (@css_sets) { - foreach my $key (keys %$set) { - push(@{ $by_type{$key} }, $set->{$key}); - } + my @yui_required_css; + foreach my $yui_name (@$yui) { + next if !$yui_css->{$yui_name}; + push(@yui_required_css, "js/yui/assets/skins/sam/$yui_name.css"); + } + unshift(@requested_css, @yui_required_css); + + my @css_sets = map { _css_link_set($_) } @requested_css; + + my %by_type = (standard => [], skin => [], custom => []); + foreach my $set (@css_sets) { + foreach my $key (keys %$set) { + push(@{$by_type{$key}}, $set->{$key}); } + } - # build unified - $by_type{unified_standard_skin} = _concatenate_css($by_type{standard}, - $by_type{skin}); - $by_type{unified_custom} = _concatenate_css($by_type{custom}); + # build unified + $by_type{unified_standard_skin} + = _concatenate_css($by_type{standard}, $by_type{skin}); + $by_type{unified_custom} = _concatenate_css($by_type{custom}); - return \%by_type; + return \%by_type; } sub _css_link_set { - my ($file_name) = @_; - - my %set = (standard => mtime_filter($file_name)); - - # We use (?:^|/) to allow Extensions to use the skins system if they want. - if ($file_name !~ m{(?:^|/)skins/standard/}) { - return \%set; - } - - my $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 ($file_name) = @_; - my $custom_file_name = $file_name; - $custom_file_name =~ s{(?:^|/)skins/standard/}{skins/custom/}; - if (my $custom_mtime = _mtime("$cgi_path/$custom_file_name")) { - $set{custom} = mtime_filter($custom_file_name, $custom_mtime); - } + my %set = (standard => mtime_filter($file_name)); + # We use (?:^|/) to allow Extensions to use the skins system if they want. + if ($file_name !~ m{(?:^|/)skins/standard/}) { return \%set; + } + + my $skin = Bugzilla->user->settings->{skin}->{value}; + my $cgi_path = bz_locations()->{'cgi_path'}; + my $skin_file_name = $file_name; + $skin_file_name =~ s{(?:^|/)skins/standard/}{skins/contrib/$skin/}; + if (my $mtime = _mtime("$cgi_path/$skin_file_name")) { + $set{skin} = mtime_filter($skin_file_name, $mtime); + } + + my $custom_file_name = $file_name; + $custom_file_name =~ s{(?:^|/)skins/standard/}{skins/custom/}; + if (my $custom_mtime = _mtime("$cgi_path/$custom_file_name")) { + $set{custom} = mtime_filter($custom_file_name, $custom_mtime); + } + + return \%set; } sub _concatenate_css { - my @sources = map { @$_ } @_; - return unless @sources; - - my %files = - map { - (my $file = $_) =~ s/(^[^\?]+)\?.+/$1/; - $_ => $file; - } @sources; - - my $cgi_path = bz_locations()->{cgi_path}; - my $skins_path = bz_locations()->{assetsdir}; - - # build minified files - my @minified; - foreach my $source (@sources) { - next unless -e "$cgi_path/$files{$source}"; - my $file = $skins_path . '/' . md5_hex($source) . '.css'; - if (!-e $file) { - my $content = read_text("$cgi_path/$files{$source}"); - - # minify - $content =~ s{/\*.*?\*/}{}sg; # comments - $content =~ s{(^\s+|\s+$)}{}mg; # leading/trailing whitespace - $content =~ s{\n}{}g; # single line - - # rewrite urls - $content =~ s{url\(([^\)]+)\)}{_css_url_rewrite($source, $1)}eig; - - write_text($file, "/* $files{$source} */\n" . $content . "\n"); - } - push @minified, $file; - } - - # concat files - my $file = $skins_path . '/' . md5_hex(join(' ', @sources)) . '.css'; + my @sources = map {@$_} @_; + return unless @sources; + + my %files = map { + (my $file = $_) =~ s/(^[^\?]+)\?.+/$1/; + $_ => $file; + } @sources; + + my $cgi_path = bz_locations()->{cgi_path}; + my $skins_path = bz_locations()->{assetsdir}; + + # build minified files + my @minified; + foreach my $source (@sources) { + next unless -e "$cgi_path/$files{$source}"; + my $file = $skins_path . '/' . md5_hex($source) . '.css'; if (!-e $file) { - my $content = ''; - foreach my $source (@minified) { - $content .= read_text($source); - } - write_text($file, $content); + my $content = read_text("$cgi_path/$files{$source}"); + + # minify + $content =~ s{/\*.*?\*/}{}sg; # comments + $content =~ s{(^\s+|\s+$)}{}mg; # leading/trailing whitespace + $content =~ s{\n}{}g; # single line + + # rewrite urls + $content =~ s{url\(([^\)]+)\)}{_css_url_rewrite($source, $1)}eig; + + write_text($file, "/* $files{$source} */\n" . $content . "\n"); + } + push @minified, $file; + } + + # concat files + my $file = $skins_path . '/' . md5_hex(join(' ', @sources)) . '.css'; + if (!-e $file) { + my $content = ''; + foreach my $source (@minified) { + $content .= read_text($source); } + write_text($file, $content); + } - $file =~ s/^\Q$cgi_path\E\///o; - return mtime_filter($file); + $file =~ s/^\Q$cgi_path\E\///o; + return mtime_filter($file); } sub _css_url_rewrite { - my ($source, $url) = @_; - # rewrite relative urls as the unified stylesheet lives in a different - # directory from the source - $url =~ s/(^['"]|['"]$)//g; - if (substr($url, 0, 1) eq '/' || substr($url, 0, 5) eq 'data:') { - return 'url(' . $url . ')'; - } - return 'url(../../' . ($ENV{'PROJECT'} ? '../' : '') . dirname($source) . '/' . $url . ')'; + my ($source, $url) = @_; + + # rewrite relative urls as the unified stylesheet lives in a different + # directory from the source + $url =~ s/(^['"]|['"]$)//g; + if (substr($url, 0, 1) eq '/' || substr($url, 0, 5) eq 'data:') { + return 'url(' . $url . ')'; + } + return + 'url(../../' + . ($ENV{'PROJECT'} ? '../' : '') + . dirname($source) . '/' + . $url . ')'; } sub _concatenate_js { - return @_ unless CONCATENATE_ASSETS; - my ($sources) = @_; - return [] unless $sources; - $sources = ref($sources) ? $sources : [ $sources ]; - - my %files = - map { - (my $file = $_) =~ s/(^[^\?]+)\?.+/$1/; - $_ => $file; - } @$sources; - - my $cgi_path = bz_locations()->{cgi_path}; - my $skins_path = bz_locations()->{assetsdir}; - - # build minified files - my @minified; - foreach my $source (@$sources) { - next unless -e "$cgi_path/$files{$source}"; - my $file = $skins_path . '/' . md5_hex($source) . '.js'; - if (!-e $file) { - my $content = read_text("$cgi_path/$files{$source}"); - - # minimal minification - $content =~ s#/\*.*?\*/##sg; # block comments - $content =~ s#(^ +| +$)##gm; # leading/trailing spaces - $content =~ s#^//.+$##gm; # single line comments - $content =~ s#\n{2,}#\n#g; # blank lines - $content =~ s#(^\s+|\s+$)##g; # whitespace at the start/end of file - - write_text($file, ";/* $files{$source} */\n" . $content . "\n"); - } - push @minified, $file; - } - - # concat files - my $file = $skins_path . '/' . md5_hex(join(' ', @$sources)) . '.js'; + return @_ unless CONCATENATE_ASSETS; + my ($sources) = @_; + return [] unless $sources; + $sources = ref($sources) ? $sources : [$sources]; + + my %files = map { + (my $file = $_) =~ s/(^[^\?]+)\?.+/$1/; + $_ => $file; + } @$sources; + + my $cgi_path = bz_locations()->{cgi_path}; + my $skins_path = bz_locations()->{assetsdir}; + + # build minified files + my @minified; + foreach my $source (@$sources) { + next unless -e "$cgi_path/$files{$source}"; + my $file = $skins_path . '/' . md5_hex($source) . '.js'; if (!-e $file) { - my $content = ''; - foreach my $source (@minified) { - $content .= read_text($source); - } - write_text($file, $content); + my $content = read_text("$cgi_path/$files{$source}"); + + # minimal minification + $content =~ s#/\*.*?\*/##sg; # block comments + $content =~ s#(^ +| +$)##gm; # leading/trailing spaces + $content =~ s#^//.+$##gm; # single line comments + $content =~ s#\n{2,}#\n#g; # blank lines + $content =~ s#(^\s+|\s+$)##g; # whitespace at the start/end of file + + write_text($file, ";/* $files{$source} */\n" . $content . "\n"); + } + push @minified, $file; + } + + # concat files + my $file = $skins_path . '/' . md5_hex(join(' ', @$sources)) . '.js'; + if (!-e $file) { + my $content = ''; + foreach my $source (@minified) { + $content .= read_text($source); } + write_text($file, $content); + } - $file =~ s/^\Q$cgi_path\E\///o; - return [ $file ]; + $file =~ s/^\Q$cgi_path\E\///o; + return [$file]; } # YUI dependency resolution sub yui_resolve_deps { - my ($yui, $yui_deps) = @_; - - my @yui_resolved; - foreach my $yui_name (@$yui) { - my $deps = $yui_deps->{$yui_name} || []; - foreach my $dep (reverse @$deps) { - push(@yui_resolved, $dep) if !grep { $_ eq $dep } @yui_resolved; - } - push(@yui_resolved, $yui_name) if !grep { $_ eq $yui_name } @yui_resolved; + my ($yui, $yui_deps) = @_; + + my @yui_resolved; + foreach my $yui_name (@$yui) { + my $deps = $yui_deps->{$yui_name} || []; + foreach my $dep (reverse @$deps) { + push(@yui_resolved, $dep) if !grep { $_ eq $dep } @yui_resolved; } - return \@yui_resolved; + push(@yui_resolved, $yui_name) if !grep { $_ eq $yui_name } @yui_resolved; + } + return \@yui_resolved; } ############################################################################### @@ -630,73 +651,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. @@ -704,14 +727,15 @@ $Template::Stash::SCALAR_OPS->{ truncate } = ############################################################################### sub process { - my $self = shift; - # All of this current_langs stuff allows template_inner to correctly - # determine what-language Template object it should instantiate. - my $current_langs = Bugzilla->request_cache->{template_current_lang} ||= []; - unshift(@$current_langs, $self->context->{bz_language}); - my $retval = $self->SUPER::process(@_); - shift @$current_langs; - return $retval; + my $self = shift; + + # All of this current_langs stuff allows template_inner to correctly + # determine what-language Template object it should instantiate. + my $current_langs = Bugzilla->request_cache->{template_current_lang} ||= []; + unshift(@$current_langs, $self->context->{bz_language}); + my $retval = $self->SUPER::process(@_); + shift @$current_langs; + return $retval; } # Construct the Template object @@ -720,604 +744,625 @@ sub process { # since we won't have a template to use... sub create { - my $class = shift; - my %opts = @_; - - # IMPORTANT - If you make any FILTER changes here, make sure to - # make them in t/004.template.t also, if required. - - my $config = { - # Colon-separated list of directories containing templates. - INCLUDE_PATH => $opts{'include_path'} - || _include_path($opts{'language'}), - - # Remove white-space before template directives (PRE_CHOMP) and at the - # beginning and end of templates and template blocks (TRIM) for better - # looking, more compact content. Use the plus sign at the beginning - # of directives to maintain white space (i.e. [%+ DIRECTIVE %]). - PRE_CHOMP => 1, - TRIM => 1, - - # Bugzilla::Template::Plugin::Hook uses the absolute (in mod_perl) - # or relative (in mod_cgi) paths of hook files to explicitly compile - # a specific file. Also, these paths may be absolute at any time - # if a packager has modified bz_locations() to contain absolute - # paths. - ABSOLUTE => 1, - RELATIVE => $ENV{MOD_PERL} ? 0 : 1, - - COMPILE_DIR => bz_locations()->{'template_cache'}, - - # Don't check for a template update until 1 hour has passed since the - # last check. - STAT_TTL => 60 * 60, - - # Initialize templates (f.e. by loading plugins like Hook). - PRE_PROCESS => ["global/variables.none.tmpl"], - - ENCODING => Bugzilla->params->{'utf8'} ? 'UTF-8' : undef, - - # Functions for processing text within templates in various ways. - # IMPORTANT! When adding a filter here that does not override a - # built-in filter, please also add a stub filter to t/004template.t. - FILTERS => { - - # Render text in required style. - - inactive => [ - sub { - my($context, $isinactive) = @_; - return sub { - return $isinactive ? ''.$_[0].'' : $_[0]; - } - }, 1 - ], - - closed => [ - sub { - my($context, $isclosed) = @_; - return sub { - return $isclosed ? ''.$_[0].'' : $_[0]; - } - }, 1 - ], - - obsolete => [ - sub { - my($context, $isobsolete) = @_; - return sub { - return $isobsolete ? ''.$_[0].'' : $_[0]; - } - }, 1 - ], - - # Returns the text with backslashes, single/double quotes, - # and newlines/carriage returns escaped for use in JS strings. - js => sub { - my ($var) = @_; - $var =~ s/([\\\'\"\/])/\\$1/g; - $var =~ s/\n/\\n/g; - $var =~ s/\r/\\r/g; - $var =~ s/\x{2028}/\\u2028/g; # unicode line separator - $var =~ s/\x{2029}/\\u2029/g; # unicode paragraph separator - $var =~ s/\@/\\x40/g; # anti-spam for email addresses - $var =~ s/\\x3c/g; - $var =~ s/>/\\x3e/g; - return $var; - }, - - # Converts data to base64 - base64 => sub { - my ($data) = @_; - return encode_base64($data); - }, - - # Strips out control characters excepting whitespace - strip_control_chars => sub { - my ($data) = @_; - state $use_utf8 = Bugzilla->params->{'utf8'}; - # Only run for utf8 to avoid issues with other multibyte encodings - # that may be reassigning meaning to ascii characters. - if ($use_utf8) { - $data =~ s/(?![\t\r\n])[[:cntrl:]]//g; - } - return $data; - }, - - # HTML collapses newlines in element attributes to a single space, - # so form elements which may have whitespace (ie comments) need - # to be encoded using - # See bugs 4928, 22983 and 32000 for more details - html_linebreak => sub { - my ($var) = @_; - $var = html_quote($var); - $var =~ s/\r\n/\ /g; - $var =~ s/\n\r/\ /g; - $var =~ s/\r/\ /g; - $var =~ s/\n/\ /g; - return $var; - }, - - xml => \&Bugzilla::Util::xml_quote , - - # This filter is similar to url_quote but used a \ instead of a % - # as prefix. In addition it replaces a ' ' by a '_'. - css_class_quote => \&Bugzilla::Util::css_class_quote , - - # Removes control characters and trims extra whitespace. - clean_text => \&Bugzilla::Util::clean_text , - - quoteUrls => [ sub { - my ($context, $bug, $comment, $user) = @_; - return sub { - my $text = shift; - return quoteUrls($text, $bug, $comment, $user); - }; - }, - 1 - ], - - bug_link => [ sub { - my ($context, $bug, $options) = @_; - return sub { - my $text = shift; - return get_bug_link($bug, $text, $options); - }; - }, - 1 - ], - - bug_list_link => sub { - my ($buglist, $options) = @_; - return join(", ", map(get_bug_link($_, $_, $options), split(/ *, */, $buglist))); - }, - - # In CSV, quotes are doubled, and any value containing a quote or a - # comma is enclosed in quotes. - # If a field starts with either "=", "+", "-" or "@", it is preceded - # by a space to prevent stupid formula execution from Excel & co. - csv => sub - { - my ($var) = @_; - $var = ' ' . $var if $var =~ /^[+=@-]/; - # backslash is not special to CSV, but it can be used to confuse some browsers... - # so we do not allow it to happen. We only do this for logged-in users. - $var =~ s/\\/\x{FF3C}/g if Bugzilla->user->id; - $var =~ s/\"/\"\"/g; - if ($var !~ /^-?(\d+\.)?\d*$/) { - $var = "\"$var\""; - } - return $var; - } , - - # Format a filesize in bytes to a human readable value - unitconvert => sub - { - my ($data) = @_; - my $retval = ""; - my %units = ( - 'KB' => 1024, - 'MB' => 1024 * 1024, - 'GB' => 1024 * 1024 * 1024, - ); - - if ($data < 1024) { - return "$data bytes"; - } - else { - my $u; - foreach $u ('GB', 'MB', 'KB') { - if ($data >= $units{$u}) { - return sprintf("%.2f %s", $data/$units{$u}, $u); - } - } - } - }, - - # Format a time for display (more info in Bugzilla::Util) - time => [ sub { - my ($context, $format, $timezone) = @_; - return sub { - my $time = shift; - return format_time($time, $format, $timezone); - }; - }, - 1 - ], - - html => \&Bugzilla::Util::html_quote, - - html_light => \&Bugzilla::Util::html_light_quote, - - email => \&Bugzilla::Util::email_filter, - - mtime => \&mtime_filter, - - # iCalendar contentline filter - ics => [ sub { - my ($context, @args) = @_; - return sub { - my ($var) = shift; - my ($par) = shift @args; - my ($output) = ""; - - $var =~ s/[\r\n]/ /g; - $var =~ s/([;\\\",])/\\$1/g; - - if ($par) { - $output = sprintf("%s:%s", $par, $var); - } else { - $output = $var; - } - - $output =~ s/(.{75,75})/$1\n /g; - - return $output; - }; - }, - 1 - ], - - # Note that using this filter is even more dangerous than - # using "none," and you should only use it when you're SURE - # the output won't be displayed directly to a web browser. - txt => sub { - my ($var) = @_; - # Trivial HTML tag remover - $var =~ s/<[^>]*>//g; - # And this basically reverses the html filter. - $var =~ s/\@/@/g; - $var =~ s/\<//g; - $var =~ s/\"/\"/g; - $var =~ s/\&/\&/g; - # Now remove extra whitespace... - my $collapse_filter = $Template::Filters::FILTERS->{collapse}; - $var = $collapse_filter->($var); - # And if we're not in the WebService, wrap the message. - # (Wrapping the message in the WebService is unnecessary - # and causes awkward things like \n's appearing in error - # messages in JSON-RPC.) - unless (i_am_webservice()) { - $var = wrap_comment($var, 72); - } - $var =~ s/\ / /g; - - return $var; - }, - - # Wrap a displayed comment to the appropriate length - wrap_comment => [ - sub { - my ($context, $cols) = @_; - return sub { wrap_comment($_[0], $cols) } - }, 1], - - # We force filtering of every variable in key security-critical - # places; we have a none filter for people to use when they - # really, really don't want a variable to be changed. - none => sub { return $_[0]; } , + my $class = shift; + my %opts = @_; + + # IMPORTANT - If you make any FILTER changes here, make sure to + # make them in t/004.template.t also, if required. + + my $config = { + + # Colon-separated list of directories containing templates. + INCLUDE_PATH => $opts{'include_path'} || _include_path($opts{'language'}), + + # Remove white-space before template directives (PRE_CHOMP) and at the + # beginning and end of templates and template blocks (TRIM) for better + # looking, more compact content. Use the plus sign at the beginning + # of directives to maintain white space (i.e. [%+ DIRECTIVE %]). + PRE_CHOMP => 1, + TRIM => 1, + + # Bugzilla::Template::Plugin::Hook uses the absolute (in mod_perl) + # or relative (in mod_cgi) paths of hook files to explicitly compile + # a specific file. Also, these paths may be absolute at any time + # if a packager has modified bz_locations() to contain absolute + # paths. + ABSOLUTE => 1, + RELATIVE => $ENV{MOD_PERL} ? 0 : 1, + + COMPILE_DIR => bz_locations()->{'template_cache'}, + + # Don't check for a template update until 1 hour has passed since the + # last check. + STAT_TTL => 60 * 60, + + # Initialize templates (f.e. by loading plugins like Hook). + PRE_PROCESS => ["global/variables.none.tmpl"], + + ENCODING => Bugzilla->params->{'utf8'} ? 'UTF-8' : undef, + + # Functions for processing text within templates in various ways. + # IMPORTANT! When adding a filter here that does not override a + # built-in filter, please also add a stub filter to t/004template.t. + FILTERS => { + + # Render text in required style. + + inactive => [ + sub { + my ($context, $isinactive) = @_; + return sub { + return $isinactive ? '' . $_[0] . '' : $_[0]; + } + }, + 1 + ], + + closed => [ + sub { + my ($context, $isclosed) = @_; + return sub { + return $isclosed ? '' . $_[0] . '' : $_[0]; + } + }, + 1 + ], + + obsolete => [ + sub { + my ($context, $isobsolete) = @_; + return sub { + return $isobsolete ? '' . $_[0] . '' : $_[0]; + } + }, + 1 + ], + + # Returns the text with backslashes, single/double quotes, + # and newlines/carriage returns escaped for use in JS strings. + js => sub { + my ($var) = @_; + $var =~ s/([\\\'\"\/])/\\$1/g; + $var =~ s/\n/\\n/g; + $var =~ s/\r/\\r/g; + $var =~ s/\x{2028}/\\u2028/g; # unicode line separator + $var =~ s/\x{2029}/\\u2029/g; # unicode paragraph separator + $var =~ s/\@/\\x40/g; # anti-spam for email addresses + $var =~ s/\\x3c/g; + $var =~ s/>/\\x3e/g; + return $var; + }, + + # Converts data to base64 + base64 => sub { + my ($data) = @_; + return encode_base64($data); + }, + + # Strips out control characters excepting whitespace + strip_control_chars => sub { + my ($data) = @_; + state $use_utf8 = Bugzilla->params->{'utf8'}; + + # Only run for utf8 to avoid issues with other multibyte encodings + # that may be reassigning meaning to ascii characters. + if ($use_utf8) { + $data =~ s/(?![\t\r\n])[[:cntrl:]]//g; + } + return $data; + }, + + # HTML collapses newlines in element attributes to a single space, + # so form elements which may have whitespace (ie comments) need + # to be encoded using + # See bugs 4928, 22983 and 32000 for more details + html_linebreak => sub { + my ($var) = @_; + $var = html_quote($var); + $var =~ s/\r\n/\ /g; + $var =~ s/\n\r/\ /g; + $var =~ s/\r/\ /g; + $var =~ s/\n/\ /g; + return $var; + }, + + xml => \&Bugzilla::Util::xml_quote, + + # This filter is similar to url_quote but used a \ instead of a % + # as prefix. In addition it replaces a ' ' by a '_'. + css_class_quote => \&Bugzilla::Util::css_class_quote, + + # Removes control characters and trims extra whitespace. + clean_text => \&Bugzilla::Util::clean_text, + + quoteUrls => [ + sub { + my ($context, $bug, $comment, $user) = @_; + return sub { + my $text = shift; + return quoteUrls($text, $bug, $comment, $user); + }; + }, + 1 + ], + + bug_link => [ + sub { + my ($context, $bug, $options) = @_; + return sub { + my $text = shift; + return get_bug_link($bug, $text, $options); + }; + }, + 1 + ], + + bug_list_link => sub { + my ($buglist, $options) = @_; + return + join(", ", map(get_bug_link($_, $_, $options), split(/ *, */, $buglist))); + }, + + # In CSV, quotes are doubled, and any value containing a quote or a + # comma is enclosed in quotes. + # If a field starts with either "=", "+", "-" or "@", it is preceded + # by a space to prevent stupid formula execution from Excel & co. + csv => sub { + my ($var) = @_; + $var = ' ' . $var if $var =~ /^[+=@-]/; + + # backslash is not special to CSV, but it can be used to confuse some browsers... + # so we do not allow it to happen. We only do this for logged-in users. + $var =~ s/\\/\x{FF3C}/g if Bugzilla->user->id; + $var =~ s/\"/\"\"/g; + if ($var !~ /^-?(\d+\.)?\d*$/) { + $var = "\"$var\""; + } + return $var; + }, + + # Format a filesize in bytes to a human readable value + unitconvert => sub { + my ($data) = @_; + my $retval = ""; + my %units = ('KB' => 1024, 'MB' => 1024 * 1024, 'GB' => 1024 * 1024 * 1024,); + + if ($data < 1024) { + return "$data bytes"; + } + else { + my $u; + foreach $u ('GB', 'MB', 'KB') { + if ($data >= $units{$u}) { + return sprintf("%.2f %s", $data / $units{$u}, $u); + } + } + } + }, + + # Format a time for display (more info in Bugzilla::Util) + time => [ + sub { + my ($context, $format, $timezone) = @_; + return sub { + my $time = shift; + return format_time($time, $format, $timezone); + }; + }, + 1 + ], + + html => \&Bugzilla::Util::html_quote, + + html_light => \&Bugzilla::Util::html_light_quote, + + email => \&Bugzilla::Util::email_filter, + + mtime => \&mtime_filter, + + # iCalendar contentline filter + ics => [ + sub { + my ($context, @args) = @_; + return sub { + my ($var) = shift; + my ($par) = shift @args; + my ($output) = ""; + + $var =~ s/[\r\n]/ /g; + $var =~ s/([;\\\",])/\\$1/g; + + if ($par) { + $output = sprintf("%s:%s", $par, $var); + } + else { + $output = $var; + } + + $output =~ s/(.{75,75})/$1\n /g; + + return $output; + }; }, + 1 + ], + + # Note that using this filter is even more dangerous than + # using "none," and you should only use it when you're SURE + # the output won't be displayed directly to a web browser. + txt => sub { + my ($var) = @_; + + # Trivial HTML tag remover + $var =~ s/<[^>]*>//g; + + # And this basically reverses the html filter. + $var =~ s/\@/@/g; + $var =~ s/\<//g; + $var =~ s/\"/\"/g; + $var =~ s/\&/\&/g; + + # Now remove extra whitespace... + my $collapse_filter = $Template::Filters::FILTERS->{collapse}; + $var = $collapse_filter->($var); + + # And if we're not in the WebService, wrap the message. + # (Wrapping the message in the WebService is unnecessary + # and causes awkward things like \n's appearing in error + # messages in JSON-RPC.) + unless (i_am_webservice()) { + $var = wrap_comment($var, 72); + } + $var =~ s/\ / /g; - PLUGIN_BASE => 'Bugzilla::Template::Plugin', - - CONSTANTS => _load_constants(), - - # Default variables for all templates - VARIABLES => { - # Function for retrieving global parameters. - 'Param' => sub { return Bugzilla->params->{$_[0]}; }, - - # Function to create date strings - 'time2str' => \&Date::Format::time2str, - - # Fixed size column formatting for bugmail. - 'format_columns' => sub { - my $cols = shift; - my $format = ($cols == 3) ? FORMAT_TRIPLE : FORMAT_DOUBLE; - my $col_size = ($cols == 3) ? FORMAT_3_SIZE : FORMAT_2_SIZE; - return multiline_sprintf($format, \@_, $col_size); - }, - - # Generic linear search function - 'lsearch' => sub { - my ($array, $item) = @_; - return firstidx { $_ eq $item } @$array; - }, - - # Currently logged in user, if any - # If an sudo session is in progress, this is the user we're faking - 'user' => sub { return Bugzilla->user; }, - - # Currenly active language - 'current_language' => sub { return Bugzilla->current_language; }, - - # If an sudo session is in progress, this is the user who - # started the session. - 'sudoer' => sub { return Bugzilla->sudoer; }, - - # Allow templates to access the "correct" URLBase value - 'urlbase' => sub { return Bugzilla::Util::correct_urlbase(); }, - 'httpbase' => sub { return Bugzilla->params->{'urlbase'}; }, - 'sslbase' => sub { return Bugzilla->params->{'sslbase'}; }, - 'ssl_redirect' => sub { return Bugzilla->params->{'ssl_redirect'}; }, - - # Allow templates to access docs url with users' preferred language - # We fall back to English if documentation in the preferred - # language is not available - 'docs_urlbase' => sub { - my $docs_urlbase; - my $lang = Bugzilla->current_language; - # Translations currently available on readthedocs.org - my @rtd_translations = ('en', 'fr'); - - if ($lang ne 'en' && -f "docs/$lang/html/index.html") { - $docs_urlbase = "docs/$lang/html/"; - } - elsif (-f "docs/en/html/index.html") { - $docs_urlbase = "docs/en/html/"; - } - else { - if (!grep { $_ eq $lang } @rtd_translations) { - $lang = "en"; - } - - my $version = BUGZILLA_VERSION; - $version =~ /^(\d+)\.(\d+)/; - if ($2 % 2 == 1) { - # second number is odd; development version - $version = 'latest'; - } - else { - $version = "$1.$2"; - } - - $docs_urlbase = "https://bugzilla.readthedocs.org/$lang/$version/"; - } - - return $docs_urlbase; - }, - - # Check whether the URL is safe. - 'is_safe_url' => sub { - my $url = shift; - return 0 unless $url; - - my $safe_url_regexp = SAFE_URL_REGEXP(); - return 1 if $url =~ /^$safe_url_regexp$/; - # Pointing to a local file with no colon in its name is fine. - return 1 if $url =~ /^[^\s<>\":]+[\w\/]$/i; - # If we come here, then we cannot guarantee it's safe. - return 0; - }, - - # Allow templates to generate a token themselves. - 'issue_hash_token' => \&Bugzilla::Token::issue_hash_token, - - 'get_login_request_token' => sub { - my $cookie = Bugzilla->cgi->cookie('Bugzilla_login_request_cookie'); - return $cookie ? issue_hash_token(['login_request', $cookie]) : ''; - }, - - 'get_api_token' => sub { - return '' unless Bugzilla->user->id; - my $cache = Bugzilla->request_cache; - return $cache->{api_token} //= issue_api_token(); - }, - - # A way for all templates to get at Field data, cached. - 'bug_fields' => sub { - my $cache = Bugzilla->request_cache; - $cache->{template_bug_fields} ||= - Bugzilla->fields({ by_name => 1 }); - return $cache->{template_bug_fields}; - }, - - # A general purpose cache to store rendered templates for reuse. - # Make sure to not mix language-specific data. - 'template_cache' => sub { - my $cache = Bugzilla->request_cache->{template_cache} ||= {}; - $cache->{users} ||= {}; - return $cache; - }, - - 'css_files' => \&css_files, - yui_resolve_deps => \&yui_resolve_deps, - concatenate_js => \&_concatenate_js, - - # All classifications (sorted by sortkey, name) - 'all_classifications' => sub { - return [map { $_->name } Bugzilla::Classification->get_all()]; - }, - - # Whether or not keywords are enabled, in this Bugzilla. - 'use_keywords' => sub { return Bugzilla::Keyword->any_exist; }, - - # All the keywords. - 'all_keywords' => sub { - return [map { $_->name } Bugzilla::Keyword->get_all()]; - }, - - 'feature_enabled' => sub { return Bugzilla->feature(@_); }, - - # field_descs can be somewhat slow to generate, so we generate - # it only once per-language no matter how many times - # $template->process() is called. - 'field_descs' => sub { return template_var('field_descs') }, - - # Calling bug/field-help.none.tmpl once per label is very - # expensive, so we generate it once per-language. - 'help_html' => sub { return template_var('help_html') }, - - # This way we don't have to load field-descs.none.tmpl in - # many templates. - 'display_value' => \&Bugzilla::Util::display_value, - - 'install_string' => \&Bugzilla::Install::Util::install_string, - - 'report_columns' => \&Bugzilla::Search::REPORT_COLUMNS, - - # These don't work as normal constants. - DB_MODULE => \&Bugzilla::Constants::DB_MODULE, - REQUIRED_MODULES => - \&Bugzilla::Install::Requirements::REQUIRED_MODULES, - OPTIONAL_MODULES => sub { - my @optional = @{OPTIONAL_MODULES()}; - foreach my $item (@optional) { - my @features; - foreach my $feat_id (@{ $item->{feature} }) { - push(@features, install_string("feature_$feat_id")); - } - $item->{feature} = \@features; - } - return \@optional; - }, - 'default_authorizer' => sub { return Bugzilla::Auth->new() }, - - 'login_not_email' => sub { - my $params = Bugzilla->params; - my $cache = Bugzilla->request_cache; - - return $cache->{login_not_email} //= - ($params->{emailsuffix} - || ($params->{user_verify_class} =~ /LDAP/ && $params->{LDAPmailattribute}) - || ($params->{user_verify_class} =~ /RADIUS/ && $params->{RADIUS_email_suffix})) - ? 1 : 0; - }, + 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} ]; - - local $Template::Config::CONTEXT = 'Bugzilla::Template::Context'; - - Bugzilla::Hook::process('template_before_create', { config => $config }); - my $template = $class->new($config) - || die("Template creation failed: " . $class->error()); - Bugzilla::Hook::process('template_after_create', { template => $template }); - - # Pass on our current language to any template hooks or inner templates - # called by this Template object. - $template->context->{bz_language} = $opts{language} || ''; - - return $template; + 1 + ], + + # We force filtering of every variable in key security-critical + # places; we have a none filter for people to use when they + # really, really don't want a variable to be changed. + none => sub { return $_[0]; }, + }, + + PLUGIN_BASE => 'Bugzilla::Template::Plugin', + + CONSTANTS => _load_constants(), + + # Default variables for all templates + VARIABLES => { + + # Function for retrieving global parameters. + 'Param' => sub { return Bugzilla->params->{$_[0]}; }, + + # Function to create date strings + 'time2str' => \&Date::Format::time2str, + + # Fixed size column formatting for bugmail. + 'format_columns' => sub { + my $cols = shift; + my $format = ($cols == 3) ? FORMAT_TRIPLE : FORMAT_DOUBLE; + my $col_size = ($cols == 3) ? FORMAT_3_SIZE : FORMAT_2_SIZE; + return multiline_sprintf($format, \@_, $col_size); + }, + + # Generic linear search function + 'lsearch' => sub { + my ($array, $item) = @_; + return firstidx { $_ eq $item } @$array; + }, + + # Currently logged in user, if any + # If an sudo session is in progress, this is the user we're faking + 'user' => sub { return Bugzilla->user; }, + + # Currenly active language + 'current_language' => sub { return Bugzilla->current_language; }, + + # If an sudo session is in progress, this is the user who + # started the session. + 'sudoer' => sub { return Bugzilla->sudoer; }, + + # Allow templates to access the "correct" URLBase value + 'urlbase' => sub { return Bugzilla::Util::correct_urlbase(); }, + 'httpbase' => sub { return Bugzilla->params->{'urlbase'}; }, + 'sslbase' => sub { return Bugzilla->params->{'sslbase'}; }, + 'ssl_redirect' => sub { return Bugzilla->params->{'ssl_redirect'}; }, + + # Allow templates to access docs url with users' preferred language + # We fall back to English if documentation in the preferred + # language is not available + 'docs_urlbase' => sub { + my $docs_urlbase; + my $lang = Bugzilla->current_language; + + # Translations currently available on readthedocs.org + my @rtd_translations = ('en', 'fr'); + + if ($lang ne 'en' && -f "docs/$lang/html/index.html") { + $docs_urlbase = "docs/$lang/html/"; + } + elsif (-f "docs/en/html/index.html") { + $docs_urlbase = "docs/en/html/"; + } + else { + if (!grep { $_ eq $lang } @rtd_translations) { + $lang = "en"; + } + + my $version = BUGZILLA_VERSION; + $version =~ /^(\d+)\.(\d+)/; + if ($2 % 2 == 1) { + + # second number is odd; development version + $version = 'latest'; + } + else { + $version = "$1.$2"; + } + + $docs_urlbase = "https://bugzilla.readthedocs.org/$lang/$version/"; + } + + return $docs_urlbase; + }, + + # Check whether the URL is safe. + 'is_safe_url' => sub { + my $url = shift; + return 0 unless $url; + + my $safe_url_regexp = SAFE_URL_REGEXP(); + return 1 if $url =~ /^$safe_url_regexp$/; + + # Pointing to a local file with no colon in its name is fine. + return 1 if $url =~ /^[^\s<>\":]+[\w\/]$/i; + + # If we come here, then we cannot guarantee it's safe. + return 0; + }, + + # Allow templates to generate a token themselves. + 'issue_hash_token' => \&Bugzilla::Token::issue_hash_token, + + 'get_login_request_token' => sub { + my $cookie = Bugzilla->cgi->cookie('Bugzilla_login_request_cookie'); + return $cookie ? issue_hash_token(['login_request', $cookie]) : ''; + }, + + 'get_api_token' => sub { + return '' unless Bugzilla->user->id; + my $cache = Bugzilla->request_cache; + return $cache->{api_token} //= issue_api_token(); + }, + + # A way for all templates to get at Field data, cached. + 'bug_fields' => sub { + my $cache = Bugzilla->request_cache; + $cache->{template_bug_fields} ||= Bugzilla->fields({by_name => 1}); + return $cache->{template_bug_fields}; + }, + + # A general purpose cache to store rendered templates for reuse. + # Make sure to not mix language-specific data. + 'template_cache' => sub { + my $cache = Bugzilla->request_cache->{template_cache} ||= {}; + $cache->{users} ||= {}; + return $cache; + }, + + 'css_files' => \&css_files, + yui_resolve_deps => \&yui_resolve_deps, + concatenate_js => \&_concatenate_js, + + # All classifications (sorted by sortkey, name) + 'all_classifications' => sub { + return [map { $_->name } Bugzilla::Classification->get_all()]; + }, + + # Whether or not keywords are enabled, in this Bugzilla. + 'use_keywords' => sub { return Bugzilla::Keyword->any_exist; }, + + # All the keywords. + 'all_keywords' => sub { + return [map { $_->name } Bugzilla::Keyword->get_all()]; + }, + + 'feature_enabled' => sub { return Bugzilla->feature(@_); }, + + # field_descs can be somewhat slow to generate, so we generate + # it only once per-language no matter how many times + # $template->process() is called. + 'field_descs' => sub { return template_var('field_descs') }, + + # Calling bug/field-help.none.tmpl once per label is very + # expensive, so we generate it once per-language. + 'help_html' => sub { return template_var('help_html') }, + + # This way we don't have to load field-descs.none.tmpl in + # many templates. + 'display_value' => \&Bugzilla::Util::display_value, + + 'install_string' => \&Bugzilla::Install::Util::install_string, + + 'report_columns' => \&Bugzilla::Search::REPORT_COLUMNS, + + # These don't work as normal constants. + DB_MODULE => \&Bugzilla::Constants::DB_MODULE, + REQUIRED_MODULES => \&Bugzilla::Install::Requirements::REQUIRED_MODULES, + OPTIONAL_MODULES => sub { + my @optional = @{OPTIONAL_MODULES()}; + foreach my $item (@optional) { + my @features; + foreach my $feat_id (@{$item->{feature}}) { + push(@features, install_string("feature_$feat_id")); + } + $item->{feature} = \@features; + } + return \@optional; + }, + 'default_authorizer' => sub { return Bugzilla::Auth->new() }, + + 'login_not_email' => sub { + my $params = Bugzilla->params; + my $cache = Bugzilla->request_cache; + + return $cache->{login_not_email} + //= ($params->{emailsuffix} + || ($params->{user_verify_class} =~ /LDAP/ && $params->{LDAPmailattribute}) + || ($params->{user_verify_class} =~ /RADIUS/ + && $params->{RADIUS_email_suffix})) ? 1 : 0; + }, + }, + }; + + # Use a per-process provider to cache compiled templates in memory across + # requests. + my $provider_key = join(':', @{$config->{INCLUDE_PATH}}); + my $shared_providers = Bugzilla->process_cache->{shared_providers} ||= {}; + $shared_providers->{$provider_key} ||= Template::Provider->new($config); + $config->{LOAD_TEMPLATES} = [$shared_providers->{$provider_key}]; + + local $Template::Config::CONTEXT = 'Bugzilla::Template::Context'; + + Bugzilla::Hook::process('template_before_create', {config => $config}); + my $template = $class->new($config) + || die("Template creation failed: " . $class->error()); + Bugzilla::Hook::process('template_after_create', {template => $template}); + + # Pass on our current language to any template hooks or inner templates + # called by this Template object. + $template->context->{bz_language} = $opts{language} || ''; + + return $template; } # Used as part of the two subroutines below. our %_templates_to_precompile; + sub precompile_templates { - my ($output) = @_; + my ($output) = @_; - # Remove the compiled templates. - my $cache_dir = bz_locations()->{'template_cache'}; - my $datadir = bz_locations()->{'datadir'}; + # 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 470e6a9ee..01c8c5981 100644 --- a/Bugzilla/Template/Context.pm +++ b/Bugzilla/Template/Context.pm @@ -18,23 +18,24 @@ use Bugzilla::Hook; use Scalar::Util qw(blessed); sub process { - my $self = shift; - # We don't want to run the template_before_process hook for - # template hooks (but we do want it to run if a hook calls - # PROCESS inside itself). The problem is that the {component}->{name} of - # hooks is unreliable--sometimes it starts with ./ and it's the - # full path to the hook template, and sometimes it's just the relative - # name (like hook/global/field-descs-end.none.tmpl). Also, calling - # template_before_process for hook templates doesn't seem too useful, - # because that's already part of the extension and they should be able - # to modify their hook if they want (or just modify the variables in the - # calling template). - if (not delete $self->{bz_in_hook}) { - $self->{bz_in_process} = 1; - } - my $result = $self->SUPER::process(@_); - delete $self->{bz_in_process}; - return $result; + my $self = shift; + + # We don't want to run the template_before_process hook for + # template hooks (but we do want it to run if a hook calls + # PROCESS inside itself). The problem is that the {component}->{name} of + # hooks is unreliable--sometimes it starts with ./ and it's the + # full path to the hook template, and sometimes it's just the relative + # name (like hook/global/field-descs-end.none.tmpl). Also, calling + # template_before_process for hook templates doesn't seem too useful, + # because that's already part of the extension and they should be able + # to modify their hook if they want (or just modify the variables in the + # calling template). + if (not delete $self->{bz_in_hook}) { + $self->{bz_in_process} = 1; + } + my $result = $self->SUPER::process(@_); + delete $self->{bz_in_process}; + return $result; } # This method is called by Template-Toolkit exactly once per template or @@ -46,58 +47,59 @@ sub process { # in the PROCESS or INCLUDE directive haven't been set, and if we're # in an INCLUDE, the stash is not yet localized during process(). sub stash { - my $self = shift; - my $stash = $self->SUPER::stash(@_); - - my $name = $stash->{component}->{name}; - my $pre_process = $self->config->{PRE_PROCESS}; - - # Checking bz_in_process tells us that we were indeed called as part of a - # Context::process, and not at some other point. - # - # Checking $name makes sure that we're processing a file, and not just a - # block, by checking that the name has a period in it. We don't allow - # blocks because their names are too unreliable--an extension could have - # a block with the same name, or multiple files could have a same-named - # block, and then your extension would malfunction. - # - # We also make sure that we don't run, ever, during the PRE_PROCESS - # templates, because if somebody calls Throw*Error globally inside of - # template_before_process, that causes an infinite recursion into - # the PRE_PROCESS templates (because Bugzilla, while inside - # global/intialize.none.tmpl, loads the template again to create the - # template object for Throw*Error). - # - # Checking Bugzilla::Hook::in prevents infinite recursion on this hook. - if ($self->{bz_in_process} and $name =~ /\./ - and !grep($_ eq $name, @$pre_process) - and !Bugzilla::Hook::in('template_before_process')) - { - Bugzilla::Hook::process("template_before_process", - { vars => $stash, context => $self, - file => $name }); - } - - # This prevents other calls to stash() that might somehow happen - # later in the file from also triggering the hook. - delete $self->{bz_in_process}; - - return $stash; + my $self = shift; + my $stash = $self->SUPER::stash(@_); + + my $name = $stash->{component}->{name}; + my $pre_process = $self->config->{PRE_PROCESS}; + + # Checking bz_in_process tells us that we were indeed called as part of a + # Context::process, and not at some other point. + # + # Checking $name makes sure that we're processing a file, and not just a + # block, by checking that the name has a period in it. We don't allow + # blocks because their names are too unreliable--an extension could have + # a block with the same name, or multiple files could have a same-named + # block, and then your extension would malfunction. + # + # We also make sure that we don't run, ever, during the PRE_PROCESS + # templates, because if somebody calls Throw*Error globally inside of + # template_before_process, that causes an infinite recursion into + # the PRE_PROCESS templates (because Bugzilla, while inside + # global/intialize.none.tmpl, loads the template again to create the + # template object for Throw*Error). + # + # Checking Bugzilla::Hook::in prevents infinite recursion on this hook. + if ( $self->{bz_in_process} + and $name =~ /\./ + and !grep($_ eq $name, @$pre_process) + and !Bugzilla::Hook::in('template_before_process')) + { + Bugzilla::Hook::process("template_before_process", + {vars => $stash, context => $self, file => $name}); + } + + # This prevents other calls to stash() that might somehow happen + # later in the file from also triggering the hook. + delete $self->{bz_in_process}; + + return $stash; } sub filter { - my ($self, $name, $args) = @_; - # If we pass an alias for the filter name, the filter code is cached - # instead of looking for it at each call. - # If the filter has arguments, then we can't cache it. - $self->SUPER::filter($name, $args, $args ? undef : $name); + my ($self, $name, $args) = @_; + + # If we pass an alias for the filter name, the filter code is cached + # instead of looking for it at each call. + # If the filter has arguments, then we can't cache it. + $self->SUPER::filter($name, $args, $args ? undef : $name); } # We need a DESTROY sub for the same reason that Bugzilla::CGI does. sub DESTROY { - my $self = shift; - $self->SUPER::DESTROY(@_); -}; + my $self = shift; + $self->SUPER::DESTROY(@_); +} 1; diff --git a/Bugzilla/Template/Plugin/Bugzilla.pm b/Bugzilla/Template/Plugin/Bugzilla.pm index 806dd903b..0734fb942 100644 --- a/Bugzilla/Template/Plugin/Bugzilla.pm +++ b/Bugzilla/Template/Plugin/Bugzilla.pm @@ -16,20 +16,20 @@ use parent qw(Template::Plugin); use Bugzilla; sub new { - my ($class, $context) = @_; + my ($class, $context) = @_; - return bless {}, $class; + return bless {}, $class; } sub AUTOLOAD { - my $class = shift; - our $AUTOLOAD; + my $class = shift; + our $AUTOLOAD; - $AUTOLOAD =~ s/^.*:://; + $AUTOLOAD =~ s/^.*:://; - return if $AUTOLOAD eq 'DESTROY'; + return if $AUTOLOAD eq 'DESTROY'; - return Bugzilla->$AUTOLOAD(@_); + return Bugzilla->$AUTOLOAD(@_); } 1; diff --git a/Bugzilla/Template/Plugin/Hook.pm b/Bugzilla/Template/Plugin/Hook.pm index 669c77614..c57db4223 100644 --- a/Bugzilla/Template/Plugin/Hook.pm +++ b/Bugzilla/Template/Plugin/Hook.pm @@ -14,81 +14,80 @@ use warnings; use parent qw(Template::Plugin); use Bugzilla::Constants; -use Bugzilla::Install::Util qw(template_include_path); +use Bugzilla::Install::Util qw(template_include_path); use Bugzilla::Util; use Bugzilla::Error; use File::Spec; sub new { - my ($class, $context) = @_; - return bless { _CONTEXT => $context }, $class; + my ($class, $context) = @_; + return bless {_CONTEXT => $context}, $class; } sub _context { return $_[0]->{_CONTEXT} } sub process { - my ($self, $hook_name, $template) = @_; - my $context = $self->_context(); - $template ||= $context->stash->{component}->{name}; - - # sanity check: - if (!$template =~ /[\w\.\/\-_\\]+/) { - ThrowCodeError('template_invalid', { name => $template }); - } - - my (undef, $path, $filename) = File::Spec->splitpath($template); - $path ||= ''; - $filename =~ m/(.+)\.(.+)\.tmpl$/; - my $template_name = $1; - my $type = $2; - - # Hooks are named like this: - my $extension_template = "$path$template_name-$hook_name.$type.tmpl"; - - # Get the hooks out of the cache if they exist. Otherwise, read them - # from the disk. - my $cache = Bugzilla->request_cache->{template_plugin_hook_cache} ||= {}; - my $lang = $context->{bz_language} || ''; - $cache->{"${lang}__$extension_template"} - ||= $self->_get_hooks($extension_template); - - # process() accepts an arrayref of templates, so we just pass the whole - # arrayref. - $context->{bz_in_hook} = 1; # See Bugzilla::Template::Context - return $context->process($cache->{"${lang}__$extension_template"}); + my ($self, $hook_name, $template) = @_; + my $context = $self->_context(); + $template ||= $context->stash->{component}->{name}; + + # sanity check: + if (!$template =~ /[\w\.\/\-_\\]+/) { + ThrowCodeError('template_invalid', {name => $template}); + } + + my (undef, $path, $filename) = File::Spec->splitpath($template); + $path ||= ''; + $filename =~ m/(.+)\.(.+)\.tmpl$/; + my $template_name = $1; + my $type = $2; + + # Hooks are named like this: + my $extension_template = "$path$template_name-$hook_name.$type.tmpl"; + + # Get the hooks out of the cache if they exist. Otherwise, read them + # from the disk. + my $cache = Bugzilla->request_cache->{template_plugin_hook_cache} ||= {}; + my $lang = $context->{bz_language} || ''; + $cache->{"${lang}__$extension_template"} + ||= $self->_get_hooks($extension_template); + + # process() accepts an arrayref of templates, so we just pass the whole + # arrayref. + $context->{bz_in_hook} = 1; # See Bugzilla::Template::Context + return $context->process($cache->{"${lang}__$extension_template"}); } sub _get_hooks { - my ($self, $extension_template) = @_; - - my $template_sets = $self->_template_hook_include_path(); - my @hooks; - foreach my $dir_set (@$template_sets) { - foreach my $template_dir (@$dir_set) { - my $file = "$template_dir/hook/$extension_template"; - if (-e $file) { - my $template = $self->_context->template($file); - push(@hooks, $template); - # Don't run the hook for more than one language. - last; - } - } + my ($self, $extension_template) = @_; + + my $template_sets = $self->_template_hook_include_path(); + my @hooks; + foreach my $dir_set (@$template_sets) { + foreach my $template_dir (@$dir_set) { + my $file = "$template_dir/hook/$extension_template"; + if (-e $file) { + my $template = $self->_context->template($file); + push(@hooks, $template); + + # Don't run the hook for more than one language. + last; + } } + } - return \@hooks; + return \@hooks; } sub _template_hook_include_path { - my $self = shift; - my $cache = Bugzilla->request_cache; - my $language = $self->_context->{bz_language} || ''; - my $cache_key = "template_plugin_hook_include_path_$language"; - $cache->{$cache_key} ||= template_include_path({ - language => $language, - hook => 1, - }); - return $cache->{$cache_key}; + my $self = shift; + my $cache = Bugzilla->request_cache; + my $language = $self->_context->{bz_language} || ''; + my $cache_key = "template_plugin_hook_include_path_$language"; + $cache->{$cache_key} + ||= template_include_path({language => $language, hook => 1,}); + return $cache->{$cache_key}; } 1; diff --git a/Bugzilla/Token.pm b/Bugzilla/Token.pm index 28122e818..62106c5e5 100644 --- a/Bugzilla/Token.pm +++ b/Bugzilla/Token.pm @@ -25,8 +25,8 @@ use Digest::SHA qw(hmac_sha256_base64); use parent qw(Exporter); @Bugzilla::Token::EXPORT = qw(issue_api_token issue_session_token - check_token_data delete_token - issue_hash_token check_hash_token); + check_token_data delete_token + issue_hash_token check_hash_token); use constant SEND_NOW => 1; @@ -36,394 +36,420 @@ use constant SEND_NOW => 1; # Create a token used for internal API authentication sub issue_api_token { - # Generates a random token, adds it to the tokens table if one does not - # already exist, and returns the token to the caller. - my $dbh = Bugzilla->dbh; - my $user = Bugzilla->user; - my ($token) = $dbh->selectrow_array(" + + # Generates a random token, adds it to the tokens table if one does not + # already exist, and returns the token to the caller. + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->user; + my ($token) = $dbh->selectrow_array(" SELECT token FROM tokens WHERE userid = ? AND tokentype = 'api_token' - AND (" . $dbh->sql_date_math('issuedate', '+', (MAX_TOKEN_AGE * 24 - 12), 'HOUR') . ") > NOW()", - undef, $user->id); - return $token // _create_token($user->id, 'api_token', ''); + AND (" + . $dbh->sql_date_math('issuedate', '+', (MAX_TOKEN_AGE * 24 - 12), 'HOUR') + . ") > NOW()", undef, $user->id); + return $token // _create_token($user->id, 'api_token', ''); } # Creates and sends a token to create a new user account. # It assumes that the login has the correct format and is not already in use. sub issue_new_user_account_token { - my $login_name = shift; - my $dbh = Bugzilla->dbh; - my $template = Bugzilla->template; - my $vars = {}; - - # Is there already a pending request for this login name? If yes, do not throw - # an error because the user may have lost their email with the token inside. - # But to prevent using this way to mailbomb an email address, make sure - # the last request is old enough before sending a new email (default: 10 minutes). - - my $pending_requests = $dbh->selectrow_array( - 'SELECT COUNT(*) + my $login_name = shift; + my $dbh = Bugzilla->dbh; + my $template = Bugzilla->template; + my $vars = {}; + +# Is there already a pending request for this login name? If yes, do not throw +# an error because the user may have lost their email with the token inside. +# But to prevent using this way to mailbomb an email address, make sure +# the last request is old enough before sending a new email (default: 10 minutes). + + my $pending_requests = $dbh->selectrow_array( + 'SELECT COUNT(*) FROM tokens WHERE tokentype = ? AND ' . $dbh->sql_istrcmp('eventdata', '?') . ' AND issuedate > ' - . $dbh->sql_date_math('NOW()', '-', ACCOUNT_CHANGE_INTERVAL, 'MINUTE'), - undef, ('account', $login_name)); + . $dbh->sql_date_math('NOW()', '-', ACCOUNT_CHANGE_INTERVAL, 'MINUTE'), + undef, ('account', $login_name) + ); - ThrowUserError('too_soon_for_new_token', {'type' => 'account'}) if $pending_requests; + ThrowUserError('too_soon_for_new_token', {'type' => 'account'}) + if $pending_requests; - my ($token, $token_ts) = _create_token(undef, 'account', $login_name); + my ($token, $token_ts) = _create_token(undef, 'account', $login_name); - $vars->{'email'} = $login_name . Bugzilla->params->{'emailsuffix'}; - $vars->{'expiration_ts'} = ctime($token_ts + MAX_TOKEN_AGE * 86400); - $vars->{'token'} = $token; + $vars->{'email'} = $login_name . Bugzilla->params->{'emailsuffix'}; + $vars->{'expiration_ts'} = ctime($token_ts + MAX_TOKEN_AGE * 86400); + $vars->{'token'} = $token; - my $message; - $template->process('account/email/request-new.txt.tmpl', $vars, \$message) - || ThrowTemplateError($template->error()); + my $message; + $template->process('account/email/request-new.txt.tmpl', $vars, \$message) + || ThrowTemplateError($template->error()); - # In 99% of cases, the user getting the confirmation email is the same one - # who made the request, and so it is reasonable to send the email in the same - # language used to view the "Create a New Account" page (we cannot use their - # user prefs as the user has no account yet!). - MessageToMTA($message, SEND_NOW); + # In 99% of cases, the user getting the confirmation email is the same one + # who made the request, and so it is reasonable to send the email in the same + # language used to view the "Create a New Account" page (we cannot use their + # user prefs as the user has no account yet!). + MessageToMTA($message, SEND_NOW); } sub IssueEmailChangeToken { - my $new_email = shift; - my $user = Bugzilla->user; + my $new_email = shift; + my $user = Bugzilla->user; - my ($token, $token_ts) = _create_token($user->id, 'emailold', $user->login . ":$new_email"); - my $newtoken = _create_token($user->id, 'emailnew', $user->login . ":$new_email"); + my ($token, $token_ts) + = _create_token($user->id, 'emailold', $user->login . ":$new_email"); + my $newtoken + = _create_token($user->id, 'emailnew', $user->login . ":$new_email"); - # Mail the user the token along with instructions for using it. + # Mail the user the token along with instructions for using it. - my $template = Bugzilla->template_inner($user->setting('lang')); - my $vars = {}; + my $template = Bugzilla->template_inner($user->setting('lang')); + my $vars = {}; - $vars->{'newemailaddress'} = $new_email . Bugzilla->params->{'emailsuffix'}; - $vars->{'expiration_ts'} = ctime($token_ts + MAX_TOKEN_AGE * 86400); + $vars->{'newemailaddress'} = $new_email . Bugzilla->params->{'emailsuffix'}; + $vars->{'expiration_ts'} = ctime($token_ts + MAX_TOKEN_AGE * 86400); - # First send an email to the new address. If this one doesn't exist, - # then the whole process must stop immediately. This means the email must - # be sent immediately and must not be stored in the queue. - $vars->{'token'} = $newtoken; + # First send an email to the new address. If this one doesn't exist, + # then the whole process must stop immediately. This means the email must + # be sent immediately and must not be stored in the queue. + $vars->{'token'} = $newtoken; - my $message; - $template->process('account/email/change-new.txt.tmpl', $vars, \$message) - || ThrowTemplateError($template->error()); + my $message; + $template->process('account/email/change-new.txt.tmpl', $vars, \$message) + || ThrowTemplateError($template->error()); - MessageToMTA($message, SEND_NOW); + MessageToMTA($message, SEND_NOW); - # If we come here, then the new address exists. We now email the current - # address, but we don't want to stop the process if it no longer exists, - # to give a chance to the user to confirm the email address change. - $vars->{'token'} = $token; + # If we come here, then the new address exists. We now email the current + # address, but we don't want to stop the process if it no longer exists, + # to give a chance to the user to confirm the email address change. + $vars->{'token'} = $token; - $message = ''; - $template->process('account/email/change-old.txt.tmpl', $vars, \$message) - || ThrowTemplateError($template->error()); + $message = ''; + $template->process('account/email/change-old.txt.tmpl', $vars, \$message) + || ThrowTemplateError($template->error()); - eval { MessageToMTA($message, SEND_NOW); }; + eval { MessageToMTA($message, SEND_NOW); }; - # Give the user a chance to cancel the process even if he never got - # the email above. The token is required. - return $token; + # Give the user a chance to cancel the process even if he never got + # the email above. The token is required. + return $token; } # Generates a random token, adds it to the tokens table, and sends it # to the user with instructions for using it to change their password. sub IssuePasswordToken { - my $user = shift; - my $dbh = Bugzilla->dbh; + my $user = shift; + my $dbh = Bugzilla->dbh; - my $too_soon = $dbh->selectrow_array( - 'SELECT 1 FROM tokens + my $too_soon = $dbh->selectrow_array( + 'SELECT 1 FROM tokens WHERE userid = ? AND tokentype = ? - AND issuedate > ' - . $dbh->sql_date_math('NOW()', '-', ACCOUNT_CHANGE_INTERVAL, 'MINUTE'), - undef, ($user->id, 'password')); + AND issuedate > ' + . $dbh->sql_date_math('NOW()', '-', ACCOUNT_CHANGE_INTERVAL, 'MINUTE'), + undef, ($user->id, 'password') + ); - ThrowUserError('too_soon_for_new_token', {'type' => 'password'}) if $too_soon; + ThrowUserError('too_soon_for_new_token', {'type' => 'password'}) if $too_soon; - my $ip_addr = remote_ip(); - my ($token, $token_ts) = _create_token($user->id, 'password', $ip_addr); + my $ip_addr = remote_ip(); + my ($token, $token_ts) = _create_token($user->id, 'password', $ip_addr); - # Mail the user the token along with instructions for using it. - my $template = Bugzilla->template_inner($user->setting('lang')); - my $vars = {}; + # Mail the user the token along with instructions for using it. + my $template = Bugzilla->template_inner($user->setting('lang')); + my $vars = {}; - $vars->{'token'} = $token; - $vars->{'ip_addr'} = $ip_addr; - $vars->{'emailaddress'} = $user->email; - $vars->{'expiration_ts'} = ctime($token_ts + MAX_TOKEN_AGE * 86400); - # The user is not logged in (else they wouldn't request a new password). - # So we have to pass this information to the template. - $vars->{'timezone'} = $user->timezone; - - my $message = ""; - $template->process("account/password/forgotten-password.txt.tmpl", - $vars, \$message) - || ThrowTemplateError($template->error()); + $vars->{'token'} = $token; + $vars->{'ip_addr'} = $ip_addr; + $vars->{'emailaddress'} = $user->email; + $vars->{'expiration_ts'} = ctime($token_ts + MAX_TOKEN_AGE * 86400); + + # The user is not logged in (else they wouldn't request a new password). + # So we have to pass this information to the template. + $vars->{'timezone'} = $user->timezone; - MessageToMTA($message); + my $message = ""; + $template->process("account/password/forgotten-password.txt.tmpl", + $vars, \$message) + || ThrowTemplateError($template->error()); + + MessageToMTA($message); } sub issue_session_token { - # Generates a random token, adds it to the tokens table, and returns - # the token to the caller. - my $data = shift; - return _create_token(Bugzilla->user->id, 'session', $data); + # Generates a random token, adds it to the tokens table, and returns + # the token to the caller. + + my $data = shift; + return _create_token(Bugzilla->user->id, 'session', $data); } sub issue_hash_token { - my ($data, $time) = @_; - $data ||= []; - $time ||= time(); - - # For the user ID, use the actual ID if the user is logged in. - # Otherwise, use the remote IP, in case this is for something - # such as creating an account or logging in. - my $user_id = Bugzilla->user->id || remote_ip(); - - # The concatenated string is of the form - # token creation time + user ID (either ID or remote IP) + data - my @args = ($time, $user_id, @$data); - - my $token = join('*', @args); - # Wide characters cause Digest::SHA to die. - if (Bugzilla->params->{'utf8'}) { - utf8::encode($token) if utf8::is_utf8($token); - } - $token = hmac_sha256_base64($token, Bugzilla->localconfig->{'site_wide_secret'}); - $token =~ s/\+/-/g; - $token =~ s/\//_/g; - - # Prepend the token creation time, unencrypted, so that the token - # lifetime can be validated. - return $time . '-' . $token; + my ($data, $time) = @_; + $data ||= []; + $time ||= time(); + + # For the user ID, use the actual ID if the user is logged in. + # Otherwise, use the remote IP, in case this is for something + # such as creating an account or logging in. + my $user_id = Bugzilla->user->id || remote_ip(); + + # The concatenated string is of the form + # token creation time + user ID (either ID or remote IP) + data + my @args = ($time, $user_id, @$data); + + my $token = join('*', @args); + + # Wide characters cause Digest::SHA to die. + if (Bugzilla->params->{'utf8'}) { + utf8::encode($token) if utf8::is_utf8($token); + } + $token + = hmac_sha256_base64($token, Bugzilla->localconfig->{'site_wide_secret'}); + $token =~ s/\+/-/g; + $token =~ s/\//_/g; + + # Prepend the token creation time, unencrypted, so that the token + # lifetime can be validated. + return $time . '-' . $token; } sub check_hash_token { - my ($token, $data) = @_; - $data ||= []; - my ($time, $expected_token); - - if ($token) { - ($time, undef) = split(/-/, $token); - # Regenerate the token based on the information we have. - $expected_token = issue_hash_token($data, $time); - } + my ($token, $data) = @_; + $data ||= []; + my ($time, $expected_token); - if (!$token - || $expected_token ne $token - || time() - $time > MAX_TOKEN_AGE * 86400) - { - my $template = Bugzilla->template; - my $vars = {}; - $vars->{'script_name'} = basename($0); - $vars->{'token'} = issue_hash_token($data); - $vars->{'reason'} = (!$token) ? 'missing_token' : - ($expected_token ne $token) ? 'invalid_token' : - 'expired_token'; - print Bugzilla->cgi->header(); - $template->process('global/confirm-action.html.tmpl', $vars) - || ThrowTemplateError($template->error()); - exit; - } + if ($token) { + ($time, undef) = split(/-/, $token); + + # Regenerate the token based on the information we have. + $expected_token = issue_hash_token($data, $time); + } + + if (!$token + || $expected_token ne $token + || time() - $time > MAX_TOKEN_AGE * 86400) + { + my $template = Bugzilla->template; + my $vars = {}; + $vars->{'script_name'} = basename($0); + $vars->{'token'} = issue_hash_token($data); + $vars->{'reason'} + = (!$token) ? 'missing_token' + : ($expected_token ne $token) ? 'invalid_token' + : 'expired_token'; + print Bugzilla->cgi->header(); + $template->process('global/confirm-action.html.tmpl', $vars) + || ThrowTemplateError($template->error()); + exit; + } - # If we come here, then the token is valid and not too old. - return 1; + # If we come here, then the token is valid and not too old. + return 1; } sub CleanTokenTable { - my $dbh = Bugzilla->dbh; - $dbh->do('DELETE FROM tokens - WHERE ' . $dbh->sql_to_days('NOW()') . ' - ' . - $dbh->sql_to_days('issuedate') . ' >= ?', - undef, MAX_TOKEN_AGE); + my $dbh = Bugzilla->dbh; + $dbh->do( + 'DELETE FROM tokens + WHERE ' + . $dbh->sql_to_days('NOW()') . ' - ' + . $dbh->sql_to_days('issuedate') + . ' >= ?', undef, MAX_TOKEN_AGE + ); } sub GenerateUniqueToken { - # Generates a unique random token. Uses generate_random_password - # for the tokens themselves and checks uniqueness by searching for - # the token in the "tokens" table. Gives up if it can't come up - # with a token after about one hundred tries. - my ($table, $column) = @_; - - my $token; - my $duplicate = 1; - my $tries = 0; - $table ||= "tokens"; - $column ||= "token"; - - my $dbh = Bugzilla->dbh; - my $sth = $dbh->prepare("SELECT 1 FROM $table WHERE $column = ?"); - - while ($duplicate) { - ++$tries; - if ($tries > 100) { - ThrowCodeError("token_generation_error"); - } - $token = generate_random_password(); - $sth->execute($token); - $duplicate = $sth->fetchrow_array; + + # Generates a unique random token. Uses generate_random_password + # for the tokens themselves and checks uniqueness by searching for + # the token in the "tokens" table. Gives up if it can't come up + # with a token after about one hundred tries. + my ($table, $column) = @_; + + my $token; + my $duplicate = 1; + my $tries = 0; + $table ||= "tokens"; + $column ||= "token"; + + my $dbh = Bugzilla->dbh; + my $sth = $dbh->prepare("SELECT 1 FROM $table WHERE $column = ?"); + + while ($duplicate) { + ++$tries; + if ($tries > 100) { + ThrowCodeError("token_generation_error"); } - return $token; + $token = generate_random_password(); + $sth->execute($token); + $duplicate = $sth->fetchrow_array; + } + return $token; } # Cancels a previously issued token and notifies the user. # This should only happen when the user accidentally makes a token request # or when a malicious hacker makes a token request on behalf of a user. sub Cancel { - my ($token, $cancelaction, $vars) = @_; - my $dbh = Bugzilla->dbh; - $vars ||= {}; - - # Get information about the token being canceled. - trick_taint($token); - my ($db_token, $issuedate, $tokentype, $eventdata, $userid) = - $dbh->selectrow_array('SELECT token, ' . $dbh->sql_date_format('issuedate') . ', + my ($token, $cancelaction, $vars) = @_; + my $dbh = Bugzilla->dbh; + $vars ||= {}; + + # Get information about the token being canceled. + trick_taint($token); + my ($db_token, $issuedate, $tokentype, $eventdata, $userid) + = $dbh->selectrow_array( + 'SELECT token, ' + . $dbh->sql_date_format('issuedate') . ', tokentype, eventdata, userid FROM tokens - WHERE token = ?', - undef, $token); - - # Some DBs such as MySQL are case-insensitive by default so we do - # a quick comparison to make sure the tokens are indeed the same. - (defined $db_token && $db_token eq $token) - || ThrowCodeError("cancel_token_does_not_exist"); - - # If we are canceling the creation of a new user account, then there - # is no entry in the 'profiles' table. - my $user = new Bugzilla::User($userid); - - $vars->{'emailaddress'} = $userid ? $user->email : $eventdata; - $vars->{'remoteaddress'} = remote_ip(); - $vars->{'token'} = $token; - $vars->{'tokentype'} = $tokentype; - $vars->{'issuedate'} = $issuedate; - # The user is probably not logged in. - # So we have to pass this information to the template. - $vars->{'timezone'} = $user->timezone; - $vars->{'eventdata'} = $eventdata; - $vars->{'cancelaction'} = $cancelaction; - - # Notify the user via email about the cancellation. - my $template = Bugzilla->template_inner($user->setting('lang')); - - my $message; - $template->process("account/cancel-token.txt.tmpl", $vars, \$message) - || ThrowTemplateError($template->error()); + WHERE token = ?', undef, $token + ); - MessageToMTA($message); + # Some DBs such as MySQL are case-insensitive by default so we do + # a quick comparison to make sure the tokens are indeed the same. + (defined $db_token && $db_token eq $token) + || ThrowCodeError("cancel_token_does_not_exist"); - # Delete the token from the database. - delete_token($token); + # If we are canceling the creation of a new user account, then there + # is no entry in the 'profiles' table. + my $user = new Bugzilla::User($userid); + + $vars->{'emailaddress'} = $userid ? $user->email : $eventdata; + $vars->{'remoteaddress'} = remote_ip(); + $vars->{'token'} = $token; + $vars->{'tokentype'} = $tokentype; + $vars->{'issuedate'} = $issuedate; + + # The user is probably not logged in. + # So we have to pass this information to the template. + $vars->{'timezone'} = $user->timezone; + $vars->{'eventdata'} = $eventdata; + $vars->{'cancelaction'} = $cancelaction; + + # Notify the user via email about the cancellation. + my $template = Bugzilla->template_inner($user->setting('lang')); + + my $message; + $template->process("account/cancel-token.txt.tmpl", $vars, \$message) + || ThrowTemplateError($template->error()); + + MessageToMTA($message); + + # Delete the token from the database. + delete_token($token); } sub DeletePasswordTokens { - my ($userid, $reason) = @_; - my $dbh = Bugzilla->dbh; + my ($userid, $reason) = @_; + my $dbh = Bugzilla->dbh; - detaint_natural($userid); - my $tokens = $dbh->selectcol_arrayref('SELECT token FROM tokens + detaint_natural($userid); + my $tokens = $dbh->selectcol_arrayref( + 'SELECT token FROM tokens WHERE userid = ? AND tokentype = ?', - undef, ($userid, 'password')); + undef, ($userid, 'password') + ); - foreach my $token (@$tokens) { - Bugzilla::Token::Cancel($token, $reason); - } + foreach my $token (@$tokens) { + Bugzilla::Token::Cancel($token, $reason); + } } -# Returns an email change token if the user has one. +# Returns an email change token if the user has one. sub HasEmailChangeToken { - my $userid = shift; - my $dbh = Bugzilla->dbh; + my $userid = shift; + my $dbh = Bugzilla->dbh; - my $token = $dbh->selectrow_array('SELECT token FROM tokens + my $token = $dbh->selectrow_array( + 'SELECT token FROM tokens WHERE userid = ? - AND (tokentype = ? OR tokentype = ?) ' . - $dbh->sql_limit(1), - undef, ($userid, 'emailnew', 'emailold')); - return $token; + AND (tokentype = ? OR tokentype = ?) ' + . $dbh->sql_limit(1), undef, ($userid, 'emailnew', 'emailold') + ); + return $token; } # Returns the userid, issuedate and eventdata for the specified token sub GetTokenData { - my ($token) = @_; - my $dbh = Bugzilla->dbh; + my ($token) = @_; + my $dbh = Bugzilla->dbh; - return unless defined $token; - $token = clean_text($token); - trick_taint($token); + return unless defined $token; + $token = clean_text($token); + trick_taint($token); - my @token_data = $dbh->selectrow_array( - "SELECT token, userid, " . $dbh->sql_date_format('issuedate') . ", eventdata, tokentype + my @token_data = $dbh->selectrow_array( + "SELECT token, userid, " + . $dbh->sql_date_format('issuedate') + . ", eventdata, tokentype FROM tokens - WHERE token = ?", undef, $token); + WHERE token = ?", undef, $token + ); - # Some DBs such as MySQL are case-insensitive by default so we do - # a quick comparison to make sure the tokens are indeed the same. - my $db_token = shift @token_data; - return undef if (!defined $db_token || $db_token ne $token); + # Some DBs such as MySQL are case-insensitive by default so we do + # a quick comparison to make sure the tokens are indeed the same. + my $db_token = shift @token_data; + return undef if (!defined $db_token || $db_token ne $token); - return @token_data; + return @token_data; } # Deletes specified token sub delete_token { - my ($token) = @_; - my $dbh = Bugzilla->dbh; + my ($token) = @_; + my $dbh = Bugzilla->dbh; - return unless defined $token; - trick_taint($token); + return unless defined $token; + trick_taint($token); - $dbh->do("DELETE FROM tokens WHERE token = ?", undef, $token); + $dbh->do("DELETE FROM tokens WHERE token = ?", undef, $token); } # Given a token, makes sure it comes from the currently logged in user # and match the expected event. Returns 1 on success, else displays a warning. sub check_token_data { - my ($token, $expected_action, $alternate_script) = @_; - my $user = Bugzilla->user; - my $template = Bugzilla->template; - my $cgi = Bugzilla->cgi; - - my ($creator_id, $date, $token_action) = GetTokenData($token); - unless ($creator_id - && $creator_id == $user->id - && $token_action eq $expected_action) - { - # Something is going wrong. Ask confirmation before processing. - # It is possible that someone tried to trick an administrator. - # In this case, we want to know their name! - require Bugzilla::User; - - my $vars = {}; - $vars->{'abuser'} = Bugzilla::User->new($creator_id)->identity; - $vars->{'token_action'} = $token_action; - $vars->{'expected_action'} = $expected_action; - $vars->{'script_name'} = basename($0); - $vars->{'alternate_script'} = $alternate_script || basename($0); - - # Now is a good time to remove old tokens from the DB. - CleanTokenTable(); - - # If no token was found, create a valid token for the given action. - unless ($creator_id) { - $token = issue_session_token($expected_action); - $cgi->param('token', $token); - } - - print $cgi->header(); - - $template->process('admin/confirm-action.html.tmpl', $vars) - || ThrowTemplateError($template->error()); - exit; + my ($token, $expected_action, $alternate_script) = @_; + my $user = Bugzilla->user; + my $template = Bugzilla->template; + my $cgi = Bugzilla->cgi; + + my ($creator_id, $date, $token_action) = GetTokenData($token); + unless ($creator_id + && $creator_id == $user->id + && $token_action eq $expected_action) + { + # Something is going wrong. Ask confirmation before processing. + # It is possible that someone tried to trick an administrator. + # In this case, we want to know their name! + require Bugzilla::User; + + my $vars = {}; + $vars->{'abuser'} = Bugzilla::User->new($creator_id)->identity; + $vars->{'token_action'} = $token_action; + $vars->{'expected_action'} = $expected_action; + $vars->{'script_name'} = basename($0); + $vars->{'alternate_script'} = $alternate_script || basename($0); + + # Now is a good time to remove old tokens from the DB. + CleanTokenTable(); + + # If no token was found, create a valid token for the given action. + unless ($creator_id) { + $token = issue_session_token($expected_action); + $cgi->param('token', $token); } - return 1; + + print $cgi->header(); + + $template->process('admin/confirm-action.html.tmpl', $vars) + || ThrowTemplateError($template->error()); + exit; + } + return 1; } ################################################################################ @@ -433,34 +459,38 @@ sub check_token_data { # Generates a unique token and inserts it into the database # Returns the token and the token timestamp sub _create_token { - my ($userid, $tokentype, $eventdata) = @_; - my $dbh = Bugzilla->dbh; + my ($userid, $tokentype, $eventdata) = @_; + my $dbh = Bugzilla->dbh; - detaint_natural($userid) if defined $userid; - trick_taint($tokentype); - trick_taint($eventdata); + detaint_natural($userid) if defined $userid; + trick_taint($tokentype); + trick_taint($eventdata); - my $is_shadow = Bugzilla->is_shadow_db; - $dbh = Bugzilla->switch_to_main_db() if $is_shadow; + my $is_shadow = Bugzilla->is_shadow_db; + $dbh = Bugzilla->switch_to_main_db() if $is_shadow; - $dbh->bz_start_transaction(); + $dbh->bz_start_transaction(); - my $token = GenerateUniqueToken(); + my $token = GenerateUniqueToken(); - $dbh->do("INSERT INTO tokens (userid, issuedate, token, tokentype, eventdata) - VALUES (?, NOW(), ?, ?, ?)", undef, ($userid, $token, $tokentype, $eventdata)); + $dbh->do( + "INSERT INTO tokens (userid, issuedate, token, tokentype, eventdata) + VALUES (?, NOW(), ?, ?, ?)", undef, + ($userid, $token, $tokentype, $eventdata) + ); - $dbh->bz_commit_transaction(); + $dbh->bz_commit_transaction(); - if (wantarray) { - my (undef, $token_ts, undef) = GetTokenData($token); - $token_ts = str2time($token_ts); - Bugzilla->switch_to_shadow_db() if $is_shadow; - return ($token, $token_ts); - } else { - Bugzilla->switch_to_shadow_db() if $is_shadow; - return $token; - } + if (wantarray) { + my (undef, $token_ts, undef) = GetTokenData($token); + $token_ts = str2time($token_ts); + Bugzilla->switch_to_shadow_db() if $is_shadow; + return ($token, $token_ts); + } + else { + Bugzilla->switch_to_shadow_db() if $is_shadow; + return $token; + } } 1; diff --git a/Bugzilla/Update.pm b/Bugzilla/Update.pm index 72a7108a8..9f9288162 100644 --- a/Bugzilla/Update.pm +++ b/Bugzilla/Update.pm @@ -13,149 +13,159 @@ use warnings; use Bugzilla::Constants; -use constant TIME_INTERVAL => 86400; # Default is one day, in seconds. -use constant TIMEOUT => 5; # Number of seconds before timeout. +use constant TIME_INTERVAL => 86400; # Default is one day, in seconds. +use constant TIMEOUT => 5; # Number of seconds before timeout. # Look for new releases and notify logged in administrators about them. sub get_notifications { - return if !Bugzilla->feature('updates'); - return if (Bugzilla->params->{'upgrade_notification'} eq 'disabled'); - - my $local_file = bz_locations()->{'datadir'} . '/' . LOCAL_FILE; - # Update the local XML file if this one doesn't exist or if - # the last modification time (stat[9]) is older than TIME_INTERVAL. - if (!-e $local_file || (time() - (stat($local_file))[9] > TIME_INTERVAL)) { - unlink $local_file; # Make sure the old copy is away. - return { 'error' => 'no_update' } if (-e $local_file); - - my $error = _synchronize_data(); - # If an error is returned, leave now. - return $error if $error; + return if !Bugzilla->feature('updates'); + return if (Bugzilla->params->{'upgrade_notification'} eq 'disabled'); + + my $local_file = bz_locations()->{'datadir'} . '/' . LOCAL_FILE; + + # Update the local XML file if this one doesn't exist or if + # the last modification time (stat[9]) is older than TIME_INTERVAL. + if (!-e $local_file || (time() - (stat($local_file))[9] > TIME_INTERVAL)) { + unlink $local_file; # Make sure the old copy is away. + return {'error' => 'no_update'} if (-e $local_file); + + my $error = _synchronize_data(); + + # If an error is returned, leave now. + return $error if $error; + } + + # If we cannot access the local XML file, ignore it. + return {'error' => 'no_access'} unless (-r $local_file); + + my $twig = XML::Twig->new(); + $twig->safe_parsefile($local_file); + + # If the XML file is invalid, return. + return {'error' => 'corrupted'} if $@; + my $root = $twig->root; + + my @releases; + foreach my $branch ($root->children('branch')) { + my $release = { + 'branch_ver' => $branch->{'att'}->{'id'}, + 'latest_ver' => $branch->{'att'}->{'vid'}, + 'status' => $branch->{'att'}->{'status'}, + 'url' => $branch->{'att'}->{'url'}, + 'date' => $branch->{'att'}->{'date'} + }; + push(@releases, $release); + } + + # On which branch is the current installation running? + my @current_version + = (BUGZILLA_VERSION =~ m/^(\d+)\.(\d+)(?:(rc|\.)(\d+))?\+?$/); + + my @release; + if (Bugzilla->params->{'upgrade_notification'} eq 'development_snapshot') { + @release = grep { $_->{'status'} eq 'development' } @releases; + + # If there is no development snapshot available, then we are in the + # process of releasing a release candidate. That's the release we want. + unless (scalar(@release)) { + @release = grep { $_->{'status'} eq 'release-candidate' } @releases; } - - # If we cannot access the local XML file, ignore it. - return { 'error' => 'no_access' } unless (-r $local_file); - - my $twig = XML::Twig->new(); - $twig->safe_parsefile($local_file); - # If the XML file is invalid, return. - return { 'error' => 'corrupted' } if $@; - my $root = $twig->root; - - my @releases; - foreach my $branch ($root->children('branch')) { - my $release = { - 'branch_ver' => $branch->{'att'}->{'id'}, - 'latest_ver' => $branch->{'att'}->{'vid'}, - 'status' => $branch->{'att'}->{'status'}, - 'url' => $branch->{'att'}->{'url'}, - 'date' => $branch->{'att'}->{'date'} - }; - push(@releases, $release); - } - - # On which branch is the current installation running? - my @current_version = - (BUGZILLA_VERSION =~ m/^(\d+)\.(\d+)(?:(rc|\.)(\d+))?\+?$/); - - my @release; - if (Bugzilla->params->{'upgrade_notification'} eq 'development_snapshot') { - @release = grep {$_->{'status'} eq 'development'} @releases; - # If there is no development snapshot available, then we are in the - # process of releasing a release candidate. That's the release we want. - unless (scalar(@release)) { - @release = grep {$_->{'status'} eq 'release-candidate'} @releases; - } - } - elsif (Bugzilla->params->{'upgrade_notification'} eq 'latest_stable_release') { - @release = grep {$_->{'status'} eq 'stable'} @releases; - } - elsif (Bugzilla->params->{'upgrade_notification'} eq 'stable_branch_release') { - # We want the latest stable version for the current branch. - # If we are running a development snapshot, we won't match anything. - my $branch_version = $current_version[0] . '.' . $current_version[1]; - - # We do a string comparison instead of a numerical one, because - # e.g. 2.2 == 2.20, but 2.2 ne 2.20 (and 2.2 is indeed much older). - @release = grep {$_->{'branch_ver'} eq $branch_version} @releases; - - # If the branch is now closed, we should strongly suggest - # to upgrade to the latest stable release available. - if (scalar(@release) && $release[0]->{'status'} eq 'closed') { - @release = grep {$_->{'status'} eq 'stable'} @releases; - return {'data' => $release[0], 'deprecated' => $branch_version}; - } + } + elsif (Bugzilla->params->{'upgrade_notification'} eq 'latest_stable_release') { + @release = grep { $_->{'status'} eq 'stable' } @releases; + } + elsif (Bugzilla->params->{'upgrade_notification'} eq 'stable_branch_release') { + + # We want the latest stable version for the current branch. + # If we are running a development snapshot, we won't match anything. + my $branch_version = $current_version[0] . '.' . $current_version[1]; + + # We do a string comparison instead of a numerical one, because + # e.g. 2.2 == 2.20, but 2.2 ne 2.20 (and 2.2 is indeed much older). + @release = grep { $_->{'branch_ver'} eq $branch_version } @releases; + + # If the branch is now closed, we should strongly suggest + # to upgrade to the latest stable release available. + if (scalar(@release) && $release[0]->{'status'} eq 'closed') { + @release = grep { $_->{'status'} eq 'stable' } @releases; + return {'data' => $release[0], 'deprecated' => $branch_version}; } - else { - # Unknown parameter. - return {'error' => 'unknown_parameter'}; - } - - # Return if no new release is available. - return unless scalar(@release); - - # Only notify the administrator if the latest version available - # is newer than the current one. - my @new_version = - ($release[0]->{'latest_ver'} =~ m/^(\d+)\.(\d+)(?:(rc|\.)(\d+))?\+?$/); - - # We convert release candidates 'rc' to integers (rc ? 0 : 1) in order - # to compare versions easily. - $current_version[2] = ($current_version[2] && $current_version[2] eq 'rc') ? 0 : 1; - $new_version[2] = ($new_version[2] && $new_version[2] eq 'rc') ? 0 : 1; - - my $is_newer = _compare_versions(\@current_version, \@new_version); - return ($is_newer == 1) ? {'data' => $release[0]} : undef; + } + else { + # Unknown parameter. + return {'error' => 'unknown_parameter'}; + } + + # Return if no new release is available. + return unless scalar(@release); + + # Only notify the administrator if the latest version available + # is newer than the current one. + my @new_version + = ($release[0]->{'latest_ver'} =~ m/^(\d+)\.(\d+)(?:(rc|\.)(\d+))?\+?$/); + + # We convert release candidates 'rc' to integers (rc ? 0 : 1) in order + # to compare versions easily. + $current_version[2] + = ($current_version[2] && $current_version[2] eq 'rc') ? 0 : 1; + $new_version[2] = ($new_version[2] && $new_version[2] eq 'rc') ? 0 : 1; + + my $is_newer = _compare_versions(\@current_version, \@new_version); + return ($is_newer == 1) ? {'data' => $release[0]} : undef; } sub _synchronize_data { - my $local_file = bz_locations()->{'datadir'} . '/' . LOCAL_FILE; - - my $ua = LWP::UserAgent->new(); - $ua->timeout(TIMEOUT); - $ua->protocols_allowed(['http', 'https']); - # If the URL of the proxy is given, use it, else get this information - # from the environment variable. - my $proxy_url = Bugzilla->params->{'proxy_url'}; - if ($proxy_url) { - $ua->proxy(['http', 'https'], $proxy_url); - } - else { - $ua->env_proxy; - } - my $response = eval { $ua->mirror(REMOTE_FILE, $local_file) }; - - # $ua->mirror() forces the modification time of the local XML file - # to match the modification time of the remote one. - # So we have to update it manually to reflect that a newer version - # of the file has effectively been requested. This will avoid - # any new download for the next TIME_INTERVAL. - if (-e $local_file) { - # Try to alter its last modification time. - my $can_alter = utime(undef, undef, $local_file); - # This error should never happen. - $can_alter || return { 'error' => 'no_update' }; - } - elsif ($response && $response->is_error) { - # We have been unable to download the file. - return { 'error' => 'cannot_download', 'reason' => $response->status_line }; - } - else { - return { 'error' => 'no_write', 'reason' => $@ }; - } - - # Everything went well. - return 0; + my $local_file = bz_locations()->{'datadir'} . '/' . LOCAL_FILE; + + my $ua = LWP::UserAgent->new(); + $ua->timeout(TIMEOUT); + $ua->protocols_allowed(['http', 'https']); + + # If the URL of the proxy is given, use it, else get this information + # from the environment variable. + my $proxy_url = Bugzilla->params->{'proxy_url'}; + if ($proxy_url) { + $ua->proxy(['http', 'https'], $proxy_url); + } + else { + $ua->env_proxy; + } + my $response = eval { $ua->mirror(REMOTE_FILE, $local_file) }; + + # $ua->mirror() forces the modification time of the local XML file + # to match the modification time of the remote one. + # So we have to update it manually to reflect that a newer version + # of the file has effectively been requested. This will avoid + # any new download for the next TIME_INTERVAL. + if (-e $local_file) { + + # Try to alter its last modification time. + my $can_alter = utime(undef, undef, $local_file); + + # This error should never happen. + $can_alter || return {'error' => 'no_update'}; + } + elsif ($response && $response->is_error) { + + # We have been unable to download the file. + return {'error' => 'cannot_download', 'reason' => $response->status_line}; + } + else { + return {'error' => 'no_write', 'reason' => $@}; + } + + # Everything went well. + return 0; } sub _compare_versions { - my ($old_ver, $new_ver) = @_; - while (scalar(@$old_ver) && scalar(@$new_ver)) { - my $old = shift(@$old_ver) || 0; - my $new = shift(@$new_ver) || 0; - return $new <=> $old if ($new <=> $old); - } - return scalar(@$new_ver) <=> scalar(@$old_ver); + my ($old_ver, $new_ver) = @_; + while (scalar(@$old_ver) && scalar(@$new_ver)) { + my $old = shift(@$old_ver) || 0; + my $new = shift(@$new_ver) || 0; + return $new <=> $old if ($new <=> $old); + } + return scalar(@$new_ver) <=> scalar(@$old_ver); } diff --git a/Bugzilla/User.pm b/Bugzilla/User.pm index 77e6cebb0..857b56801 100644 --- a/Bugzilla/User.pm +++ b/Bugzilla/User.pm @@ -33,9 +33,9 @@ use URI::QueryParam; use parent qw(Bugzilla::Object Exporter); @Bugzilla::User::EXPORT = qw(is_available_username - login_to_id validate_password validate_password_check - USER_MATCH_MULTIPLE USER_MATCH_FAILED USER_MATCH_SUCCESS - MATCH_SKIP_CONFIRM + login_to_id validate_password validate_password_check + USER_MATCH_MULTIPLE USER_MATCH_FAILED USER_MATCH_SUCCESS + MATCH_SKIP_CONFIRM ); ##################################################################### @@ -46,16 +46,16 @@ use constant USER_MATCH_MULTIPLE => -1; use constant USER_MATCH_FAILED => 0; use constant USER_MATCH_SUCCESS => 1; -use constant MATCH_SKIP_CONFIRM => 1; +use constant MATCH_SKIP_CONFIRM => 1; use constant DEFAULT_USER => { - 'userid' => 0, - 'realname' => '', - 'login_name' => '', - 'showmybugslink' => 0, - 'disabledtext' => '', - 'disable_mail' => 0, - 'is_enabled' => 1, + 'userid' => 0, + 'realname' => '', + 'login_name' => '', + 'showmybugslink' => 0, + 'disabledtext' => '', + 'disable_mail' => 0, + 'is_enabled' => 1, }; use constant DB_TABLE => 'profiles'; @@ -65,18 +65,19 @@ use constant DB_TABLE => 'profiles'; # Bugzilla::User used "name" for the realname field. This should be # fixed one day. sub DB_COLUMNS { - my $dbh = Bugzilla->dbh; - return ( - 'profiles.userid', - 'profiles.login_name', - 'profiles.realname', - 'profiles.mybugslink AS showmybugslink', - 'profiles.disabledtext', - 'profiles.disable_mail', - 'profiles.extern_id', - 'profiles.is_enabled', - $dbh->sql_date_format('last_seen_date', '%Y-%m-%d') . ' AS last_seen_date', + my $dbh = Bugzilla->dbh; + return ( + 'profiles.userid', + 'profiles.login_name', + 'profiles.realname', + 'profiles.mybugslink AS showmybugslink', + 'profiles.disabledtext', + 'profiles.disable_mail', + 'profiles.extern_id', + 'profiles.is_enabled', + $dbh->sql_date_format('last_seen_date', '%Y-%m-%d') . ' AS last_seen_date', ), + ; } use constant NAME_FIELD => 'login_name'; @@ -84,32 +85,30 @@ use constant ID_FIELD => 'userid'; use constant LIST_ORDER => NAME_FIELD; use constant VALIDATORS => { - cryptpassword => \&_check_password, - disable_mail => \&_check_disable_mail, - disabledtext => \&_check_disabledtext, - login_name => \&check_login_name, - realname => \&_check_realname, - extern_id => \&_check_extern_id, - is_enabled => \&_check_is_enabled, + cryptpassword => \&_check_password, + disable_mail => \&_check_disable_mail, + disabledtext => \&_check_disabledtext, + login_name => \&check_login_name, + realname => \&_check_realname, + extern_id => \&_check_extern_id, + is_enabled => \&_check_is_enabled, }; sub UPDATE_COLUMNS { - my $self = shift; - my @cols = qw( - disable_mail - disabledtext - login_name - realname - extern_id - is_enabled - ); - push(@cols, 'cryptpassword') if exists $self->{cryptpassword}; - return @cols; -}; + my $self = shift; + my @cols = qw( + disable_mail + disabledtext + login_name + realname + extern_id + is_enabled + ); + push(@cols, 'cryptpassword') if exists $self->{cryptpassword}; + return @cols; +} -use constant VALIDATOR_DEPENDENCIES => { - is_enabled => ['disabledtext'], -}; +use constant VALIDATOR_DEPENDENCIES => {is_enabled => ['disabledtext'],}; use constant EXTRA_REQUIRED_FIELDS => qw(is_enabled); @@ -118,129 +117,127 @@ use constant EXTRA_REQUIRED_FIELDS => qw(is_enabled); ################################################################################ sub new { - my $invocant = shift; - my $class = ref($invocant) || $invocant; - my ($param) = @_; - - my $user = { %{ DEFAULT_USER() } }; - bless ($user, $class); - return $user unless $param; - - if (ref($param) eq 'HASH') { - if (defined $param->{extern_id}) { - $param = { condition => 'extern_id = ?' , values => [$param->{extern_id}] }; - $_[0] = $param; - } + my $invocant = shift; + my $class = ref($invocant) || $invocant; + my ($param) = @_; + + my $user = {%{DEFAULT_USER()}}; + bless($user, $class); + return $user unless $param; + + if (ref($param) eq 'HASH') { + if (defined $param->{extern_id}) { + $param = {condition => 'extern_id = ?', values => [$param->{extern_id}]}; + $_[0] = $param; } - return $class->SUPER::new(@_); + } + return $class->SUPER::new(@_); } sub super_user { - my $invocant = shift; - my $class = ref($invocant) || $invocant; - my ($param) = @_; + my $invocant = shift; + my $class = ref($invocant) || $invocant; + my ($param) = @_; - my $user = { %{ DEFAULT_USER() } }; - $user->{groups} = [Bugzilla::Group->get_all]; - $user->{bless_groups} = [Bugzilla::Group->get_all]; - bless $user, $class; - return $user; + my $user = {%{DEFAULT_USER()}}; + $user->{groups} = [Bugzilla::Group->get_all]; + $user->{bless_groups} = [Bugzilla::Group->get_all]; + bless $user, $class; + return $user; } sub _update_groups { - my $self = shift; - my $group_changes = shift; - my $changes = shift; - my $dbh = Bugzilla->dbh; - - # Update group settings. - my $sth_add_mapping = $dbh->prepare( - qq{INSERT INTO user_group_map ( + my $self = shift; + my $group_changes = shift; + my $changes = shift; + my $dbh = Bugzilla->dbh; + + # Update group settings. + my $sth_add_mapping = $dbh->prepare( + qq{INSERT INTO user_group_map ( user_id, group_id, isbless, grant_type ) VALUES ( ?, ?, ?, ? ) - }); - my $sth_remove_mapping = $dbh->prepare( - qq{DELETE FROM user_group_map + } + ); + my $sth_remove_mapping = $dbh->prepare( + qq{DELETE FROM user_group_map WHERE user_id = ? AND group_id = ? AND isbless = ? AND grant_type = ? - }); + } + ); - foreach my $is_bless (keys %$group_changes) { - my ($removed, $added) = @{$group_changes->{$is_bless}}; + foreach my $is_bless (keys %$group_changes) { + my ($removed, $added) = @{$group_changes->{$is_bless}}; - foreach my $group (@$removed) { - $sth_remove_mapping->execute( - $self->id, $group->id, $is_bless, GRANT_DIRECT - ); - } - foreach my $group (@$added) { - $sth_add_mapping->execute( - $self->id, $group->id, $is_bless, GRANT_DIRECT - ); - } + foreach my $group (@$removed) { + $sth_remove_mapping->execute($self->id, $group->id, $is_bless, GRANT_DIRECT); + } + foreach my $group (@$added) { + $sth_add_mapping->execute($self->id, $group->id, $is_bless, GRANT_DIRECT); + } - if (! $is_bless) { - my $query = qq{ + if (!$is_bless) { + my $query = qq{ INSERT INTO profiles_activity (userid, who, profiles_when, fieldid, oldvalue, newvalue) VALUES ( ?, ?, now(), ?, ?, ?) }; - $dbh->do( - $query, undef, - $self->id, Bugzilla->user->id, - get_field_id('bug_group'), - join(', ', map { $_->name } @$removed), - join(', ', map { $_->name } @$added) - ); - } - else { - # XXX: should create profiles_activity entries for blesser changes. - } + $dbh->do( + $query, undef, $self->id, Bugzilla->user->id, + get_field_id('bug_group'), + join(', ', map { $_->name } @$removed), + join(', ', map { $_->name } @$added) + ); + } + else { + # XXX: should create profiles_activity entries for blesser changes. + } - Bugzilla->memcached->clear_config({ key => 'user_groups.' . $self->id }); + Bugzilla->memcached->clear_config({key => 'user_groups.' . $self->id}); - my $type = $is_bless ? 'bless_groups' : 'groups'; - $changes->{$type} = [ - [ map { $_->name } @$removed ], - [ map { $_->name } @$added ], - ]; - } + my $type = $is_bless ? 'bless_groups' : 'groups'; + $changes->{$type} = [[map { $_->name } @$removed], [map { $_->name } @$added],]; + } } sub update { - my $self = shift; - my $options = shift; + my $self = shift; + my $options = shift; - my $group_changes = delete $self->{_group_changes}; + my $group_changes = delete $self->{_group_changes}; - my $changes = $self->SUPER::update(@_); - my $dbh = Bugzilla->dbh; - $self->_update_groups($group_changes, $changes); + my $changes = $self->SUPER::update(@_); + my $dbh = Bugzilla->dbh; + $self->_update_groups($group_changes, $changes); - if (exists $changes->{login_name}) { - # Delete all the tokens related to the userid - $dbh->do('DELETE FROM tokens WHERE userid = ?', undef, $self->id) - unless $options->{keep_tokens}; - # And rederive regex groups - $self->derive_regexp_groups(); - } + if (exists $changes->{login_name}) { + + # Delete all the tokens related to the userid + $dbh->do('DELETE FROM tokens WHERE userid = ?', undef, $self->id) + unless $options->{keep_tokens}; - # Logout the user if necessary. - Bugzilla->logout_user($self) - if (!$options->{keep_session} - && (exists $changes->{login_name} - || exists $changes->{disabledtext} - || exists $changes->{cryptpassword})); + # And rederive regex groups + $self->derive_regexp_groups(); + } + + # Logout the user if necessary. + Bugzilla->logout_user($self) + if ( + !$options->{keep_session} + && ( exists $changes->{login_name} + || exists $changes->{disabledtext} + || exists $changes->{cryptpassword}) + ); - # XXX Can update profiles_activity here as soon as it understands - # field names like login_name. - - return $changes; + # XXX Can update profiles_activity here as soon as it understands + # field names like login_name. + + return $changes; } ################################################################################ @@ -252,62 +249,63 @@ sub _check_disabledtext { return trim($_[1]) || ''; } # Check whether the extern_id is unique. sub _check_extern_id { - my ($invocant, $extern_id) = @_; - $extern_id = trim($extern_id); - return undef unless defined($extern_id) && $extern_id ne ""; - if (!ref($invocant) || $invocant->extern_id ne $extern_id) { - my $existing_login = $invocant->new({ extern_id => $extern_id }); - if ($existing_login) { - ThrowUserError( 'extern_id_exists', - { extern_id => $extern_id, - existing_login_name => $existing_login->login }); - } + my ($invocant, $extern_id) = @_; + $extern_id = trim($extern_id); + return undef unless defined($extern_id) && $extern_id ne ""; + if (!ref($invocant) || $invocant->extern_id ne $extern_id) { + my $existing_login = $invocant->new({extern_id => $extern_id}); + if ($existing_login) { + ThrowUserError('extern_id_exists', + {extern_id => $extern_id, existing_login_name => $existing_login->login}); } - return $extern_id; + } + return $extern_id; } # This is public since createaccount.cgi needs to use it before issuing # a token for account creation. sub check_login_name { - my ($invocant, $name) = @_; - $name = trim($name); - $name || ThrowUserError('user_login_required'); - check_email_syntax($name); - - # Check the name if it's a new user, or if we're changing the name. - if (!ref($invocant) || lc($invocant->login) ne lc($name)) { - my @params = ($name); - push(@params, $invocant->login) if ref($invocant); - is_available_username(@params) - || ThrowUserError('account_exists', { email => $name }); - } + my ($invocant, $name) = @_; + $name = trim($name); + $name || ThrowUserError('user_login_required'); + check_email_syntax($name); - return $name; + # Check the name if it's a new user, or if we're changing the name. + if (!ref($invocant) || lc($invocant->login) ne lc($name)) { + my @params = ($name); + push(@params, $invocant->login) if ref($invocant); + is_available_username(@params) + || ThrowUserError('account_exists', {email => $name}); + } + + return $name; } sub _check_password { - my ($self, $pass) = @_; + my ($self, $pass) = @_; - # If the password is '*', do not encrypt it or validate it further--we - # are creating a user who should not be able to log in using DB - # authentication. - return $pass if $pass eq '*'; + # If the password is '*', do not encrypt it or validate it further--we + # are creating a user who should not be able to log in using DB + # authentication. + return $pass if $pass eq '*'; - validate_password($pass); - my $cryptpassword = bz_crypt($pass); - return $cryptpassword; + validate_password($pass); + my $cryptpassword = bz_crypt($pass); + return $cryptpassword; } sub _check_realname { return trim($_[1]) || ''; } sub _check_is_enabled { - my ($invocant, $is_enabled, undef, $params) = @_; - # is_enabled is set automatically on creation depending on whether - # disabledtext is empty (enabled) or not empty (disabled). - # When updating the user, is_enabled is set by calling set_disabledtext(). - # Any value passed into this validator is ignored. - my $disabledtext = ref($invocant) ? $invocant->disabledtext : $params->{disabledtext}; - return $disabledtext ? 0 : 1; + my ($invocant, $is_enabled, undef, $params) = @_; + + # is_enabled is set automatically on creation depending on whether + # disabledtext is empty (enabled) or not empty (disabled). + # When updating the user, is_enabled is set by calling set_disabledtext(). + # Any value passed into this validator is ignored. + my $disabledtext + = ref($invocant) ? $invocant->disabledtext : $params->{disabledtext}; + return $disabledtext ? 0 : 1; } ################################################################################ @@ -316,150 +314,151 @@ sub _check_is_enabled { sub set_disable_mail { $_[0]->set('disable_mail', $_[1]); } sub set_email_enabled { $_[0]->set('disable_mail', !$_[1]); } -sub set_extern_id { $_[0]->set('extern_id', $_[1]); } +sub set_extern_id { $_[0]->set('extern_id', $_[1]); } sub set_login { - my ($self, $login) = @_; - $self->set('login_name', $login); - delete $self->{identity}; - delete $self->{nick}; + my ($self, $login) = @_; + $self->set('login_name', $login); + delete $self->{identity}; + delete $self->{nick}; } sub set_name { - my ($self, $name) = @_; - $self->set('realname', $name); - delete $self->{identity}; + my ($self, $name) = @_; + $self->set('realname', $name); + delete $self->{identity}; } sub set_password { $_[0]->set('cryptpassword', $_[1]); } sub set_disabledtext { - $_[0]->set('disabledtext', $_[1]); - $_[0]->set('is_enabled', $_[1] ? 0 : 1); + $_[0]->set('disabledtext', $_[1]); + $_[0]->set('is_enabled', $_[1] ? 0 : 1); } sub set_groups { - my $self = shift; - $self->_set_groups(GROUP_MEMBERSHIP, @_); + my $self = shift; + $self->_set_groups(GROUP_MEMBERSHIP, @_); } sub set_bless_groups { - my $self = shift; + my $self = shift; - # The person making the change needs to be in the editusers group - Bugzilla->user->in_group('editusers') - || ThrowUserError("auth_failure", {group => "editusers", - reason => "cant_bless", - action => "edit", - object => "users"}); + # The person making the change needs to be in the editusers group + Bugzilla->user->in_group('editusers') || ThrowUserError( + "auth_failure", + { + group => "editusers", + reason => "cant_bless", + action => "edit", + object => "users" + } + ); - $self->_set_groups(GROUP_BLESS, @_); + $self->_set_groups(GROUP_BLESS, @_); } sub _set_groups { - my $self = shift; - my $is_bless = shift; - my $changes = shift; - my $dbh = Bugzilla->dbh; + my $self = shift; + my $is_bless = shift; + my $changes = shift; + my $dbh = Bugzilla->dbh; - # The person making the change is $user, $self is the person being changed - my $user = Bugzilla->user; + # The person making the change is $user, $self is the person being changed + my $user = Bugzilla->user; - # Input is a hash of arrays. Key is 'set', 'add' or 'remove'. The array - # is a list of group ids and/or names. + # Input is a hash of arrays. Key is 'set', 'add' or 'remove'. The array + # is a list of group ids and/or names. - # First turn the arrays into group objects. - $changes = $self->_set_groups_to_object($changes); + # First turn the arrays into group objects. + $changes = $self->_set_groups_to_object($changes); - # Get a list of the groups the user currently is a member of - my $ids = $dbh->selectcol_arrayref( - q{SELECT DISTINCT group_id + # Get a list of the groups the user currently is a member of + my $ids = $dbh->selectcol_arrayref( + q{SELECT DISTINCT group_id FROM user_group_map - WHERE user_id = ? AND isbless = ? AND grant_type = ?}, - undef, $self->id, $is_bless, GRANT_DIRECT); - - my $current_groups = Bugzilla::Group->new_from_list($ids); - my $new_groups = dclone($current_groups); - - # Record the changes - if (exists $changes->{set}) { - $new_groups = $changes->{set}; - - # We need to check the user has bless rights on the existing groups - # If they don't, then we need to add them back to new_groups - foreach my $group (@$current_groups) { - if (! $user->can_bless($group->id)) { - push @$new_groups, $group - unless grep { $_->id eq $group->id } @$new_groups; - } - } + WHERE user_id = ? AND isbless = ? AND grant_type = ?}, undef, $self->id, + $is_bless, GRANT_DIRECT + ); + + my $current_groups = Bugzilla::Group->new_from_list($ids); + my $new_groups = dclone($current_groups); + + # Record the changes + if (exists $changes->{set}) { + $new_groups = $changes->{set}; + + # We need to check the user has bless rights on the existing groups + # If they don't, then we need to add them back to new_groups + foreach my $group (@$current_groups) { + if (!$user->can_bless($group->id)) { + push @$new_groups, $group unless grep { $_->id eq $group->id } @$new_groups; + } } - else { - foreach my $group (@{$changes->{remove} // []}) { - @$new_groups = grep { $_->id ne $group->id } @$new_groups; - } - foreach my $group (@{$changes->{add} // []}) { - push @$new_groups, $group - unless grep { $_->id eq $group->id } @$new_groups; - } + } + else { + foreach my $group (@{$changes->{remove} // []}) { + @$new_groups = grep { $_->id ne $group->id } @$new_groups; } - - # Stash the changes, so self->update can actually make them - my @diffs = diff_arrays($current_groups, $new_groups, 'id'); - if (scalar(@{$diffs[0]}) || scalar(@{$diffs[1]})) { - $self->{_group_changes}{$is_bless} = \@diffs; + foreach my $group (@{$changes->{add} // []}) { + push @$new_groups, $group unless grep { $_->id eq $group->id } @$new_groups; } + } + + # Stash the changes, so self->update can actually make them + my @diffs = diff_arrays($current_groups, $new_groups, 'id'); + if (scalar(@{$diffs[0]}) || scalar(@{$diffs[1]})) { + $self->{_group_changes}{$is_bless} = \@diffs; + } } sub _set_groups_to_object { - my $self = shift; - my $changes = shift; - my $user = Bugzilla->user; - - foreach my $key (keys %$changes) { - # Check we were given an array - unless (ref($changes->{$key}) eq 'ARRAY') { - ThrowCodeError( - 'param_invalid', - { param => $changes->{$key}, function => $key } - ); - } + my $self = shift; + my $changes = shift; + my $user = Bugzilla->user; - # Go through the array, and turn items into group objects - my @groups = (); - foreach my $value (@{$changes->{$key}}) { - my $type = $value =~ /^\d+$/ ? 'id' : 'name'; - my $group = Bugzilla::Group->new({$type => $value}); - - if (! $group || ! $user->can_bless($group->id)) { - ThrowUserError('auth_failure', - { group => $value, reason => 'cant_bless', - action => 'edit', object => 'users' }); - } - push @groups, $group; - } - $changes->{$key} = \@groups; + foreach my $key (keys %$changes) { + + # Check we were given an array + unless (ref($changes->{$key}) eq 'ARRAY') { + ThrowCodeError('param_invalid', {param => $changes->{$key}, function => $key}); + } + + # Go through the array, and turn items into group objects + my @groups = (); + foreach my $value (@{$changes->{$key}}) { + my $type = $value =~ /^\d+$/ ? 'id' : 'name'; + my $group = Bugzilla::Group->new({$type => $value}); + + if (!$group || !$user->can_bless($group->id)) { + ThrowUserError('auth_failure', + {group => $value, reason => 'cant_bless', action => 'edit', object => 'users'}); + } + push @groups, $group; } + $changes->{$key} = \@groups; + } - return $changes; + return $changes; } sub update_last_seen_date { - my $self = shift; - return unless $self->id; - my $dbh = Bugzilla->dbh; - my $date = $dbh->selectrow_array( - 'SELECT ' . $dbh->sql_date_format('NOW()', '%Y-%m-%d')); - - if (!$self->last_seen_date or $date ne $self->last_seen_date) { - $self->{last_seen_date} = $date; - # We don't use the normal update() routine here as we only - # want to update the last_seen_date column, not any other - # pending changes - $dbh->do("UPDATE profiles SET last_seen_date = ? WHERE userid = ?", - undef, $date, $self->id); - Bugzilla->memcached->clear({ table => 'profiles', id => $self->id }); - } + my $self = shift; + return unless $self->id; + my $dbh = Bugzilla->dbh; + my $date = $dbh->selectrow_array( + 'SELECT ' . $dbh->sql_date_format('NOW()', '%Y-%m-%d')); + + if (!$self->last_seen_date or $date ne $self->last_seen_date) { + $self->{last_seen_date} = $date; + + # We don't use the normal update() routine here as we only + # want to update the last_seen_date column, not any other + # pending changes + $dbh->do("UPDATE profiles SET last_seen_date = ? WHERE userid = ?", + undef, $date, $self->id); + Bugzilla->memcached->clear({table => 'profiles', id => $self->id}); + } } ################################################################################ @@ -467,171 +466,181 @@ sub update_last_seen_date { ################################################################################ # Accessors for user attributes -sub name { $_[0]->{realname}; } -sub login { $_[0]->{login_name}; } -sub extern_id { $_[0]->{extern_id}; } -sub email { $_[0]->login . Bugzilla->params->{'emailsuffix'}; } -sub disabledtext { $_[0]->{'disabledtext'}; } -sub is_enabled { $_[0]->{'is_enabled'} ? 1 : 0; } +sub name { $_[0]->{realname}; } +sub login { $_[0]->{login_name}; } +sub extern_id { $_[0]->{extern_id}; } +sub email { $_[0]->login . Bugzilla->params->{'emailsuffix'}; } +sub disabledtext { $_[0]->{'disabledtext'}; } +sub is_enabled { $_[0]->{'is_enabled'} ? 1 : 0; } sub showmybugslink { $_[0]->{showmybugslink}; } sub email_disabled { $_[0]->{disable_mail}; } -sub email_enabled { !($_[0]->{disable_mail}); } +sub email_enabled { !($_[0]->{disable_mail}); } sub last_seen_date { $_[0]->{last_seen_date}; } + sub cryptpassword { - my $self = shift; - # We don't store it because we never want it in the object (we - # don't want to accidentally dump even the hash somewhere). - my ($pw) = Bugzilla->dbh->selectrow_array( - 'SELECT cryptpassword FROM profiles WHERE userid = ?', - undef, $self->id); - return $pw; + my $self = shift; + + # We don't store it because we never want it in the object (we + # don't want to accidentally dump even the hash somewhere). + my ($pw) + = Bugzilla->dbh->selectrow_array( + 'SELECT cryptpassword FROM profiles WHERE userid = ?', + undef, $self->id); + return $pw; } sub set_authorizer { - my ($self, $authorizer) = @_; - $self->{authorizer} = $authorizer; + my ($self, $authorizer) = @_; + $self->{authorizer} = $authorizer; } + sub authorizer { - my ($self) = @_; - if (!$self->{authorizer}) { - require Bugzilla::Auth; - $self->{authorizer} = new Bugzilla::Auth(); - } - return $self->{authorizer}; + my ($self) = @_; + if (!$self->{authorizer}) { + require Bugzilla::Auth; + $self->{authorizer} = new Bugzilla::Auth(); + } + return $self->{authorizer}; } # Generate a string to identify the user by name + login if the user # has a name or by login only if they don't. sub identity { - my $self = shift; + my $self = shift; - return "" unless $self->id; + return "" unless $self->id; - if (!defined $self->{identity}) { - $self->{identity} = - $self->name ? $self->name . " <" . $self->login. ">" : $self->login; - } + if (!defined $self->{identity}) { + $self->{identity} + = $self->name ? $self->name . " <" . $self->login . ">" : $self->login; + } - return $self->{identity}; + return $self->{identity}; } sub nick { - my $self = shift; + my $self = shift; - return "" unless $self->id; + return "" unless $self->id; - if (!defined $self->{nick}) { - $self->{nick} = (split(/@/, $self->login, 2))[0]; - } + if (!defined $self->{nick}) { + $self->{nick} = (split(/@/, $self->login, 2))[0]; + } - return $self->{nick}; + return $self->{nick}; } sub queries { - my $self = shift; - return $self->{queries} if defined $self->{queries}; - return [] unless $self->id; + my $self = shift; + return $self->{queries} if defined $self->{queries}; + return [] unless $self->id; - my $dbh = Bugzilla->dbh; - my $query_ids = $dbh->selectcol_arrayref( - 'SELECT id FROM namedqueries WHERE userid = ?', undef, $self->id); - require Bugzilla::Search::Saved; - $self->{queries} = Bugzilla::Search::Saved->new_from_list($query_ids); + my $dbh = Bugzilla->dbh; + my $query_ids + = $dbh->selectcol_arrayref('SELECT id FROM namedqueries WHERE userid = ?', + undef, $self->id); + require Bugzilla::Search::Saved; + $self->{queries} = Bugzilla::Search::Saved->new_from_list($query_ids); - # We preload link_in_footer from here as this information is always requested. - # This only works if the user object represents the current logged in user. - Bugzilla::Search::Saved::preload($self->{queries}) if $self->id == Bugzilla->user->id; + # We preload link_in_footer from here as this information is always requested. + # This only works if the user object represents the current logged in user. + Bugzilla::Search::Saved::preload($self->{queries}) + if $self->id == Bugzilla->user->id; - return $self->{queries}; + return $self->{queries}; } sub queries_subscribed { - my $self = shift; - return $self->{queries_subscribed} if defined $self->{queries_subscribed}; - return [] unless $self->id; - - # Exclude the user's own queries. - my @my_query_ids = map($_->id, @{$self->queries}); - my $query_id_string = join(',', @my_query_ids) || '-1'; - - # Only show subscriptions that we can still actually see. If a - # user changes the shared group of a query, our subscription - # will remain but we won't have access to the query anymore. - my $subscribed_query_ids = Bugzilla->dbh->selectcol_arrayref( - "SELECT lif.namedquery_id + my $self = shift; + return $self->{queries_subscribed} if defined $self->{queries_subscribed}; + return [] unless $self->id; + + # Exclude the user's own queries. + my @my_query_ids = map($_->id, @{$self->queries}); + my $query_id_string = join(',', @my_query_ids) || '-1'; + + # Only show subscriptions that we can still actually see. If a + # user changes the shared group of a query, our subscription + # will remain but we won't have access to the query anymore. + my $subscribed_query_ids = Bugzilla->dbh->selectcol_arrayref( + "SELECT lif.namedquery_id FROM namedqueries_link_in_footer lif INNER JOIN namedquery_group_map ngm ON ngm.namedquery_id = lif.namedquery_id WHERE lif.user_id = ? AND lif.namedquery_id NOT IN ($query_id_string) - AND " . $self->groups_in_sql, - undef, $self->id); - require Bugzilla::Search::Saved; - $self->{queries_subscribed} = - Bugzilla::Search::Saved->new_from_list($subscribed_query_ids); - return $self->{queries_subscribed}; + AND " . $self->groups_in_sql, undef, $self->id + ); + require Bugzilla::Search::Saved; + $self->{queries_subscribed} + = Bugzilla::Search::Saved->new_from_list($subscribed_query_ids); + return $self->{queries_subscribed}; } sub queries_available { - my $self = shift; - return $self->{queries_available} if defined $self->{queries_available}; - return [] unless $self->id; + my $self = shift; + return $self->{queries_available} if defined $self->{queries_available}; + return [] unless $self->id; - # Exclude the user's own queries. - my @my_query_ids = map($_->id, @{$self->queries}); - my $query_id_string = join(',', @my_query_ids) || '-1'; + # Exclude the user's own queries. + my @my_query_ids = map($_->id, @{$self->queries}); + my $query_id_string = join(',', @my_query_ids) || '-1'; - my $avail_query_ids = Bugzilla->dbh->selectcol_arrayref( - 'SELECT namedquery_id FROM namedquery_group_map - WHERE ' . $self->groups_in_sql . " - AND namedquery_id NOT IN ($query_id_string)"); - require Bugzilla::Search::Saved; - $self->{queries_available} = - Bugzilla::Search::Saved->new_from_list($avail_query_ids); - return $self->{queries_available}; + my $avail_query_ids = Bugzilla->dbh->selectcol_arrayref( + 'SELECT namedquery_id FROM namedquery_group_map + WHERE ' . $self->groups_in_sql . " + AND namedquery_id NOT IN ($query_id_string)" + ); + require Bugzilla::Search::Saved; + $self->{queries_available} + = Bugzilla::Search::Saved->new_from_list($avail_query_ids); + return $self->{queries_available}; } sub tags { - my $self = shift; - my $dbh = Bugzilla->dbh; - - if (!defined $self->{tags}) { - # We must use LEFT JOIN instead of INNER JOIN as we may be - # in the process of inserting a new tag to some bugs, - # in which case there are no bugs with this tag yet. - $self->{tags} = $dbh->selectall_hashref( - 'SELECT name, id, COUNT(bug_id) AS bug_count + my $self = shift; + my $dbh = Bugzilla->dbh; + + if (!defined $self->{tags}) { + + # We must use LEFT JOIN instead of INNER JOIN as we may be + # in the process of inserting a new tag to some bugs, + # in which case there are no bugs with this tag yet. + $self->{tags} = $dbh->selectall_hashref( + 'SELECT name, id, COUNT(bug_id) AS bug_count FROM tag LEFT JOIN bug_tag ON bug_tag.tag_id = tag.id - WHERE user_id = ? ' . $dbh->sql_group_by('id', 'name'), - 'name', undef, $self->id); - } - return $self->{tags}; + WHERE user_id = ? ' . $dbh->sql_group_by('id', 'name'), 'name', undef, + $self->id + ); + } + return $self->{tags}; } sub bugs_ignored { - my ($self) = @_; - my $dbh = Bugzilla->dbh; - if (!defined $self->{'bugs_ignored'}) { - $self->{'bugs_ignored'} = $dbh->selectall_arrayref( - 'SELECT bugs.bug_id AS id, + my ($self) = @_; + my $dbh = Bugzilla->dbh; + if (!defined $self->{'bugs_ignored'}) { + $self->{'bugs_ignored'} = $dbh->selectall_arrayref( + 'SELECT bugs.bug_id AS id, bugs.bug_status AS status, bugs.short_desc AS summary FROM bugs INNER JOIN email_bug_ignore ON bugs.bug_id = email_bug_ignore.bug_id - WHERE user_id = ?', - { Slice => {} }, $self->id); - # Go ahead and load these into the visible bugs cache - # to speed up can_see_bug checks later - $self->visible_bugs([ map { $_->{'id'} } @{ $self->{'bugs_ignored'} } ]); - } - return $self->{'bugs_ignored'}; + WHERE user_id = ?', {Slice => {}}, $self->id + ); + + # Go ahead and load these into the visible bugs cache + # to speed up can_see_bug checks later + $self->visible_bugs([map { $_->{'id'} } @{$self->{'bugs_ignored'}}]); + } + return $self->{'bugs_ignored'}; } sub is_bug_ignored { - my ($self, $bug_id) = @_; - return (grep {$_->{'id'} == $bug_id} @{$self->bugs_ignored}) ? 1 : 0; + my ($self, $bug_id) = @_; + return (grep { $_->{'id'} == $bug_id } @{$self->bugs_ignored}) ? 1 : 0; } ########################## @@ -639,309 +648,312 @@ sub is_bug_ignored { ########################## sub recent_searches { - my $self = shift; - $self->{recent_searches} ||= - Bugzilla::Search::Recent->match({ user_id => $self->id }); - return $self->{recent_searches}; + my $self = shift; + $self->{recent_searches} + ||= Bugzilla::Search::Recent->match({user_id => $self->id}); + return $self->{recent_searches}; } sub recent_search_containing { - my ($self, $bug_id) = @_; - my $searches = $self->recent_searches; + my ($self, $bug_id) = @_; + my $searches = $self->recent_searches; - foreach my $search (@$searches) { - return $search if grep($_ == $bug_id, @{ $search->bug_list }); - } + foreach my $search (@$searches) { + return $search if grep($_ == $bug_id, @{$search->bug_list}); + } - return undef; + return undef; } sub recent_search_for { - my ($self, $bug) = @_; - my $params = Bugzilla->input_params; - my $cgi = Bugzilla->cgi; - - if ($self->id) { - # First see if there's a list_id parameter in the query string. - my $list_id = $params->{list_id}; - if (!$list_id) { - # If not, check for "list_id" in the query string of the referer. - my $referer = $cgi->referer; - if ($referer) { - my $uri = URI->new($referer); - if ($uri->path =~ /buglist\.cgi$/) { - $list_id = $uri->query_param('list_id') - || $uri->query_param('regetlastlist'); - } - } + my ($self, $bug) = @_; + my $params = Bugzilla->input_params; + my $cgi = Bugzilla->cgi; + + if ($self->id) { + + # First see if there's a list_id parameter in the query string. + my $list_id = $params->{list_id}; + if (!$list_id) { + + # If not, check for "list_id" in the query string of the referer. + my $referer = $cgi->referer; + if ($referer) { + my $uri = URI->new($referer); + if ($uri->path =~ /buglist\.cgi$/) { + $list_id = $uri->query_param('list_id') || $uri->query_param('regetlastlist'); } + } + } - if ($list_id && $list_id ne 'cookie') { - # If we got a bad list_id (either some other user's or an expired - # one) don't crash, just don't return that list. - my $search = Bugzilla::Search::Recent->check_quietly( - { id => $list_id }); - return $search if $search; - } + if ($list_id && $list_id ne 'cookie') { - # If there's no list_id, see if the current bug's id is contained - # in any of the user's saved lists. - my $search = $self->recent_search_containing($bug->id); - return $search if $search; + # If we got a bad list_id (either some other user's or an expired + # one) don't crash, just don't return that list. + my $search = Bugzilla::Search::Recent->check_quietly({id => $list_id}); + return $search if $search; } - # Finally (or always, if we're logged out), if there's a BUGLIST cookie - # and the selected bug is in the list, then return the cookie as a fake - # Search::Recent object. - if (my $list = $cgi->cookie('BUGLIST')) { - # Also split on colons, which was used as a separator in old cookies. - my @bug_ids = split(/[:-]/, $list); - if (grep { $_ == $bug->id } @bug_ids) { - my $search = Bugzilla::Search::Recent->new_from_cookie(\@bug_ids); - return $search; - } + # If there's no list_id, see if the current bug's id is contained + # in any of the user's saved lists. + my $search = $self->recent_search_containing($bug->id); + return $search if $search; + } + + # Finally (or always, if we're logged out), if there's a BUGLIST cookie + # and the selected bug is in the list, then return the cookie as a fake + # Search::Recent object. + if (my $list = $cgi->cookie('BUGLIST')) { + + # Also split on colons, which was used as a separator in old cookies. + my @bug_ids = split(/[:-]/, $list); + if (grep { $_ == $bug->id } @bug_ids) { + my $search = Bugzilla::Search::Recent->new_from_cookie(\@bug_ids); + return $search; } + } - return undef; + return undef; } sub save_last_search { - my ($self, $params) = @_; - my ($bug_ids, $order, $vars, $list_id) = - @$params{qw(bugs order vars list_id)}; - - my $cgi = Bugzilla->cgi; - if ($order) { - $cgi->send_cookie(-name => 'LASTORDER', - -value => $order, - -expires => 'Fri, 01-Jan-2038 00:00:00 GMT'); - } + my ($self, $params) = @_; + my ($bug_ids, $order, $vars, $list_id) = @$params{qw(bugs order vars list_id)}; + + my $cgi = Bugzilla->cgi; + if ($order) { + $cgi->send_cookie( + -name => 'LASTORDER', + -value => $order, + -expires => 'Fri, 01-Jan-2038 00:00:00 GMT' + ); + } - return if !@$bug_ids; - - my $search; - if ($self->id) { - on_main_db { - if ($list_id) { - $search = Bugzilla::Search::Recent->check_quietly({ id => $list_id }); - } - - if ($search) { - if (join(',', @{$search->bug_list}) ne join(',', @$bug_ids)) { - $search->set_bug_list($bug_ids); - } - if (!$search->list_order || $order ne $search->list_order) { - $search->set_list_order($order); - } - $search->update(); - } - else { - # If we already have an existing search with a totally - # identical bug list, then don't create a new one. This - # prevents people from writing over their whole - # recent-search list by just refreshing a saved search - # (which doesn't have list_id in the header) over and over. - my $list_string = join(',', @$bug_ids); - my $existing_search = Bugzilla::Search::Recent->match({ - user_id => $self->id, bug_list => $list_string }); - - if (!scalar(@$existing_search)) { - $search = Bugzilla::Search::Recent->create({ - user_id => $self->id, - bug_list => $bug_ids, - list_order => $order }); - } - else { - $search = $existing_search->[0]; - } - } - }; - delete $self->{recent_searches}; - } - # Logged-out users use a cookie to store a single last search. We don't - # override that cookie with the logged-in user's latest search, because - # if they did one search while logged out and another while logged in, - # they may still want to navigate through the search they made while - # logged out. - else { - my $bug_list = join('-', @$bug_ids); - if (length($bug_list) < 4000) { - $cgi->send_cookie(-name => 'BUGLIST', - -value => $bug_list, - -expires => 'Fri, 01-Jan-2038 00:00:00 GMT'); + return if !@$bug_ids; + + my $search; + if ($self->id) { + on_main_db { + if ($list_id) { + $search = Bugzilla::Search::Recent->check_quietly({id => $list_id}); + } + + if ($search) { + if (join(',', @{$search->bug_list}) ne join(',', @$bug_ids)) { + $search->set_bug_list($bug_ids); + } + if (!$search->list_order || $order ne $search->list_order) { + $search->set_list_order($order); + } + $search->update(); + } + else { + # If we already have an existing search with a totally + # identical bug list, then don't create a new one. This + # prevents people from writing over their whole + # recent-search list by just refreshing a saved search + # (which doesn't have list_id in the header) over and over. + my $list_string = join(',', @$bug_ids); + my $existing_search = Bugzilla::Search::Recent->match( + {user_id => $self->id, bug_list => $list_string}); + + if (!scalar(@$existing_search)) { + $search = Bugzilla::Search::Recent->create( + {user_id => $self->id, bug_list => $bug_ids, list_order => $order}); } else { - $cgi->remove_cookie('BUGLIST'); - $vars->{'toolong'} = 1; + $search = $existing_search->[0]; } + } + }; + delete $self->{recent_searches}; + } + + # Logged-out users use a cookie to store a single last search. We don't + # override that cookie with the logged-in user's latest search, because + # if they did one search while logged out and another while logged in, + # they may still want to navigate through the search they made while + # logged out. + else { + my $bug_list = join('-', @$bug_ids); + if (length($bug_list) < 4000) { + $cgi->send_cookie( + -name => 'BUGLIST', + -value => $bug_list, + -expires => 'Fri, 01-Jan-2038 00:00:00 GMT' + ); } - return $search; + else { + $cgi->remove_cookie('BUGLIST'); + $vars->{'toolong'} = 1; + } + } + return $search; } sub reports { - my $self = shift; - return $self->{reports} if defined $self->{reports}; - return [] unless $self->id; + my $self = shift; + return $self->{reports} if defined $self->{reports}; + return [] unless $self->id; - my $dbh = Bugzilla->dbh; - my $report_ids = $dbh->selectcol_arrayref( - 'SELECT id FROM reports WHERE user_id = ?', undef, $self->id); - require Bugzilla::Report; - $self->{reports} = Bugzilla::Report->new_from_list($report_ids); - return $self->{reports}; + my $dbh = Bugzilla->dbh; + my $report_ids + = $dbh->selectcol_arrayref('SELECT id FROM reports WHERE user_id = ?', + undef, $self->id); + require Bugzilla::Report; + $self->{reports} = Bugzilla::Report->new_from_list($report_ids); + return $self->{reports}; } sub flush_reports_cache { - my $self = shift; + my $self = shift; - delete $self->{reports}; + delete $self->{reports}; } sub settings { - my ($self) = @_; + my ($self) = @_; - return $self->{'settings'} if (defined $self->{'settings'}); + return $self->{'settings'} if (defined $self->{'settings'}); - # IF the user is logged in - # THEN get the user's settings - # ELSE get default settings - if ($self->id) { - $self->{'settings'} = get_all_settings($self->id); - } else { - $self->{'settings'} = get_defaults(); - } + # IF the user is logged in + # THEN get the user's settings + # ELSE get default settings + if ($self->id) { + $self->{'settings'} = get_all_settings($self->id); + } + else { + $self->{'settings'} = get_defaults(); + } - return $self->{'settings'}; + return $self->{'settings'}; } sub setting { - my ($self, $name) = @_; - return $self->settings->{$name}->{'value'}; + my ($self, $name) = @_; + return $self->settings->{$name}->{'value'}; } sub timezone { - my $self = shift; + my $self = shift; - if (!defined $self->{timezone}) { - my $tz = $self->setting('timezone'); - if ($tz eq 'local') { - # The user wants the local timezone of the server. - $self->{timezone} = Bugzilla->local_timezone; - } - else { - $self->{timezone} = DateTime::TimeZone->new(name => $tz); - } + if (!defined $self->{timezone}) { + my $tz = $self->setting('timezone'); + if ($tz eq 'local') { + + # The user wants the local timezone of the server. + $self->{timezone} = Bugzilla->local_timezone; + } + 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 @@ -949,296 +961,311 @@ sub is_involved_in_bug { # when profiling xt/search.t.) So we cache the group ids separately from # groups for functions that need the group ids. sub _group_ids { - my ($self) = @_; - $self->{group_ids} ||= [map { $_->id } @{ $self->groups }]; - return $self->{group_ids}; + my ($self) = @_; + $self->{group_ids} ||= [map { $_->id } @{$self->groups}]; + return $self->{group_ids}; } sub groups_as_string { - my $self = shift; - my $ids = $self->_group_ids; - return scalar(@$ids) ? join(',', @$ids) : '-1'; + my $self = shift; + my $ids = $self->_group_ids; + return scalar(@$ids) ? join(',', @$ids) : '-1'; } sub groups_in_sql { - my ($self, $field) = @_; - $field ||= 'group_id'; - my $ids = $self->_group_ids; - $ids = [-1] if !scalar @$ids; - return Bugzilla->dbh->sql_in($field, $ids); + my ($self, $field) = @_; + $field ||= 'group_id'; + my $ids = $self->_group_ids; + $ids = [-1] if !scalar @$ids; + return Bugzilla->dbh->sql_in($field, $ids); } sub bless_groups { - my $self = shift; + my $self = shift; - return $self->{'bless_groups'} if defined $self->{'bless_groups'}; - return [] unless $self->id; + return $self->{'bless_groups'} if defined $self->{'bless_groups'}; + return [] unless $self->id; - if ($self->in_group('editusers')) { - # Users having editusers permissions may bless all groups. - $self->{'bless_groups'} = [Bugzilla::Group->get_all]; - return $self->{'bless_groups'}; - } + if ($self->in_group('editusers')) { - if (Bugzilla->params->{usevisibilitygroups} - && !@{ $self->visible_groups_inherited }) { - return []; - } + # Users having editusers permissions may bless all groups. + $self->{'bless_groups'} = [Bugzilla::Group->get_all]; + return $self->{'bless_groups'}; + } + + if (Bugzilla->params->{usevisibilitygroups} + && !@{$self->visible_groups_inherited}) + { + return []; + } - my $dbh = Bugzilla->dbh; + my $dbh = Bugzilla->dbh; - # Get all groups for the user where they have direct bless privileges. - my $query = " + # Get all groups for the user where they have direct bless privileges. + my $query = " SELECT DISTINCT group_id FROM user_group_map WHERE user_id = ? AND isbless = 1"; - if (Bugzilla->params->{usevisibilitygroups}) { - $query .= " AND " - . $dbh->sql_in('group_id', $self->visible_groups_inherited); - } - - # Get all groups for the user where they are a member of a group that - # inherits bless privs. - my @group_ids = map { $_->id } @{ $self->groups }; - if (@group_ids) { - $query .= " + if (Bugzilla->params->{usevisibilitygroups}) { + $query .= " AND " . $dbh->sql_in('group_id', $self->visible_groups_inherited); + } + + # Get all groups for the user where they are a member of a group that + # inherits bless privs. + my @group_ids = map { $_->id } @{$self->groups}; + if (@group_ids) { + $query .= " UNION SELECT DISTINCT grantor_id FROM group_group_map WHERE grant_type = " . GROUP_BLESS . " AND " . $dbh->sql_in('member_id', \@group_ids); - if (Bugzilla->params->{usevisibilitygroups}) { - $query .= " AND " - . $dbh->sql_in('grantor_id', $self->visible_groups_inherited); - } + if (Bugzilla->params->{usevisibilitygroups}) { + $query .= " AND " . $dbh->sql_in('grantor_id', $self->visible_groups_inherited); } + } - my $ids = $dbh->selectcol_arrayref($query, undef, $self->id); - return $self->{bless_groups} = Bugzilla::Group->new_from_list($ids); + my $ids = $dbh->selectcol_arrayref($query, undef, $self->id); + return $self->{bless_groups} = Bugzilla::Group->new_from_list($ids); } sub in_group { - my ($self, $group, $product_id) = @_; - $group = $group->name if blessed $group; - if (scalar grep($_->name eq $group, @{ $self->groups })) { - return 1; - } - elsif ($product_id && detaint_natural($product_id)) { - # Make sure $group exists on a per-product basis. - return 0 unless (grep {$_ eq $group} PER_PRODUCT_PRIVILEGES); - - $self->{"product_$product_id"} = {} unless exists $self->{"product_$product_id"}; - if (!defined $self->{"product_$product_id"}->{$group}) { - my $dbh = Bugzilla->dbh; - my $in_group = $dbh->selectrow_array( - "SELECT 1 + my ($self, $group, $product_id) = @_; + $group = $group->name if blessed $group; + if (scalar grep($_->name eq $group, @{$self->groups})) { + return 1; + } + elsif ($product_id && detaint_natural($product_id)) { + + # Make sure $group exists on a per-product basis. + return 0 unless (grep { $_ eq $group } PER_PRODUCT_PRIVILEGES); + + $self->{"product_$product_id"} = {} + unless exists $self->{"product_$product_id"}; + if (!defined $self->{"product_$product_id"}->{$group}) { + my $dbh = Bugzilla->dbh; + my $in_group = $dbh->selectrow_array( + "SELECT 1 FROM group_control_map WHERE product_id = ? AND $group != 0 - AND " . $self->groups_in_sql . ' ' . - $dbh->sql_limit(1), - undef, $product_id); + AND " + . $self->groups_in_sql . ' ' . $dbh->sql_limit(1), undef, $product_id + ); - $self->{"product_$product_id"}->{$group} = $in_group ? 1 : 0; - } - return $self->{"product_$product_id"}->{$group}; + $self->{"product_$product_id"}->{$group} = $in_group ? 1 : 0; } - # If we come here, then the user is not in the requested group. - return 0; + return $self->{"product_$product_id"}->{$group}; + } + + # If we come here, then the user is not in the requested group. + return 0; } sub in_group_id { - my ($self, $id) = @_; - return grep($_->id == $id, @{ $self->groups }) ? 1 : 0; + my ($self, $id) = @_; + return grep($_->id == $id, @{$self->groups}) ? 1 : 0; } # This is a helper to get all groups which have an icon to be displayed # besides the name of the commenter. sub groups_with_icon { - my $self = shift; + my $self = shift; - return $self->{groups_with_icon} //= [grep { $_->icon_url } @{ $self->groups }]; + return $self->{groups_with_icon} //= [grep { $_->icon_url } @{$self->groups}]; } sub get_products_by_permission { - my ($self, $group) = @_; - # Make sure $group exists on a per-product basis. - return [] unless (grep {$_ eq $group} PER_PRODUCT_PRIVILEGES); + my ($self, $group) = @_; + + # Make sure $group exists on a per-product basis. + return [] unless (grep { $_ eq $group } PER_PRODUCT_PRIVILEGES); - my $product_ids = Bugzilla->dbh->selectcol_arrayref( - "SELECT DISTINCT product_id + my $product_ids = Bugzilla->dbh->selectcol_arrayref( + "SELECT DISTINCT product_id FROM group_control_map WHERE $group != 0 - AND " . $self->groups_in_sql); + AND " . $self->groups_in_sql + ); - # No need to go further if the user has no "special" privs. - return [] unless scalar(@$product_ids); - my %product_map = map { $_ => 1 } @$product_ids; + # No need to go further if the user has no "special" privs. + return [] unless scalar(@$product_ids); + my %product_map = map { $_ => 1 } @$product_ids; - # We will restrict the list to products the user can see. - my $selectable_products = $self->get_selectable_products; - my @products = grep { $product_map{$_->id} } @$selectable_products; - return \@products; + # We will restrict the list to products the user can see. + my $selectable_products = $self->get_selectable_products; + my @products = grep { $product_map{$_->id} } @$selectable_products; + return \@products; } sub can_see_user { - my ($self, $otherUser) = @_; - my $query; + my ($self, $otherUser) = @_; + my $query; - if (Bugzilla->params->{'usevisibilitygroups'}) { - # If the user can see no groups, then no users are visible either. - my $visibleGroups = $self->visible_groups_as_string() || return 0; - $query = qq{SELECT COUNT(DISTINCT userid) + if (Bugzilla->params->{'usevisibilitygroups'}) { + + # If the user can see no groups, then no users are visible either. + my $visibleGroups = $self->visible_groups_as_string() || return 0; + $query = qq{SELECT COUNT(DISTINCT userid) FROM profiles, user_group_map WHERE userid = ? AND user_id = userid AND isbless = 0 AND group_id IN ($visibleGroups) }; - } else { - $query = qq{SELECT COUNT(userid) + } + else { + $query = qq{SELECT COUNT(userid) FROM profiles WHERE userid = ? }; - } - return Bugzilla->dbh->selectrow_array($query, undef, $otherUser->id); + } + return Bugzilla->dbh->selectrow_array($query, undef, $otherUser->id); } sub can_edit_product { - my ($self, $prod_id) = @_; - my $dbh = Bugzilla->dbh; + my ($self, $prod_id) = @_; + my $dbh = Bugzilla->dbh; - if (Bugzilla->params->{'or_groups'}) { - my $groups = $self->groups_as_string; - # For or-groups, we check if there are any can_edit groups for the - # product, and if the user is in any of them. If there are none or - # the user is in at least one of them, they can edit the product - my ($cnt_can_edit, $cnt_group_member) = $dbh->selectrow_array( - "SELECT SUM(p.cnt_can_edit), + if (Bugzilla->params->{'or_groups'}) { + my $groups = $self->groups_as_string; + + # For or-groups, we check if there are any can_edit groups for the + # product, and if the user is in any of them. If there are none or + # the user is in at least one of them, they can edit the product + my ($cnt_can_edit, $cnt_group_member) = $dbh->selectrow_array( + "SELECT SUM(p.cnt_can_edit), SUM(p.cnt_group_member) FROM (SELECT CASE WHEN canedit = 1 THEN 1 ELSE 0 END AS cnt_can_edit, CASE WHEN canedit = 1 AND group_id IN ($groups) THEN 1 ELSE 0 END AS cnt_group_member FROM group_control_map - WHERE product_id = $prod_id) AS p"); - return (!$cnt_can_edit or $cnt_group_member); - } - else { - # For and-groups, a user needs to be in all canedit groups. Therefore - # if the user is not in a can_edit group for the product, they cannot - # edit the product. - my $has_external_groups = - $dbh->selectrow_array('SELECT 1 + WHERE product_id = $prod_id) AS p" + ); + return (!$cnt_can_edit or $cnt_group_member); + } + else { + # For and-groups, a user needs to be in all canedit groups. Therefore + # if the user is not in a can_edit group for the product, they cannot + # edit the product. + my $has_external_groups = $dbh->selectrow_array( + 'SELECT 1 FROM group_control_map WHERE product_id = ? AND canedit != 0 - AND group_id NOT IN(' . $self->groups_as_string . ')', - undef, $prod_id); + AND group_id NOT IN(' + . $self->groups_as_string . ')', undef, $prod_id + ); - return !$has_external_groups; - } + return !$has_external_groups; + } } sub can_see_bug { - my ($self, $bug_id) = @_; - return @{ $self->visible_bugs([$bug_id]) } ? 1 : 0; + my ($self, $bug_id) = @_; + return @{$self->visible_bugs([$bug_id])} ? 1 : 0; } sub visible_bugs { - my ($self, $bugs) = @_; - # Allow users to pass in Bug objects and bug ids both. - my @bug_ids = map { blessed $_ ? $_->id : $_ } @$bugs; - - # We only check the visibility of bugs that we haven't - # checked yet. - # Bugzilla::Bug->update automatically removes updated bugs - # from the cache to force them to be checked again. - my $visible_cache = $self->{_visible_bugs_cache} ||= {}; - my @check_ids = grep(!exists $visible_cache->{$_}, @bug_ids); - - if (@check_ids) { - foreach my $id (@check_ids) { - my $orig_id = $id; - detaint_natural($id) - || ThrowCodeError('param_must_be_numeric', { param => $orig_id, - function => 'Bugzilla::User->visible_bugs'}); - } + my ($self, $bugs) = @_; + + # Allow users to pass in Bug objects and bug ids both. + my @bug_ids = map { blessed $_ ? $_->id : $_ } @$bugs; + + # We only check the visibility of bugs that we haven't + # checked yet. + # Bugzilla::Bug->update automatically removes updated bugs + # from the cache to force them to be checked again. + my $visible_cache = $self->{_visible_bugs_cache} ||= {}; + my @check_ids = grep(!exists $visible_cache->{$_}, @bug_ids); - Bugzilla->params->{'or_groups'} - ? $self->_visible_bugs_check_or(\@check_ids) - : $self->_visible_bugs_check_and(\@check_ids); + if (@check_ids) { + foreach my $id (@check_ids) { + my $orig_id = $id; + detaint_natural($id) + || ThrowCodeError('param_must_be_numeric', + {param => $orig_id, function => 'Bugzilla::User->visible_bugs'}); } - return [grep { $visible_cache->{blessed $_ ? $_->id : $_} } @$bugs]; + Bugzilla->params->{'or_groups'} + ? $self->_visible_bugs_check_or(\@check_ids) + : $self->_visible_bugs_check_and(\@check_ids); + } + + return [grep { $visible_cache->{blessed $_ ? $_->id : $_} } @$bugs]; } sub _visible_bugs_check_or { - my ($self, $check_ids) = @_; - my $visible_cache = $self->{_visible_bugs_cache}; - my $dbh = Bugzilla->dbh; - my $user_id = $self->id; - - my $sth; - # Speed up the can_see_bug case. - if (scalar(@$check_ids) == 1) { - $sth = $self->{_sth_one_visible_bug}; - } - my $query = qq{ + my ($self, $check_ids) = @_; + my $visible_cache = $self->{_visible_bugs_cache}; + my $dbh = Bugzilla->dbh; + my $user_id = $self->id; + + my $sth; + + # Speed up the can_see_bug case. + if (scalar(@$check_ids) == 1) { + $sth = $self->{_sth_one_visible_bug}; + } + my $query = qq{ SELECT DISTINCT bugs.bug_id FROM bugs LEFT JOIN bug_group_map AS security_map ON bugs.bug_id = security_map.bug_id LEFT JOIN cc AS security_cc ON bugs.bug_id = security_cc.bug_id AND security_cc.who = $user_id WHERE bugs.bug_id IN (} . join(',', ('?') x @$check_ids) . qq{) - AND ((security_map.group_id IS NULL OR security_map.group_id IN (} . $self->groups_as_string . qq{)) + AND ((security_map.group_id IS NULL OR security_map.group_id IN (} + . $self->groups_as_string . qq{)) OR (bugs.reporter_accessible = 1 AND bugs.reporter = $user_id) OR (bugs.cclist_accessible = 1 AND security_cc.who IS NOT NULL) OR bugs.assigned_to = $user_id }; - if (Bugzilla->params->{'useqacontact'}) { - $query .= " OR bugs.qa_contact = $user_id"; - } - $query .= ')'; + if (Bugzilla->params->{'useqacontact'}) { + $query .= " OR bugs.qa_contact = $user_id"; + } + $query .= ')'; - $sth ||= $dbh->prepare($query); - if (scalar(@$check_ids) == 1) { - $self->{_sth_one_visible_bug} = $sth; - } + $sth ||= $dbh->prepare($query); + if (scalar(@$check_ids) == 1) { + $self->{_sth_one_visible_bug} = $sth; + } - # Set all bugs as non visible - foreach my $bug_id (@$check_ids) { - $visible_cache->{$bug_id} = 0; - } + # Set all bugs as non visible + foreach my $bug_id (@$check_ids) { + $visible_cache->{$bug_id} = 0; + } - # Now get the bugs the user can see - my $visible_bug_ids = $dbh->selectcol_arrayref($sth, undef, @$check_ids); - foreach my $bug_id (@$visible_bug_ids) { - $visible_cache->{$bug_id} = 1; - } + # Now get the bugs the user can see + my $visible_bug_ids = $dbh->selectcol_arrayref($sth, undef, @$check_ids); + foreach my $bug_id (@$visible_bug_ids) { + $visible_cache->{$bug_id} = 1; + } } sub _visible_bugs_check_and { - my ($self, $check_ids) = @_; - my $visible_cache = $self->{_visible_bugs_cache}; - my $dbh = Bugzilla->dbh; - my $user_id = $self->id; - - my $sth; - # Speed up the can_see_bug case. - if (scalar(@$check_ids) == 1) { - $sth = $self->{_sth_one_visible_bug}; - } - $sth ||= $dbh->prepare( - # This checks for groups that the bug is in that the user - # *isn't* in. Then, in the Perl code below, we check if - # the user can otherwise access the bug (for example, by being - # the assignee or QA Contact). - # - # The DISTINCT exists because the bug could be in *several* - # groups that the user isn't in, but they will all return the - # same result for bug_group_map.bug_id (so DISTINCT filters - # out duplicate rows). - "SELECT DISTINCT bugs.bug_id, reporter, assigned_to, qa_contact, + my ($self, $check_ids) = @_; + my $visible_cache = $self->{_visible_bugs_cache}; + my $dbh = Bugzilla->dbh; + my $user_id = $self->id; + + my $sth; + + # Speed up the can_see_bug case. + if (scalar(@$check_ids) == 1) { + $sth = $self->{_sth_one_visible_bug}; + } + $sth ||= $dbh->prepare( + + # This checks for groups that the bug is in that the user + # *isn't* in. Then, in the Perl code below, we check if + # the user can otherwise access the bug (for example, by being + # the assignee or QA Contact). + # + # The DISTINCT exists because the bug could be in *several* + # groups that the user isn't in, but they will all return the + # same result for bug_group_map.bug_id (so DISTINCT filters + # out duplicate rows). + "SELECT DISTINCT bugs.bug_id, reporter, assigned_to, qa_contact, reporter_accessible, cclist_accessible, cc.who, bug_group_map.bug_id FROM bugs @@ -1248,1056 +1275,1124 @@ sub _visible_bugs_check_and { LEFT JOIN bug_group_map ON bugs.bug_id = bug_group_map.bug_id AND bug_group_map.group_id NOT IN (" - . $self->groups_as_string . ') + . $self->groups_as_string . ') WHERE bugs.bug_id IN (' . join(',', ('?') x @$check_ids) . ') - AND creation_ts IS NOT NULL '); - if (scalar(@$check_ids) == 1) { - $self->{_sth_one_visible_bug} = $sth; - } - - $sth->execute(@$check_ids); - my $use_qa_contact = Bugzilla->params->{'useqacontact'}; - while (my $row = $sth->fetchrow_arrayref) { - my ($bug_id, $reporter, $owner, $qacontact, $reporter_access, - $cclist_access, $isoncclist, $missinggroup) = @$row; - $visible_cache->{$bug_id} ||= - ((($reporter == $user_id) && $reporter_access) - || ($use_qa_contact - && $qacontact && ($qacontact == $user_id)) - || ($owner == $user_id) - || ($isoncclist && $cclist_access) - || !$missinggroup) ? 1 : 0; - } + AND creation_ts IS NOT NULL ' + ); + if (scalar(@$check_ids) == 1) { + $self->{_sth_one_visible_bug} = $sth; + } + + $sth->execute(@$check_ids); + my $use_qa_contact = Bugzilla->params->{'useqacontact'}; + while (my $row = $sth->fetchrow_arrayref) { + my ($bug_id, $reporter, $owner, $qacontact, $reporter_access, $cclist_access, + $isoncclist, $missinggroup) + = @$row; + $visible_cache->{$bug_id} + ||= ((($reporter == $user_id) && $reporter_access) + || ($use_qa_contact && $qacontact && ($qacontact == $user_id)) + || ($owner == $user_id) + || ($isoncclist && $cclist_access) + || !$missinggroup) ? 1 : 0; + } } sub clear_product_cache { - my $self = shift; - delete $self->{enterable_products}; - delete $self->{selectable_products}; - delete $self->{selectable_classifications}; + my $self = shift; + delete $self->{enterable_products}; + delete $self->{selectable_products}; + delete $self->{selectable_classifications}; } sub can_see_product { - my ($self, $product_name) = @_; + my ($self, $product_name) = @_; - return scalar(grep {$_->name eq $product_name} @{$self->get_selectable_products}); + return + scalar(grep { $_->name eq $product_name } @{$self->get_selectable_products}); } sub get_selectable_products { - my $self = shift; - my $class_id = shift; - my $class_restricted = Bugzilla->params->{'useclassification'} && $class_id; + my $self = shift; + my $class_id = shift; + my $class_restricted = Bugzilla->params->{'useclassification'} && $class_id; - if (!defined $self->{selectable_products}) { - my $query = "SELECT id + if (!defined $self->{selectable_products}) { + my $query = "SELECT id FROM products LEFT JOIN group_control_map ON group_control_map.product_id = products.id - AND group_control_map.membercontrol = " . CONTROLMAPMANDATORY; - - if (Bugzilla->params->{'or_groups'}) { - # Either the user is in at least one of the MANDATORY groups, or - # there are no such groups for the product. - $query .= " WHERE group_id IN (" . $self->groups_as_string . ") + AND group_control_map.membercontrol = " + . CONTROLMAPMANDATORY; + + if (Bugzilla->params->{'or_groups'}) { + + # Either the user is in at least one of the MANDATORY groups, or + # there are no such groups for the product. + $query .= " WHERE group_id IN (" . $self->groups_as_string . ") OR group_id IS NULL"; - } - else { - # There must be no MANDATORY groups that the user is not in. - $query .= " AND group_id NOT IN (" . $self->groups_as_string . ") + } + else { + # There must be no MANDATORY groups that the user is not in. + $query .= " AND group_id NOT IN (" . $self->groups_as_string . ") WHERE group_id IS NULL"; - } - - my $prod_ids = Bugzilla->dbh->selectcol_arrayref($query); - $self->{selectable_products} = Bugzilla::Product->new_from_list($prod_ids); } - # Restrict the list of products to those being in the classification, if any. - if ($class_restricted) { - return [grep {$_->classification_id == $class_id} @{$self->{selectable_products}}]; - } - # If we come here, then we want all selectable products. - return $self->{selectable_products}; + my $prod_ids = Bugzilla->dbh->selectcol_arrayref($query); + $self->{selectable_products} = Bugzilla::Product->new_from_list($prod_ids); + } + + # Restrict the list of products to those being in the classification, if any. + if ($class_restricted) { + return [grep { $_->classification_id == $class_id } + @{$self->{selectable_products}}]; + } + + # If we come here, then we want all selectable products. + return $self->{selectable_products}; } sub get_selectable_classifications { - my ($self) = @_; + my ($self) = @_; - if (!defined $self->{selectable_classifications}) { - my $products = $self->get_selectable_products; - my %class_ids = map { $_->classification_id => 1 } @$products; + if (!defined $self->{selectable_classifications}) { + my $products = $self->get_selectable_products; + my %class_ids = map { $_->classification_id => 1 } @$products; - $self->{selectable_classifications} = Bugzilla::Classification->new_from_list([keys %class_ids]); - } - return $self->{selectable_classifications}; + $self->{selectable_classifications} + = Bugzilla::Classification->new_from_list([keys %class_ids]); + } + return $self->{selectable_classifications}; } sub can_enter_product { - my ($self, $input, $warn) = @_; - my $dbh = Bugzilla->dbh; - $warn ||= 0; - - $input = trim($input) if !ref $input; - if (!defined $input or $input eq '') { - return unless $warn == THROW_ERROR; - ThrowUserError('object_not_specified', - { class => 'Bugzilla::Product' }); - } + my ($self, $input, $warn) = @_; + my $dbh = Bugzilla->dbh; + $warn ||= 0; - if (!scalar @{ $self->get_enterable_products }) { - return unless $warn == THROW_ERROR; - ThrowUserError('no_products'); - } + $input = trim($input) if !ref $input; + if (!defined $input or $input eq '') { + return unless $warn == THROW_ERROR; + ThrowUserError('object_not_specified', {class => 'Bugzilla::Product'}); + } - my $product = blessed($input) ? $input - : new Bugzilla::Product({ name => $input }); - my $can_enter = - $product && grep($_->name eq $product->name, - @{ $self->get_enterable_products }); + if (!scalar @{$self->get_enterable_products}) { + return unless $warn == THROW_ERROR; + ThrowUserError('no_products'); + } - return $product if $can_enter; + my $product + = blessed($input) ? $input : new Bugzilla::Product({name => $input}); + my $can_enter = $product + && grep($_->name eq $product->name, @{$self->get_enterable_products}); - return 0 unless $warn == THROW_ERROR; + return $product if $can_enter; - # Check why access was denied. These checks are slow, - # but that's fine, because they only happen if we fail. + return 0 unless $warn == THROW_ERROR; - # We don't just use $product->name for error messages, because if it - # changes case from $input, then that's a clue that the product does - # exist but is hidden. - my $name = blessed($input) ? $input->name : $input; + # Check why access was denied. These checks are slow, + # but that's fine, because they only happen if we fail. - # The product could not exist or you could be denied... - if (!$product || !$product->user_has_access($self)) { - ThrowUserError('entry_access_denied', { product => $name }); - } - # It could be closed for bug entry... - elsif (!$product->is_active) { - ThrowUserError('product_disabled', { product => $product }); - } - # It could have no components... - elsif (!@{$product->components} - || !grep { $_->is_active } @{$product->components}) - { - ThrowUserError('missing_component', { product => $product }); - } - # It could have no versions... - elsif (!@{$product->versions} - || !grep { $_->is_active } @{$product->versions}) - { - ThrowUserError ('missing_version', { product => $product }); - } + # We don't just use $product->name for error messages, because if it + # changes case from $input, then that's a clue that the product does + # exist but is hidden. + my $name = blessed($input) ? $input->name : $input; + + # The product could not exist or you could be denied... + if (!$product || !$product->user_has_access($self)) { + ThrowUserError('entry_access_denied', {product => $name}); + } + + # It could be closed for bug entry... + elsif (!$product->is_active) { + ThrowUserError('product_disabled', {product => $product}); + } + + # It could have no components... + elsif (!@{$product->components} + || !grep { $_->is_active } @{$product->components}) + { + ThrowUserError('missing_component', {product => $product}); + } - die "can_enter_product reached an unreachable location."; + # It could have no versions... + elsif (!@{$product->versions} || !grep { $_->is_active } @{$product->versions}) + { + ThrowUserError('missing_version', {product => $product}); + } + + die "can_enter_product reached an unreachable location."; } sub get_enterable_products { - my $self = shift; - my $dbh = Bugzilla->dbh; + my $self = shift; + my $dbh = Bugzilla->dbh; - if (defined $self->{enterable_products}) { - return $self->{enterable_products}; - } + if (defined $self->{enterable_products}) { + return $self->{enterable_products}; + } - # All products which the user has "Entry" access to. - my $query = - 'SELECT products.id FROM products + # All products which the user has "Entry" access to. + my $query = 'SELECT products.id FROM products LEFT JOIN group_control_map ON group_control_map.product_id = products.id AND group_control_map.entry != 0'; - if (Bugzilla->params->{'or_groups'}) { - $query .= " WHERE (group_id IN (" . $self->groups_as_string . ")" . - " OR group_id IS NULL)"; - } else { - $query .= " AND group_id NOT IN (" . $self->groups_as_string . ")" . - " WHERE group_id IS NULL" - } - $query .= " AND products.isactive = 1"; - my $enterable_ids = $dbh->selectcol_arrayref($query); - - if (scalar @$enterable_ids) { - # And all of these products must have at least one component - # and one version. - $enterable_ids = $dbh->selectcol_arrayref( - 'SELECT DISTINCT products.id FROM products - WHERE ' . $dbh->sql_in('products.id', $enterable_ids) . - ' AND products.id IN (SELECT DISTINCT components.product_id + if (Bugzilla->params->{'or_groups'}) { + $query + .= " WHERE (group_id IN (" + . $self->groups_as_string . ")" + . " OR group_id IS NULL)"; + } + else { + $query + .= " AND group_id NOT IN (" + . $self->groups_as_string . ")" + . " WHERE group_id IS NULL"; + } + $query .= " AND products.isactive = 1"; + my $enterable_ids = $dbh->selectcol_arrayref($query); + + if (scalar @$enterable_ids) { + + # And all of these products must have at least one component + # and one version. + $enterable_ids = $dbh->selectcol_arrayref( + 'SELECT DISTINCT products.id FROM products + WHERE ' + . $dbh->sql_in('products.id', $enterable_ids) + . ' AND products.id IN (SELECT DISTINCT components.product_id FROM components WHERE components.isactive = 1) AND products.id IN (SELECT DISTINCT versions.product_id FROM versions - WHERE versions.isactive = 1)'); - } + WHERE versions.isactive = 1)' + ); + } - $self->{enterable_products} = - Bugzilla::Product->new_from_list($enterable_ids); - return $self->{enterable_products}; + $self->{enterable_products} = Bugzilla::Product->new_from_list($enterable_ids); + return $self->{enterable_products}; } sub can_access_product { - my ($self, $product) = @_; - my $product_name = blessed($product) ? $product->name : $product; - return scalar(grep {$_->name eq $product_name} @{$self->get_accessible_products}); + my ($self, $product) = @_; + my $product_name = blessed($product) ? $product->name : $product; + return + scalar(grep { $_->name eq $product_name } @{$self->get_accessible_products}); } sub get_accessible_products { - my $self = shift; - - # Map the objects into a hash using the ids as keys - my %products = map { $_->id => $_ } - @{$self->get_selectable_products}, - @{$self->get_enterable_products}; - - return [ sort { $a->name cmp $b->name } values %products ]; + my $self = shift; + + # Map the objects into a hash using the ids as keys + my %products = map { $_->id => $_ } @{$self->get_selectable_products}, + @{$self->get_enterable_products}; + + return [sort { $a->name cmp $b->name } values %products]; } sub can_administer { - my $self = shift; - - if (not defined $self->{can_administer}) { - my $can_administer = 0; - - $can_administer = 1 if $self->in_group('admin') - || $self->in_group('tweakparams') - || $self->in_group('editusers') - || $self->can_bless - || (Bugzilla->params->{'useclassification'} && $self->in_group('editclassifications')) - || $self->in_group('editcomponents') - || scalar(@{$self->get_products_by_permission('editcomponents')}) - || $self->in_group('creategroups') - || $self->in_group('editkeywords') - || $self->in_group('bz_canusewhines'); - - Bugzilla::Hook::process('user_can_administer', { can_administer => \$can_administer }); - $self->{can_administer} = $can_administer; - } + my $self = shift; + + if (not defined $self->{can_administer}) { + my $can_administer = 0; + + $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'); - return $self->{can_administer}; + Bugzilla::Hook::process('user_can_administer', + {can_administer => \$can_administer}); + $self->{can_administer} = $can_administer; + } + + return $self->{can_administer}; } sub check_can_admin_product { - my ($self, $product_name) = @_; + my ($self, $product_name) = @_; - # First make sure the product name is valid. - my $product = Bugzilla::Product->check($product_name); + # First make sure the product name is valid. + my $product = Bugzilla::Product->check($product_name); - ($self->in_group('editcomponents', $product->id) - && $self->can_see_product($product->name)) - || ThrowUserError('product_admin_denied', {product => $product->name}); + ( $self->in_group('editcomponents', $product->id) + && $self->can_see_product($product->name)) + || ThrowUserError('product_admin_denied', {product => $product->name}); - # Return the validated product object. - return $product; + # Return the validated product object. + return $product; } sub check_can_admin_flagtype { - my ($self, $flagtype_id) = @_; - - my $flagtype = Bugzilla::FlagType->check({ id => $flagtype_id }); - my $can_fully_edit = 1; - - if (!$self->in_group('editcomponents')) { - my $products = $self->get_products_by_permission('editcomponents'); - # You need editcomponents privs for at least one product to have - # a chance to edit the flagtype. - scalar(@$products) - || ThrowUserError('auth_failure', {group => 'editcomponents', - action => 'edit', - object => 'flagtypes'}); - my $can_admin = 0; - my $i = $flagtype->inclusions_as_hash; - my $e = $flagtype->exclusions_as_hash; - - # If there is at least one product for which the user doesn't have - # editcomponents privs, then don't allow them to do everything with - # this flagtype, independently of whether this product is in the - # exclusion list or not. - my %product_ids; - map { $product_ids{$_->id} = 1 } @$products; - $can_fully_edit = 0 if grep { !$product_ids{$_} } keys %$i; - - unless ($e->{0}->{0}) { - foreach my $product (@$products) { - my $id = $product->id; - next if $e->{$id}->{0}; - # If we are here, the product has not been explicitly excluded. - # Check whether it's explicitly included, or at least one of - # its components. - $can_admin = ($i->{0}->{0} || $i->{$id}->{0} - || scalar(grep { !$e->{$id}->{$_} } keys %{$i->{$id}})); - last if $can_admin; - } - } - $can_admin || ThrowUserError('flag_type_not_editable', { flagtype => $flagtype }); - } - return wantarray ? ($flagtype, $can_fully_edit) : $flagtype; + my ($self, $flagtype_id) = @_; + + my $flagtype = Bugzilla::FlagType->check({id => $flagtype_id}); + my $can_fully_edit = 1; + + if (!$self->in_group('editcomponents')) { + my $products = $self->get_products_by_permission('editcomponents'); + + # You need editcomponents privs for at least one product to have + # a chance to edit the flagtype. + scalar(@$products) + || ThrowUserError('auth_failure', + {group => 'editcomponents', action => 'edit', object => 'flagtypes'}); + my $can_admin = 0; + my $i = $flagtype->inclusions_as_hash; + my $e = $flagtype->exclusions_as_hash; + + # If there is at least one product for which the user doesn't have + # editcomponents privs, then don't allow them to do everything with + # this flagtype, independently of whether this product is in the + # exclusion list or not. + my %product_ids; + map { $product_ids{$_->id} = 1 } @$products; + $can_fully_edit = 0 if grep { !$product_ids{$_} } keys %$i; + + unless ($e->{0}->{0}) { + foreach my $product (@$products) { + my $id = $product->id; + next if $e->{$id}->{0}; + + # If we are here, the product has not been explicitly excluded. + # Check whether it's explicitly included, or at least one of + # its components. + $can_admin + = ( $i->{0}->{0} + || $i->{$id}->{0} + || scalar(grep { !$e->{$id}->{$_} } keys %{$i->{$id}})); + last if $can_admin; + } + } + $can_admin || ThrowUserError('flag_type_not_editable', {flagtype => $flagtype}); + } + return wantarray ? ($flagtype, $can_fully_edit) : $flagtype; } sub can_request_flag { - my ($self, $flag_type) = @_; + my ($self, $flag_type) = @_; - return ($self->can_set_flag($flag_type) - || !$flag_type->request_group_id - || $self->in_group_id($flag_type->request_group_id)) ? 1 : 0; + return ($self->can_set_flag($flag_type) + || !$flag_type->request_group_id + || $self->in_group_id($flag_type->request_group_id)) ? 1 : 0; } sub can_set_flag { - my ($self, $flag_type) = @_; + my ($self, $flag_type) = @_; - return (!$flag_type->grant_group_id - || $self->in_group_id($flag_type->grant_group_id)) ? 1 : 0; + return (!$flag_type->grant_group_id + || $self->in_group_id($flag_type->grant_group_id)) ? 1 : 0; } # visible_groups_inherited returns a reference to a list of all the groups # whose members are visible to this user. sub visible_groups_inherited { - my $self = shift; - return $self->{visible_groups_inherited} if defined $self->{visible_groups_inherited}; - return [] unless $self->id; - my @visgroups = @{$self->visible_groups_direct}; - @visgroups = @{Bugzilla::Group->flatten_group_membership(@visgroups)}; - $self->{visible_groups_inherited} = \@visgroups; - return $self->{visible_groups_inherited}; + my $self = shift; + return $self->{visible_groups_inherited} + if defined $self->{visible_groups_inherited}; + return [] unless $self->id; + my @visgroups = @{$self->visible_groups_direct}; + @visgroups = @{Bugzilla::Group->flatten_group_membership(@visgroups)}; + $self->{visible_groups_inherited} = \@visgroups; + return $self->{visible_groups_inherited}; } # visible_groups_direct returns a reference to a list of all the groups that # are visible to this user. sub visible_groups_direct { - my $self = shift; - my @visgroups = (); - return $self->{visible_groups_direct} if defined $self->{visible_groups_direct}; - return [] unless $self->id; - - my $dbh = Bugzilla->dbh; - my $sth; - - if (Bugzilla->params->{'usevisibilitygroups'}) { - $sth = $dbh->prepare("SELECT DISTINCT grantor_id + my $self = shift; + my @visgroups = (); + return $self->{visible_groups_direct} if defined $self->{visible_groups_direct}; + return [] unless $self->id; + + my $dbh = Bugzilla->dbh; + my $sth; + + if (Bugzilla->params->{'usevisibilitygroups'}) { + $sth = $dbh->prepare( + "SELECT DISTINCT grantor_id FROM group_group_map WHERE " . $self->groups_in_sql('member_id') . " - AND grant_type=" . GROUP_VISIBLE); - } - else { - # All groups are visible if usevisibilitygroups is off. - $sth = $dbh->prepare('SELECT id FROM groups'); - } - $sth->execute(); + AND grant_type=" . GROUP_VISIBLE + ); + } + else { + # All groups are visible if usevisibilitygroups is off. + $sth = $dbh->prepare('SELECT id FROM groups'); + } + $sth->execute(); - while (my ($row) = $sth->fetchrow_array) { - push @visgroups,$row; - } - $self->{visible_groups_direct} = \@visgroups; + while (my ($row) = $sth->fetchrow_array) { + push @visgroups, $row; + } + $self->{visible_groups_direct} = \@visgroups; - return $self->{visible_groups_direct}; + return $self->{visible_groups_direct}; } sub visible_groups_as_string { - my $self = shift; - return join(', ', @{$self->visible_groups_inherited()}); + my $self = shift; + return join(', ', @{$self->visible_groups_inherited()}); } # This function defines the groups a user may share a query with. # More restrictive sites may want to build this reference to a list of group IDs # from bless_groups instead of mirroring visible_groups_inherited, perhaps. sub queryshare_groups { - my $self = shift; - my @queryshare_groups; - - return $self->{queryshare_groups} if defined $self->{queryshare_groups}; - - if ($self->in_group(Bugzilla->params->{'querysharegroup'})) { - # We want to be allowed to share with groups we're in only. - # If usevisibilitygroups is on, then we need to restrict this to groups - # we may see. - if (Bugzilla->params->{'usevisibilitygroups'}) { - foreach(@{$self->visible_groups_inherited()}) { - next unless $self->in_group_id($_); - push(@queryshare_groups, $_); - } - } - else { - @queryshare_groups = @{ $self->_group_ids }; - } + my $self = shift; + my @queryshare_groups; + + return $self->{queryshare_groups} if defined $self->{queryshare_groups}; + + if ($self->in_group(Bugzilla->params->{'querysharegroup'})) { + + # We want to be allowed to share with groups we're in only. + # If usevisibilitygroups is on, then we need to restrict this to groups + # we may see. + if (Bugzilla->params->{'usevisibilitygroups'}) { + foreach (@{$self->visible_groups_inherited()}) { + next unless $self->in_group_id($_); + push(@queryshare_groups, $_); + } } + else { + @queryshare_groups = @{$self->_group_ids}; + } + } - return $self->{queryshare_groups} = \@queryshare_groups; + return $self->{queryshare_groups} = \@queryshare_groups; } sub queryshare_groups_as_string { - my $self = shift; - return join(', ', @{$self->queryshare_groups()}); + my $self = shift; + return join(', ', @{$self->queryshare_groups()}); } sub derive_regexp_groups { - my ($self) = @_; + my ($self) = @_; - my $id = $self->id; - return unless $id; + my $id = $self->id; + return unless $id; - my $dbh = Bugzilla->dbh; + my $dbh = Bugzilla->dbh; - my $sth; + my $sth; - # add derived records for any matching regexps + # add derived records for any matching regexps - $sth = $dbh->prepare("SELECT id, userregexp, user_group_map.group_id + $sth = $dbh->prepare( + "SELECT id, userregexp, user_group_map.group_id FROM groups LEFT JOIN user_group_map ON groups.id = user_group_map.group_id AND user_group_map.user_id = ? - AND user_group_map.grant_type = ?"); - $sth->execute($id, GRANT_REGEXP); + AND user_group_map.grant_type = ?" + ); + $sth->execute($id, GRANT_REGEXP); - my $group_insert = $dbh->prepare(q{INSERT INTO user_group_map + my $group_insert = $dbh->prepare( + q{INSERT INTO user_group_map (user_id, group_id, isbless, grant_type) - VALUES (?, ?, 0, ?)}); - my $group_delete = $dbh->prepare(q{DELETE FROM user_group_map + VALUES (?, ?, 0, ?)} + ); + my $group_delete = $dbh->prepare( + q{DELETE FROM user_group_map WHERE user_id = ? AND group_id = ? AND isbless = 0 - AND grant_type = ?}); - while (my ($group, $regexp, $present) = $sth->fetchrow_array()) { - if (($regexp ne '') && ($self->login =~ m/$regexp/i)) { - $group_insert->execute($id, $group, GRANT_REGEXP) unless $present; - } else { - $group_delete->execute($id, $group, GRANT_REGEXP) if $present; - } + AND grant_type = ?} + ); + while (my ($group, $regexp, $present) = $sth->fetchrow_array()) { + if (($regexp ne '') && ($self->login =~ m/$regexp/i)) { + $group_insert->execute($id, $group, GRANT_REGEXP) unless $present; + } + else { + $group_delete->execute($id, $group, GRANT_REGEXP) if $present; } + } - Bugzilla->memcached->clear_config({ key => "user_groups.$id" }); + Bugzilla->memcached->clear_config({key => "user_groups.$id"}); } sub product_responsibilities { - my $self = shift; - my $dbh = Bugzilla->dbh; + my $self = shift; + my $dbh = Bugzilla->dbh; - return $self->{'product_resp'} if defined $self->{'product_resp'}; - return [] unless $self->id; + return $self->{'product_resp'} if defined $self->{'product_resp'}; + return [] unless $self->id; - my $list = $dbh->selectall_arrayref('SELECT components.product_id, components.id + my $list = $dbh->selectall_arrayref( + 'SELECT components.product_id, components.id FROM components LEFT JOIN component_cc ON components.id = component_cc.component_id WHERE components.initialowner = ? OR components.initialqacontact = ? OR component_cc.user_id = ?', - {Slice => {}}, ($self->id, $self->id, $self->id)); - - unless ($list) { - $self->{'product_resp'} = []; - return $self->{'product_resp'}; - } + {Slice => {}}, ($self->id, $self->id, $self->id) + ); - my @prod_ids = map {$_->{'product_id'}} @$list; - my $products = Bugzilla::Product->new_from_list(\@prod_ids); - # We cannot |use| it, because Component.pm already |use|s User.pm. - require Bugzilla::Component; - my @comp_ids = map {$_->{'id'}} @$list; - my $components = Bugzilla::Component->new_from_list(\@comp_ids); - - my @prod_list; - # @$products is already sorted alphabetically. - foreach my $prod (@$products) { - # We use @components instead of $prod->components because we only want - # components where the user is either the default assignee or QA contact. - push(@prod_list, {product => $prod, - components => [grep {$_->product_id == $prod->id} @$components]}); - } - $self->{'product_resp'} = \@prod_list; + unless ($list) { + $self->{'product_resp'} = []; return $self->{'product_resp'}; + } + + my @prod_ids = map { $_->{'product_id'} } @$list; + my $products = Bugzilla::Product->new_from_list(\@prod_ids); + + # We cannot |use| it, because Component.pm already |use|s User.pm. + require Bugzilla::Component; + my @comp_ids = map { $_->{'id'} } @$list; + my $components = Bugzilla::Component->new_from_list(\@comp_ids); + + my @prod_list; + + # @$products is already sorted alphabetically. + foreach my $prod (@$products) { + + # We use @components instead of $prod->components because we only want + # components where the user is either the default assignee or QA contact. + push( + @prod_list, + { + product => $prod, + components => [grep { $_->product_id == $prod->id } @$components] + } + ); + } + $self->{'product_resp'} = \@prod_list; + return $self->{'product_resp'}; } sub can_bless { - my $self = shift; + my $self = shift; - if (!scalar(@_)) { - # If we're called without an argument, just return - # whether or not we can bless at all. - return scalar(@{ $self->bless_groups }) ? 1 : 0; - } + if (!scalar(@_)) { - # Otherwise, we're checking a specific group - my $group_id = shift; - return grep($_->id == $group_id, @{ $self->bless_groups }) ? 1 : 0; + # If we're called without an argument, just return + # whether or not we can bless at all. + return scalar(@{$self->bless_groups}) ? 1 : 0; + } + + # Otherwise, we're checking a specific group + my $group_id = shift; + return grep($_->id == $group_id, @{$self->bless_groups}) ? 1 : 0; } sub match { - # Generates a list of users whose login name (email address) or real name - # matches a substring or wildcard. - # This is also called if matches are disabled (for error checking), but - # in this case only the exact match code will end up running. - - # $str contains the string to match, while $limit contains the - # maximum number of records to retrieve. - my ($str, $limit, $exclude_disabled) = @_; - my $user = Bugzilla->user; - my $dbh = Bugzilla->dbh; - - $str = trim($str); - - my @users = (); - return \@users if $str =~ /^\s*$/; - - # The search order is wildcards, then exact match, then substring search. - # Wildcard matching is skipped if there is no '*', and exact matches will - # not (?) have a '*' in them. If any search comes up with something, the - # ones following it will not execute. - - # first try wildcards - my $wildstr = $str; - - # Do not do wildcards if there is no '*' in the string. - if ($wildstr =~ s/\*/\%/g && $user->id) { - # Build the query. - trick_taint($wildstr); - my $query = "SELECT DISTINCT userid FROM profiles "; - if (Bugzilla->params->{'usevisibilitygroups'}) { - $query .= "INNER JOIN user_group_map - ON user_group_map.user_id = profiles.userid "; - } - $query .= "WHERE (" - . $dbh->sql_istrcmp('login_name', '?', "LIKE") . " OR " . - $dbh->sql_istrcmp('realname', '?', "LIKE") . ") "; - if (Bugzilla->params->{'usevisibilitygroups'}) { - $query .= "AND isbless = 0 " . - "AND group_id IN(" . - join(', ', (-1, @{$user->visible_groups_inherited})) . ") "; - } - $query .= " AND is_enabled = 1 " if $exclude_disabled; - $query .= $dbh->sql_limit($limit) if $limit; - # Execute the query, retrieve the results, and make them into - # User objects. - my $user_ids = $dbh->selectcol_arrayref($query, undef, ($wildstr, $wildstr)); - @users = @{Bugzilla::User->new_from_list($user_ids)}; - } - else { # try an exact match - # Exact matches don't care if a user is disabled. - trick_taint($str); - my $user_id = $dbh->selectrow_array('SELECT userid FROM profiles - WHERE ' . $dbh->sql_istrcmp('login_name', '?'), - undef, $str); - - push(@users, new Bugzilla::User($user_id)) if $user_id; + # Generates a list of users whose login name (email address) or real name + # matches a substring or wildcard. + # This is also called if matches are disabled (for error checking), but + # in this case only the exact match code will end up running. + + # $str contains the string to match, while $limit contains the + # maximum number of records to retrieve. + my ($str, $limit, $exclude_disabled) = @_; + my $user = Bugzilla->user; + my $dbh = Bugzilla->dbh; + + $str = trim($str); + + my @users = (); + return \@users if $str =~ /^\s*$/; + + # The search order is wildcards, then exact match, then substring search. + # Wildcard matching is skipped if there is no '*', and exact matches will + # not (?) have a '*' in them. If any search comes up with something, the + # ones following it will not execute. + + # first try wildcards + my $wildstr = $str; + + # Do not do wildcards if there is no '*' in the string. + if ($wildstr =~ s/\*/\%/g && $user->id) { + + # Build the query. + trick_taint($wildstr); + my $query = "SELECT DISTINCT userid FROM profiles "; + if (Bugzilla->params->{'usevisibilitygroups'}) { + $query .= "INNER JOIN user_group_map + ON user_group_map.user_id = profiles.userid "; } + $query + .= "WHERE (" + . $dbh->sql_istrcmp('login_name', '?', "LIKE") . " OR " + . $dbh->sql_istrcmp('realname', '?', "LIKE") . ") "; + if (Bugzilla->params->{'usevisibilitygroups'}) { + $query + .= "AND isbless = 0 " + . "AND group_id IN(" + . join(', ', (-1, @{$user->visible_groups_inherited})) . ") "; + } + $query .= " AND is_enabled = 1 " if $exclude_disabled; + $query .= $dbh->sql_limit($limit) if $limit; + + # Execute the query, retrieve the results, and make them into + # User objects. + my $user_ids = $dbh->selectcol_arrayref($query, undef, ($wildstr, $wildstr)); + @users = @{Bugzilla::User->new_from_list($user_ids)}; + } + else { # try an exact match + # Exact matches don't care if a user is disabled. + trick_taint($str); + my $user_id = $dbh->selectrow_array( + 'SELECT userid FROM profiles + WHERE ' + . $dbh->sql_istrcmp('login_name', '?'), undef, $str + ); - # then try substring search - if (!scalar(@users) && length($str) >= 3 && $user->id) { - trick_taint($str); + push(@users, new Bugzilla::User($user_id)) if $user_id; + } - my $query = "SELECT DISTINCT userid FROM profiles "; - if (Bugzilla->params->{'usevisibilitygroups'}) { - $query .= "INNER JOIN user_group_map + # 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 ON user_group_map.user_id = profiles.userid "; - } - $query .= " WHERE (" . - $dbh->sql_iposition('?', 'login_name') . " > 0" . " OR " . - $dbh->sql_iposition('?', 'realname') . " > 0) "; - if (Bugzilla->params->{'usevisibilitygroups'}) { - $query .= " AND isbless = 0" . - " AND group_id IN(" . - join(', ', (-1, @{$user->visible_groups_inherited})) . ") "; - } - $query .= " AND is_enabled = 1 " if $exclude_disabled; - $query .= $dbh->sql_limit($limit) if $limit; - my $user_ids = $dbh->selectcol_arrayref($query, undef, ($str, $str)); - @users = @{Bugzilla::User->new_from_list($user_ids)}; } - return \@users; + $query + .= " WHERE (" + . $dbh->sql_iposition('?', 'login_name') . " > 0" . " OR " + . $dbh->sql_iposition('?', 'realname') + . " > 0) "; + if (Bugzilla->params->{'usevisibilitygroups'}) { + $query + .= " AND isbless = 0" + . " AND group_id IN(" + . join(', ', (-1, @{$user->visible_groups_inherited})) . ") "; + } + $query .= " AND is_enabled = 1 " if $exclude_disabled; + $query .= $dbh->sql_limit($limit) if $limit; + my $user_ids = $dbh->selectcol_arrayref($query, undef, ($str, $str)); + @users = @{Bugzilla::User->new_from_list($user_ids)}; + } + return \@users; } sub match_field { - my $fields = shift; # arguments as a hash - my $data = shift || Bugzilla->input_params; # hash to look up fields in - my $behavior = shift || 0; # A constant that tells us how to act - my $matches = {}; # the values sent to the template - my $matchsuccess = 1; # did the match fail? - my $need_confirm = 0; # whether to display confirmation screen - my $match_multiple = 0; # whether we ever matched more than one user - my @non_conclusive_fields; # fields which don't have a unique user. - - my $params = Bugzilla->params; - - # prepare default form values - - # Fields can be regular expressions matching multiple form fields - # (f.e. "requestee-(\d+)"), so expand each non-literal field - # into the list of form fields it matches. - my $expanded_fields = {}; - foreach my $field_pattern (keys %{$fields}) { - # Check if the field has any non-word characters. Only those fields - # can be regular expressions, so don't expand the field if it doesn't - # have any of those characters. - if ($field_pattern =~ /^\w+$/) { - $expanded_fields->{$field_pattern} = $fields->{$field_pattern}; - } - else { - my @field_names = grep(/$field_pattern/, keys %$data); - - foreach my $field_name (@field_names) { - $expanded_fields->{$field_name} = - { type => $fields->{$field_pattern}->{'type'} }; - - # The field is a requestee field; in order for its name - # to show up correctly on the confirmation page, we need - # to find out the name of its flag type. - if ($field_name =~ /^requestee(_type)?-(\d+)$/) { - my $flag_type; - if ($1) { - require Bugzilla::FlagType; - $flag_type = new Bugzilla::FlagType($2); - } - else { - require Bugzilla::Flag; - my $flag = new Bugzilla::Flag($2); - $flag_type = $flag->type if $flag; - } - if ($flag_type) { - $expanded_fields->{$field_name}->{'flag_type'} = $flag_type; - } - else { - # No need to look for a valid requestee if the flag(type) - # has been deleted (may occur in race conditions). - delete $expanded_fields->{$field_name}; - delete $data->{$field_name}; - } - } - } + my $fields = shift; # arguments as a hash + my $data = shift || Bugzilla->input_params; # hash to look up fields in + my $behavior = shift || 0; # A constant that tells us how to act + my $matches = {}; # the values sent to the template + my $matchsuccess = 1; # did the match fail? + my $need_confirm = 0; # whether to display confirmation screen + my $match_multiple = 0; # whether we ever matched more than one user + my @non_conclusive_fields; # fields which don't have a unique user. + + my $params = Bugzilla->params; + + # prepare default form values + + # Fields can be regular expressions matching multiple form fields + # (f.e. "requestee-(\d+)"), so expand each non-literal field + # into the list of form fields it matches. + my $expanded_fields = {}; + foreach my $field_pattern (keys %{$fields}) { + + # Check if the field has any non-word characters. Only those fields + # can be regular expressions, so don't expand the field if it doesn't + # have any of those characters. + if ($field_pattern =~ /^\w+$/) { + $expanded_fields->{$field_pattern} = $fields->{$field_pattern}; + } + else { + my @field_names = grep(/$field_pattern/, keys %$data); + + foreach my $field_name (@field_names) { + $expanded_fields->{$field_name} = {type => $fields->{$field_pattern}->{'type'}}; + + # The field is a requestee field; in order for its name + # to show up correctly on the confirmation page, we need + # to find out the name of its flag type. + if ($field_name =~ /^requestee(_type)?-(\d+)$/) { + my $flag_type; + if ($1) { + require Bugzilla::FlagType; + $flag_type = new Bugzilla::FlagType($2); + } + else { + require Bugzilla::Flag; + my $flag = new Bugzilla::Flag($2); + $flag_type = $flag->type if $flag; + } + if ($flag_type) { + $expanded_fields->{$field_name}->{'flag_type'} = $flag_type; + } + else { + # No need to look for a valid requestee if the flag(type) + # has been deleted (may occur in race conditions). + delete $expanded_fields->{$field_name}; + delete $data->{$field_name}; + } } + } } - $fields = $expanded_fields; + } + $fields = $expanded_fields; - foreach my $field (keys %{$fields}) { - next unless defined $data->{$field}; + foreach my $field (keys %{$fields}) { + next unless defined $data->{$field}; - #Concatenate login names, so that we have a common way to handle them. - my $raw_field; - if (ref $data->{$field}) { - $raw_field = join(",", @{$data->{$field}}); - } - else { - $raw_field = $data->{$field}; - } - $raw_field = clean_text($raw_field || ''); - - # Now we either split $raw_field by spaces/commas and put the list - # into @queries, or in the case of fields which only accept single - # entries, we simply use the verbatim text. - my @queries; - if ($fields->{$field}->{'type'} eq 'single') { - @queries = ($raw_field); - # We will repopulate it later if a match is found, else it must - # be set to an empty string so that the field remains defined. - $data->{$field} = ''; - } - elsif ($fields->{$field}->{'type'} eq 'multi') { - @queries = split(/[,;]+/, $raw_field); - # We will repopulate it later if a match is found, else it must - # be undefined. - delete $data->{$field}; - } - else { - # bad argument - ThrowCodeError('bad_arg', - { argument => $fields->{$field}->{'type'}, - function => 'Bugzilla::User::match_field', - }); - } + #Concatenate login names, so that we have a common way to handle them. + my $raw_field; + if (ref $data->{$field}) { + $raw_field = join(",", @{$data->{$field}}); + } + else { + $raw_field = $data->{$field}; + } + $raw_field = clean_text($raw_field || ''); - # Tolerate fields that do not exist (in case you specify - # e.g. the QA contact, and it's currently not in use). - next unless (defined $raw_field && $raw_field ne ''); + # Now we either split $raw_field by spaces/commas and put the list + # into @queries, or in the case of fields which only accept single + # entries, we simply use the verbatim text. + my @queries; + if ($fields->{$field}->{'type'} eq 'single') { + @queries = ($raw_field); - my $limit = 0; - if ($params->{'maxusermatches'}) { - $limit = $params->{'maxusermatches'} + 1; - } + # We will repopulate it later if a match is found, else it must + # be set to an empty string so that the field remains defined. + $data->{$field} = ''; + } + elsif ($fields->{$field}->{'type'} eq 'multi') { + @queries = split(/[,;]+/, $raw_field); - my @logins; - for my $query (@queries) { - $query = trim($query); - next if $query eq ''; - - my $users = match( - $query, # match string - $limit, # match limit - 1 # exclude_disabled - ); - - # here is where it checks for multiple matches - if (scalar(@{$users}) == 1) { # exactly one match - push(@logins, @{$users}[0]->login); - - # skip confirmation for exact matches - next if (lc(@{$users}[0]->login) eq lc($query)); - - $matches->{$field}->{$query}->{'status'} = 'success'; - $need_confirm = 1 if $params->{'confirmuniqueusermatch'}; - - } - elsif ((scalar(@{$users}) > 1) - && ($params->{'maxusermatches'} != 1)) { - $need_confirm = 1; - $match_multiple = 1; - push(@non_conclusive_fields, $field); - - if (($params->{'maxusermatches'}) - && (scalar(@{$users}) > $params->{'maxusermatches'})) - { - $matches->{$field}->{$query}->{'status'} = 'trunc'; - pop @{$users}; # take the last one out - } - else { - $matches->{$field}->{$query}->{'status'} = 'success'; - } - - } - else { - # everything else fails - $matchsuccess = 0; # fail - push(@non_conclusive_fields, $field); - $matches->{$field}->{$query}->{'status'} = 'fail'; - $need_confirm = 1; # confirmation screen shows failures - } - - $matches->{$field}->{$query}->{'users'} = $users; + # We will repopulate it later if a match is found, else it must + # be undefined. + delete $data->{$field}; + } + else { + # bad argument + ThrowCodeError( + 'bad_arg', + { + argument => $fields->{$field}->{'type'}, + function => 'Bugzilla::User::match_field', } + ); + } + + # Tolerate fields that do not exist (in case you specify + # e.g. the QA contact, and it's currently not in use). + next unless (defined $raw_field && $raw_field ne ''); + + my $limit = 0; + if ($params->{'maxusermatches'}) { + $limit = $params->{'maxusermatches'} + 1; + } + + my @logins; + for my $query (@queries) { + $query = trim($query); + next if $query eq ''; + + my $users = match( + $query, # match string + $limit, # match limit + 1 # exclude_disabled + ); + + # here is where it checks for multiple matches + if (scalar(@{$users}) == 1) { # exactly one match + push(@logins, @{$users}[0]->login); + + # skip confirmation for exact matches + next if (lc(@{$users}[0]->login) eq lc($query)); + + $matches->{$field}->{$query}->{'status'} = 'success'; + $need_confirm = 1 if $params->{'confirmuniqueusermatch'}; - # If no match or more than one match has been found for a field - # expecting only one match (type eq "single"), we set it back to '' - # so that the caller of this function can still check whether this - # field was defined or not (and it was if we came here). - if ($fields->{$field}->{'type'} eq 'single') { - $data->{$field} = $logins[0] || ''; + } + elsif ((scalar(@{$users}) > 1) && ($params->{'maxusermatches'} != 1)) { + $need_confirm = 1; + $match_multiple = 1; + push(@non_conclusive_fields, $field); + + if ( ($params->{'maxusermatches'}) + && (scalar(@{$users}) > $params->{'maxusermatches'})) + { + $matches->{$field}->{$query}->{'status'} = 'trunc'; + pop @{$users}; # take the last one out } - elsif (scalar @logins) { - $data->{$field} = \@logins; + else { + $matches->{$field}->{$query}->{'status'} = 'success'; } + + } + else { + # everything else fails + $matchsuccess = 0; # fail + push(@non_conclusive_fields, $field); + $matches->{$field}->{$query}->{'status'} = 'fail'; + $need_confirm = 1; # confirmation screen shows failures + } + + $matches->{$field}->{$query}->{'users'} = $users; } - my $retval; - if (!$matchsuccess) { - $retval = USER_MATCH_FAILED; + # If no match or more than one match has been found for a field + # expecting only one match (type eq "single"), we set it back to '' + # so that the caller of this function can still check whether this + # field was defined or not (and it was if we came here). + if ($fields->{$field}->{'type'} eq 'single') { + $data->{$field} = $logins[0] || ''; } - elsif ($match_multiple) { - $retval = USER_MATCH_MULTIPLE; - } - else { - $retval = USER_MATCH_SUCCESS; + elsif (scalar @logins) { + $data->{$field} = \@logins; } + } - # Skip confirmation if we were told to, or if we don't need to confirm. - if ($behavior == MATCH_SKIP_CONFIRM || !$need_confirm) { - return wantarray ? ($retval, \@non_conclusive_fields) : $retval; - } + my $retval; + if (!$matchsuccess) { + $retval = USER_MATCH_FAILED; + } + elsif ($match_multiple) { + $retval = USER_MATCH_MULTIPLE; + } + else { + $retval = USER_MATCH_SUCCESS; + } + + # 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) = @_; - - # Make a list of the events which have happened during this bug change, - # from the point of view of this user. - my %events; - foreach my $change (@$fieldDiffs) { - my $fieldName = $change->{field_name}; - # A change to any of the above fields sets the corresponding event - if (defined($names_to_events{$fieldName})) { - $events{$names_to_events{$fieldName}} = 1; - } - else { - # Catch-all for any change not caught by a more specific event - $events{+EVT_OTHER} = 1; - } + my $self = shift; + my ($bug, $relationship, $fieldDiffs, $comments, $dep_mail, $changer) = @_; - # If the user is in a particular role and the value of that role - # changed, we need the ADDED_REMOVED event. - if (($fieldName eq "assigned_to" && $relationship == REL_ASSIGNEE) || - ($fieldName eq "qa_contact" && $relationship == REL_QA)) - { - $events{+EVT_ADDED_REMOVED} = 1; - } - - if ($fieldName eq "cc") { - my $login = $self->login; - my $inold = ($change->{old} =~ /^(.*,\s*)?\Q$login\E(,.*)?$/); - my $innew = ($change->{new} =~ /^(.*,\s*)?\Q$login\E(,.*)?$/); - if ($inold != $innew) - { - $events{+EVT_ADDED_REMOVED} = 1; - } - } - } + # Make a list of the events which have happened during this bug change, + # from the point of view of this user. + my %events; + foreach my $change (@$fieldDiffs) { + my $fieldName = $change->{field_name}; - if (!$bug->lastdiffed) { - # Notify about new bugs. - $events{+EVT_BUG_CREATED} = 1; - - # You role is new if the bug itself is. - # Only makes sense for the assignee, QA contact and the CC list. - if ($relationship == REL_ASSIGNEE - || $relationship == REL_QA - || $relationship == REL_CC) - { - $events{+EVT_ADDED_REMOVED} = 1; - } + # A change to any of the above fields sets the corresponding event + if (defined($names_to_events{$fieldName})) { + $events{$names_to_events{$fieldName}} = 1; } - - if (grep { $_->type == CMT_ATTACHMENT_CREATED } @$comments) { - $events{+EVT_ATTACHMENT} = 1; + else { + # Catch-all for any change not caught by a more specific event + $events{+EVT_OTHER} = 1; } - elsif (defined($$comments[0])) { - $events{+EVT_COMMENT} = 1; + + # If the user is in a particular role and the value of that role + # changed, we need the ADDED_REMOVED event. + if ( ($fieldName eq "assigned_to" && $relationship == REL_ASSIGNEE) + || ($fieldName eq "qa_contact" && $relationship == REL_QA)) + { + $events{+EVT_ADDED_REMOVED} = 1; } - - # Dependent changed bugmails must have an event to ensure the bugmail is - # emailed. - if ($dep_mail) { - $events{+EVT_DEPEND_BLOCK} = 1; + + if ($fieldName eq "cc") { + my $login = $self->login; + my $inold = ($change->{old} =~ /^(.*,\s*)?\Q$login\E(,.*)?$/); + my $innew = ($change->{new} =~ /^(.*,\s*)?\Q$login\E(,.*)?$/); + if ($inold != $innew) { + $events{+EVT_ADDED_REMOVED} = 1; + } } + } - 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 (!$bug->lastdiffed) { + + # Notify about new bugs. + $events{+EVT_BUG_CREATED} = 1; + + # You role is new if the bug itself is. + # Only makes sense for the assignee, QA contact and the CC list. + if ( $relationship == REL_ASSIGNEE + || $relationship == REL_QA + || $relationship == REL_CC) + { + $events{+EVT_ADDED_REMOVED} = 1; } - - return $wants_mail; + } + + if (grep { $_->type == CMT_ATTACHMENT_CREATED } @$comments) { + $events{+EVT_ATTACHMENT} = 1; + } + elsif (defined($$comments[0])) { + $events{+EVT_COMMENT} = 1; + } + + # Dependent changed bugmails must have an event to ensure the bugmail is + # emailed. + if ($dep_mail) { + $events{+EVT_DEPEND_BLOCK} = 1; + } + + my @event_list = keys %events; + + my $wants_mail = $self->wants_mail(\@event_list, $relationship); + + # The negative events are handled separately - they can't be incorporated + # into the first wants_mail call, because they are of the opposite sense. + # + # We do them separately because if _any_ of them are set, we don't want + # the mail. + if ($wants_mail && $changer && ($self->id == $changer->id)) { + $wants_mail &= $self->wants_mail([EVT_CHANGED_BY_ME], $relationship); + } + + if ($wants_mail && $bug->bug_status eq 'UNCONFIRMED') { + $wants_mail &= $self->wants_mail([EVT_UNCONFIRMED], $relationship); + } + + return $wants_mail; } # Returns true if the user wants mail for a given set of events. sub wants_mail { - my $self = shift; - my ($events, $relationship) = @_; - - # Don't send any mail, ever, if account is disabled - # XXX Temporary Compatibility Change 1 of 2: - # This code is disabled for the moment to make the behaviour like the old - # system, which sent bugmail to disabled accounts. - # return 0 if $self->{'disabledtext'}; - - # No mail if there are no events - return 0 if !scalar(@$events); - - # If a relationship isn't given, default to REL_ANY. - if (!defined($relationship)) { - $relationship = REL_ANY; - } + my $self = shift; + my ($events, $relationship) = @_; + + # Don't send any mail, ever, if account is disabled + # XXX Temporary Compatibility Change 1 of 2: + # This code is disabled for the moment to make the behaviour like the old + # system, which sent bugmail to disabled accounts. + # return 0 if $self->{'disabledtext'}; + + # No mail if there are no events + return 0 if !scalar(@$events); - # Skip DB query if relationship is explicit - return 1 if $relationship == REL_GLOBAL_WATCHER; + # If a relationship isn't given, default to REL_ANY. + if (!defined($relationship)) { + $relationship = REL_ANY; + } - my $wants_mail = grep { $self->mail_settings->{$relationship}{$_} } @$events; - return $wants_mail ? 1 : 0; + # Skip DB query if relationship is explicit + return 1 if $relationship == REL_GLOBAL_WATCHER; + + my $wants_mail = grep { $self->mail_settings->{$relationship}{$_} } @$events; + return $wants_mail ? 1 : 0; } sub mail_settings { - my $self = shift; - my $dbh = Bugzilla->dbh; - - if (!defined $self->{'mail_settings'}) { - my $data = - $dbh->selectall_arrayref('SELECT relationship, event FROM email_setting - WHERE user_id = ?', undef, $self->id); - my %mail; - # The hash is of the form $mail{$relationship}{$event} = 1. - $mail{$_->[0]}{$_->[1]} = 1 foreach @$data; - - $self->{'mail_settings'} = \%mail; - } - return $self->{'mail_settings'}; + my $self = shift; + my $dbh = Bugzilla->dbh; + + if (!defined $self->{'mail_settings'}) { + my $data = $dbh->selectall_arrayref( + 'SELECT relationship, event FROM email_setting + WHERE user_id = ?', undef, $self->id + ); + my %mail; + + # The hash is of the form $mail{$relationship}{$event} = 1. + $mail{$_->[0]}{$_->[1]} = 1 foreach @$data; + + $self->{'mail_settings'} = \%mail; + } + return $self->{'mail_settings'}; } sub has_audit_entries { - my $self = shift; - my $dbh = Bugzilla->dbh; + my $self = shift; + my $dbh = Bugzilla->dbh; - if (!exists $self->{'has_audit_entries'}) { - $self->{'has_audit_entries'} = - $dbh->selectrow_array('SELECT 1 FROM audit_log WHERE user_id = ? ' . - $dbh->sql_limit(1), undef, $self->id); - } - return $self->{'has_audit_entries'}; + if (!exists $self->{'has_audit_entries'}) { + $self->{'has_audit_entries'} + = $dbh->selectrow_array( + 'SELECT 1 FROM audit_log WHERE user_id = ? ' . $dbh->sql_limit(1), + undef, $self->id); + } + return $self->{'has_audit_entries'}; } sub is_insider { - my $self = shift; + my $self = shift; - if (!defined $self->{'is_insider'}) { - my $insider_group = Bugzilla->params->{'insidergroup'}; - $self->{'is_insider'} = - ($insider_group && $self->in_group($insider_group)) ? 1 : 0; - } - return $self->{'is_insider'}; + if (!defined $self->{'is_insider'}) { + my $insider_group = Bugzilla->params->{'insidergroup'}; + $self->{'is_insider'} + = ($insider_group && $self->in_group($insider_group)) ? 1 : 0; + } + return $self->{'is_insider'}; } sub is_global_watcher { - my $self = shift; + my $self = shift; - if (!defined $self->{'is_global_watcher'}) { - my @watchers = split(/[,;]+/, Bugzilla->params->{'globalwatchers'}); - $self->{'is_global_watcher'} = scalar(grep { $_ eq $self->login } @watchers) ? 1 : 0; - } - return $self->{'is_global_watcher'}; + if (!defined $self->{'is_global_watcher'}) { + my @watchers = split(/[,;]+/, Bugzilla->params->{'globalwatchers'}); + $self->{'is_global_watcher'} + = scalar(grep { $_ eq $self->login } @watchers) ? 1 : 0; + } + return $self->{'is_global_watcher'}; } sub is_timetracker { - my $self = shift; + my $self = shift; - if (!defined $self->{'is_timetracker'}) { - my $tt_group = Bugzilla->params->{'timetrackinggroup'}; - $self->{'is_timetracker'} = - ($tt_group && $self->in_group($tt_group)) ? 1 : 0; - } - return $self->{'is_timetracker'}; + if (!defined $self->{'is_timetracker'}) { + my $tt_group = Bugzilla->params->{'timetrackinggroup'}; + $self->{'is_timetracker'} = ($tt_group && $self->in_group($tt_group)) ? 1 : 0; + } + return $self->{'is_timetracker'}; } sub can_tag_comments { - my $self = shift; + my $self = shift; - if (!defined $self->{'can_tag_comments'}) { - my $group = Bugzilla->params->{'comment_taggers_group'}; - $self->{'can_tag_comments'} = - ($group && $self->in_group($group)) ? 1 : 0; - } - return $self->{'can_tag_comments'}; + if (!defined $self->{'can_tag_comments'}) { + my $group = Bugzilla->params->{'comment_taggers_group'}; + $self->{'can_tag_comments'} = ($group && $self->in_group($group)) ? 1 : 0; + } + return $self->{'can_tag_comments'}; } sub get_userlist { - my $self = shift; - - return $self->{'userlist'} if defined $self->{'userlist'}; - - my $dbh = Bugzilla->dbh; - my $query = "SELECT DISTINCT login_name, realname,"; - if (Bugzilla->params->{'usevisibilitygroups'}) { - $query .= " COUNT(group_id) "; - } else { - $query .= " 1 "; - } - $query .= "FROM profiles "; - if (Bugzilla->params->{'usevisibilitygroups'}) { - $query .= "LEFT JOIN user_group_map " . - "ON user_group_map.user_id = userid AND isbless = 0 " . - "AND group_id IN(" . - join(', ', (-1, @{$self->visible_groups_inherited})) . ")"; - } - $query .= " WHERE is_enabled = 1 "; - $query .= $dbh->sql_group_by('userid', 'login_name, realname'); - - my $sth = $dbh->prepare($query); - $sth->execute; - - my @userlist; - while (my($login, $name, $visible) = $sth->fetchrow_array) { - push @userlist, { - login => $login, - identity => $name ? "$name <$login>" : $login, - visible => $visible, - }; - } - @userlist = sort { lc $$a{'identity'} cmp lc $$b{'identity'} } @userlist; - - $self->{'userlist'} = \@userlist; - return $self->{'userlist'}; + my $self = shift; + + return $self->{'userlist'} if defined $self->{'userlist'}; + + my $dbh = Bugzilla->dbh; + my $query = "SELECT DISTINCT login_name, realname,"; + if (Bugzilla->params->{'usevisibilitygroups'}) { + $query .= " COUNT(group_id) "; + } + else { + $query .= " 1 "; + } + $query .= "FROM profiles "; + if (Bugzilla->params->{'usevisibilitygroups'}) { + $query + .= "LEFT JOIN user_group_map " + . "ON user_group_map.user_id = userid AND isbless = 0 " + . "AND group_id IN(" + . join(', ', (-1, @{$self->visible_groups_inherited})) . ")"; + } + $query .= " WHERE is_enabled = 1 "; + $query .= $dbh->sql_group_by('userid', 'login_name, realname'); + + my $sth = $dbh->prepare($query); + $sth->execute; + + my @userlist; + while (my ($login, $name, $visible) = $sth->fetchrow_array) { + push @userlist, + { + login => $login, + identity => $name ? "$name <$login>" : $login, + visible => $visible, + }; + } + @userlist = sort { lc $$a{'identity'} cmp lc $$b{'identity'} } @userlist; + + $self->{'userlist'} = \@userlist; + return $self->{'userlist'}; } sub create { - my $invocant = shift; - my $class = ref($invocant) || $invocant; - my $dbh = Bugzilla->dbh; - - $dbh->bz_start_transaction(); - - my $user = $class->SUPER::create(@_); - - # Turn on all email for the new user - require Bugzilla::BugMail; - my %relationships = Bugzilla::BugMail::relationships(); - foreach my $rel (keys %relationships) { - foreach my $event (POS_EVENTS, NEG_EVENTS) { - # These "exceptions" define the default email preferences. - # - # We enable mail unless the change was made by the user, or it's - # just a CC list addition and the user is not the reporter. - next if ($event == EVT_CHANGED_BY_ME); - next if (($event == EVT_CC) && ($rel != REL_REPORTER)); - - $dbh->do('INSERT INTO email_setting (user_id, relationship, event) - VALUES (?, ?, ?)', undef, ($user->id, $rel, $event)); - } - } - - foreach my $event (GLOBAL_EVENTS) { - $dbh->do('INSERT INTO email_setting (user_id, relationship, event) - VALUES (?, ?, ?)', undef, ($user->id, REL_ANY, $event)); - } + my $invocant = shift; + my $class = ref($invocant) || $invocant; + my $dbh = Bugzilla->dbh; + + $dbh->bz_start_transaction(); + + my $user = $class->SUPER::create(@_); + + # Turn on all email for the new user + require Bugzilla::BugMail; + my %relationships = Bugzilla::BugMail::relationships(); + foreach my $rel (keys %relationships) { + foreach my $event (POS_EVENTS, NEG_EVENTS) { + + # These "exceptions" define the default email preferences. + # + # We enable mail unless the change was made by the user, or it's + # just a CC list addition and the user is not the reporter. + next if ($event == EVT_CHANGED_BY_ME); + next if (($event == EVT_CC) && ($rel != REL_REPORTER)); + + $dbh->do( + 'INSERT INTO email_setting (user_id, relationship, event) + VALUES (?, ?, ?)', undef, ($user->id, $rel, $event) + ); + } + } + + foreach my $event (GLOBAL_EVENTS) { + $dbh->do( + 'INSERT INTO email_setting (user_id, relationship, event) + VALUES (?, ?, ?)', undef, ($user->id, REL_ANY, $event) + ); + } - $user->derive_regexp_groups(); + $user->derive_regexp_groups(); - # Add the creation date to the profiles_activity table. - # $who is the user who created the new user account, i.e. either an - # admin or the new user himself. - my $who = Bugzilla->user->id || $user->id; - my $creation_date_fieldid = get_field_id('creation_ts'); + # Add the creation date to the profiles_activity table. + # $who is the user who created the new user account, i.e. either an + # admin or the new user himself. + my $who = Bugzilla->user->id || $user->id; + my $creation_date_fieldid = get_field_id('creation_ts'); - $dbh->do('INSERT INTO profiles_activity + $dbh->do( + 'INSERT INTO profiles_activity (userid, who, profiles_when, fieldid, newvalue) - VALUES (?, ?, NOW(), ?, NOW())', - undef, ($user->id, $who, $creation_date_fieldid)); + VALUES (?, ?, NOW(), ?, NOW())', undef, + ($user->id, $who, $creation_date_fieldid) + ); - $dbh->bz_commit_transaction(); + $dbh->bz_commit_transaction(); - # Return the newly created user account. - return $user; + # Return the newly created user account. + return $user; } ########################### @@ -2305,44 +2400,45 @@ sub create { ########################### sub account_is_locked_out { - my $self = shift; - my $login_failures = scalar @{ $self->account_ip_login_failures }; - return $login_failures >= MAX_LOGIN_ATTEMPTS ? 1 : 0; + my $self = shift; + my $login_failures = scalar @{$self->account_ip_login_failures}; + return $login_failures >= MAX_LOGIN_ATTEMPTS ? 1 : 0; } sub note_login_failure { - my $self = shift; - my $ip_addr = remote_ip(); - trick_taint($ip_addr); - Bugzilla->dbh->do("INSERT INTO login_failure (user_id, ip_addr, login_time) - VALUES (?, ?, LOCALTIMESTAMP(0))", - undef, $self->id, $ip_addr); - delete $self->{account_ip_login_failures}; + my $self = shift; + my $ip_addr = remote_ip(); + trick_taint($ip_addr); + Bugzilla->dbh->do( + "INSERT INTO login_failure (user_id, ip_addr, login_time) + VALUES (?, ?, LOCALTIMESTAMP(0))", undef, $self->id, $ip_addr + ); + delete $self->{account_ip_login_failures}; } sub clear_login_failures { - my $self = shift; - my $ip_addr = remote_ip(); - trick_taint($ip_addr); - Bugzilla->dbh->do( - 'DELETE FROM login_failure WHERE user_id = ? AND ip_addr = ?', - undef, $self->id, $ip_addr); - delete $self->{account_ip_login_failures}; + my $self = shift; + my $ip_addr = remote_ip(); + trick_taint($ip_addr); + Bugzilla->dbh->do('DELETE FROM login_failure WHERE user_id = ? AND ip_addr = ?', + undef, $self->id, $ip_addr); + delete $self->{account_ip_login_failures}; } sub account_ip_login_failures { - my $self = shift; - my $dbh = Bugzilla->dbh; - my $time = $dbh->sql_date_math('LOCALTIMESTAMP(0)', '-', - LOGIN_LOCKOUT_INTERVAL, 'MINUTE'); - my $ip_addr = remote_ip(); - trick_taint($ip_addr); - $self->{account_ip_login_failures} ||= Bugzilla->dbh->selectall_arrayref( - "SELECT login_time, ip_addr, user_id FROM login_failure + my $self = shift; + my $dbh = Bugzilla->dbh; + my $time = $dbh->sql_date_math('LOCALTIMESTAMP(0)', '-', LOGIN_LOCKOUT_INTERVAL, + 'MINUTE'); + my $ip_addr = remote_ip(); + trick_taint($ip_addr); + $self->{account_ip_login_failures} ||= Bugzilla->dbh->selectall_arrayref( + "SELECT login_time, ip_addr, user_id FROM login_failure WHERE user_id = ? AND login_time > $time AND ip_addr = ? - ORDER BY login_time", {Slice => {}}, $self->id, $ip_addr); - return $self->{account_ip_login_failures}; + ORDER BY login_time", {Slice => {}}, $self->id, $ip_addr + ); + return $self->{account_ip_login_failures}; } ############### @@ -2350,145 +2446,162 @@ sub account_ip_login_failures { ############### sub is_available_username { - my ($username, $old_username) = @_; - - if(login_to_id($username) != 0) { - return 0; - } + my ($username, $old_username) = @_; - my $dbh = Bugzilla->dbh; - # $username is safe because it is only used in SELECT placeholders. - trick_taint($username); - # Reject if the new login is part of an email change which is - # still in progress - # - # substring/locate stuff: bug 165221; this used to use regexes, but that - # was unsafe and required weird escaping; using substring to pull out - # the new/old email addresses and sql_position() to find the delimiter (':') - # is cleaner/safer - my ($tokentype, $eventdata) = $dbh->selectrow_array( - "SELECT tokentype, eventdata + if (login_to_id($username) != 0) { + return 0; + } + + my $dbh = Bugzilla->dbh; + + # $username is safe because it is only used in SELECT placeholders. + trick_taint($username); + + # Reject if the new login is part of an email change which is + # still in progress + # + # substring/locate stuff: bug 165221; this used to use regexes, but that + # was unsafe and required weird escaping; using substring to pull out + # the new/old email addresses and sql_position() to find the delimiter (':') + # is cleaner/safer + my ($tokentype, $eventdata) = $dbh->selectrow_array( + "SELECT tokentype, eventdata FROM tokens WHERE (tokentype = 'emailold' - AND SUBSTRING(eventdata, 1, (" . - $dbh->sql_position(q{':'}, 'eventdata') . "- 1)) = ?) + AND SUBSTRING(eventdata, 1, (" + . $dbh->sql_position(q{':'}, 'eventdata') . "- 1)) = ?) OR (tokentype = 'emailnew' - AND SUBSTRING(eventdata, (" . - $dbh->sql_position(q{':'}, 'eventdata') . "+ 1), LENGTH(eventdata)) = ?)", - undef, ($username, $username)); - - if ($eventdata) { - # Allow thru owner of token - if ($old_username - && (($tokentype eq 'emailnew' && $eventdata eq "$old_username:$username") - || ($tokentype eq 'emailold' && $eventdata eq "$username:$old_username"))) - { - return 1; - } - return 0; + AND SUBSTRING(eventdata, (" + . $dbh->sql_position(q{':'}, 'eventdata') + . "+ 1), LENGTH(eventdata)) = ?)", + undef, ($username, $username) + ); + + if ($eventdata) { + + # Allow thru owner of token + if ( + $old_username + && ( ($tokentype eq 'emailnew' && $eventdata eq "$old_username:$username") + || ($tokentype eq 'emailold' && $eventdata eq "$username:$old_username")) + ) + { + return 1; } + return 0; + } - return 1; + return 1; } sub check_account_creation_enabled { - my $self = shift; + my $self = shift; - # If we're using e.g. LDAP for login, then we can't create a new account. - $self->authorizer->user_can_create_account - || ThrowUserError('auth_cant_create_account'); + # If we're using e.g. LDAP for login, then we can't create a new account. + $self->authorizer->user_can_create_account + || ThrowUserError('auth_cant_create_account'); - Bugzilla->params->{'createemailregexp'} - || ThrowUserError('account_creation_disabled'); + Bugzilla->params->{'createemailregexp'} + || ThrowUserError('account_creation_disabled'); } sub check_and_send_account_creation_confirmation { - my ($self, $login) = @_; - my $dbh = Bugzilla->dbh; + my ($self, $login) = @_; + my $dbh = Bugzilla->dbh; - $dbh->bz_start_transaction; + $dbh->bz_start_transaction; - $login = $self->check_login_name($login); - my $creation_regexp = Bugzilla->params->{'createemailregexp'}; + $login = $self->check_login_name($login); + my $creation_regexp = Bugzilla->params->{'createemailregexp'}; - if ($login !~ /$creation_regexp/i) { - ThrowUserError('account_creation_restricted'); - } + if ($login !~ /$creation_regexp/i) { + ThrowUserError('account_creation_restricted'); + } - # Allow extensions to do extra checks. - Bugzilla::Hook::process('user_check_account_creation', { login => $login }); + # Allow extensions to do extra checks. + Bugzilla::Hook::process('user_check_account_creation', {login => $login}); - # Create and send a token for this new account. - require Bugzilla::Token; - Bugzilla::Token::issue_new_user_account_token($login); + # Create and send a token for this new account. + require Bugzilla::Token; + Bugzilla::Token::issue_new_user_account_token($login); - $dbh->bz_commit_transaction; + $dbh->bz_commit_transaction; } # This is used in a few performance-critical areas where we don't want to # do check() and pull all the user data from the database. sub login_to_id { - my ($login, $throw_error) = @_; - my $dbh = Bugzilla->dbh; - my $cache = Bugzilla->request_cache->{user_login_to_id} ||= {}; - - # We cache lookups because this function showed up as taking up a - # significant amount of time in profiles of xt/search.t. However, - # for users that don't exist, we re-do the check every time, because - # otherwise we break is_available_username. - my $user_id; - if (defined $cache->{$login}) { - $user_id = $cache->{$login}; - } - else { - # No need to validate $login -- it will be used by the following SELECT - # statement only, so it's safe to simply trick_taint. - trick_taint($login); - $user_id = $dbh->selectrow_array( - "SELECT userid FROM profiles - WHERE " . $dbh->sql_istrcmp('login_name', '?'), undef, $login); - $cache->{$login} = $user_id; - } - - if ($user_id) { - return $user_id; - } elsif ($throw_error) { - ThrowUserError('invalid_username', { name => $login }); - } else { - return 0; - } + my ($login, $throw_error) = @_; + my $dbh = Bugzilla->dbh; + my $cache = Bugzilla->request_cache->{user_login_to_id} ||= {}; + + # We cache lookups because this function showed up as taking up a + # significant amount of time in profiles of xt/search.t. However, + # for users that don't exist, we re-do the check every time, because + # otherwise we break is_available_username. + my $user_id; + if (defined $cache->{$login}) { + $user_id = $cache->{$login}; + } + else { + # No need to validate $login -- it will be used by the following SELECT + # statement only, so it's safe to simply trick_taint. + trick_taint($login); + $user_id = $dbh->selectrow_array( + "SELECT userid FROM profiles + WHERE " . $dbh->sql_istrcmp('login_name', '?'), undef, $login + ); + $cache->{$login} = $user_id; + } + + if ($user_id) { + return $user_id; + } + elsif ($throw_error) { + ThrowUserError('invalid_username', {name => $login}); + } + else { + return 0; + } } sub validate_password { - my $check = validate_password_check(@_); - ThrowUserError($check) if $check; - return 1; + my $check = validate_password_check(@_); + ThrowUserError($check) if $check; + return 1; } sub validate_password_check { - my ($password, $matchpassword) = @_; - - if (length($password) < USER_PASSWORD_MIN_LENGTH) { - return 'password_too_short'; - } elsif ((defined $matchpassword) && ($password ne $matchpassword)) { - return 'passwords_dont_match'; - } - - my $complexity_level = Bugzilla->params->{password_complexity}; - if ($complexity_level eq 'letters_numbers_specialchars') { - return 'password_not_complex' - if ($password !~ /[[:alpha:]]/ || $password !~ /\d/ || $password !~ /[[:punct:]]/); - } elsif ($complexity_level eq 'letters_numbers') { - return 'password_not_complex' - if ($password !~ /[[:lower:]]/ || $password !~ /[[:upper:]]/ || $password !~ /\d/); - } elsif ($complexity_level eq 'mixed_letters') { - return 'password_not_complex' - if ($password !~ /[[:lower:]]/ || $password !~ /[[:upper:]]/); - } - - # Having done these checks makes us consider the password untainted. - trick_taint($_[0]); - return; + my ($password, $matchpassword) = @_; + + if (length($password) < USER_PASSWORD_MIN_LENGTH) { + return 'password_too_short'; + } + elsif ((defined $matchpassword) && ($password ne $matchpassword)) { + return 'passwords_dont_match'; + } + + my $complexity_level = Bugzilla->params->{password_complexity}; + if ($complexity_level eq 'letters_numbers_specialchars') { + return 'password_not_complex' + if ($password !~ /[[:alpha:]]/ + || $password !~ /\d/ + || $password !~ /[[:punct:]]/); + } + elsif ($complexity_level eq 'letters_numbers') { + return 'password_not_complex' + if ($password !~ /[[:lower:]]/ + || $password !~ /[[:upper:]]/ + || $password !~ /\d/); + } + elsif ($complexity_level eq 'mixed_letters') { + return 'password_not_complex' + if ($password !~ /[[:lower:]]/ || $password !~ /[[:upper:]]/); + } + + # Having done these checks makes us consider the password untainted. + trick_taint($_[0]); + return; } diff --git a/Bugzilla/User/APIKey.pm b/Bugzilla/User/APIKey.pm index d268a0a93..d2e337c5e 100644 --- a/Bugzilla/User/APIKey.pm +++ b/Bugzilla/User/APIKey.pm @@ -20,52 +20,54 @@ use Bugzilla::Util qw(generate_random_password trim); # Overriden Constants that are used as methods ##################################################################### -use constant DB_TABLE => 'user_api_keys'; -use constant DB_COLUMNS => qw( - id - user_id - api_key - description - revoked - last_used +use constant DB_TABLE => 'user_api_keys'; +use constant DB_COLUMNS => qw( + id + user_id + api_key + description + revoked + last_used ); use constant UPDATE_COLUMNS => qw(description revoked last_used); use constant VALIDATORS => { - api_key => \&_check_api_key, - description => \&_check_description, - revoked => \&Bugzilla::Object::check_boolean, + api_key => \&_check_api_key, + description => \&_check_description, + revoked => \&Bugzilla::Object::check_boolean, }; -use constant LIST_ORDER => 'id'; -use constant NAME_FIELD => 'api_key'; +use constant LIST_ORDER => 'id'; +use constant NAME_FIELD => 'api_key'; # turn off auditing and exclude these objects from memcached -use constant { AUDIT_CREATES => 0, - AUDIT_UPDATES => 0, - AUDIT_REMOVES => 0, - USE_MEMCACHED => 0 }; +use constant { + AUDIT_CREATES => 0, + AUDIT_UPDATES => 0, + AUDIT_REMOVES => 0, + USE_MEMCACHED => 0 +}; # Accessors -sub id { return $_[0]->{id} } -sub user_id { return $_[0]->{user_id} } -sub api_key { return $_[0]->{api_key} } -sub description { return $_[0]->{description} } -sub revoked { return $_[0]->{revoked} } -sub last_used { return $_[0]->{last_used} } +sub id { return $_[0]->{id} } +sub user_id { return $_[0]->{user_id} } +sub api_key { return $_[0]->{api_key} } +sub description { return $_[0]->{description} } +sub revoked { return $_[0]->{revoked} } +sub last_used { return $_[0]->{last_used} } # Helpers sub user { - my $self = shift; - $self->{user} //= Bugzilla::User->new({name => $self->user_id, cache => 1}); - return $self->{user}; + my $self = shift; + $self->{user} //= Bugzilla::User->new({name => $self->user_id, cache => 1}); + return $self->{user}; } sub update_last_used { - my $self = shift; - my $timestamp = shift - || Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); - $self->set('last_used', $timestamp); - $self->update; + my $self = shift; + my $timestamp + = shift || Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); + $self->set('last_used', $timestamp); + $self->update; } # Setters @@ -73,8 +75,8 @@ sub set_description { $_[0]->set('description', $_[1]); } sub set_revoked { $_[0]->set('revoked', $_[1]); } # Validators -sub _check_api_key { return generate_random_password(40); } -sub _check_description { return trim($_[1]) || ''; } +sub _check_api_key { return generate_random_password(40); } +sub _check_description { return trim($_[1]) || ''; } 1; __END__ diff --git a/Bugzilla/User/Setting.pm b/Bugzilla/User/Setting.pm index aece3b7de..94171a5d9 100644 --- a/Bugzilla/User/Setting.pm +++ b/Bugzilla/User/Setting.pm @@ -17,10 +17,10 @@ use parent qw(Exporter); # Module stuff @Bugzilla::User::Setting::EXPORT = qw( - get_all_settings - get_defaults - add_setting - clear_settings_cache + get_all_settings + get_defaults + add_setting + clear_settings_cache ); use Bugzilla::Error; @@ -31,88 +31,84 @@ use Bugzilla::Util qw(trick_taint get_text); ############################### sub new { - my $invocant = shift; - my $setting_name = shift; - my $user_id = shift; - - my $class = ref($invocant) || $invocant; - my $subclass = ''; - - # Create a ref to an empty hash and bless it - my $self = {}; - - my $dbh = Bugzilla->dbh; - - # Confirm that the $setting_name is properly formed; - # if not, throw a code error. - # - # NOTE: due to the way that setting names are used in templates, - # they must conform to to the limitations set for HTML NAMEs and IDs. - # - if ( !($setting_name =~ /^[a-zA-Z][-.:\w]*$/) ) { - ThrowCodeError("setting_name_invalid", { name => $setting_name }); - } - - # If there were only two parameters passed in, then we need - # to retrieve the information for this setting ourselves. - if (scalar @_ == 0) { - - my ($default, $is_enabled, $value); - ($default, $is_enabled, $value, $subclass) = - $dbh->selectrow_array( - q{SELECT default_value, is_enabled, setting_value, subclass + my $invocant = shift; + my $setting_name = shift; + my $user_id = shift; + + my $class = ref($invocant) || $invocant; + my $subclass = ''; + + # Create a ref to an empty hash and bless it + my $self = {}; + + my $dbh = Bugzilla->dbh; + + # Confirm that the $setting_name is properly formed; + # if not, throw a code error. + # + # NOTE: due to the way that setting names are used in templates, + # they must conform to to the limitations set for HTML NAMEs and IDs. + # + if (!($setting_name =~ /^[a-zA-Z][-.:\w]*$/)) { + ThrowCodeError("setting_name_invalid", {name => $setting_name}); + } + + # If there were only two parameters passed in, then we need + # to retrieve the information for this setting ourselves. + if (scalar @_ == 0) { + + my ($default, $is_enabled, $value); + ($default, $is_enabled, $value, $subclass) = $dbh->selectrow_array( + q{SELECT default_value, is_enabled, setting_value, subclass FROM setting LEFT JOIN profile_setting ON setting.name = profile_setting.setting_name WHERE name = ? - AND profile_setting.user_id = ?}, - undef, - $setting_name, $user_id); - - # if not defined, then grab the default value - if (! defined $value) { - ($default, $is_enabled, $subclass) = - $dbh->selectrow_array( - q{SELECT default_value, is_enabled, subclass + AND profile_setting.user_id = ?}, undef, $setting_name, $user_id + ); + + # if not defined, then grab the default value + if (!defined $value) { + ($default, $is_enabled, $subclass) = $dbh->selectrow_array( + q{SELECT default_value, is_enabled, subclass FROM setting - WHERE name = ?}, - undef, - $setting_name); - } - - $self->{'is_enabled'} = $is_enabled; - $self->{'default_value'} = $default; - - # IF the setting is enabled, AND the user has chosen a setting - # THEN return that value - # ELSE return the site default, and note that it is the default. - if ( ($is_enabled) && (defined $value) ) { - $self->{'value'} = $value; - } else { - $self->{'value'} = $default; - $self->{'isdefault'} = 1; - } - } - else { - # If the values were passed in, simply assign them and return. - $self->{'is_enabled'} = shift; - $self->{'default_value'} = shift; - $self->{'value'} = shift; - $self->{'is_default'} = shift; - $subclass = shift; - } - if ($subclass) { - eval('require ' . $class . '::' . $subclass); - $@ && ThrowCodeError('setting_subclass_invalid', - {'subclass' => $subclass}); - $class = $class . '::' . $subclass; + WHERE name = ?}, undef, $setting_name + ); } - bless($self, $class); - $self->{'_setting_name'} = $setting_name; - $self->{'_user_id'} = $user_id; + $self->{'is_enabled'} = $is_enabled; + $self->{'default_value'} = $default; - return $self; + # IF the setting is enabled, AND the user has chosen a setting + # THEN return that value + # ELSE return the site default, and note that it is the default. + if (($is_enabled) && (defined $value)) { + $self->{'value'} = $value; + } + else { + $self->{'value'} = $default; + $self->{'isdefault'} = 1; + } + } + else { + # If the values were passed in, simply assign them and return. + $self->{'is_enabled'} = shift; + $self->{'default_value'} = shift; + $self->{'value'} = shift; + $self->{'is_default'} = shift; + $subclass = shift; + } + if ($subclass) { + eval('require ' . $class . '::' . $subclass); + $@ && ThrowCodeError('setting_subclass_invalid', {'subclass' => $subclass}); + $class = $class . '::' . $subclass; + } + bless($self, $class); + + $self->{'_setting_name'} = $setting_name; + $self->{'_user_id'} = $user_id; + + return $self; } ############################### @@ -120,191 +116,205 @@ sub new { ############################### sub add_setting { - my ($name, $values, $default_value, $subclass, $force_check, - $silently) = @_; - my $dbh = Bugzilla->dbh; - - my $exists = _setting_exists($name); - return if ($exists && !$force_check); - - ($name && length( $default_value // '' )) - || ThrowCodeError("setting_info_invalid"); - - if ($exists) { - # If this setting exists, we delete it and regenerate it. - $dbh->do('DELETE FROM setting_value WHERE name = ?', undef, $name); - $dbh->do('DELETE FROM setting WHERE name = ?', undef, $name); - # Remove obsolete user preferences for this setting. - if (defined $values && scalar(@$values)) { - my $list = join(', ', map {$dbh->quote($_)} @$values); - $dbh->do("DELETE FROM profile_setting - WHERE setting_name = ? AND setting_value NOT IN ($list)", - undef, $name); - } - } - elsif (!$silently) { - print get_text('install_setting_new', { name => $name }) . "\n"; - } - $dbh->do(q{INSERT INTO setting (name, default_value, is_enabled, subclass) - VALUES (?, ?, 1, ?)}, - undef, ($name, $default_value, $subclass)); + my ($name, $values, $default_value, $subclass, $force_check, $silently) = @_; + my $dbh = Bugzilla->dbh; + + my $exists = _setting_exists($name); + return if ($exists && !$force_check); + + ($name && length($default_value // '')) + || ThrowCodeError("setting_info_invalid"); - my $sth = $dbh->prepare(q{INSERT INTO setting_value (name, value, sortindex) - VALUES (?, ?, ?)}); + if ($exists) { - my $sortindex = 5; - foreach my $key (@$values){ - $sth->execute($name, $key, $sortindex); - $sortindex += 5; + # If this setting exists, we delete it and regenerate it. + $dbh->do('DELETE FROM setting_value WHERE name = ?', undef, $name); + $dbh->do('DELETE FROM setting WHERE name = ?', undef, $name); + + # Remove obsolete user preferences for this setting. + if (defined $values && scalar(@$values)) { + my $list = join(', ', map { $dbh->quote($_) } @$values); + $dbh->do( + "DELETE FROM profile_setting + WHERE setting_name = ? AND setting_value NOT IN ($list)", undef, + $name + ); } + } + elsif (!$silently) { + print get_text('install_setting_new', {name => $name}) . "\n"; + } + $dbh->do( + q{INSERT INTO setting (name, default_value, is_enabled, subclass) + VALUES (?, ?, 1, ?)}, undef, ($name, $default_value, $subclass) + ); + + my $sth = $dbh->prepare( + q{INSERT INTO setting_value (name, value, sortindex) + VALUES (?, ?, ?)} + ); + + my $sortindex = 5; + foreach my $key (@$values) { + $sth->execute($name, $key, $sortindex); + $sortindex += 5; + } } sub get_all_settings { - my ($user_id) = @_; - my $settings = {}; - my $dbh = Bugzilla->dbh; - - my $cache_key = "user_settings.$user_id"; - my $rows = Bugzilla->memcached->get_config({ key => $cache_key }); - if (!$rows) { - $rows = $dbh->selectall_arrayref( - q{SELECT name, default_value, is_enabled, setting_value, subclass + my ($user_id) = @_; + my $settings = {}; + my $dbh = Bugzilla->dbh; + + my $cache_key = "user_settings.$user_id"; + my $rows = Bugzilla->memcached->get_config({key => $cache_key}); + if (!$rows) { + $rows = $dbh->selectall_arrayref( + q{SELECT name, default_value, is_enabled, setting_value, subclass FROM setting LEFT JOIN profile_setting ON setting.name = profile_setting.setting_name - AND profile_setting.user_id = ?}, undef, ($user_id)); - Bugzilla->memcached->set_config({ key => $cache_key, data => $rows }); - } + AND profile_setting.user_id = ?}, undef, ($user_id) + ); + Bugzilla->memcached->set_config({key => $cache_key, data => $rows}); + } - foreach my $row (@$rows) { - my ($name, $default_value, $is_enabled, $value, $subclass) = @$row; + foreach my $row (@$rows) { + my ($name, $default_value, $is_enabled, $value, $subclass) = @$row; - my $is_default; + my $is_default; - if ( ($is_enabled) && (defined $value) ) { - $is_default = 0; - } else { - $value = $default_value; - $is_default = 1; - } - - $settings->{$name} = new Bugzilla::User::Setting( - $name, $user_id, $is_enabled, - $default_value, $value, $is_default, $subclass); + if (($is_enabled) && (defined $value)) { + $is_default = 0; } + else { + $value = $default_value; + $is_default = 1; + } + + $settings->{$name} + = new Bugzilla::User::Setting($name, $user_id, $is_enabled, $default_value, + $value, $is_default, $subclass); + } - return $settings; + return $settings; } sub clear_settings_cache { - my ($user_id) = @_; - Bugzilla->memcached->clear_config({ key => "user_settings.$user_id" }); + my ($user_id) = @_; + Bugzilla->memcached->clear_config({key => "user_settings.$user_id"}); } sub get_defaults { - my ($user_id) = @_; - my $dbh = Bugzilla->dbh; - my $default_settings = {}; + my ($user_id) = @_; + my $dbh = Bugzilla->dbh; + my $default_settings = {}; - $user_id ||= 0; + $user_id ||= 0; - my $rows = $dbh->selectall_arrayref(q{SELECT name, default_value, is_enabled, subclass - FROM setting}); + my $rows = $dbh->selectall_arrayref( + q{SELECT name, default_value, is_enabled, subclass + FROM setting} + ); - foreach my $row (@$rows) { - my ($name, $default_value, $is_enabled, $subclass) = @$row; + foreach my $row (@$rows) { + my ($name, $default_value, $is_enabled, $subclass) = @$row; - $default_settings->{$name} = new Bugzilla::User::Setting( - $name, $user_id, $is_enabled, $default_value, $default_value, 1, - $subclass); - } + $default_settings->{$name} + = new Bugzilla::User::Setting($name, $user_id, $is_enabled, $default_value, + $default_value, 1, $subclass); + } - return $default_settings; + return $default_settings; } sub set_default { - my ($setting_name, $default_value, $is_enabled) = @_; - my $dbh = Bugzilla->dbh; + my ($setting_name, $default_value, $is_enabled) = @_; + my $dbh = Bugzilla->dbh; - my $sth = $dbh->prepare(q{UPDATE setting + my $sth = $dbh->prepare( + q{UPDATE setting SET default_value = ?, is_enabled = ? - WHERE name = ?}); - $sth->execute($default_value, $is_enabled, $setting_name); + WHERE name = ?} + ); + $sth->execute($default_value, $is_enabled, $setting_name); } sub _setting_exists { - my ($setting_name) = @_; - my $dbh = Bugzilla->dbh; - return $dbh->selectrow_arrayref( - "SELECT 1 FROM setting WHERE name = ?", undef, $setting_name) || 0; + my ($setting_name) = @_; + my $dbh = Bugzilla->dbh; + return $dbh->selectrow_arrayref("SELECT 1 FROM setting WHERE name = ?", + undef, $setting_name) + || 0; } sub legal_values { - my ($self) = @_; + my ($self) = @_; - return $self->{'legal_values'} if defined $self->{'legal_values'}; + return $self->{'legal_values'} if defined $self->{'legal_values'}; - my $dbh = Bugzilla->dbh; - $self->{'legal_values'} = $dbh->selectcol_arrayref( - q{SELECT value + my $dbh = Bugzilla->dbh; + $self->{'legal_values'} = $dbh->selectcol_arrayref( + q{SELECT value FROM setting_value WHERE name = ? - ORDER BY sortindex}, - undef, $self->{'_setting_name'}); + ORDER BY sortindex}, undef, $self->{'_setting_name'} + ); - return $self->{'legal_values'}; + return $self->{'legal_values'}; } sub validate_value { - my $self = shift; - - if (grep(/^$_[0]$/, @{$self->legal_values()})) { - trick_taint($_[0]); - } - else { - ThrowCodeError('setting_value_invalid', - {'name' => $self->{'_setting_name'}, - 'value' => $_[0]}); - } + my $self = shift; + + if (grep(/^$_[0]$/, @{$self->legal_values()})) { + trick_taint($_[0]); + } + else { + ThrowCodeError('setting_value_invalid', + {'name' => $self->{'_setting_name'}, 'value' => $_[0]}); + } } sub reset_to_default { - my ($self) = @_; + my ($self) = @_; - my $dbh = Bugzilla->dbh; - my $sth = $dbh->do(q{ DELETE + my $dbh = Bugzilla->dbh; + my $sth = $dbh->do( + q{ DELETE FROM profile_setting WHERE setting_name = ? - AND user_id = ?}, - undef, $self->{'_setting_name'}, $self->{'_user_id'}); - $self->{'value'} = $self->{'default_value'}; - $self->{'is_default'} = 1; + AND user_id = ?}, undef, $self->{'_setting_name'}, + $self->{'_user_id'} + ); + $self->{'value'} = $self->{'default_value'}; + $self->{'is_default'} = 1; } sub set { - my ($self, $value) = @_; - my $dbh = Bugzilla->dbh; - my $query; + my ($self, $value) = @_; + my $dbh = Bugzilla->dbh; + my $query; - if ($self->{'is_default'}) { - $query = q{INSERT INTO profile_setting + if ($self->{'is_default'}) { + $query = q{INSERT INTO profile_setting (setting_value, setting_name, user_id) VALUES (?,?,?)}; - } else { - $query = q{UPDATE profile_setting + } + else { + $query = q{UPDATE profile_setting SET setting_value = ? WHERE setting_name = ? AND user_id = ?}; - } - $dbh->do($query, undef, $value, $self->{'_setting_name'}, $self->{'_user_id'}); + } + $dbh->do($query, undef, $value, $self->{'_setting_name'}, $self->{'_user_id'}); - $self->{'value'} = $value; - $self->{'is_default'} = 0; + $self->{'value'} = $value; + $self->{'is_default'} = 0; } - 1; __END__ diff --git a/Bugzilla/User/Setting/Lang.pm b/Bugzilla/User/Setting/Lang.pm index d980b7a92..d1aeb3421 100644 --- a/Bugzilla/User/Setting/Lang.pm +++ b/Bugzilla/User/Setting/Lang.pm @@ -16,11 +16,11 @@ use parent qw(Bugzilla::User::Setting); use Bugzilla::Constants; sub legal_values { - my ($self) = @_; + my ($self) = @_; - return $self->{'legal_values'} if defined $self->{'legal_values'}; + return $self->{'legal_values'} if defined $self->{'legal_values'}; - return $self->{'legal_values'} = Bugzilla->languages; + return $self->{'legal_values'} = Bugzilla->languages; } 1; diff --git a/Bugzilla/User/Setting/Skin.pm b/Bugzilla/User/Setting/Skin.pm index 7b0688c0c..0447b02ab 100644 --- a/Bugzilla/User/Setting/Skin.pm +++ b/Bugzilla/User/Setting/Skin.pm @@ -21,24 +21,26 @@ use File::Basename; use constant BUILTIN_SKIN_NAMES => ['standard']; sub legal_values { - my ($self) = @_; + my ($self) = @_; - return $self->{'legal_values'} if defined $self->{'legal_values'}; + return $self->{'legal_values'} if defined $self->{'legal_values'}; - my $dirbase = bz_locations()->{'skinsdir'} . '/contrib'; - # Avoid modification of the list BUILTIN_SKIN_NAMES points to by copying the - # list over instead of simply writing $legal_values = BUILTIN_SKIN_NAMES. - my @legal_values = @{(BUILTIN_SKIN_NAMES)}; + my $dirbase = bz_locations()->{'skinsdir'} . '/contrib'; - foreach my $direntry (glob(catdir($dirbase, '*'))) { - if (-d $direntry) { - next if basename($direntry) =~ /^cvs$/i; - # Stylesheet set found - push(@legal_values, basename($direntry)); - } + # Avoid modification of the list BUILTIN_SKIN_NAMES points to by copying the + # list over instead of simply writing $legal_values = BUILTIN_SKIN_NAMES. + my @legal_values = @{(BUILTIN_SKIN_NAMES)}; + + foreach my $direntry (glob(catdir($dirbase, '*'))) { + if (-d $direntry) { + next if basename($direntry) =~ /^cvs$/i; + + # Stylesheet set found + push(@legal_values, basename($direntry)); } + } - return $self->{'legal_values'} = \@legal_values; + return $self->{'legal_values'} = \@legal_values; } 1; diff --git a/Bugzilla/User/Setting/Timezone.pm b/Bugzilla/User/Setting/Timezone.pm index 8959d1dda..b6b2503b5 100644 --- a/Bugzilla/User/Setting/Timezone.pm +++ b/Bugzilla/User/Setting/Timezone.pm @@ -18,19 +18,21 @@ use parent qw(Bugzilla::User::Setting); use Bugzilla::Constants; sub legal_values { - my ($self) = @_; + my ($self) = @_; - return $self->{'legal_values'} if defined $self->{'legal_values'}; + return $self->{'legal_values'} if defined $self->{'legal_values'}; - my @timezones = DateTime::TimeZone->all_names; - # Remove old formats, such as CST6CDT, EST, EST5EDT. - @timezones = grep { $_ =~ m#.+/.+#} @timezones; - # Append 'local' to the list, which will use the timezone - # given by the server. - push(@timezones, 'local'); - push(@timezones, 'UTC'); + my @timezones = DateTime::TimeZone->all_names; - return $self->{'legal_values'} = \@timezones; + # Remove old formats, such as CST6CDT, EST, EST5EDT. + @timezones = grep { $_ =~ m#.+/.+# } @timezones; + + # Append 'local' to the list, which will use the timezone + # given by the server. + push(@timezones, 'local'); + push(@timezones, 'UTC'); + + return $self->{'legal_values'} = \@timezones; } 1; diff --git a/Bugzilla/UserAgent.pm b/Bugzilla/UserAgent.pm index 14637038c..1995cc82f 100644 --- a/Bugzilla/UserAgent.pm +++ b/Bugzilla/UserAgent.pm @@ -20,176 +20,200 @@ use List::MoreUtils qw(natatime); use constant DEFAULT_VALUE => 'Other'; use constant PLATFORMS_MAP => ( - # PowerPC - qr/\(.*PowerPC.*\)/i => ["PowerPC", "Macintosh"], - # AMD64, Intel x86_64 - qr/\(.*[ix0-9]86 (?:on |\()x86_64.*\)/ => ["IA32", "x86", "PC"], - qr/\(.*amd64.*\)/ => ["AMD64", "x86_64", "PC"], - qr/\(.*x86_64.*\)/ => ["AMD64", "x86_64", "PC"], - # Intel IA64 - qr/\(.*IA64.*\)/ => ["IA64", "PC"], - # Intel x86 - qr/\(.*Intel.*\)/ => ["IA32", "x86", "PC"], - qr/\(.*[ix0-9]86.*\)/ => ["IA32", "x86", "PC"], - # Versions of Windows that only run on Intel x86 - qr/\(.*Win(?:dows |)[39M].*\)/ => ["IA32", "x86", "PC"], - qr/\(.*Win(?:dows |)16.*\)/ => ["IA32", "x86", "PC"], - # Sparc - qr/\(.*sparc.*\)/ => ["Sparc", "Sun"], - qr/\(.*sun4.*\)/ => ["Sparc", "Sun"], - # Alpha - qr/\(.*AXP.*\)/i => ["Alpha", "DEC"], - qr/\(.*[ _]Alpha.\D/i => ["Alpha", "DEC"], - qr/\(.*[ _]Alpha\)/i => ["Alpha", "DEC"], - # MIPS - qr/\(.*IRIX.*\)/i => ["MIPS", "SGI"], - qr/\(.*MIPS.*\)/i => ["MIPS", "SGI"], - # 68k - qr/\(.*68K.*\)/ => ["68k", "Macintosh"], - qr/\(.*680[x0]0.*\)/ => ["68k", "Macintosh"], - # HP - qr/\(.*9000.*\)/ => ["PA-RISC", "HP"], - # ARM - qr/\(.*(?:iPod|iPad|iPhone).*\)/ => ["ARM"], - qr/\(.*ARM.*\)/ => ["ARM", "PocketPC"], - # PocketPC intentionally before PowerPC - qr/\(.*Windows CE.*PPC.*\)/ => ["ARM", "PocketPC"], - # PowerPC - qr/\(.*PPC.*\)/ => ["PowerPC", "Macintosh"], - qr/\(.*AIX.*\)/ => ["PowerPC", "Macintosh"], - # Stereotypical and broken - qr/\(.*Windows CE.*\)/ => ["ARM", "PocketPC"], - qr/\(.*Macintosh.*\)/ => ["68k", "Macintosh"], - qr/\(.*Mac OS [89].*\)/ => ["68k", "Macintosh"], - qr/\(.*WOW64.*\)/ => ["x86_64"], - qr/\(.*Win64.*\)/ => ["IA64"], - qr/\(Win.*\)/ => ["IA32", "x86", "PC"], - qr/\(.*Win(?:dows[ -])NT.*\)/ => ["IA32", "x86", "PC"], - qr/\(.*OSF.*\)/ => ["Alpha", "DEC"], - qr/\(.*HP-?UX.*\)/i => ["PA-RISC", "HP"], - qr/\(.*IRIX.*\)/i => ["MIPS", "SGI"], - qr/\(.*(SunOS|Solaris).*\)/ => ["Sparc", "Sun"], - # Braindead old browsers who didn't follow convention: - qr/Amiga/ => ["68k", "Macintosh"], - qr/WinMosaic/ => ["IA32", "x86", "PC"], + + # PowerPC + qr/\(.*PowerPC.*\)/i => ["PowerPC", "Macintosh"], + + # AMD64, Intel x86_64 + qr/\(.*[ix0-9]86 (?:on |\()x86_64.*\)/ => ["IA32", "x86", "PC"], + qr/\(.*amd64.*\)/ => ["AMD64", "x86_64", "PC"], + qr/\(.*x86_64.*\)/ => ["AMD64", "x86_64", "PC"], + + # Intel IA64 + qr/\(.*IA64.*\)/ => ["IA64", "PC"], + + # Intel x86 + qr/\(.*Intel.*\)/ => ["IA32", "x86", "PC"], + qr/\(.*[ix0-9]86.*\)/ => ["IA32", "x86", "PC"], + + # Versions of Windows that only run on Intel x86 + qr/\(.*Win(?:dows |)[39M].*\)/ => ["IA32", "x86", "PC"], + qr/\(.*Win(?:dows |)16.*\)/ => ["IA32", "x86", "PC"], + + # Sparc + qr/\(.*sparc.*\)/ => ["Sparc", "Sun"], + qr/\(.*sun4.*\)/ => ["Sparc", "Sun"], + + # Alpha + qr/\(.*AXP.*\)/i => ["Alpha", "DEC"], + qr/\(.*[ _]Alpha.\D/i => ["Alpha", "DEC"], + qr/\(.*[ _]Alpha\)/i => ["Alpha", "DEC"], + + # MIPS + qr/\(.*IRIX.*\)/i => ["MIPS", "SGI"], + qr/\(.*MIPS.*\)/i => ["MIPS", "SGI"], + + # 68k + qr/\(.*68K.*\)/ => ["68k", "Macintosh"], + qr/\(.*680[x0]0.*\)/ => ["68k", "Macintosh"], + + # HP + qr/\(.*9000.*\)/ => ["PA-RISC", "HP"], + + # ARM + qr/\(.*(?:iPod|iPad|iPhone).*\)/ => ["ARM"], + qr/\(.*ARM.*\)/ => ["ARM", "PocketPC"], + + # PocketPC intentionally before PowerPC + qr/\(.*Windows CE.*PPC.*\)/ => ["ARM", "PocketPC"], + + # PowerPC + qr/\(.*PPC.*\)/ => ["PowerPC", "Macintosh"], + qr/\(.*AIX.*\)/ => ["PowerPC", "Macintosh"], + + # Stereotypical and broken + qr/\(.*Windows CE.*\)/ => ["ARM", "PocketPC"], + qr/\(.*Macintosh.*\)/ => ["68k", "Macintosh"], + qr/\(.*Mac OS [89].*\)/ => ["68k", "Macintosh"], + qr/\(.*WOW64.*\)/ => ["x86_64"], + qr/\(.*Win64.*\)/ => ["IA64"], + qr/\(Win.*\)/ => ["IA32", "x86", "PC"], + qr/\(.*Win(?:dows[ -])NT.*\)/ => ["IA32", "x86", "PC"], + qr/\(.*OSF.*\)/ => ["Alpha", "DEC"], + qr/\(.*HP-?UX.*\)/i => ["PA-RISC", "HP"], + qr/\(.*IRIX.*\)/i => ["MIPS", "SGI"], + qr/\(.*(SunOS|Solaris).*\)/ => ["Sparc", "Sun"], + + # Braindead old browsers who didn't follow convention: + qr/Amiga/ => ["68k", "Macintosh"], + qr/WinMosaic/ => ["IA32", "x86", "PC"], ); use constant OS_MAP => ( - # Sun - qr/\(.*Solaris.*\)/ => ["Solaris"], - qr/\(.*SunOS 5.11.*\)/ => [("OpenSolaris", "Opensolaris", "Solaris 11")], - qr/\(.*SunOS 5.10.*\)/ => ["Solaris 10"], - qr/\(.*SunOS 5.9.*\)/ => ["Solaris 9"], - qr/\(.*SunOS 5.8.*\)/ => ["Solaris 8"], - qr/\(.*SunOS 5.7.*\)/ => ["Solaris 7"], - qr/\(.*SunOS 5.6.*\)/ => ["Solaris 6"], - qr/\(.*SunOS 5.5.*\)/ => ["Solaris 5"], - qr/\(.*SunOS 5.*\)/ => ["Solaris"], - qr/\(.*SunOS.*sun4u.*\)/ => ["Solaris"], - qr/\(.*SunOS.*i86pc.*\)/ => ["Solaris"], - qr/\(.*SunOS.*\)/ => ["SunOS"], - # BSD - qr/\(.*BSD\/(?:OS|386).*\)/ => ["BSDI"], - qr/\(.*FreeBSD.*\)/ => ["FreeBSD"], - qr/\(.*OpenBSD.*\)/ => ["OpenBSD"], - qr/\(.*NetBSD.*\)/ => ["NetBSD"], - # Misc POSIX - qr/\(.*IRIX.*\)/ => ["IRIX"], - qr/\(.*OSF.*\)/ => ["OSF/1"], - qr/\(.*Linux.*\)/ => ["Linux"], - qr/\(.*BeOS.*\)/ => ["BeOS"], - qr/\(.*AIX.*\)/ => ["AIX"], - qr/\(.*OS\/2.*\)/ => ["OS/2"], - qr/\(.*QNX.*\)/ => ["Neutrino"], - qr/\(.*VMS.*\)/ => ["OpenVMS"], - qr/\(.*HP-?UX.*\)/ => ["HP-UX"], - qr/\(.*Android.*\)/ => ["Android"], - # Windows - qr/\(.*Windows XP.*\)/ => ["Windows XP"], - qr/\(.*Windows NT 10\.0.*\)/ => ["Windows 10"], - qr/\(.*Windows NT 6\.4.*\)/ => ["Windows 10"], - qr/\(.*Windows NT 6\.3.*\)/ => ["Windows 8.1"], - qr/\(.*Windows NT 6\.2.*\)/ => ["Windows 8"], - qr/\(.*Windows NT 6\.1.*\)/ => ["Windows 7"], - qr/\(.*Windows NT 6\.0.*\)/ => ["Windows Vista"], - qr/\(.*Windows NT 5\.2.*\)/ => ["Windows Server 2003"], - qr/\(.*Windows NT 5\.1.*\)/ => ["Windows XP"], - qr/\(.*Windows 2000.*\)/ => ["Windows 2000"], - qr/\(.*Windows NT 5.*\)/ => ["Windows 2000"], - qr/\(.*Win.*9[8x].*4\.9.*\)/ => ["Windows ME"], - qr/\(.*Win(?:dows |)M[Ee].*\)/ => ["Windows ME"], - qr/\(.*Win(?:dows |)98.*\)/ => ["Windows 98"], - qr/\(.*Win(?:dows |)95.*\)/ => ["Windows 95"], - qr/\(.*Win(?:dows |)16.*\)/ => ["Windows 3.1"], - qr/\(.*Win(?:dows[ -]|)NT.*\)/ => ["Windows NT"], - qr/\(.*Windows.*NT.*\)/ => ["Windows NT"], - # OS X - qr/\(.*(?:iPad|iPhone).*OS 7.*\)/ => ["iOS 7"], - qr/\(.*(?:iPad|iPhone).*OS 6.*\)/ => ["iOS 6"], - qr/\(.*(?:iPad|iPhone).*OS 5.*\)/ => ["iOS 5"], - qr/\(.*(?:iPad|iPhone).*OS 4.*\)/ => ["iOS 4"], - qr/\(.*(?:iPad|iPhone).*OS 3.*\)/ => ["iOS 3"], - qr/\(.*(?:iPod|iPad|iPhone).*\)/ => ["iOS"], - qr/\(.*Mac OS X (?:|Mach-O |\()10.8.*\)/ => ["Mac OS X 10.8"], - qr/\(.*Mac OS X (?:|Mach-O |\()10.7.*\)/ => ["Mac OS X 10.7"], - qr/\(.*Mac OS X (?:|Mach-O |\()10.6.*\)/ => ["Mac OS X 10.6"], - qr/\(.*Mac OS X (?:|Mach-O |\()10.5.*\)/ => ["Mac OS X 10.5"], - qr/\(.*Mac OS X (?:|Mach-O |\()10.4.*\)/ => ["Mac OS X 10.4"], - qr/\(.*Mac OS X (?:|Mach-O |\()10.3.*\)/ => ["Mac OS X 10.3"], - qr/\(.*Mac OS X (?:|Mach-O |\()10.2.*\)/ => ["Mac OS X 10.2"], - qr/\(.*Mac OS X (?:|Mach-O |\()10.1.*\)/ => ["Mac OS X 10.1"], - # Unfortunately, OS X 10.4 was the first to support Intel. This is fallback - # support because some browsers refused to include the OS Version. - qr/\(.*Intel.*Mac OS X.*\)/ => ["Mac OS X 10.4"], - # OS X 10.3 is the most likely default version of PowerPC Macs - # OS X 10.0 is more for configurations which didn't setup 10.x versions - qr/\(.*Mac OS X.*\)/ => [("Mac OS X 10.3", "Mac OS X 10.0", "Mac OS X")], - qr/\(.*Mac OS 9.*\)/ => [("Mac System 9.x", "Mac System 9.0")], - qr/\(.*Mac OS 8\.6.*\)/ => [("Mac System 8.6", "Mac System 8.5")], - qr/\(.*Mac OS 8\.5.*\)/ => ["Mac System 8.5"], - qr/\(.*Mac OS 8\.1.*\)/ => [("Mac System 8.1", "Mac System 8.0")], - qr/\(.*Mac OS 8\.0.*\)/ => ["Mac System 8.0"], - qr/\(.*Mac OS 8[^.].*\)/ => ["Mac System 8.0"], - qr/\(.*Mac OS 8.*\)/ => ["Mac System 8.6"], - qr/\(.*Darwin.*\)/ => [("Mac OS X 10.0", "Mac OS X")], - # Silly - qr/\(.*Mac.*PowerPC.*\)/ => ["Mac System 9.x"], - qr/\(.*Mac.*PPC.*\)/ => ["Mac System 9.x"], - qr/\(.*Mac.*68k.*\)/ => ["Mac System 8.0"], - # Evil - qr/Amiga/i => ["Other"], - qr/WinMosaic/ => ["Windows 95"], - qr/\(.*32bit.*\)/ => ["Windows 95"], - qr/\(.*16bit.*\)/ => ["Windows 3.1"], - qr/\(.*PowerPC.*\)/ => ["Mac System 9.x"], - qr/\(.*PPC.*\)/ => ["Mac System 9.x"], - qr/\(.*68K.*\)/ => ["Mac System 8.0"], + + # Sun + qr/\(.*Solaris.*\)/ => ["Solaris"], + qr/\(.*SunOS 5.11.*\)/ => [("OpenSolaris", "Opensolaris", "Solaris 11")], + qr/\(.*SunOS 5.10.*\)/ => ["Solaris 10"], + qr/\(.*SunOS 5.9.*\)/ => ["Solaris 9"], + qr/\(.*SunOS 5.8.*\)/ => ["Solaris 8"], + qr/\(.*SunOS 5.7.*\)/ => ["Solaris 7"], + qr/\(.*SunOS 5.6.*\)/ => ["Solaris 6"], + qr/\(.*SunOS 5.5.*\)/ => ["Solaris 5"], + qr/\(.*SunOS 5.*\)/ => ["Solaris"], + qr/\(.*SunOS.*sun4u.*\)/ => ["Solaris"], + qr/\(.*SunOS.*i86pc.*\)/ => ["Solaris"], + qr/\(.*SunOS.*\)/ => ["SunOS"], + + # BSD + qr/\(.*BSD\/(?:OS|386).*\)/ => ["BSDI"], + qr/\(.*FreeBSD.*\)/ => ["FreeBSD"], + qr/\(.*OpenBSD.*\)/ => ["OpenBSD"], + qr/\(.*NetBSD.*\)/ => ["NetBSD"], + + # Misc POSIX + qr/\(.*IRIX.*\)/ => ["IRIX"], + qr/\(.*OSF.*\)/ => ["OSF/1"], + qr/\(.*Linux.*\)/ => ["Linux"], + qr/\(.*BeOS.*\)/ => ["BeOS"], + qr/\(.*AIX.*\)/ => ["AIX"], + qr/\(.*OS\/2.*\)/ => ["OS/2"], + qr/\(.*QNX.*\)/ => ["Neutrino"], + qr/\(.*VMS.*\)/ => ["OpenVMS"], + qr/\(.*HP-?UX.*\)/ => ["HP-UX"], + qr/\(.*Android.*\)/ => ["Android"], + + # Windows + qr/\(.*Windows XP.*\)/ => ["Windows XP"], + qr/\(.*Windows NT 10\.0.*\)/ => ["Windows 10"], + qr/\(.*Windows NT 6\.4.*\)/ => ["Windows 10"], + qr/\(.*Windows NT 6\.3.*\)/ => ["Windows 8.1"], + qr/\(.*Windows NT 6\.2.*\)/ => ["Windows 8"], + qr/\(.*Windows NT 6\.1.*\)/ => ["Windows 7"], + qr/\(.*Windows NT 6\.0.*\)/ => ["Windows Vista"], + qr/\(.*Windows NT 5\.2.*\)/ => ["Windows Server 2003"], + qr/\(.*Windows NT 5\.1.*\)/ => ["Windows XP"], + qr/\(.*Windows 2000.*\)/ => ["Windows 2000"], + qr/\(.*Windows NT 5.*\)/ => ["Windows 2000"], + qr/\(.*Win.*9[8x].*4\.9.*\)/ => ["Windows ME"], + qr/\(.*Win(?:dows |)M[Ee].*\)/ => ["Windows ME"], + qr/\(.*Win(?:dows |)98.*\)/ => ["Windows 98"], + qr/\(.*Win(?:dows |)95.*\)/ => ["Windows 95"], + qr/\(.*Win(?:dows |)16.*\)/ => ["Windows 3.1"], + qr/\(.*Win(?:dows[ -]|)NT.*\)/ => ["Windows NT"], + qr/\(.*Windows.*NT.*\)/ => ["Windows NT"], + + # OS X + qr/\(.*(?:iPad|iPhone).*OS 7.*\)/ => ["iOS 7"], + qr/\(.*(?:iPad|iPhone).*OS 6.*\)/ => ["iOS 6"], + qr/\(.*(?:iPad|iPhone).*OS 5.*\)/ => ["iOS 5"], + qr/\(.*(?:iPad|iPhone).*OS 4.*\)/ => ["iOS 4"], + qr/\(.*(?:iPad|iPhone).*OS 3.*\)/ => ["iOS 3"], + qr/\(.*(?:iPod|iPad|iPhone).*\)/ => ["iOS"], + qr/\(.*Mac OS X (?:|Mach-O |\()10.8.*\)/ => ["Mac OS X 10.8"], + qr/\(.*Mac OS X (?:|Mach-O |\()10.7.*\)/ => ["Mac OS X 10.7"], + qr/\(.*Mac OS X (?:|Mach-O |\()10.6.*\)/ => ["Mac OS X 10.6"], + qr/\(.*Mac OS X (?:|Mach-O |\()10.5.*\)/ => ["Mac OS X 10.5"], + qr/\(.*Mac OS X (?:|Mach-O |\()10.4.*\)/ => ["Mac OS X 10.4"], + qr/\(.*Mac OS X (?:|Mach-O |\()10.3.*\)/ => ["Mac OS X 10.3"], + qr/\(.*Mac OS X (?:|Mach-O |\()10.2.*\)/ => ["Mac OS X 10.2"], + qr/\(.*Mac OS X (?:|Mach-O |\()10.1.*\)/ => ["Mac OS X 10.1"], + + # Unfortunately, OS X 10.4 was the first to support Intel. This is fallback + # support because some browsers refused to include the OS Version. + qr/\(.*Intel.*Mac OS X.*\)/ => ["Mac OS X 10.4"], + + # OS X 10.3 is the most likely default version of PowerPC Macs + # OS X 10.0 is more for configurations which didn't setup 10.x versions + qr/\(.*Mac OS X.*\)/ => [("Mac OS X 10.3", "Mac OS X 10.0", "Mac OS X")], + qr/\(.*Mac OS 9.*\)/ => [("Mac System 9.x", "Mac System 9.0")], + qr/\(.*Mac OS 8\.6.*\)/ => [("Mac System 8.6", "Mac System 8.5")], + qr/\(.*Mac OS 8\.5.*\)/ => ["Mac System 8.5"], + qr/\(.*Mac OS 8\.1.*\)/ => [("Mac System 8.1", "Mac System 8.0")], + qr/\(.*Mac OS 8\.0.*\)/ => ["Mac System 8.0"], + qr/\(.*Mac OS 8[^.].*\)/ => ["Mac System 8.0"], + qr/\(.*Mac OS 8.*\)/ => ["Mac System 8.6"], + qr/\(.*Darwin.*\)/ => [("Mac OS X 10.0", "Mac OS X")], + + # Silly + qr/\(.*Mac.*PowerPC.*\)/ => ["Mac System 9.x"], + qr/\(.*Mac.*PPC.*\)/ => ["Mac System 9.x"], + qr/\(.*Mac.*68k.*\)/ => ["Mac System 8.0"], + + # Evil + qr/Amiga/i => ["Other"], + qr/WinMosaic/ => ["Windows 95"], + qr/\(.*32bit.*\)/ => ["Windows 95"], + qr/\(.*16bit.*\)/ => ["Windows 3.1"], + qr/\(.*PowerPC.*\)/ => ["Mac System 9.x"], + qr/\(.*PPC.*\)/ => ["Mac System 9.x"], + qr/\(.*68K.*\)/ => ["Mac System 8.0"], ); sub detect_platform { - my $userAgent = $ENV{'HTTP_USER_AGENT'}; - my @detected; - my $iterator = natatime(2, PLATFORMS_MAP); - while (my($re, $ra) = $iterator->()) { - if ($userAgent =~ $re) { - push @detected, @$ra; - } + my $userAgent = $ENV{'HTTP_USER_AGENT'}; + my @detected; + my $iterator = natatime(2, PLATFORMS_MAP); + while (my ($re, $ra) = $iterator->()) { + if ($userAgent =~ $re) { + push @detected, @$ra; } - return _pick_valid_field_value('rep_platform', @detected); + } + return _pick_valid_field_value('rep_platform', @detected); } sub detect_op_sys { - my $userAgent = $ENV{'HTTP_USER_AGENT'} || ''; - my @detected; - my $iterator = natatime(2, OS_MAP); - while (my($re, $ra) = $iterator->()) { - if ($userAgent =~ $re) { - push @detected, @$ra; - } + my $userAgent = $ENV{'HTTP_USER_AGENT'} || ''; + my @detected; + my $iterator = natatime(2, OS_MAP); + while (my ($re, $ra) = $iterator->()) { + if ($userAgent =~ $re) { + push @detected, @$ra; } - push(@detected, "Windows") if grep(/^Windows /, @detected); - push(@detected, "Mac OS") if grep(/^Mac /, @detected); - return _pick_valid_field_value('op_sys', @detected); + } + push(@detected, "Windows") if grep(/^Windows /, @detected); + push(@detected, "Mac OS") if grep(/^Mac /, @detected); + return _pick_valid_field_value('op_sys', @detected); } # Takes the name of a field and a list of possible values for that field. @@ -197,11 +221,11 @@ sub detect_op_sys { # field. # Returns 'Other' if none of the values match. sub _pick_valid_field_value { - my ($field, @values) = @_; - foreach my $value (@values) { - return $value if check_field($field, $value, undef, 1); - } - return DEFAULT_VALUE; + my ($field, @values) = @_; + foreach my $value (@values) { + return $value if check_field($field, $value, undef, 1); + } + return DEFAULT_VALUE; } 1; diff --git a/Bugzilla/Util.pm b/Bugzilla/Util.pm index 57ce5f6b6..0edd361ce 100644 --- a/Bugzilla/Util.pm +++ b/Bugzilla/Util.pm @@ -13,18 +13,18 @@ use warnings; use parent qw(Exporter); @Bugzilla::Util::EXPORT = qw(trick_taint detaint_natural detaint_signed - html_quote url_quote xml_quote - css_class_quote html_light_quote - i_am_cgi i_am_webservice correct_urlbase remote_ip - validate_ip do_ssl_redirect_if_required use_attachbase - diff_arrays on_main_db - trim wrap_hard wrap_comment find_wrap_point - format_time validate_date validate_time datetime_from - is_7bit_clean bz_crypt generate_random_password - validate_email_syntax check_email_syntax clean_text - get_text template_var display_value disable_utf8 - detect_encoding email_filter - join_activity_entries read_text write_text); + html_quote url_quote xml_quote + css_class_quote html_light_quote + i_am_cgi i_am_webservice correct_urlbase remote_ip + validate_ip do_ssl_redirect_if_required use_attachbase + diff_arrays on_main_db + trim wrap_hard wrap_comment find_wrap_point + format_time validate_date validate_time datetime_from + is_7bit_clean bz_crypt generate_random_password + validate_email_syntax check_email_syntax clean_text + get_text template_var display_value disable_utf8 + detect_encoding email_filter + join_activity_entries read_text write_text); use Bugzilla::Constants; use Bugzilla::RNG qw(irand); @@ -43,642 +43,684 @@ use File::Basename qw(dirname); use File::Temp qw(tempfile); sub trick_taint { - require Carp; - Carp::confess("Undef to trick_taint") unless defined $_[0]; - my $match = $_[0] =~ /^(.*)$/s; - $_[0] = $match ? $1 : undef; - return (defined($_[0])); + require Carp; + Carp::confess("Undef to trick_taint") unless defined $_[0]; + my $match = $_[0] =~ /^(.*)$/s; + $_[0] = $match ? $1 : undef; + return (defined($_[0])); } sub detaint_natural { - my $match = $_[0] =~ /^([0-9]+)$/; - $_[0] = $match ? int($1) : undef; - return (defined($_[0])); + my $match = $_[0] =~ /^([0-9]+)$/; + $_[0] = $match ? int($1) : undef; + return (defined($_[0])); } sub detaint_signed { - my $match = $_[0] =~ /^([-+]?[0-9]+)$/; - # The "int()" call removes any leading plus sign. - $_[0] = $match ? int($1) : undef; - return (defined($_[0])); + my $match = $_[0] =~ /^([-+]?[0-9]+)$/; + + # The "int()" call removes any leading plus sign. + $_[0] = $match ? int($1) : undef; + return (defined($_[0])); } # Bug 120030: Override html filter to obscure the '@' in user # visible strings. # Bug 319331: Handle BiDi disruptions. sub html_quote { - my $var = shift; - $var =~ s/&/&/g; - $var =~ s/</g; - $var =~ s/>/>/g; - $var =~ s/"/"/g; - # Obscure '@'. - $var =~ s/\@/\@/g; - - state $use_utf8 = Bugzilla->params->{'utf8'}; - - if ($use_utf8) { - # Remove control characters if the encoding is utf8. - # Other multibyte encodings may be using this range; so ignore if not utf8. - $var =~ s/(?![\t\r\n])[[:cntrl:]]//g; - - # Remove the following characters because they're - # influencing BiDi: - # -------------------------------------------------------- - # |Code |Name |UTF-8 representation| - # |------|--------------------------|--------------------| - # |U+202a|Left-To-Right Embedding |0xe2 0x80 0xaa | - # |U+202b|Right-To-Left Embedding |0xe2 0x80 0xab | - # |U+202c|Pop Directional Formatting|0xe2 0x80 0xac | - # |U+202d|Left-To-Right Override |0xe2 0x80 0xad | - # |U+202e|Right-To-Left Override |0xe2 0x80 0xae | - # -------------------------------------------------------- - # - # The following are characters influencing BiDi, too, but - # they can be spared from filtering because they don't - # influence more than one character right or left: - # -------------------------------------------------------- - # |Code |Name |UTF-8 representation| - # |------|--------------------------|--------------------| - # |U+200e|Left-To-Right Mark |0xe2 0x80 0x8e | - # |U+200f|Right-To-Left Mark |0xe2 0x80 0x8f | - # -------------------------------------------------------- - $var =~ tr/\x{202a}-\x{202e}//d; - } - return $var; + my $var = shift; + $var =~ s/&/&/g; + $var =~ s/</g; + $var =~ s/>/>/g; + $var =~ s/"/"/g; + + # Obscure '@'. + $var =~ s/\@/\@/g; + + state $use_utf8 = Bugzilla->params->{'utf8'}; + + if ($use_utf8) { + + # Remove control characters if the encoding is utf8. + # Other multibyte encodings may be using this range; so ignore if not utf8. + $var =~ s/(?![\t\r\n])[[:cntrl:]]//g; + + # Remove the following characters because they're + # influencing BiDi: + # -------------------------------------------------------- + # |Code |Name |UTF-8 representation| + # |------|--------------------------|--------------------| + # |U+202a|Left-To-Right Embedding |0xe2 0x80 0xaa | + # |U+202b|Right-To-Left Embedding |0xe2 0x80 0xab | + # |U+202c|Pop Directional Formatting|0xe2 0x80 0xac | + # |U+202d|Left-To-Right Override |0xe2 0x80 0xad | + # |U+202e|Right-To-Left Override |0xe2 0x80 0xae | + # -------------------------------------------------------- + # + # The following are characters influencing BiDi, too, but + # they can be spared from filtering because they don't + # influence more than one character right or left: + # -------------------------------------------------------- + # |Code |Name |UTF-8 representation| + # |------|--------------------------|--------------------| + # |U+200e|Left-To-Right Mark |0xe2 0x80 0x8e | + # |U+200f|Right-To-Left Mark |0xe2 0x80 0x8f | + # -------------------------------------------------------- + $var =~ tr/\x{202a}-\x{202e}//d; + } + return $var; } sub read_text { - my ($filename) = @_; - open my $fh, '<:encoding(utf-8)', $filename; - local $/ = undef; - my $content = <$fh>; - close $fh; - return $content; + my ($filename) = @_; + open my $fh, '<:encoding(utf-8)', $filename; + local $/ = undef; + my $content = <$fh>; + close $fh; + return $content; } sub write_text { - my ($filename, $content) = @_; - my ($tmp_fh, $tmp_filename) = tempfile('.tmp.XXXXXXXXXX', - DIR => dirname($filename), - UNLINK => 0, - ); - binmode $tmp_fh, ':encoding(utf-8)'; - print $tmp_fh $content; - close $tmp_fh; - # File::Temp tries for secure files, but File::Slurp used the umask. - chmod(0666 & ~umask, $tmp_filename); - rename $tmp_filename, $filename; + my ($filename, $content) = @_; + my ($tmp_fh, $tmp_filename) + = tempfile('.tmp.XXXXXXXXXX', DIR => dirname($filename), UNLINK => 0,); + binmode $tmp_fh, ':encoding(utf-8)'; + print $tmp_fh $content; + close $tmp_fh; + + # File::Temp tries for secure files, but File::Slurp used the umask. + chmod(0666 & ~umask, $tmp_filename); + rename $tmp_filename, $filename; } sub html_light_quote { - my ($text) = @_; - # admin/table.html.tmpl calls |FILTER html_light| many times. - # There is no need to recreate the HTML::Scrubber object again and again. - my $scrubber = Bugzilla->process_cache->{html_scrubber}; - - # List of allowed HTML elements having no attributes. - my @allow = qw(b strong em i u p br abbr acronym ins del cite code var - dfn samp kbd big small sub sup tt dd dt dl ul li ol - fieldset legend); - - if (!Bugzilla->feature('html_desc')) { - my $safe = join('|', @allow); - my $chr = chr(1); - - # First, escape safe elements. - $text =~ s#<($safe)>#$chr$1$chr#go; - $text =~ s#($safe)>#$chr/$1$chr#go; - # Now filter < and >. - $text =~ s#<#<#g; - $text =~ s#>#>#g; - # Restore safe elements. - $text =~ s#$chr/($safe)$chr#$1>#go; - $text =~ s#$chr($safe)$chr#<$1>#go; - return $text; - } - elsif (!$scrubber) { - # We can be less restrictive. We can accept elements with attributes. - push(@allow, qw(a blockquote q span)); - - # Allowed protocols. - my $safe_protocols = join('|', SAFE_PROTOCOLS); - my $protocol_regexp = qr{(^(?:$safe_protocols):|^[^:]+$)}i; - - # Deny all elements and attributes unless explicitly authorized. - my @default = (0 => { - id => 1, - name => 1, - class => 1, - '*' => 0, # Reject all other attributes. - } - ); - - # Specific rules for allowed elements. If no specific rule is set - # for a given element, then the default is used. - my @rules = (a => { - href => $protocol_regexp, - target => qr{^(?:_blank|_parent|_self|_top)$}i, - title => 1, - id => 1, - name => 1, - class => 1, - '*' => 0, # Reject all other attributes. - }, - blockquote => { - cite => $protocol_regexp, - id => 1, - name => 1, - class => 1, - '*' => 0, # Reject all other attributes. - }, - 'q' => { - cite => $protocol_regexp, - id => 1, - name => 1, - class => 1, - '*' => 0, # Reject all other attributes. - }, - ); - - Bugzilla->process_cache->{html_scrubber} = $scrubber = - HTML::Scrubber->new(default => \@default, - allow => \@allow, - rules => \@rules, - comment => 0, - process => 0); - } - return $scrubber->scrub($text); + my ($text) = @_; + + # admin/table.html.tmpl calls |FILTER html_light| many times. + # There is no need to recreate the HTML::Scrubber object again and again. + my $scrubber = Bugzilla->process_cache->{html_scrubber}; + + # List of allowed HTML elements having no attributes. + my @allow = qw(b strong em i u p br abbr acronym ins del cite code var + dfn samp kbd big small sub sup tt dd dt dl ul li ol + fieldset legend); + + if (!Bugzilla->feature('html_desc')) { + my $safe = join('|', @allow); + my $chr = chr(1); + + # First, escape safe elements. + $text =~ s#<($safe)>#$chr$1$chr#go; + $text =~ s#($safe)>#$chr/$1$chr#go; + + # Now filter < and >. + $text =~ s#<#<#g; + $text =~ s#>#>#g; + + # Restore safe elements. + $text =~ s#$chr/($safe)$chr#$1>#go; + $text =~ s#$chr($safe)$chr#<$1>#go; + return $text; + } + elsif (!$scrubber) { + + # We can be less restrictive. We can accept elements with attributes. + push(@allow, qw(a blockquote q span)); + + # Allowed protocols. + my $safe_protocols = join('|', SAFE_PROTOCOLS); + my $protocol_regexp = qr{(^(?:$safe_protocols):|^[^:]+$)}i; + + # Deny all elements and attributes unless explicitly authorized. + my @default = ( + 0 => { + id => 1, + name => 1, + class => 1, + '*' => 0, # Reject all other attributes. + } + ); + + # Specific rules for allowed elements. If no specific rule is set + # for a given element, then the default is used. + my @rules = ( + a => { + href => $protocol_regexp, + target => qr{^(?:_blank|_parent|_self|_top)$}i, + title => 1, + id => 1, + name => 1, + class => 1, + '*' => 0, # Reject all other attributes. + }, + blockquote => { + cite => $protocol_regexp, + id => 1, + name => 1, + class => 1, + '*' => 0, # Reject all other attributes. + }, + 'q' => { + cite => $protocol_regexp, + id => 1, + name => 1, + class => 1, + '*' => 0, # Reject all other attributes. + }, + ); + + Bugzilla->process_cache->{html_scrubber} = $scrubber = HTML::Scrubber->new( + default => \@default, + allow => \@allow, + rules => \@rules, + comment => 0, + process => 0 + ); + } + return $scrubber->scrub($text); } sub email_filter { - my ($toencode) = @_; - if (!Bugzilla->user->id) { - my @emails = Email::Address->parse($toencode); - if (scalar @emails) { - my @hosts = map { quotemeta($_->host) } @emails; - my $hosts_re = join('|', @hosts); - $toencode =~ s/\@(?:$hosts_re)//g; - return $toencode; - } + my ($toencode) = @_; + if (!Bugzilla->user->id) { + my @emails = Email::Address->parse($toencode); + if (scalar @emails) { + my @hosts = map { quotemeta($_->host) } @emails; + my $hosts_re = join('|', @hosts); + $toencode =~ s/\@(?:$hosts_re)//g; + return $toencode; } - return $toencode; + } + return $toencode; } # This originally came from CGI.pm, by Lincoln D. Stein sub url_quote { - my ($toencode) = (@_); - utf8::encode($toencode) # The below regex works only on bytes - if Bugzilla->params->{'utf8'} && utf8::is_utf8($toencode); - $toencode =~ s/([^a-zA-Z0-9_\-.])/uc sprintf("%%%02x",ord($1))/eg; - return $toencode; + my ($toencode) = (@_); + utf8::encode($toencode) # The below regex works only on bytes + if Bugzilla->params->{'utf8'} && utf8::is_utf8($toencode); + $toencode =~ s/([^a-zA-Z0-9_\-.])/uc sprintf("%%%02x",ord($1))/eg; + return $toencode; } sub css_class_quote { - my ($toencode) = (@_); - $toencode =~ s#[ /]#_#g; - $toencode =~ s/([^a-zA-Z0-9_\-.])/uc sprintf("%x;",ord($1))/eg; - return $toencode; + my ($toencode) = (@_); + $toencode =~ s#[ /]#_#g; + $toencode =~ s/([^a-zA-Z0-9_\-.])/uc sprintf("%x;",ord($1))/eg; + return $toencode; } sub xml_quote { - my ($var) = (@_); - $var =~ s/\&/\&/g; - $var =~ s/\</g; - $var =~ s/>/\>/g; - $var =~ s/\"/\"/g; - $var =~ s/\'/\'/g; - - # the following nukes characters disallowed by the XML 1.0 - # spec, Production 2.2. 1.0 declares that only the following - # are valid: - # (#x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF]) - $var =~ s/([\x{0001}-\x{0008}]| + my ($var) = (@_); + $var =~ s/\&/\&/g; + $var =~ s/\</g; + $var =~ s/>/\>/g; + $var =~ s/\"/\"/g; + $var =~ s/\'/\'/g; + + # the following nukes characters disallowed by the XML 1.0 + # spec, Production 2.2. 1.0 declares that only the following + # are valid: + # (#x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF]) + $var =~ s/([\x{0001}-\x{0008}]| [\x{000B}-\x{000C}]| [\x{000E}-\x{001F}]| [\x{D800}-\x{DFFF}]| [\x{FFFE}-\x{FFFF}])//gx; - return $var; + return $var; } sub i_am_cgi { - # I use SERVER_SOFTWARE because it's required to be - # defined for all requests in the CGI spec. - return exists $ENV{'SERVER_SOFTWARE'} ? 1 : 0; + + # I use SERVER_SOFTWARE because it's required to be + # defined for all requests in the CGI spec. + return exists $ENV{'SERVER_SOFTWARE'} ? 1 : 0; } sub i_am_webservice { - my $usage_mode = Bugzilla->usage_mode; - return $usage_mode == USAGE_MODE_XMLRPC - || $usage_mode == USAGE_MODE_JSON - || $usage_mode == USAGE_MODE_REST; + my $usage_mode = Bugzilla->usage_mode; + return + $usage_mode == USAGE_MODE_XMLRPC + || $usage_mode == USAGE_MODE_JSON + || $usage_mode == USAGE_MODE_REST; } # This exists as a separate function from Bugzilla::CGI::redirect_to_https # because we don't want to create a CGI object during XML-RPC calls # (doing so can mess up XML-RPC). sub do_ssl_redirect_if_required { - return if !i_am_cgi(); - return if !Bugzilla->params->{'ssl_redirect'}; - - my $sslbase = Bugzilla->params->{'sslbase'}; - - # If we're already running under SSL, never redirect. - return if uc($ENV{HTTPS} || '') eq 'ON'; - # Never redirect if there isn't an sslbase. - return if !$sslbase; - Bugzilla->cgi->redirect_to_https(); + return if !i_am_cgi(); + return if !Bugzilla->params->{'ssl_redirect'}; + + my $sslbase = Bugzilla->params->{'sslbase'}; + + # If we're already running under SSL, never redirect. + return if uc($ENV{HTTPS} || '') eq 'ON'; + + # Never redirect if there isn't an sslbase. + return if !$sslbase; + Bugzilla->cgi->redirect_to_https(); } sub correct_urlbase { - my $ssl = Bugzilla->params->{'ssl_redirect'}; - my $urlbase = Bugzilla->params->{'urlbase'}; - my $sslbase = Bugzilla->params->{'sslbase'}; - - if (!$sslbase) { - return $urlbase; - } - elsif ($ssl) { - return $sslbase; - } - else { - # Return what the user currently uses. - return (uc($ENV{HTTPS} || '') eq 'ON') ? $sslbase : $urlbase; - } + my $ssl = Bugzilla->params->{'ssl_redirect'}; + my $urlbase = Bugzilla->params->{'urlbase'}; + my $sslbase = Bugzilla->params->{'sslbase'}; + + if (!$sslbase) { + return $urlbase; + } + elsif ($ssl) { + return $sslbase; + } + else { + # Return what the user currently uses. + return (uc($ENV{HTTPS} || '') eq 'ON') ? $sslbase : $urlbase; + } } sub remote_ip { - my $ip = $ENV{'REMOTE_ADDR'} || '127.0.0.1'; - my @proxies = split(/[\s,]+/, Bugzilla->params->{'inbound_proxies'}); - - # If the IP address is one of our trusted proxies, then we look at - # the X-Forwarded-For header to determine the real remote IP address. - if ($ENV{'HTTP_X_FORWARDED_FOR'} && first { $_ eq $ip } @proxies) { - my @ips = split(/[\s,]+/, $ENV{'HTTP_X_FORWARDED_FOR'}); - # This header can contain several IP addresses. We want the - # IP address of the machine which connected to our proxies as - # all other IP addresses may be fake or internal ones. - # Note that this may block a whole external proxy, but we have - # no way to determine if this proxy is malicious or trustable. - foreach my $remote_ip (reverse @ips) { - if (!first { $_ eq $remote_ip } @proxies) { - # Keep the original IP address if the remote IP is invalid. - $ip = validate_ip($remote_ip) || $ip; - last; - } - } + my $ip = $ENV{'REMOTE_ADDR'} || '127.0.0.1'; + my @proxies = split(/[\s,]+/, Bugzilla->params->{'inbound_proxies'}); + + # If the IP address is one of our trusted proxies, then we look at + # the X-Forwarded-For header to determine the real remote IP address. + if ($ENV{'HTTP_X_FORWARDED_FOR'} && first { $_ eq $ip } @proxies) { + my @ips = split(/[\s,]+/, $ENV{'HTTP_X_FORWARDED_FOR'}); + + # This header can contain several IP addresses. We want the + # IP address of the machine which connected to our proxies as + # all other IP addresses may be fake or internal ones. + # Note that this may block a whole external proxy, but we have + # no way to determine if this proxy is malicious or trustable. + foreach my $remote_ip (reverse @ips) { + if (!first { $_ eq $remote_ip } @proxies) { + + # Keep the original IP address if the remote IP is invalid. + $ip = validate_ip($remote_ip) || $ip; + last; + } } - return $ip; + } + return $ip; } sub validate_ip { - my $ip = shift; - return is_ipv4($ip) || is_ipv6($ip); + my $ip = shift; + return is_ipv4($ip) || is_ipv6($ip); } # Copied from Data::Validate::IP::is_ipv4(). sub is_ipv4 { - my $ip = shift; - return unless defined $ip; + my $ip = shift; + return unless defined $ip; - my @octets = $ip =~ /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/; - return unless scalar(@octets) == 4; + my @octets = $ip =~ /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/; + return unless scalar(@octets) == 4; - foreach my $octet (@octets) { - return unless ($octet >= 0 && $octet <= 255 && $octet !~ /^0\d{1,2}$/); - } + foreach my $octet (@octets) { + return unless ($octet >= 0 && $octet <= 255 && $octet !~ /^0\d{1,2}$/); + } - # The IP address is valid and can now be detainted. - return join('.', @octets); + # The IP address is valid and can now be detainted. + return join('.', @octets); } # Copied from Data::Validate::IP::is_ipv6(). sub is_ipv6 { - my $ip = shift; - return unless defined $ip; - - # If there is a :: then there must be only one :: and the length - # can be variable. Without it, the length must be 8 groups. - my @chunks = split(':', $ip); - - # Need to check if the last chunk is an IPv4 address, if it is we - # pop it off and exempt it from the normal IPv6 checking and stick - # it back on at the end. If there is only one chunk and it's an IPv4 - # address, then it isn't an IPv6 address. - my $ipv4; - my $expected_chunks = 8; - if (@chunks > 1 && is_ipv4($chunks[$#chunks])) { - $ipv4 = pop(@chunks); - $expected_chunks--; - } + my $ip = shift; + return unless defined $ip; + + # If there is a :: then there must be only one :: and the length + # can be variable. Without it, the length must be 8 groups. + my @chunks = split(':', $ip); + + # Need to check if the last chunk is an IPv4 address, if it is we + # pop it off and exempt it from the normal IPv6 checking and stick + # it back on at the end. If there is only one chunk and it's an IPv4 + # address, then it isn't an IPv6 address. + my $ipv4; + my $expected_chunks = 8; + if (@chunks > 1 && is_ipv4($chunks[$#chunks])) { + $ipv4 = pop(@chunks); + $expected_chunks--; + } + + my $empty = 0; + + # Workaround to handle trailing :: being valid. + if ($ip =~ /[0-9a-f]{1,4}::$/) { + $empty++; - my $empty = 0; - # Workaround to handle trailing :: being valid. - if ($ip =~ /[0-9a-f]{1,4}::$/) { - $empty++; # Single trailing ':' is invalid. - } elsif ($ip =~ /:$/) { - return; - } + } + elsif ($ip =~ /:$/) { + return; + } - foreach my $chunk (@chunks) { - return unless $chunk =~ /^[0-9a-f]{0,4}$/i; - $empty++ if $chunk eq ''; - } - # More than one :: block is bad, but if it starts with :: it will - # look like two, so we need an exception. - if ($empty == 2 && $ip =~ /^::/) { - # This is ok - } elsif ($empty > 1) { - return; - } + foreach my $chunk (@chunks) { + return unless $chunk =~ /^[0-9a-f]{0,4}$/i; + $empty++ if $chunk eq ''; + } + + # More than one :: block is bad, but if it starts with :: it will + # look like two, so we need an exception. + if ($empty == 2 && $ip =~ /^::/) { + + # This is ok + } + elsif ($empty > 1) { + return; + } - push(@chunks, $ipv4) if $ipv4; - # Need 8 chunks, or we need an empty section that could be filled - # to represent the missing '0' sections. - return unless (@chunks == $expected_chunks || @chunks < $expected_chunks && $empty); + push(@chunks, $ipv4) if $ipv4; - my $ipv6 = join(':', @chunks); - # The IP address is valid and can now be detainted. - trick_taint($ipv6); + # Need 8 chunks, or we need an empty section that could be filled + # to represent the missing '0' sections. + return + unless (@chunks == $expected_chunks || @chunks < $expected_chunks && $empty); - # Need to handle the exception of trailing :: being valid. - return "${ipv6}::" if $ip =~ /::$/; - return $ipv6; + my $ipv6 = join(':', @chunks); + + # The IP address is valid and can now be detainted. + trick_taint($ipv6); + + # Need to handle the exception of trailing :: being valid. + return "${ipv6}::" if $ip =~ /::$/; + return $ipv6; } sub use_attachbase { - my $attachbase = Bugzilla->params->{'attachment_base'}; - return ($attachbase ne '' - && $attachbase ne Bugzilla->params->{'urlbase'} - && $attachbase ne Bugzilla->params->{'sslbase'}) ? 1 : 0; + my $attachbase = Bugzilla->params->{'attachment_base'}; + return ($attachbase ne '' + && $attachbase ne Bugzilla->params->{'urlbase'} + && $attachbase ne Bugzilla->params->{'sslbase'}) ? 1 : 0; } sub diff_arrays { - my ($old_ref, $new_ref, $attrib) = @_; - $attrib ||= 'name'; - - my (%counts, %pos); - # We are going to alter the old array. - my @old = @$old_ref; - my $i = 0; - - # $counts{foo}-- means old, $counts{foo}++ means new. - # If $counts{foo} becomes positive, then we are adding new items, - # else we simply cancel one old existing item. Remaining items - # in the old list have been removed. - foreach (@old) { - next unless defined $_; - my $value = blessed($_) ? $_->$attrib : $_; - $counts{$value}--; - push @{$pos{$value}}, $i++; + my ($old_ref, $new_ref, $attrib) = @_; + $attrib ||= 'name'; + + my (%counts, %pos); + + # We are going to alter the old array. + my @old = @$old_ref; + my $i = 0; + + # $counts{foo}-- means old, $counts{foo}++ means new. + # If $counts{foo} becomes positive, then we are adding new items, + # else we simply cancel one old existing item. Remaining items + # in the old list have been removed. + foreach (@old) { + next unless defined $_; + my $value = blessed($_) ? $_->$attrib : $_; + $counts{$value}--; + push @{$pos{$value}}, $i++; + } + my @added; + foreach (@$new_ref) { + next unless defined $_; + my $value = blessed($_) ? $_->$attrib : $_; + if (++$counts{$value} > 0) { + + # Ignore empty strings, but objects having an empty string + # as attribute are fine. + push(@added, $_) unless ($value eq '' && !blessed($_)); } - my @added; - foreach (@$new_ref) { - next unless defined $_; - my $value = blessed($_) ? $_->$attrib : $_; - if (++$counts{$value} > 0) { - # Ignore empty strings, but objects having an empty string - # as attribute are fine. - push(@added, $_) unless ($value eq '' && !blessed($_)); - } - else { - my $old_pos = shift @{$pos{$value}}; - $old[$old_pos] = undef; - } + else { + my $old_pos = shift @{$pos{$value}}; + $old[$old_pos] = undef; } - # Ignore canceled items as well as empty strings. - my @removed = grep { defined $_ && $_ ne '' } @old; - return (\@removed, \@added); + } + + # Ignore canceled items as well as empty strings. + my @removed = grep { defined $_ && $_ ne '' } @old; + return (\@removed, \@added); } sub trim { - my ($str) = @_; - if ($str) { - $str =~ s/^\s+//g; - $str =~ s/\s+$//g; - } - return $str; + my ($str) = @_; + if ($str) { + $str =~ s/^\s+//g; + $str =~ s/\s+$//g; + } + return $str; } sub wrap_comment { - my ($comment, $cols) = @_; - my $wrappedcomment = ""; - - # Use 'local', as recommended by Text::Wrap's perldoc. - local $Text::Wrap::columns = $cols || COMMENT_COLS; - # Make words that are longer than COMMENT_COLS not wrap. - local $Text::Wrap::huge = 'overflow'; - # Don't mess with tabs. - local $Text::Wrap::unexpand = 0; - - # If the line starts with ">", don't wrap it. Otherwise, wrap. - foreach my $line (split(/\r\n|\r|\n/, $comment)) { - if ($line =~ qr/^>/) { - $wrappedcomment .= ($line . "\n"); - } - else { - $wrappedcomment .= (wrap('', '', $line) . "\n"); - } + my ($comment, $cols) = @_; + my $wrappedcomment = ""; + + # Use 'local', as recommended by Text::Wrap's perldoc. + local $Text::Wrap::columns = $cols || COMMENT_COLS; + + # Make words that are longer than COMMENT_COLS not wrap. + local $Text::Wrap::huge = 'overflow'; + + # Don't mess with tabs. + local $Text::Wrap::unexpand = 0; + + # If the line starts with ">", don't wrap it. Otherwise, wrap. + foreach my $line (split(/\r\n|\r|\n/, $comment)) { + if ($line =~ qr/^>/) { + $wrappedcomment .= ($line . "\n"); } + else { + $wrappedcomment .= (wrap('', '', $line) . "\n"); + } + } - chomp($wrappedcomment); # Text::Wrap adds an extra newline at the end. - return $wrappedcomment; + chomp($wrappedcomment); # Text::Wrap adds an extra newline at the end. + return $wrappedcomment; } sub find_wrap_point { - my ($string, $maxpos) = @_; - if (!$string) { return 0 } - if (length($string) < $maxpos) { return length($string) } - my $wrappoint = rindex($string, ",", $maxpos); # look for comma - if ($wrappoint <= 0) { # can't find comma - $wrappoint = rindex($string, " ", $maxpos); # look for space - if ($wrappoint <= 0) { # can't find space - $wrappoint = rindex($string, "-", $maxpos); # look for hyphen - if ($wrappoint <= 0) { # can't find hyphen - $wrappoint = $maxpos; # just truncate it - } else { - $wrappoint++; # leave hyphen on the left side - } - } + my ($string, $maxpos) = @_; + if (!$string) { return 0 } + if (length($string) < $maxpos) { return length($string) } + my $wrappoint = rindex($string, ",", $maxpos); # look for comma + if ($wrappoint <= 0) { # can't find comma + $wrappoint = rindex($string, " ", $maxpos); # look for space + if ($wrappoint <= 0) { # can't find space + $wrappoint = rindex($string, "-", $maxpos); # look for hyphen + if ($wrappoint <= 0) { # can't find hyphen + $wrappoint = $maxpos; # just truncate it + } + else { + $wrappoint++; # leave hyphen on the left side + } } - return $wrappoint; + } + return $wrappoint; } sub join_activity_entries { - my ($field, $current_change, $new_change) = @_; - # We need to insert characters as these were removed by old - # LogActivityEntry code. - - return $new_change if $current_change eq ''; - - # Buglists and see_also need the comma restored - if ($field eq 'dependson' || $field eq 'blocked' || $field eq 'see_also') { - if (substr($new_change, 0, 1) eq ',' || substr($new_change, 0, 1) eq ' ') { - return $current_change . $new_change; - } else { - return $current_change . ', ' . $new_change; - } - } + my ($field, $current_change, $new_change) = @_; - # Assume bug_file_loc contain a single url, don't insert a delimiter - if ($field eq 'bug_file_loc') { - return $current_change . $new_change; - } + # We need to insert characters as these were removed by old + # LogActivityEntry code. + + return $new_change if $current_change eq ''; - # All other fields get a space unless the first character of the second - # string is a comma or space + # Buglists and see_also need the comma restored + if ($field eq 'dependson' || $field eq 'blocked' || $field eq 'see_also') { if (substr($new_change, 0, 1) eq ',' || substr($new_change, 0, 1) eq ' ') { - return $current_change . $new_change; - } else { - return $current_change . ' ' . $new_change; + return $current_change . $new_change; + } + else { + return $current_change . ', ' . $new_change; } + } + + # Assume bug_file_loc contain a single url, don't insert a delimiter + if ($field eq 'bug_file_loc') { + return $current_change . $new_change; + } + + # All other fields get a space unless the first character of the second + # string is a comma or space + if (substr($new_change, 0, 1) eq ',' || substr($new_change, 0, 1) eq ' ') { + return $current_change . $new_change; + } + else { + return $current_change . ' ' . $new_change; + } } sub wrap_hard { - my ($string, $columns) = @_; - local $Text::Wrap::columns = $columns; - local $Text::Wrap::unexpand = 0; - local $Text::Wrap::huge = 'wrap'; - - my $wrapped = wrap('', '', $string); - chomp($wrapped); - return $wrapped; + my ($string, $columns) = @_; + local $Text::Wrap::columns = $columns; + local $Text::Wrap::unexpand = 0; + local $Text::Wrap::huge = 'wrap'; + + my $wrapped = wrap('', '', $string); + chomp($wrapped); + return $wrapped; } sub format_time { - my ($date, $format, $timezone) = @_; - - # If $format is not set, try to guess the correct date format. - if (!$format) { - if (!ref $date - && $date =~ /^(\d{4})[-\.](\d{2})[-\.](\d{2}) (\d{2}):(\d{2})(:(\d{2}))?$/) - { - my $sec = $7; - if (defined $sec) { - $format = "%Y-%m-%d %T %Z"; - } else { - $format = "%Y-%m-%d %R %Z"; - } - } else { - # Default date format. See DateTime for other formats available. - $format = "%Y-%m-%d %R %Z"; - } - } - - my $dt = ref $date ? $date : datetime_from($date, $timezone); - $date = defined $dt ? $dt->strftime($format) : ''; - return trim($date); -} - -sub datetime_from { - my ($date, $timezone) = @_; - - # In the database, this is the "0" date. - return undef if $date =~ /^0000/; + my ($date, $format, $timezone) = @_; - my @time; - # Most dates will be in this format, avoid strptime's generic parser - if ($date =~ /^(\d{4})[\.-](\d{2})[\.-](\d{2})(?: (\d{2}):(\d{2}):(\d{2}))?$/) { - @time = ($6, $5, $4, $3, $2 - 1, $1 - 1900, undef); + # If $format is not set, try to guess the correct date format. + if (!$format) { + if (!ref $date + && $date =~ /^(\d{4})[-\.](\d{2})[-\.](\d{2}) (\d{2}):(\d{2})(:(\d{2}))?$/) + { + my $sec = $7; + if (defined $sec) { + $format = "%Y-%m-%d %T %Z"; + } + else { + $format = "%Y-%m-%d %R %Z"; + } } else { - @time = strptime($date); - } - - unless (scalar @time) { - # If an unknown timezone is passed (such as MSK, for Moskow), - # strptime() is unable to parse the date. We try again, but we first - # remove the timezone. - $date =~ s/\s+\S+$//; - @time = strptime($date); + # Default date format. See DateTime for other formats available. + $format = "%Y-%m-%d %R %Z"; } + } - return undef if !@time; - - # strptime() counts years from 1900, except if they are older than 1901 - # in which case it returns the full year (so 1890 -> 1890, but 1984 -> 84, - # and 3790 -> 1890). We make a guess and assume that 1100 <= year < 3000. - $time[5] += 1900 if $time[5] < 1100; - - my %args = ( - year => $time[5], - # Months start from 0 (January). - month => $time[4] + 1, - day => $time[3], - hour => $time[2], - minute => $time[1], - # DateTime doesn't like fractional seconds. - # Also, sometimes seconds are undef. - second => defined($time[0]) ? int($time[0]) : undef, - # If a timezone was specified, use it. Otherwise, use the - # local timezone. - time_zone => DateTime::TimeZone->offset_as_string($time[6]) - || Bugzilla->local_timezone, - ); - - # If something wasn't specified in the date, it's best to just not - # pass it to DateTime at all. (This is important for doing datetime_from - # on the deadline field, which is usually just a date with no time.) - foreach my $arg (keys %args) { - delete $args{$arg} if !defined $args{$arg}; - } - - # This module takes time to load and is only used here, so we - # |require| it here rather than |use| it. - require DateTime; - my $dt = new DateTime(\%args); + my $dt = ref $date ? $date : datetime_from($date, $timezone); + $date = defined $dt ? $dt->strftime($format) : ''; + return trim($date); +} - # Now display the date using the given timezone, - # or the user's timezone if none is given. - $dt->set_time_zone($timezone || Bugzilla->user->timezone); - return $dt; +sub datetime_from { + my ($date, $timezone) = @_; + + # In the database, this is the "0" date. + return undef if $date =~ /^0000/; + + my @time; + + # Most dates will be in this format, avoid strptime's generic parser + if ($date =~ /^(\d{4})[\.-](\d{2})[\.-](\d{2})(?: (\d{2}):(\d{2}):(\d{2}))?$/) { + @time = ($6, $5, $4, $3, $2 - 1, $1 - 1900, undef); + } + else { + @time = strptime($date); + } + + unless (scalar @time) { + + # If an unknown timezone is passed (such as MSK, for Moskow), + # strptime() is unable to parse the date. We try again, but we first + # remove the timezone. + $date =~ s/\s+\S+$//; + @time = strptime($date); + } + + return undef if !@time; + + # strptime() counts years from 1900, except if they are older than 1901 + # in which case it returns the full year (so 1890 -> 1890, but 1984 -> 84, + # and 3790 -> 1890). We make a guess and assume that 1100 <= year < 3000. + $time[5] += 1900 if $time[5] < 1100; + + my %args = ( + year => $time[5], + + # Months start from 0 (January). + month => $time[4] + 1, + day => $time[3], + hour => $time[2], + minute => $time[1], + + # DateTime doesn't like fractional seconds. + # Also, sometimes seconds are undef. + second => defined($time[0]) ? int($time[0]) : undef, + + # If a timezone was specified, use it. Otherwise, use the + # local timezone. + time_zone => DateTime::TimeZone->offset_as_string($time[6]) + || Bugzilla->local_timezone, + ); + + # If something wasn't specified in the date, it's best to just not + # pass it to DateTime at all. (This is important for doing datetime_from + # on the deadline field, which is usually just a date with no time.) + foreach my $arg (keys %args) { + delete $args{$arg} if !defined $args{$arg}; + } + + # This module takes time to load and is only used here, so we + # |require| it here rather than |use| it. + require DateTime; + my $dt = new DateTime(\%args); + + # Now display the date using the given timezone, + # or the user's timezone if none is given. + $dt->set_time_zone($timezone || Bugzilla->user->timezone); + return $dt; } sub bz_crypt { - my ($password, $salt) = @_; - - my $algorithm; - if (!defined $salt) { - # If you don't use a salt, then people can create tables of - # hashes that map to particular passwords, and then break your - # hashing very easily if they have a large-enough table of common - # (or even uncommon) passwords. So we generate a unique salt for - # each password in the database, and then just prepend it to - # the hash. - $salt = generate_random_password(PASSWORD_SALT_LENGTH); - $algorithm = PASSWORD_DIGEST_ALGORITHM; - } - - # We append the algorithm used to the string. This is good because then - # we can change the algorithm being used, in the future, without - # disrupting the validation of existing passwords. Also, this tells - # us if a password is using the old "crypt" method of hashing passwords, - # because the algorithm will be missing from the string. - if ($salt =~ /{([^}]+)}$/) { - $algorithm = $1; - } - - # Wide characters cause crypt and Digest to die. - if (Bugzilla->params->{'utf8'}) { - utf8::encode($password) if utf8::is_utf8($password); - } - - my $crypted_password; - if (!$algorithm) { - # Crypt the password. - $crypted_password = crypt($password, $salt); - } - else { - my $hasher = Digest->new($algorithm); - # Newly created salts won't yet have a comma. - ($salt) = $salt =~ /^([^,]+),?/; - $hasher->add($password, $salt); - $crypted_password = $salt . ',' . $hasher->b64digest . "{$algorithm}"; - } - - # Return the crypted password. - return $crypted_password; + my ($password, $salt) = @_; + + my $algorithm; + if (!defined $salt) { + + # If you don't use a salt, then people can create tables of + # hashes that map to particular passwords, and then break your + # hashing very easily if they have a large-enough table of common + # (or even uncommon) passwords. So we generate a unique salt for + # each password in the database, and then just prepend it to + # the hash. + $salt = generate_random_password(PASSWORD_SALT_LENGTH); + $algorithm = PASSWORD_DIGEST_ALGORITHM; + } + + # We append the algorithm used to the string. This is good because then + # we can change the algorithm being used, in the future, without + # disrupting the validation of existing passwords. Also, this tells + # us if a password is using the old "crypt" method of hashing passwords, + # because the algorithm will be missing from the string. + if ($salt =~ /{([^}]+)}$/) { + $algorithm = $1; + } + + # Wide characters cause crypt and Digest to die. + if (Bugzilla->params->{'utf8'}) { + utf8::encode($password) if utf8::is_utf8($password); + } + + my $crypted_password; + if (!$algorithm) { + + # Crypt the password. + $crypted_password = crypt($password, $salt); + } + else { + my $hasher = Digest->new($algorithm); + + # Newly created salts won't yet have a comma. + ($salt) = $salt =~ /^([^,]+),?/; + $hasher->add($password, $salt); + $crypted_password = $salt . ',' . $hasher->b64digest . "{$algorithm}"; + } + + # Return the crypted password. + return $crypted_password; } # If you want to understand the security of strings generated by this @@ -688,191 +730,199 @@ sub bz_crypt { # by the number of characters you generate, and that gets you the equivalent # strength of the string in bits. sub generate_random_password { - my $size = shift || 10; # default to 10 chars if nothing specified - return join("", map{ ('0'..'9','a'..'z','A'..'Z')[irand 62] } (1..$size)); + my $size = shift || 10; # default to 10 chars if nothing specified + return + join("", map { ('0' .. '9', 'a' .. 'z', 'A' .. 'Z')[irand 62] } (1 .. $size)); } sub validate_email_syntax { - my ($addr) = @_; - my $match = Bugzilla->params->{'emailregexp'}; - my $email = $addr . Bugzilla->params->{'emailsuffix'}; - # This regexp follows RFC 2822 section 3.4.1. - my $addr_spec = $Email::Address::addr_spec; - # RFC 2822 section 2.1 specifies that email addresses must - # be made of US-ASCII characters only. - # Email::Address::addr_spec doesn't enforce this. - # We set the max length to 127 to ensure addresses aren't truncated when - # inserted into the tokens.eventdata field. - if ($addr =~ /$match/ - && $email !~ /\P{ASCII}/ - && $email =~ /^$addr_spec$/ - && length($email) <= 127) - { - # We assume these checks to suffice to consider the address untainted. - trick_taint($_[0]); - return 1; - } - return 0; + my ($addr) = @_; + my $match = Bugzilla->params->{'emailregexp'}; + my $email = $addr . Bugzilla->params->{'emailsuffix'}; + + # This regexp follows RFC 2822 section 3.4.1. + my $addr_spec = $Email::Address::addr_spec; + + # RFC 2822 section 2.1 specifies that email addresses must + # be made of US-ASCII characters only. + # Email::Address::addr_spec doesn't enforce this. + # We set the max length to 127 to ensure addresses aren't truncated when + # inserted into the tokens.eventdata field. + if ( $addr =~ /$match/ + && $email !~ /\P{ASCII}/ + && $email =~ /^$addr_spec$/ + && length($email) <= 127) + { + # We assume these checks to suffice to consider the address untainted. + trick_taint($_[0]); + return 1; + } + return 0; } sub check_email_syntax { - my ($addr) = @_; + my ($addr) = @_; - unless (validate_email_syntax(@_)) { - my $email = $addr . Bugzilla->params->{'emailsuffix'}; - ThrowUserError('illegal_email_address', { addr => $email }); - } + unless (validate_email_syntax(@_)) { + my $email = $addr . Bugzilla->params->{'emailsuffix'}; + ThrowUserError('illegal_email_address', {addr => $email}); + } } sub validate_date { - my ($date) = @_; - my $date2; - - # $ts is undefined if the parser fails. - my $ts = str2time($date); - if ($ts) { - $date2 = time2str("%Y-%m-%d", $ts); - - $date =~ s/(\d+)-0*(\d+?)-0*(\d+?)/$1-$2-$3/; - $date2 =~ s/(\d+)-0*(\d+?)-0*(\d+?)/$1-$2-$3/; - } - my $ret = ($ts && $date eq $date2); - return $ret ? 1 : 0; + my ($date) = @_; + my $date2; + + # $ts is undefined if the parser fails. + my $ts = str2time($date); + if ($ts) { + $date2 = time2str("%Y-%m-%d", $ts); + + $date =~ s/(\d+)-0*(\d+?)-0*(\d+?)/$1-$2-$3/; + $date2 =~ s/(\d+)-0*(\d+?)-0*(\d+?)/$1-$2-$3/; + } + my $ret = ($ts && $date eq $date2); + return $ret ? 1 : 0; } sub validate_time { - my ($time) = @_; - my $time2; - - # $ts is undefined if the parser fails. - my $ts = str2time($time); - if ($ts) { - $time2 = time2str("%H:%M:%S", $ts); - if ($time =~ /^(\d{1,2}):(\d\d)(?::(\d\d))?$/) { - $time = sprintf("%02d:%02d:%02d", $1, $2, $3 || 0); - } + my ($time) = @_; + my $time2; + + # $ts is undefined if the parser fails. + my $ts = str2time($time); + if ($ts) { + $time2 = time2str("%H:%M:%S", $ts); + if ($time =~ /^(\d{1,2}):(\d\d)(?::(\d\d))?$/) { + $time = sprintf("%02d:%02d:%02d", $1, $2, $3 || 0); } - my $ret = ($ts && $time eq $time2); - return $ret ? 1 : 0; + } + my $ret = ($ts && $time eq $time2); + return $ret ? 1 : 0; } sub is_7bit_clean { - return $_[0] !~ /[^\x20-\x7E\x0A\x0D]/; + return $_[0] !~ /[^\x20-\x7E\x0A\x0D]/; } sub clean_text { - my $dtext = shift; - if ($dtext) { - # change control characters into a space - $dtext =~ s/[\x00-\x1F\x7F]+/ /g; - } - return trim($dtext); + my $dtext = shift; + if ($dtext) { + + # change control characters into a space + $dtext =~ s/[\x00-\x1F\x7F]+/ /g; + } + return trim($dtext); } sub on_main_db (&) { - my $code = shift; - my $original_dbh = Bugzilla->dbh; - Bugzilla->request_cache->{dbh} = Bugzilla->dbh_main; - $code->(); - Bugzilla->request_cache->{dbh} = $original_dbh; + my $code = shift; + my $original_dbh = Bugzilla->dbh; + Bugzilla->request_cache->{dbh} = Bugzilla->dbh_main; + $code->(); + Bugzilla->request_cache->{dbh} = $original_dbh; } sub get_text { - my ($name, $vars) = @_; - my $template = Bugzilla->template_inner; - $vars ||= {}; - $vars->{'message'} = $name; - my $message; - $template->process('global/message.txt.tmpl', $vars, \$message) - || ThrowTemplateError($template->error()); - - # Remove the indenting that exists in messages.html.tmpl. - $message =~ s/^ //gm; - return $message; + my ($name, $vars) = @_; + my $template = Bugzilla->template_inner; + $vars ||= {}; + $vars->{'message'} = $name; + my $message; + $template->process('global/message.txt.tmpl', $vars, \$message) + || ThrowTemplateError($template->error()); + + # Remove the indenting that exists in messages.html.tmpl. + $message =~ s/^ //gm; + return $message; } sub template_var { - my $name = shift; - my $request_cache = Bugzilla->request_cache; - my $cache = $request_cache->{util_template_var} ||= {}; - my $lang = $request_cache->{template_current_lang}->[0] || ''; - return $cache->{$lang}->{$name} if defined $cache->{$lang}; - - my $template = Bugzilla->template_inner($lang); - my %vars; - # Note: If we suddenly start needing a lot of template_var variables, - # they should move into their own template, not field-descs. - $template->process('global/field-descs.none.tmpl', - { vars => \%vars, in_template_var => 1 }) - || ThrowTemplateError($template->error()); - - $cache->{$lang} = \%vars; - return $vars{$name}; + my $name = shift; + my $request_cache = Bugzilla->request_cache; + my $cache = $request_cache->{util_template_var} ||= {}; + my $lang = $request_cache->{template_current_lang}->[0] || ''; + return $cache->{$lang}->{$name} if defined $cache->{$lang}; + + my $template = Bugzilla->template_inner($lang); + my %vars; + + # Note: If we suddenly start needing a lot of template_var variables, + # they should move into their own template, not field-descs. + $template->process('global/field-descs.none.tmpl', + {vars => \%vars, in_template_var => 1}) + || ThrowTemplateError($template->error()); + + $cache->{$lang} = \%vars; + return $vars{$name}; } sub display_value { - my ($field, $value) = @_; - return template_var('value_descs')->{$field}->{$value} // $value; + my ($field, $value) = @_; + return template_var('value_descs')->{$field}->{$value} // $value; } sub disable_utf8 { - if (Bugzilla->params->{'utf8'}) { - binmode STDOUT, ':bytes'; # Turn off UTF8 encoding. - } + if (Bugzilla->params->{'utf8'}) { + binmode STDOUT, ':bytes'; # Turn off UTF8 encoding. + } } use constant UTF8_ACCIDENTAL => qw(shiftjis big5-eten euc-kr euc-jp); sub detect_encoding { - my $data = shift; - - Bugzilla->feature('detect_charset') - || ThrowUserError('feature_disabled', { feature => 'detect_charset' }); - - require Encode::Detect::Detector; - import Encode::Detect::Detector 'detect'; - - my $encoding = detect($data); - $encoding = resolve_alias($encoding) if $encoding; - - # Encode::Detect is bad at detecting certain charsets, but Encode::Guess - # is better at them. Here's the details: - - # shiftjis, big5-eten, euc-kr, and euc-jp: (Encode::Detect - # tends to accidentally mis-detect UTF-8 strings as being - # these encodings.) - if ($encoding && grep($_ eq $encoding, UTF8_ACCIDENTAL)) { - $encoding = undef; - my $decoder = guess_encoding($data, UTF8_ACCIDENTAL); - $encoding = $decoder->name if ref $decoder; - } - - # Encode::Detect sometimes mis-detects various ISO encodings as iso-8859-8, - # or cp1255, but Encode::Guess can usually tell which one it is. - if ($encoding && ($encoding eq 'iso-8859-8' || $encoding eq 'cp1255')) { - my $decoded_as = _guess_iso($data, 'iso-8859-8', - # These are ordered this way because it gives the most - # accurate results. - qw(cp1252 iso-8859-7 iso-8859-2)); - $encoding = $decoded_as if $decoded_as; - } + my $data = shift; + + Bugzilla->feature('detect_charset') + || ThrowUserError('feature_disabled', {feature => 'detect_charset'}); + + require Encode::Detect::Detector; + import Encode::Detect::Detector 'detect'; + + my $encoding = detect($data); + $encoding = resolve_alias($encoding) if $encoding; + + # Encode::Detect is bad at detecting certain charsets, but Encode::Guess + # is better at them. Here's the details: + + # shiftjis, big5-eten, euc-kr, and euc-jp: (Encode::Detect + # tends to accidentally mis-detect UTF-8 strings as being + # these encodings.) + if ($encoding && grep($_ eq $encoding, UTF8_ACCIDENTAL)) { + $encoding = undef; + my $decoder = guess_encoding($data, UTF8_ACCIDENTAL); + $encoding = $decoder->name if ref $decoder; + } + + # Encode::Detect sometimes mis-detects various ISO encodings as iso-8859-8, + # or cp1255, but Encode::Guess can usually tell which one it is. + if ($encoding && ($encoding eq 'iso-8859-8' || $encoding eq 'cp1255')) { + my $decoded_as = _guess_iso( + $data, 'iso-8859-8', + + # These are ordered this way because it gives the most + # accurate results. + qw(cp1252 iso-8859-7 iso-8859-2) + ); + $encoding = $decoded_as if $decoded_as; + } - return $encoding; + return $encoding; } # A helper for detect_encoding. sub _guess_iso { - my ($data, $versus, @isos) = (shift, shift, shift); - - my $encoding; - foreach my $iso (@isos) { - my $decoder = guess_encoding($data, ($iso, $versus)); - if (ref $decoder) { - $encoding = $decoder->name if ref $decoder; - last; - } + my ($data, $versus, @isos) = (shift, shift, shift); + + my $encoding; + foreach my $iso (@isos) { + my $decoder = guess_encoding($data, ($iso, $versus)); + if (ref $decoder) { + $encoding = $decoder->name if ref $decoder; + last; } - return $encoding; + } + return $encoding; } 1; diff --git a/Bugzilla/Version.pm b/Bugzilla/Version.pm index 4b332ff2b..6a5930574 100644 --- a/Bugzilla/Version.pm +++ b/Bugzilla/Version.pm @@ -26,134 +26,131 @@ use Scalar::Util qw(blessed); use constant DEFAULT_VERSION => 'unspecified'; -use constant DB_TABLE => 'versions'; +use constant DB_TABLE => 'versions'; use constant NAME_FIELD => 'value'; + # This is "id" because it has to be filled in and id is probably the fastest. # We do a custom sort in new_from_list below. use constant LIST_ORDER => 'id'; use constant DB_COLUMNS => qw( - id - value - product_id - isactive + id + value + product_id + isactive ); -use constant REQUIRED_FIELD_MAP => { - product_id => 'product', -}; +use constant REQUIRED_FIELD_MAP => {product_id => 'product',}; use constant UPDATE_COLUMNS => qw( - value - isactive + value + isactive ); use constant VALIDATORS => { - product => \&_check_product, - value => \&_check_value, - isactive => \&Bugzilla::Object::check_boolean, + product => \&_check_product, + value => \&_check_value, + isactive => \&Bugzilla::Object::check_boolean, }; -use constant VALIDATOR_DEPENDENCIES => { - value => ['product'], -}; +use constant VALIDATOR_DEPENDENCIES => {value => ['product'],}; ################################ # Methods ################################ sub new { - my $class = shift; - my $param = shift; - my $dbh = Bugzilla->dbh; - - my $product; - if (ref $param and !defined $param->{id}) { - $product = $param->{product}; - my $name = $param->{name}; - if (!defined $product) { - ThrowCodeError('bad_arg', - {argument => 'product', - function => "${class}::new"}); - } - if (!defined $name) { - ThrowCodeError('bad_arg', - {argument => 'name', - function => "${class}::new"}); - } - - my $condition = 'product_id = ? AND value = ?'; - my @values = ($product->id, $name); - $param = { condition => $condition, values => \@values }; + my $class = shift; + my $param = shift; + my $dbh = Bugzilla->dbh; + + my $product; + if (ref $param and !defined $param->{id}) { + $product = $param->{product}; + my $name = $param->{name}; + if (!defined $product) { + ThrowCodeError('bad_arg', {argument => 'product', function => "${class}::new"}); + } + if (!defined $name) { + ThrowCodeError('bad_arg', {argument => 'name', function => "${class}::new"}); } - unshift @_, $param; - return $class->SUPER::new(@_); + my $condition = 'product_id = ? AND value = ?'; + my @values = ($product->id, $name); + $param = {condition => $condition, values => \@values}; + } + + unshift @_, $param; + return $class->SUPER::new(@_); } sub new_from_list { - my $self = shift; - my $list = $self->SUPER::new_from_list(@_); - return [sort { vers_cmp(lc($a->name), lc($b->name)) } @$list]; + my $self = shift; + my $list = $self->SUPER::new_from_list(@_); + return [sort { vers_cmp(lc($a->name), lc($b->name)) } @$list]; } sub run_create_validators { - my $class = shift; - my $params = $class->SUPER::run_create_validators(@_); - my $product = delete $params->{product}; - $params->{product_id} = $product->id; - return $params; + my $class = shift; + my $params = $class->SUPER::run_create_validators(@_); + my $product = delete $params->{product}; + $params->{product_id} = $product->id; + return $params; } sub bug_count { - my $self = shift; - my $dbh = Bugzilla->dbh; + my $self = shift; + my $dbh = Bugzilla->dbh; - if (!defined $self->{'bug_count'}) { - $self->{'bug_count'} = $dbh->selectrow_array(qq{ + if (!defined $self->{'bug_count'}) { + $self->{'bug_count'} = $dbh->selectrow_array( + qq{ SELECT COUNT(*) FROM bugs WHERE product_id = ? AND version = ?}, undef, - ($self->product_id, $self->name)) || 0; - } - return $self->{'bug_count'}; + ($self->product_id, $self->name) + ) || 0; + } + return $self->{'bug_count'}; } sub update { - my $self = shift; - my $dbh = Bugzilla->dbh; - - $dbh->bz_start_transaction(); - my ($changes, $old_self) = $self->SUPER::update(@_); - - if (exists $changes->{value}) { - $dbh->do('UPDATE bugs SET version = ? - WHERE version = ? AND product_id = ?', - undef, ($self->name, $old_self->name, $self->product_id)); - } - $dbh->bz_commit_transaction(); - - return $changes; + my $self = shift; + my $dbh = Bugzilla->dbh; + + $dbh->bz_start_transaction(); + my ($changes, $old_self) = $self->SUPER::update(@_); + + if (exists $changes->{value}) { + $dbh->do( + 'UPDATE bugs SET version = ? + WHERE version = ? AND product_id = ?', undef, + ($self->name, $old_self->name, $self->product_id) + ); + } + $dbh->bz_commit_transaction(); + + return $changes; } sub remove_from_db { - my $self = shift; - my $dbh = Bugzilla->dbh; + my $self = shift; + my $dbh = Bugzilla->dbh; - $dbh->bz_start_transaction(); + $dbh->bz_start_transaction(); - # Products must have at least one version. - if (scalar(@{$self->product->versions}) == 1) { - ThrowUserError('version_is_last', { version => $self }); - } + # Products must have at least one version. + if (scalar(@{$self->product->versions}) == 1) { + ThrowUserError('version_is_last', {version => $self}); + } - # The version cannot be removed if there are bugs - # associated with it. - if ($self->bug_count) { - ThrowUserError("version_has_bugs", { nb => $self->bug_count }); - } - $self->SUPER::remove_from_db(); + # The version cannot be removed if there are bugs + # associated with it. + if ($self->bug_count) { + ThrowUserError("version_has_bugs", {nb => $self->bug_count}); + } + $self->SUPER::remove_from_db(); - $dbh->bz_commit_transaction(); + $dbh->bz_commit_transaction(); } ############################### @@ -161,45 +158,47 @@ sub remove_from_db { ############################### sub product_id { return $_[0]->{'product_id'}; } -sub is_active { return $_[0]->{'isactive'}; } +sub is_active { return $_[0]->{'isactive'}; } sub product { - my $self = shift; + my $self = shift; - require Bugzilla::Product; - $self->{'product'} ||= new Bugzilla::Product($self->product_id); - return $self->{'product'}; + require Bugzilla::Product; + $self->{'product'} ||= new Bugzilla::Product($self->product_id); + return $self->{'product'}; } ################################ # Validators ################################ -sub set_value { $_[0]->set('value', $_[1]); } +sub set_value { $_[0]->set('value', $_[1]); } sub set_isactive { $_[0]->set('isactive', $_[1]); } sub _check_value { - my ($invocant, $name, undef, $params) = @_; - my $product = blessed($invocant) ? $invocant->product : $params->{product}; - - $name = trim($name); - $name || ThrowUserError('version_blank_name'); - # Remove unprintable characters - $name = clean_text($name); - - my $version = new Bugzilla::Version({ product => $product, name => $name }); - if ($version && (!ref $invocant || $version->id != $invocant->id)) { - ThrowUserError('version_already_exists', { name => $version->name, - product => $product->name }); - } - return $name; + my ($invocant, $name, undef, $params) = @_; + my $product = blessed($invocant) ? $invocant->product : $params->{product}; + + $name = trim($name); + $name || ThrowUserError('version_blank_name'); + + # Remove unprintable characters + $name = clean_text($name); + + my $version = new Bugzilla::Version({product => $product, name => $name}); + if ($version && (!ref $invocant || $version->id != $invocant->id)) { + ThrowUserError('version_already_exists', + {name => $version->name, product => $product->name}); + } + return $name; } sub _check_product { - my ($invocant, $product) = @_; - $product || ThrowCodeError('param_required', - { function => "$invocant->create", param => 'product' }); - return Bugzilla->user->check_can_admin_product($product->name); + my ($invocant, $product) = @_; + $product + || ThrowCodeError('param_required', + {function => "$invocant->create", param => 'product'}); + return Bugzilla->user->check_can_admin_product($product->name); } ############################### @@ -209,44 +208,52 @@ sub _check_product { # This is taken straight from Sort::Versions 1.5, which is not included # with perl by default. sub vers_cmp { - my ($a, $b) = @_; - - # Remove leading zeroes - Bug 344661 - $a =~ s/^0*(\d.+)/$1/; - $b =~ s/^0*(\d.+)/$1/; - - my @A = ($a =~ /([-.]|\d+|[^-.\d]+)/g); - my @B = ($b =~ /([-.]|\d+|[^-.\d]+)/g); - - my ($A, $B); - while (@A and @B) { - $A = shift @A; - $B = shift @B; - if ($A eq '-' and $B eq '-') { - next; - } elsif ( $A eq '-' ) { - return -1; - } elsif ( $B eq '-') { - return 1; - } elsif ($A eq '.' and $B eq '.') { - next; - } elsif ( $A eq '.' ) { - return -1; - } elsif ( $B eq '.' ) { - return 1; - } elsif ($A =~ /^\d+$/ and $B =~ /^\d+$/) { - if ($A =~ /^0/ || $B =~ /^0/) { - return $A cmp $B if $A cmp $B; - } else { - return $A <=> $B if $A <=> $B; - } - } else { - $A = uc $A; - $B = uc $B; - return $A cmp $B if $A cmp $B; - } + my ($a, $b) = @_; + + # Remove leading zeroes - Bug 344661 + $a =~ s/^0*(\d.+)/$1/; + $b =~ s/^0*(\d.+)/$1/; + + my @A = ($a =~ /([-.]|\d+|[^-.\d]+)/g); + my @B = ($b =~ /([-.]|\d+|[^-.\d]+)/g); + + my ($A, $B); + while (@A and @B) { + $A = shift @A; + $B = shift @B; + if ($A eq '-' and $B eq '-') { + next; + } + elsif ($A eq '-') { + return -1; + } + elsif ($B eq '-') { + return 1; + } + elsif ($A eq '.' and $B eq '.') { + next; + } + elsif ($A eq '.') { + return -1; + } + elsif ($B eq '.') { + return 1; + } + elsif ($A =~ /^\d+$/ and $B =~ /^\d+$/) { + if ($A =~ /^0/ || $B =~ /^0/) { + return $A cmp $B if $A cmp $B; + } + else { + return $A <=> $B if $A <=> $B; + } + } + else { + $A = uc $A; + $B = uc $B; + return $A cmp $B if $A cmp $B; } - return @A <=> @B; + } + return @A <=> @B; } 1; diff --git a/Bugzilla/WebService.pm b/Bugzilla/WebService.pm index f80813744..2630e3565 100644 --- a/Bugzilla/WebService.pm +++ b/Bugzilla/WebService.pm @@ -5,7 +5,7 @@ # This Source Code Form is "Incompatible With Secondary Licenses", as # defined by the Mozilla Public License, v. 2.0. -# This is the base class for $self in WebService method calls. For the +# This is the base class for $self in WebService method calls. For the # actual RPC server, see Bugzilla::WebService::Server and its subclasses. package Bugzilla::WebService; @@ -17,11 +17,12 @@ use Bugzilla::WebService::Server; # Used by the JSON-RPC server to convert incoming date fields apprpriately. use constant DATE_FIELDS => {}; + # Used by the JSON-RPC server to convert incoming base64 fields appropriately. use constant BASE64_FIELDS => {}; # For some methods, we shouldn't call Bugzilla->login before we call them -use constant LOGIN_EXEMPT => { }; +use constant LOGIN_EXEMPT => {}; # Used to allow methods to be called in the JSON-RPC WebService via GET. # Methods that can modify data MUST not be listed here. @@ -32,8 +33,8 @@ use constant READ_ONLY => (); use constant PUBLIC_METHODS => (); sub login_exempt { - my ($class, $method) = @_; - return $class->LOGIN_EXEMPT->{$method}; + my ($class, $method) = @_; + return $class->LOGIN_EXEMPT->{$method}; } 1; diff --git a/Bugzilla/WebService/Bug.pm b/Bugzilla/WebService/Bug.pm index b07d3cb01..2cdc37443 100644 --- a/Bugzilla/WebService/Bug.pm +++ b/Bugzilla/WebService/Bug.pm @@ -19,7 +19,8 @@ use Bugzilla::Constants; use Bugzilla::Error; use Bugzilla::Field; use Bugzilla::WebService::Constants; -use Bugzilla::WebService::Util qw(extract_flags filter filter_wants validate translate); +use Bugzilla::WebService::Util + qw(extract_flags filter filter_wants validate translate); use Bugzilla::Bug; use Bugzilla::BugMail; use Bugzilla::Util qw(trick_taint trim diff_arrays detaint_natural); @@ -43,58 +44,54 @@ use Storable qw(dclone); use constant PRODUCT_SPECIFIC_FIELDS => qw(version target_milestone component); use constant DATE_FIELDS => { - comments => ['new_since'], - history => ['new_since'], - search => ['last_change_time', 'creation_time'], + comments => ['new_since'], + history => ['new_since'], + search => ['last_change_time', 'creation_time'], }; -use constant BASE64_FIELDS => { - add_attachment => ['data'], -}; +use constant BASE64_FIELDS => {add_attachment => ['data'],}; use constant READ_ONLY => qw( - attachments - comments - fields - get - history - legal_values - search + attachments + comments + fields + get + history + legal_values + search ); use constant PUBLIC_METHODS => qw( - add_attachment - add_comment - attachments - comments - create - fields - get - history - legal_values - possible_duplicates - render_comment - search - search_comment_tags - update - update_attachment - update_comment_tags - update_see_also - update_tags + add_attachment + add_comment + attachments + comments + create + fields + get + history + legal_values + possible_duplicates + render_comment + search + search_comment_tags + update + update_attachment + update_comment_tags + update_see_also + update_tags ); -use constant ATTACHMENT_MAPPED_SETTERS => { - file_name => 'filename', - summary => 'description', -}; +use constant ATTACHMENT_MAPPED_SETTERS => + {file_name => 'filename', summary => 'description',}; use constant ATTACHMENT_MAPPED_RETURNS => { - description => 'summary', - ispatch => 'is_patch', - isprivate => 'is_private', - isobsolete => 'is_obsolete', - filename => 'file_name', - mimetype => 'content_type', + description => 'summary', + ispatch => 'is_patch', + isprivate => 'is_private', + isobsolete => 'is_obsolete', + filename => 'file_name', + mimetype => 'content_type', }; ########### @@ -102,1089 +99,1101 @@ use constant ATTACHMENT_MAPPED_RETURNS => { ########### sub fields { - my ($self, $params) = validate(@_, 'ids', 'names'); + my ($self, $params) = validate(@_, 'ids', 'names'); - Bugzilla->switch_to_shadow_db(); + Bugzilla->switch_to_shadow_db(); - my @fields; - if (defined $params->{ids}) { - my $ids = $params->{ids}; - foreach my $id (@$ids) { - my $loop_field = Bugzilla::Field->check({ id => $id }); - push(@fields, $loop_field); - } + my @fields; + if (defined $params->{ids}) { + my $ids = $params->{ids}; + foreach my $id (@$ids) { + my $loop_field = Bugzilla::Field->check({id => $id}); + push(@fields, $loop_field); } - - if (defined $params->{names}) { - my $names = $params->{names}; - foreach my $field_name (@$names) { - my $loop_field = Bugzilla::Field->check($field_name); - # Don't push in duplicate fields if we also asked for this field - # in "ids". - if (!grep($_->id == $loop_field->id, @fields)) { - push(@fields, $loop_field); - } - } + } + + if (defined $params->{names}) { + my $names = $params->{names}; + foreach my $field_name (@$names) { + my $loop_field = Bugzilla::Field->check($field_name); + + # Don't push in duplicate fields if we also asked for this field + # in "ids". + if (!grep($_->id == $loop_field->id, @fields)) { + push(@fields, $loop_field); + } } - - if (!defined $params->{ids} and !defined $params->{names}) { - @fields = @{ Bugzilla->fields({ obsolete => 0 }) }; + } + + if (!defined $params->{ids} and !defined $params->{names}) { + @fields = @{Bugzilla->fields({obsolete => 0})}; + } + + my @fields_out; + foreach my $field (@fields) { + my $visibility_field + = $field->visibility_field ? $field->visibility_field->name : undef; + my $vis_values = $field->visibility_values; + my $value_field = $field->value_field ? $field->value_field->name : undef; + + my (@values, $has_values); + if ( ($field->is_select and $field->name ne 'product') + or grep($_ eq $field->name, PRODUCT_SPECIFIC_FIELDS) + or $field->name eq 'keywords') + { + $has_values = 1; + @values = @{$self->_legal_field_values({field => $field})}; } - my @fields_out; - foreach my $field (@fields) { - my $visibility_field = $field->visibility_field - ? $field->visibility_field->name : undef; - my $vis_values = $field->visibility_values; - my $value_field = $field->value_field - ? $field->value_field->name : undef; - - my (@values, $has_values); - if ( ($field->is_select and $field->name ne 'product') - or grep($_ eq $field->name, PRODUCT_SPECIFIC_FIELDS) - or $field->name eq 'keywords') - { - $has_values = 1; - @values = @{ $self->_legal_field_values({ field => $field }) }; - } - - if (grep($_ eq $field->name, PRODUCT_SPECIFIC_FIELDS)) { - $value_field = 'product'; - } + if (grep($_ eq $field->name, PRODUCT_SPECIFIC_FIELDS)) { + $value_field = 'product'; + } - my %field_data = ( - id => $self->type('int', $field->id), - type => $self->type('int', $field->type), - is_custom => $self->type('boolean', $field->custom), - name => $self->type('string', $field->name), - display_name => $self->type('string', $field->description), - is_mandatory => $self->type('boolean', $field->is_mandatory), - is_on_bug_entry => $self->type('boolean', $field->enter_bug), - visibility_field => $self->type('string', $visibility_field), - visibility_values => - [ map { $self->type('string', $_->name) } @$vis_values ], - ); - if ($has_values) { - $field_data{value_field} = $self->type('string', $value_field); - $field_data{values} = \@values; - }; - push(@fields_out, filter $params, \%field_data); + my %field_data = ( + id => $self->type('int', $field->id), + type => $self->type('int', $field->type), + is_custom => $self->type('boolean', $field->custom), + name => $self->type('string', $field->name), + display_name => $self->type('string', $field->description), + is_mandatory => $self->type('boolean', $field->is_mandatory), + is_on_bug_entry => $self->type('boolean', $field->enter_bug), + visibility_field => $self->type('string', $visibility_field), + visibility_values => [map { $self->type('string', $_->name) } @$vis_values], + ); + if ($has_values) { + $field_data{value_field} = $self->type('string', $value_field); + $field_data{values} = \@values; } + push(@fields_out, filter $params, \%field_data); + } - return { fields => \@fields_out }; + return {fields => \@fields_out}; } sub _legal_field_values { - my ($self, $params) = @_; - my $field = $params->{field}; - my $field_name = $field->name; - my $user = Bugzilla->user; - - my @result; - if (grep($_ eq $field_name, PRODUCT_SPECIFIC_FIELDS)) { - my @list; - if ($field_name eq 'version') { - @list = Bugzilla::Version->get_all; - } - elsif ($field_name eq 'component') { - @list = Bugzilla::Component->get_all; - } - else { - @list = Bugzilla::Milestone->get_all; - } + my ($self, $params) = @_; + my $field = $params->{field}; + my $field_name = $field->name; + my $user = Bugzilla->user; + + my @result; + if (grep($_ eq $field_name, PRODUCT_SPECIFIC_FIELDS)) { + my @list; + if ($field_name eq 'version') { + @list = Bugzilla::Version->get_all; + } + elsif ($field_name eq 'component') { + @list = Bugzilla::Component->get_all; + } + else { + @list = Bugzilla::Milestone->get_all; + } - foreach my $value (@list) { - my $sortkey = $field_name eq 'target_milestone' - ? $value->sortkey : 0; - # XXX This is very slow for large numbers of values. - my $product_name = $value->product->name; - if ($user->can_see_product($product_name)) { - push(@result, { - name => $self->type('string', $value->name), - sort_key => $self->type('int', $sortkey), - sortkey => $self->type('int', $sortkey), # deprecated - visibility_values => [$self->type('string', $product_name)], - is_active => $self->type('boolean', $value->is_active), - }); - } - } + foreach my $value (@list) { + my $sortkey = $field_name eq 'target_milestone' ? $value->sortkey : 0; + + # XXX This is very slow for large numbers of values. + my $product_name = $value->product->name; + if ($user->can_see_product($product_name)) { + push( + @result, + { + name => $self->type('string', $value->name), + sort_key => $self->type('int', $sortkey), + sortkey => $self->type('int', $sortkey), # deprecated + visibility_values => [$self->type('string', $product_name)], + is_active => $self->type('boolean', $value->is_active), + } + ); + } } + } + + elsif ($field_name eq 'bug_status') { + my @status_all = Bugzilla::Status->get_all; + my $initial_status = bless( + { + id => 0, + name => '', + is_open => 1, + sortkey => 0, + can_change_to => Bugzilla::Status->can_change_to + }, + 'Bugzilla::Status' + ); + unshift(@status_all, $initial_status); + + foreach my $status (@status_all) { + my @can_change_to; + foreach my $change_to (@{$status->can_change_to}) { + + # There's no need to note that a status can transition + # to itself. + next if $change_to->id == $status->id; + my %change_to_hash = ( + name => $self->type('string', $change_to->name), + comment_required => + $self->type('boolean', $change_to->comment_required_on_change_from($status)), + ); + push(@can_change_to, \%change_to_hash); + } - elsif ($field_name eq 'bug_status') { - my @status_all = Bugzilla::Status->get_all; - my $initial_status = bless({ id => 0, name => '', is_open => 1, sortkey => 0, - can_change_to => Bugzilla::Status->can_change_to }, - 'Bugzilla::Status'); - unshift(@status_all, $initial_status); - - foreach my $status (@status_all) { - my @can_change_to; - foreach my $change_to (@{ $status->can_change_to }) { - # There's no need to note that a status can transition - # to itself. - next if $change_to->id == $status->id; - my %change_to_hash = ( - name => $self->type('string', $change_to->name), - comment_required => $self->type('boolean', - $change_to->comment_required_on_change_from($status)), - ); - push(@can_change_to, \%change_to_hash); - } - - push (@result, { - name => $self->type('string', $status->name), - is_open => $self->type('boolean', $status->is_open), - sort_key => $self->type('int', $status->sortkey), - sortkey => $self->type('int', $status->sortkey), # deprecated - can_change_to => \@can_change_to, - visibility_values => [], - }); + push( + @result, + { + name => $self->type('string', $status->name), + is_open => $self->type('boolean', $status->is_open), + sort_key => $self->type('int', $status->sortkey), + sortkey => $self->type('int', $status->sortkey), # deprecated + can_change_to => \@can_change_to, + visibility_values => [], } + ); } + } - elsif ($field_name eq 'keywords') { - my @legal_keywords = Bugzilla::Keyword->get_all; - foreach my $value (@legal_keywords) { - push (@result, { - name => $self->type('string', $value->name), - description => $self->type('string', $value->description), - }); + elsif ($field_name eq 'keywords') { + my @legal_keywords = Bugzilla::Keyword->get_all; + foreach my $value (@legal_keywords) { + push( + @result, + { + name => $self->type('string', $value->name), + description => $self->type('string', $value->description), } + ); } - else { - my @values = Bugzilla::Field::Choice->type($field)->get_all(); - foreach my $value (@values) { - my $vis_val = $value->visibility_value; - push(@result, { - name => $self->type('string', $value->name), - sort_key => $self->type('int' , $value->sortkey), - sortkey => $self->type('int' , $value->sortkey), # deprecated - visibility_values => [ - defined $vis_val ? $self->type('string', $vis_val->name) - : () - ], - }); + } + else { + my @values = Bugzilla::Field::Choice->type($field)->get_all(); + foreach my $value (@values) { + my $vis_val = $value->visibility_value; + push( + @result, + { + name => $self->type('string', $value->name), + sort_key => $self->type('int', $value->sortkey), + sortkey => $self->type('int', $value->sortkey), # deprecated + visibility_values => + [defined $vis_val ? $self->type('string', $vis_val->name) : ()], } + ); } + } - return \@result; + return \@result; } sub comments { - my ($self, $params) = validate(@_, 'ids', 'comment_ids'); + my ($self, $params) = validate(@_, 'ids', 'comment_ids'); - if (!(defined $params->{ids} || defined $params->{comment_ids})) { - ThrowCodeError('params_required', - { function => 'Bug.comments', - params => ['ids', 'comment_ids'] }); - } + if (!(defined $params->{ids} || defined $params->{comment_ids})) { + ThrowCodeError('params_required', + {function => 'Bug.comments', params => ['ids', 'comment_ids']}); + } - my $bug_ids = $params->{ids} || []; - my $comment_ids = $params->{comment_ids} || []; - - my $dbh = Bugzilla->switch_to_shadow_db(); - my $user = Bugzilla->user; - - my %bugs; - foreach my $bug_id (@$bug_ids) { - my $bug = Bugzilla::Bug->check($bug_id); - # We want the API to always return comments in the same order. - - my $comments = $bug->comments({ order => 'oldest_to_newest', - after => $params->{new_since} }); - my @result; - foreach my $comment (@$comments) { - next if $comment->is_private && !$user->is_insider; - push(@result, $self->_translate_comment($comment, $params)); - } - $bugs{$bug->id}{'comments'} = \@result; + my $bug_ids = $params->{ids} || []; + my $comment_ids = $params->{comment_ids} || []; + + my $dbh = Bugzilla->switch_to_shadow_db(); + my $user = Bugzilla->user; + + my %bugs; + foreach my $bug_id (@$bug_ids) { + my $bug = Bugzilla::Bug->check($bug_id); + + # We want the API to always return comments in the same order. + + my $comments = $bug->comments( + {order => 'oldest_to_newest', after => $params->{new_since}}); + my @result; + foreach my $comment (@$comments) { + next if $comment->is_private && !$user->is_insider; + push(@result, $self->_translate_comment($comment, $params)); + } + $bugs{$bug->id}{'comments'} = \@result; + } + + my %comments; + if (scalar @$comment_ids) { + my @ids = map { trim($_) } @$comment_ids; + my $comment_data = Bugzilla::Comment->new_from_list(\@ids); + + # See if we were passed any invalid comment ids. + my %got_ids = map { $_->id => 1 } @$comment_data; + foreach my $comment_id (@ids) { + if (!$got_ids{$comment_id}) { + ThrowUserError('comment_id_invalid', {id => $comment_id}); + } } - my %comments; - if (scalar @$comment_ids) { - my @ids = map { trim($_) } @$comment_ids; - my $comment_data = Bugzilla::Comment->new_from_list(\@ids); - - # See if we were passed any invalid comment ids. - my %got_ids = map { $_->id => 1 } @$comment_data; - foreach my $comment_id (@ids) { - if (!$got_ids{$comment_id}) { - ThrowUserError('comment_id_invalid', { id => $comment_id }); - } - } - - # Now make sure that we can see all the associated bugs. - my %got_bug_ids = map { $_->bug_id => 1 } @$comment_data; - Bugzilla::Bug->check($_) foreach (keys %got_bug_ids); - - foreach my $comment (@$comment_data) { - if ($comment->is_private && !$user->is_insider) { - ThrowUserError('comment_is_private', { id => $comment->id }); - } - $comments{$comment->id} = - $self->_translate_comment($comment, $params); - } + # Now make sure that we can see all the associated bugs. + my %got_bug_ids = map { $_->bug_id => 1 } @$comment_data; + Bugzilla::Bug->check($_) foreach (keys %got_bug_ids); + + foreach my $comment (@$comment_data) { + if ($comment->is_private && !$user->is_insider) { + ThrowUserError('comment_is_private', {id => $comment->id}); + } + $comments{$comment->id} = $self->_translate_comment($comment, $params); } + } - return { bugs => \%bugs, comments => \%comments }; + return {bugs => \%bugs, comments => \%comments}; } sub render_comment { - my ($self, $params) = @_; + my ($self, $params) = @_; - unless (defined $params->{text}) { - ThrowCodeError('params_required', - { function => 'Bug.render_comment', - params => ['text'] }); - } + unless (defined $params->{text}) { + ThrowCodeError('params_required', + {function => 'Bug.render_comment', params => ['text']}); + } - Bugzilla->switch_to_shadow_db(); - my $bug = $params->{id} ? Bugzilla::Bug->check($params->{id}) : undef; + Bugzilla->switch_to_shadow_db(); + my $bug = $params->{id} ? Bugzilla::Bug->check($params->{id}) : undef; - my $tmpl = '[% text FILTER quoteUrls(bug) %]'; - my $html; - my $template = Bugzilla->template; - $template->process( - \$tmpl, - { bug => $bug, text => $params->{text}}, - \$html - ); + my $tmpl = '[% text FILTER quoteUrls(bug) %]'; + my $html; + my $template = Bugzilla->template; + $template->process(\$tmpl, {bug => $bug, text => $params->{text}}, \$html); - return { html => $html }; + return {html => $html}; } # Helper for Bug.comments sub _translate_comment { - my ($self, $comment, $filters, $types, $prefix) = @_; - my $attach_id = $comment->is_about_attachment ? $comment->extra_data - : undef; - - my $comment_hash = { - id => $self->type('int', $comment->id), - bug_id => $self->type('int', $comment->bug_id), - creator => $self->type('email', $comment->author->login), - time => $self->type('dateTime', $comment->creation_ts), - creation_time => $self->type('dateTime', $comment->creation_ts), - is_private => $self->type('boolean', $comment->is_private), - text => $self->type('string', $comment->body_full), - attachment_id => $self->type('int', $attach_id), - count => $self->type('int', $comment->count), - }; - - # Don't load comment tags unless enabled - if (Bugzilla->params->{'comment_taggers_group'}) { - $comment_hash->{tags} = [ - map { $self->type('string', $_) } - @{ $comment->tags } - ]; - } - - return filter($filters, $comment_hash, $types, $prefix); + my ($self, $comment, $filters, $types, $prefix) = @_; + my $attach_id = $comment->is_about_attachment ? $comment->extra_data : undef; + + my $comment_hash = { + id => $self->type('int', $comment->id), + bug_id => $self->type('int', $comment->bug_id), + creator => $self->type('email', $comment->author->login), + time => $self->type('dateTime', $comment->creation_ts), + creation_time => $self->type('dateTime', $comment->creation_ts), + is_private => $self->type('boolean', $comment->is_private), + text => $self->type('string', $comment->body_full), + attachment_id => $self->type('int', $attach_id), + count => $self->type('int', $comment->count), + }; + + # Don't load comment tags unless enabled + if (Bugzilla->params->{'comment_taggers_group'}) { + $comment_hash->{tags} = [map { $self->type('string', $_) } @{$comment->tags}]; + } + + return filter($filters, $comment_hash, $types, $prefix); } sub get { - my ($self, $params) = validate(@_, 'ids'); - - Bugzilla->switch_to_shadow_db() unless Bugzilla->user->id; - - my $ids = $params->{ids}; - defined $ids || ThrowCodeError('param_required', { param => 'ids' }); - - my (@bugs, @faults, @hashes); - - # Cache permissions for bugs. This highly reduces the number of calls to the DB. - # visible_bugs() is only able to handle bug IDs, so we have to skip aliases. - my @int = grep { $_ =~ /^\d+$/ } @$ids; - Bugzilla->user->visible_bugs(\@int); - - foreach my $bug_id (@$ids) { - my $bug; - if ($params->{permissive}) { - eval { $bug = Bugzilla::Bug->check($bug_id); }; - if ($@) { - push(@faults, {id => $bug_id, - faultString => $@->faultstring, - faultCode => $@->faultcode, - } - ); - undef $@; - next; - } - } - else { - $bug = Bugzilla::Bug->check($bug_id); - } - push(@bugs, $bug); - push(@hashes, $self->_bug_to_hash($bug, $params)); + my ($self, $params) = validate(@_, 'ids'); + + Bugzilla->switch_to_shadow_db() unless Bugzilla->user->id; + + my $ids = $params->{ids}; + defined $ids || ThrowCodeError('param_required', {param => 'ids'}); + + my (@bugs, @faults, @hashes); + + # Cache permissions for bugs. This highly reduces the number of calls to the DB. + # visible_bugs() is only able to handle bug IDs, so we have to skip aliases. + my @int = grep { $_ =~ /^\d+$/ } @$ids; + Bugzilla->user->visible_bugs(\@int); + + foreach my $bug_id (@$ids) { + my $bug; + if ($params->{permissive}) { + eval { $bug = Bugzilla::Bug->check($bug_id); }; + if ($@) { + push(@faults, + {id => $bug_id, faultString => $@->faultstring, faultCode => $@->faultcode,}); + undef $@; + next; + } } + else { + $bug = Bugzilla::Bug->check($bug_id); + } + push(@bugs, $bug); + push(@hashes, $self->_bug_to_hash($bug, $params)); + } - # Set the ETag before inserting the update tokens - # since the tokens will always be unique even if - # the data has not changed. - $self->bz_etag(\@hashes); + # Set the ETag before inserting the update tokens + # since the tokens will always be unique even if + # the data has not changed. + $self->bz_etag(\@hashes); - $self->_add_update_tokens($params, \@bugs, \@hashes); + $self->_add_update_tokens($params, \@bugs, \@hashes); - return { bugs => \@hashes, faults => \@faults }; + return {bugs => \@hashes, faults => \@faults}; } -# this is a function that gets bug activity for list of bug ids +# this is a function that gets bug activity for list of bug ids # it can be called as the following: # $call = $rpc->call( 'Bug.history', { ids => [1,2] }); sub history { - my ($self, $params) = validate(@_, 'ids'); - - Bugzilla->switch_to_shadow_db(); - - my $ids = $params->{ids}; - defined $ids || ThrowCodeError('param_required', { param => 'ids' }); - - my %api_name = reverse %{ Bugzilla::Bug::FIELD_MAP() }; - $api_name{'bug_group'} = 'groups'; - - my @return; - foreach my $bug_id (@$ids) { - my %item; - my $bug = Bugzilla::Bug->check($bug_id); - $bug_id = $bug->id; - $item{id} = $self->type('int', $bug_id); - - my ($activity) = $bug->get_activity(undef, $params->{new_since}); - - my @history; - foreach my $changeset (@$activity) { - my %bug_history; - $bug_history{when} = $self->type('dateTime', $changeset->{when}); - $bug_history{who} = $self->type('string', $changeset->{who}); - $bug_history{changes} = []; - foreach my $change (@{ $changeset->{changes} }) { - my $api_field = $api_name{$change->{fieldname}} || $change->{fieldname}; - my $attach_id = delete $change->{attachid}; - if ($attach_id) { - $change->{attachment_id} = $self->type('int', $attach_id); - } - $change->{removed} = $self->type('string', $change->{removed}); - $change->{added} = $self->type('string', $change->{added}); - $change->{field_name} = $self->type('string', $api_field); - delete $change->{fieldname}; - push (@{$bug_history{changes}}, $change); - } - - push (@history, \%bug_history); + my ($self, $params) = validate(@_, 'ids'); + + Bugzilla->switch_to_shadow_db(); + + my $ids = $params->{ids}; + defined $ids || ThrowCodeError('param_required', {param => 'ids'}); + + my %api_name = reverse %{Bugzilla::Bug::FIELD_MAP()}; + $api_name{'bug_group'} = 'groups'; + + my @return; + foreach my $bug_id (@$ids) { + my %item; + my $bug = Bugzilla::Bug->check($bug_id); + $bug_id = $bug->id; + $item{id} = $self->type('int', $bug_id); + + my ($activity) = $bug->get_activity(undef, $params->{new_since}); + + my @history; + foreach my $changeset (@$activity) { + my %bug_history; + $bug_history{when} = $self->type('dateTime', $changeset->{when}); + $bug_history{who} = $self->type('string', $changeset->{who}); + $bug_history{changes} = []; + foreach my $change (@{$changeset->{changes}}) { + my $api_field = $api_name{$change->{fieldname}} || $change->{fieldname}; + my $attach_id = delete $change->{attachid}; + if ($attach_id) { + $change->{attachment_id} = $self->type('int', $attach_id); } + $change->{removed} = $self->type('string', $change->{removed}); + $change->{added} = $self->type('string', $change->{added}); + $change->{field_name} = $self->type('string', $api_field); + delete $change->{fieldname}; + push(@{$bug_history{changes}}, $change); + } + + push(@history, \%bug_history); + } - $item{history} = \@history; + $item{history} = \@history; - # alias is returned in case users passes a mixture of ids and aliases - # then they get to know which bug activity relates to which value - # they passed - $item{alias} = [ map { $self->type('string', $_) } @{ $bug->alias } ]; + # alias is returned in case users passes a mixture of ids and aliases + # then they get to know which bug activity relates to which value + # they passed + $item{alias} = [map { $self->type('string', $_) } @{$bug->alias}]; - push(@return, \%item); - } + push(@return, \%item); + } - return { bugs => \@return }; + return {bugs => \@return}; } sub search { - my ($self, $params) = @_; - my $user = Bugzilla->user; - my $dbh = Bugzilla->dbh; - - Bugzilla->switch_to_shadow_db(); - - my $match_params = dclone($params); - delete $match_params->{include_fields}; - delete $match_params->{exclude_fields}; - - # Determine whether this is a quicksearch query - if (exists $match_params->{quicksearch}) { - my $quicksearch = quicksearch($match_params->{'quicksearch'}); - my $cgi = Bugzilla::CGI->new($quicksearch); - $match_params = $cgi->Vars; - } - - if ( defined($match_params->{offset}) and !defined($match_params->{limit}) ) { - ThrowCodeError('param_required', - { param => 'limit', function => 'Bug.search()' }); - } - - my $max_results = Bugzilla->params->{max_search_results}; - unless (defined $match_params->{limit} && $match_params->{limit} == 0) { - if (!defined $match_params->{limit} || $match_params->{limit} > $max_results) { - $match_params->{limit} = $max_results; - } - } - else { - delete $match_params->{limit}; - delete $match_params->{offset}; - } + my ($self, $params) = @_; + my $user = Bugzilla->user; + my $dbh = Bugzilla->dbh; - $match_params = Bugzilla::Bug::map_fields($match_params); + Bugzilla->switch_to_shadow_db(); - my %options = ( fields => ['bug_id'] ); + my $match_params = dclone($params); + delete $match_params->{include_fields}; + delete $match_params->{exclude_fields}; - # Find the highest custom field id - my @field_ids = grep(/^f(\d+)$/, keys %$match_params); - my $last_field_id = @field_ids ? max @field_ids + 1 : 1; + # Determine whether this is a quicksearch query + if (exists $match_params->{quicksearch}) { + my $quicksearch = quicksearch($match_params->{'quicksearch'}); + my $cgi = Bugzilla::CGI->new($quicksearch); + $match_params = $cgi->Vars; + } - # Do special search types for certain fields. - if (my $change_when = delete $match_params->{'delta_ts'}) { - $match_params->{"f${last_field_id}"} = 'delta_ts'; - $match_params->{"o${last_field_id}"} = 'greaterthaneq'; - $match_params->{"v${last_field_id}"} = $change_when; - $last_field_id++; - } - if (my $creation_when = delete $match_params->{'creation_ts'}) { - $match_params->{"f${last_field_id}"} = 'creation_ts'; - $match_params->{"o${last_field_id}"} = 'greaterthaneq'; - $match_params->{"v${last_field_id}"} = $creation_when; - $last_field_id++; - } - - # Some fields require a search type such as short desc, keywords, etc. - foreach my $param (qw(short_desc longdesc status_whiteboard bug_file_loc)) { - if (defined $match_params->{$param} && !defined $match_params->{$param . '_type'}) { - $match_params->{$param . '_type'} = 'allwordssubstr'; - } - } - if (defined $match_params->{'keywords'} && !defined $match_params->{'keywords_type'}) { - $match_params->{'keywords_type'} = 'allwords'; - } + if (defined($match_params->{offset}) and !defined($match_params->{limit})) { + ThrowCodeError('param_required', + {param => 'limit', function => 'Bug.search()'}); + } - # Backwards compatibility with old method regarding role search - $match_params->{'reporter'} = delete $match_params->{'creator'} if $match_params->{'creator'}; - foreach my $role (qw(assigned_to reporter qa_contact longdesc cc)) { - next if !exists $match_params->{$role}; - my $value = delete $match_params->{$role}; - $match_params->{"f${last_field_id}"} = $role; - $match_params->{"o${last_field_id}"} = "anywordssubstr"; - $match_params->{"v${last_field_id}"} = ref $value ? join(" ", @{$value}) : $value; - $last_field_id++; + my $max_results = Bugzilla->params->{max_search_results}; + unless (defined $match_params->{limit} && $match_params->{limit} == 0) { + if (!defined $match_params->{limit} || $match_params->{limit} > $max_results) { + $match_params->{limit} = $max_results; } - - # If no other parameters have been passed other than limit and offset - # then we throw error if system is configured to do so. - if (!grep(!/^(limit|offset)$/, keys %$match_params) - && !Bugzilla->params->{search_allow_no_criteria}) + } + else { + delete $match_params->{limit}; + delete $match_params->{offset}; + } + + $match_params = Bugzilla::Bug::map_fields($match_params); + + my %options = (fields => ['bug_id']); + + # Find the highest custom field id + my @field_ids = grep(/^f(\d+)$/, keys %$match_params); + my $last_field_id = @field_ids ? max @field_ids + 1 : 1; + + # Do special search types for certain fields. + if (my $change_when = delete $match_params->{'delta_ts'}) { + $match_params->{"f${last_field_id}"} = 'delta_ts'; + $match_params->{"o${last_field_id}"} = 'greaterthaneq'; + $match_params->{"v${last_field_id}"} = $change_when; + $last_field_id++; + } + if (my $creation_when = delete $match_params->{'creation_ts'}) { + $match_params->{"f${last_field_id}"} = 'creation_ts'; + $match_params->{"o${last_field_id}"} = 'greaterthaneq'; + $match_params->{"v${last_field_id}"} = $creation_when; + $last_field_id++; + } + + # Some fields require a search type such as short desc, keywords, etc. + foreach my $param (qw(short_desc longdesc status_whiteboard bug_file_loc)) { + if (defined $match_params->{$param} + && !defined $match_params->{$param . '_type'}) { - ThrowUserError('buglist_parameters_required'); + $match_params->{$param . '_type'} = 'allwordssubstr'; } - - $options{order} = [ split(/\s*,\s*/, delete $match_params->{order}) ] if $match_params->{order}; - $options{params} = $match_params; - - my $search = new Bugzilla::Search(%options); - my ($data) = $search->data; - - if (!scalar @$data) { - return { bugs => [] }; - } - - # Search.pm won't return bugs that the user shouldn't see so no filtering is needed. - my @bug_ids = map { $_->[0] } @$data; - my %bug_objects = map { $_->id => $_ } @{ Bugzilla::Bug->new_from_list(\@bug_ids) }; - my @bugs = map { $bug_objects{$_} } @bug_ids; - @bugs = map { $self->_bug_to_hash($_, $params) } @bugs; - - return { bugs => \@bugs }; + } + if (defined $match_params->{'keywords'} + && !defined $match_params->{'keywords_type'}) + { + $match_params->{'keywords_type'} = 'allwords'; + } + + # Backwards compatibility with old method regarding role search + $match_params->{'reporter'} = delete $match_params->{'creator'} + if $match_params->{'creator'}; + foreach my $role (qw(assigned_to reporter qa_contact longdesc cc)) { + next if !exists $match_params->{$role}; + my $value = delete $match_params->{$role}; + $match_params->{"f${last_field_id}"} = $role; + $match_params->{"o${last_field_id}"} = "anywordssubstr"; + $match_params->{"v${last_field_id}"} + = ref $value ? join(" ", @{$value}) : $value; + $last_field_id++; + } + + # If no other parameters have been passed other than limit and offset + # then we throw error if system is configured to do so. + if ( !grep(!/^(limit|offset)$/, keys %$match_params) + && !Bugzilla->params->{search_allow_no_criteria}) + { + ThrowUserError('buglist_parameters_required'); + } + + $options{order} = [split(/\s*,\s*/, delete $match_params->{order})] + if $match_params->{order}; + $options{params} = $match_params; + + my $search = new Bugzilla::Search(%options); + my ($data) = $search->data; + + if (!scalar @$data) { + return {bugs => []}; + } + +# Search.pm won't return bugs that the user shouldn't see so no filtering is needed. + my @bug_ids = map { $_->[0] } @$data; + my %bug_objects + = map { $_->id => $_ } @{Bugzilla::Bug->new_from_list(\@bug_ids)}; + my @bugs = map { $bug_objects{$_} } @bug_ids; + @bugs = map { $self->_bug_to_hash($_, $params) } @bugs; + + return {bugs => \@bugs}; } sub possible_duplicates { - my ($self, $params) = validate(@_, 'products'); - my $user = Bugzilla->user; - - Bugzilla->switch_to_shadow_db(); - - # Undo the array-ification that validate() does, for "summary". - $params->{summary} || ThrowCodeError('param_required', - { function => 'Bug.possible_duplicates', param => 'summary' }); - - my @products; - foreach my $name (@{ $params->{'products'} || [] }) { - my $object = $user->can_enter_product($name, THROW_ERROR); - push(@products, $object); - } - - my $possible_dupes = Bugzilla::Bug->possible_duplicates( - { summary => $params->{summary}, products => \@products, - limit => $params->{limit} }); - my @hashes = map { $self->_bug_to_hash($_, $params) } @$possible_dupes; - $self->_add_update_tokens($params, $possible_dupes, \@hashes); - return { bugs => \@hashes }; + my ($self, $params) = validate(@_, 'products'); + my $user = Bugzilla->user; + + Bugzilla->switch_to_shadow_db(); + + # Undo the array-ification that validate() does, for "summary". + $params->{summary} + || ThrowCodeError('param_required', + {function => 'Bug.possible_duplicates', param => 'summary'}); + + my @products; + foreach my $name (@{$params->{'products'} || []}) { + my $object = $user->can_enter_product($name, THROW_ERROR); + push(@products, $object); + } + + my $possible_dupes = Bugzilla::Bug->possible_duplicates({ + summary => $params->{summary}, + products => \@products, + limit => $params->{limit} + }); + my @hashes = map { $self->_bug_to_hash($_, $params) } @$possible_dupes; + $self->_add_update_tokens($params, $possible_dupes, \@hashes); + return {bugs => \@hashes}; } sub update { - my ($self, $params) = validate(@_, 'ids'); + my ($self, $params) = validate(@_, 'ids'); - my $user = Bugzilla->login(LOGIN_REQUIRED); - my $dbh = Bugzilla->dbh; + my $user = Bugzilla->login(LOGIN_REQUIRED); + my $dbh = Bugzilla->dbh; - # We skip certain fields because their set_ methods actually use - # the external names instead of the internal names. - $params = Bugzilla::Bug::map_fields($params, - { summary => 1, platform => 1, severity => 1, url => 1 }); + # We skip certain fields because their set_ methods actually use + # the external names instead of the internal names. + $params = Bugzilla::Bug::map_fields($params, + {summary => 1, platform => 1, severity => 1, url => 1}); - my $ids = delete $params->{ids}; - defined $ids || ThrowCodeError('param_required', { param => 'ids' }); + my $ids = delete $params->{ids}; + defined $ids || ThrowCodeError('param_required', {param => 'ids'}); - my @bugs = map { Bugzilla::Bug->check_for_edit($_) } @$ids; + my @bugs = map { Bugzilla::Bug->check_for_edit($_) } @$ids; - my %values = %$params; - $values{other_bugs} = \@bugs; + my %values = %$params; + $values{other_bugs} = \@bugs; - if (exists $values{comment} and exists $values{comment}{comment}) { - $values{comment}{body} = delete $values{comment}{comment}; - } + if (exists $values{comment} and exists $values{comment}{comment}) { + $values{comment}{body} = delete $values{comment}{comment}; + } - # Prevent bugs that could be triggered by specifying fields that - # have valid "set_" functions in Bugzilla::Bug, but shouldn't be - # called using those field names. - delete $values{dependencies}; + # Prevent bugs that could be triggered by specifying fields that + # have valid "set_" functions in Bugzilla::Bug, but shouldn't be + # called using those field names. + delete $values{dependencies}; - # For backwards compatibility, treat alias string or array as a set action - if (exists $values{alias}) { - if (not ref $values{alias}) { - $values{alias} = { set => [ $values{alias} ] }; - } - elsif (ref $values{alias} eq 'ARRAY') { - $values{alias} = { set => $values{alias} }; - } + # For backwards compatibility, treat alias string or array as a set action + if (exists $values{alias}) { + if (not ref $values{alias}) { + $values{alias} = {set => [$values{alias}]}; } - - my $flags = delete $values{flags}; - - foreach my $bug (@bugs) { - $bug->set_all(\%values); - if ($flags) { - my ($old_flags, $new_flags) = extract_flags($flags, $bug); - $bug->set_flags($old_flags, $new_flags); - } + elsif (ref $values{alias} eq 'ARRAY') { + $values{alias} = {set => $values{alias}}; } + } - my %all_changes; - $dbh->bz_start_transaction(); - foreach my $bug (@bugs) { - $all_changes{$bug->id} = $bug->update(); - } - $dbh->bz_commit_transaction(); + my $flags = delete $values{flags}; - foreach my $bug (@bugs) { - $bug->send_changes($all_changes{$bug->id}); + foreach my $bug (@bugs) { + $bug->set_all(\%values); + if ($flags) { + my ($old_flags, $new_flags) = extract_flags($flags, $bug); + $bug->set_flags($old_flags, $new_flags); } + } + + my %all_changes; + $dbh->bz_start_transaction(); + foreach my $bug (@bugs) { + $all_changes{$bug->id} = $bug->update(); + } + $dbh->bz_commit_transaction(); + + foreach my $bug (@bugs) { + $bug->send_changes($all_changes{$bug->id}); + } + + my %api_name = reverse %{Bugzilla::Bug::FIELD_MAP()}; + + # This doesn't normally belong in FIELD_MAP, but we do want to translate + # "bug_group" back into "groups". + $api_name{'bug_group'} = 'groups'; + + my @result; + foreach my $bug (@bugs) { + my %hash = ( + id => $self->type('int', $bug->id), + last_change_time => $self->type('dateTime', $bug->delta_ts), + changes => {}, + ); - my %api_name = reverse %{ Bugzilla::Bug::FIELD_MAP() }; - # This doesn't normally belong in FIELD_MAP, but we do want to translate - # "bug_group" back into "groups". - $api_name{'bug_group'} = 'groups'; - - my @result; - foreach my $bug (@bugs) { - my %hash = ( - id => $self->type('int', $bug->id), - last_change_time => $self->type('dateTime', $bug->delta_ts), - changes => {}, - ); - - # alias is returned in case users pass a mixture of ids and aliases, - # so that they can know which set of changes relates to which value - # they passed. - $hash{alias} = [ map { $self->type('string', $_) } @{ $bug->alias } ]; - - my %changes = %{ $all_changes{$bug->id} }; - foreach my $field (keys %changes) { - my $change = $changes{$field}; - my $api_field = $api_name{$field} || $field; - # We normalize undef to an empty string, so that the API - # stays consistent for things like Deadline that can become - # empty. - $change->[0] = '' if !defined $change->[0]; - $change->[1] = '' if !defined $change->[1]; - $hash{changes}->{$api_field} = { - removed => $self->type('string', $change->[0]), - added => $self->type('string', $change->[1]) - }; - } - - push(@result, \%hash); + # alias is returned in case users pass a mixture of ids and aliases, + # so that they can know which set of changes relates to which value + # they passed. + $hash{alias} = [map { $self->type('string', $_) } @{$bug->alias}]; + + my %changes = %{$all_changes{$bug->id}}; + foreach my $field (keys %changes) { + my $change = $changes{$field}; + my $api_field = $api_name{$field} || $field; + + # We normalize undef to an empty string, so that the API + # stays consistent for things like Deadline that can become + # empty. + $change->[0] = '' if !defined $change->[0]; + $change->[1] = '' if !defined $change->[1]; + $hash{changes}->{$api_field} = { + removed => $self->type('string', $change->[0]), + added => $self->type('string', $change->[1]) + }; } - return { bugs => \@result }; + push(@result, \%hash); + } + + return {bugs => \@result}; } sub create { - my ($self, $params) = @_; - my $dbh = Bugzilla->dbh; + my ($self, $params) = @_; + my $dbh = Bugzilla->dbh; - Bugzilla->login(LOGIN_REQUIRED); + Bugzilla->login(LOGIN_REQUIRED); - $params = Bugzilla::Bug::map_fields($params); + $params = Bugzilla::Bug::map_fields($params); - my $flags = delete $params->{flags}; + my $flags = delete $params->{flags}; - # We start a nested transaction in case flag setting fails - # we want the bug creation to roll back as well. - $dbh->bz_start_transaction(); + # We start a nested transaction in case flag setting fails + # we want the bug creation to roll back as well. + $dbh->bz_start_transaction(); - my $bug = Bugzilla::Bug->create($params); + my $bug = Bugzilla::Bug->create($params); - # Set bug flags - if ($flags) { - my ($flags, $new_flags) = extract_flags($flags, $bug); - $bug->set_flags($flags, $new_flags); - $bug->update($bug->creation_ts); - } + # Set bug flags + if ($flags) { + my ($flags, $new_flags) = extract_flags($flags, $bug); + $bug->set_flags($flags, $new_flags); + $bug->update($bug->creation_ts); + } - $dbh->bz_commit_transaction(); + $dbh->bz_commit_transaction(); - $bug->send_changes(); + $bug->send_changes(); - return { id => $self->type('int', $bug->bug_id) }; + return {id => $self->type('int', $bug->bug_id)}; } sub legal_values { - my ($self, $params) = @_; + my ($self, $params) = @_; - Bugzilla->switch_to_shadow_db(); + Bugzilla->switch_to_shadow_db(); - defined $params->{field} - or ThrowCodeError('param_required', { param => 'field' }); + defined $params->{field} + or ThrowCodeError('param_required', {param => 'field'}); - my $field = Bugzilla::Bug::FIELD_MAP->{$params->{field}} - || $params->{field}; + my $field = Bugzilla::Bug::FIELD_MAP->{$params->{field}} || $params->{field}; - my @global_selects = - @{ Bugzilla->fields({ is_select => 1, is_abnormal => 0 }) }; + my @global_selects = @{Bugzilla->fields({is_select => 1, is_abnormal => 0})}; - my $values; - if (grep($_->name eq $field, @global_selects)) { - # The field is a valid one. - trick_taint($field); - $values = get_legal_field_values($field); - } - elsif (grep($_ eq $field, PRODUCT_SPECIFIC_FIELDS)) { - my $id = $params->{product_id}; - defined $id || ThrowCodeError('param_required', - { function => 'Bug.legal_values', param => 'product_id' }); - grep($_->id eq $id, @{Bugzilla->user->get_accessible_products}) - || ThrowUserError('product_access_denied', { id => $id }); - - my $product = new Bugzilla::Product($id); - my @objects; - if ($field eq 'version') { - @objects = @{$product->versions}; - } - elsif ($field eq 'target_milestone') { - @objects = @{$product->milestones}; - } - elsif ($field eq 'component') { - @objects = @{$product->components}; - } + my $values; + if (grep($_->name eq $field, @global_selects)) { - $values = [map { $_->name } @objects]; + # The field is a valid one. + trick_taint($field); + $values = get_legal_field_values($field); + } + elsif (grep($_ eq $field, PRODUCT_SPECIFIC_FIELDS)) { + my $id = $params->{product_id}; + defined $id + || ThrowCodeError('param_required', + {function => 'Bug.legal_values', param => 'product_id'}); + grep($_->id eq $id, @{Bugzilla->user->get_accessible_products}) + || ThrowUserError('product_access_denied', {id => $id}); + + my $product = new Bugzilla::Product($id); + my @objects; + if ($field eq 'version') { + @objects = @{$product->versions}; } - else { - ThrowCodeError('invalid_field_name', { field => $params->{field} }); + elsif ($field eq 'target_milestone') { + @objects = @{$product->milestones}; } - - my @result; - foreach my $val (@$values) { - push(@result, $self->type('string', $val)); + elsif ($field eq 'component') { + @objects = @{$product->components}; } - return { values => \@result }; + $values = [map { $_->name } @objects]; + } + else { + ThrowCodeError('invalid_field_name', {field => $params->{field}}); + } + + my @result; + foreach my $val (@$values) { + push(@result, $self->type('string', $val)); + } + + return {values => \@result}; } sub add_attachment { - my ($self, $params) = validate(@_, 'ids'); - my $dbh = Bugzilla->dbh; - - Bugzilla->login(LOGIN_REQUIRED); - defined $params->{ids} - || ThrowCodeError('param_required', { param => 'ids' }); - defined $params->{data} - || ThrowCodeError('param_required', { param => 'data' }); - - my @bugs = map { Bugzilla::Bug->check_for_edit($_) } @{ $params->{ids} }; - - my @created; - $dbh->bz_start_transaction(); - my $timestamp = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); - - my $flags = delete $params->{flags}; - - foreach my $bug (@bugs) { - my $attachment = Bugzilla::Attachment->create({ - bug => $bug, - creation_ts => $timestamp, - data => $params->{data}, - description => $params->{summary}, - filename => $params->{file_name}, - mimetype => $params->{content_type}, - ispatch => $params->{is_patch}, - isprivate => $params->{is_private}, - }); - - if ($flags) { - my ($old_flags, $new_flags) = extract_flags($flags, $bug, $attachment); - $attachment->set_flags($old_flags, $new_flags); - } + my ($self, $params) = validate(@_, 'ids'); + my $dbh = Bugzilla->dbh; + + Bugzilla->login(LOGIN_REQUIRED); + defined $params->{ids} || ThrowCodeError('param_required', {param => 'ids'}); + defined $params->{data} || ThrowCodeError('param_required', {param => 'data'}); + + my @bugs = map { Bugzilla::Bug->check_for_edit($_) } @{$params->{ids}}; + + my @created; + $dbh->bz_start_transaction(); + my $timestamp = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); + + my $flags = delete $params->{flags}; + + foreach my $bug (@bugs) { + my $attachment = Bugzilla::Attachment->create({ + bug => $bug, + creation_ts => $timestamp, + data => $params->{data}, + description => $params->{summary}, + filename => $params->{file_name}, + mimetype => $params->{content_type}, + ispatch => $params->{is_patch}, + isprivate => $params->{is_private}, + }); - $attachment->update($timestamp); - my $comment = $params->{comment} || ''; - $attachment->bug->add_comment($comment, - { isprivate => $attachment->isprivate, - type => CMT_ATTACHMENT_CREATED, - extra_data => $attachment->id }); - push(@created, $attachment); + if ($flags) { + my ($old_flags, $new_flags) = extract_flags($flags, $bug, $attachment); + $attachment->set_flags($old_flags, $new_flags); } - $_->bug->update($timestamp) foreach @created; - $dbh->bz_commit_transaction(); - $_->send_changes() foreach @bugs; + $attachment->update($timestamp); + my $comment = $params->{comment} || ''; + $attachment->bug->add_comment( + $comment, + { + isprivate => $attachment->isprivate, + type => CMT_ATTACHMENT_CREATED, + extra_data => $attachment->id + } + ); + push(@created, $attachment); + } + $_->bug->update($timestamp) foreach @created; + $dbh->bz_commit_transaction(); + + $_->send_changes() foreach @bugs; - my @created_ids = map { $_->id } @created; + my @created_ids = map { $_->id } @created; - return { ids => \@created_ids }; + return {ids => \@created_ids}; } sub update_attachment { - my ($self, $params) = validate(@_, 'ids'); + my ($self, $params) = validate(@_, 'ids'); + + my $user = Bugzilla->login(LOGIN_REQUIRED); + my $dbh = Bugzilla->dbh; + + my $ids = delete $params->{ids}; + defined $ids || ThrowCodeError('param_required', {param => 'ids'}); + + # Some fields cannot be sent to set_all + foreach my $key (qw(login password token)) { + delete $params->{$key}; + } + + $params = translate($params, ATTACHMENT_MAPPED_SETTERS); + + # Get all the attachments, after verifying that they exist and are editable + my @attachments = (); + my %bugs = (); + foreach my $id (@$ids) { + my $attachment = Bugzilla::Attachment->new($id) + || ThrowUserError("invalid_attach_id", {attach_id => $id}); + my $bug = $attachment->bug; + $attachment->_check_bug; + + push @attachments, $attachment; + $bugs{$bug->id} = $bug; + } + + my $flags = delete $params->{flags}; + my $comment = delete $params->{comment}; + + # Update the values + foreach my $attachment (@attachments) { + my ($update_flags, $new_flags) + = $flags ? extract_flags($flags, $attachment->bug, $attachment) : ([], []); + if ($attachment->validate_can_edit) { + $attachment->set_all($params); + $attachment->set_flags($update_flags, $new_flags) if $flags; + } + elsif (scalar @$update_flags && !scalar(@$new_flags) && !scalar keys %$params) { + + # Requestees can set flags targetted to them, even if they cannot + # edit the attachment. Flag setters can edit their own flags too. + my %flag_list = map { $_->{id} => $_ } @$update_flags; + my $flag_objs = Bugzilla::Flag->new_from_list([keys %flag_list]); + my @editable_flags; + foreach my $flag_obj (@$flag_objs) { + if ($flag_obj->setter_id == $user->id + || ($flag_obj->requestee_id && $flag_obj->requestee_id == $user->id)) + { + push(@editable_flags, $flag_list{$flag_obj->id}); + } + } + if (!scalar @editable_flags) { + ThrowUserError("illegal_attachment_edit", {attach_id => $attachment->id}); + } + $attachment->set_flags(\@editable_flags, []); + } + else { + ThrowUserError("illegal_attachment_edit", {attach_id => $attachment->id}); + } + } - my $user = Bugzilla->login(LOGIN_REQUIRED); - my $dbh = Bugzilla->dbh; + $dbh->bz_start_transaction(); - my $ids = delete $params->{ids}; - defined $ids || ThrowCodeError('param_required', { param => 'ids' }); + # Do the actual update and get information to return to user + my @result; + foreach my $attachment (@attachments) { + my $changes = $attachment->update(); - # Some fields cannot be sent to set_all - foreach my $key (qw(login password token)) { - delete $params->{$key}; + if ($comment = trim($comment)) { + $attachment->bug->add_comment( + $comment, + { + isprivate => $attachment->isprivate, + type => CMT_ATTACHMENT_UPDATED, + extra_data => $attachment->id + } + ); } - $params = translate($params, ATTACHMENT_MAPPED_SETTERS); + $changes = translate($changes, ATTACHMENT_MAPPED_RETURNS); - # Get all the attachments, after verifying that they exist and are editable - my @attachments = (); - my %bugs = (); - foreach my $id (@$ids) { - my $attachment = Bugzilla::Attachment->new($id) - || ThrowUserError("invalid_attach_id", { attach_id => $id }); - my $bug = $attachment->bug; - $attachment->_check_bug; + my %hash = ( + id => $self->type('int', $attachment->id), + last_change_time => $self->type('dateTime', $attachment->modification_time), + changes => {}, + ); - push @attachments, $attachment; - $bugs{$bug->id} = $bug; - } + foreach my $field (keys %$changes) { + my $change = $changes->{$field}; - my $flags = delete $params->{flags}; - my $comment = delete $params->{comment}; - - # Update the values - foreach my $attachment (@attachments) { - my ($update_flags, $new_flags) = $flags - ? extract_flags($flags, $attachment->bug, $attachment) - : ([], []); - if ($attachment->validate_can_edit) { - $attachment->set_all($params); - $attachment->set_flags($update_flags, $new_flags) if $flags; - } - elsif (scalar @$update_flags && !scalar(@$new_flags) && !scalar keys %$params) { - # Requestees can set flags targetted to them, even if they cannot - # edit the attachment. Flag setters can edit their own flags too. - my %flag_list = map { $_->{id} => $_ } @$update_flags; - my $flag_objs = Bugzilla::Flag->new_from_list([ keys %flag_list ]); - my @editable_flags; - foreach my $flag_obj (@$flag_objs) { - if ($flag_obj->setter_id == $user->id - || ($flag_obj->requestee_id && $flag_obj->requestee_id == $user->id)) - { - push(@editable_flags, $flag_list{$flag_obj->id}); - } - } - if (!scalar @editable_flags) { - ThrowUserError("illegal_attachment_edit", { attach_id => $attachment->id }); - } - $attachment->set_flags(\@editable_flags, []); - } - else { - ThrowUserError("illegal_attachment_edit", { attach_id => $attachment->id }); - } + # We normalize undef to an empty string, so that the API + # stays consistent for things like Deadline that can become + # empty. + $hash{changes}->{$field} = { + removed => $self->type('string', $change->[0] // ''), + added => $self->type('string', $change->[1] // '') + }; } - $dbh->bz_start_transaction(); - - # Do the actual update and get information to return to user - my @result; - foreach my $attachment (@attachments) { - my $changes = $attachment->update(); - - if ($comment = trim($comment)) { - $attachment->bug->add_comment($comment, - { isprivate => $attachment->isprivate, - type => CMT_ATTACHMENT_UPDATED, - extra_data => $attachment->id }); - } + push(@result, \%hash); + } - $changes = translate($changes, ATTACHMENT_MAPPED_RETURNS); + $dbh->bz_commit_transaction(); - my %hash = ( - id => $self->type('int', $attachment->id), - last_change_time => $self->type('dateTime', $attachment->modification_time), - changes => {}, - ); + # Email users about the change + foreach my $bug (values %bugs) { + $bug->update(); + $bug->send_changes(); + } - foreach my $field (keys %$changes) { - my $change = $changes->{$field}; + # Return the information to the user + return {attachments => \@result}; +} - # We normalize undef to an empty string, so that the API - # stays consistent for things like Deadline that can become - # empty. - $hash{changes}->{$field} = { - removed => $self->type('string', $change->[0] // ''), - added => $self->type('string', $change->[1] // '') - }; - } +sub add_comment { + my ($self, $params) = @_; - push(@result, \%hash); - } + # The user must login in order add a comment + my $user = Bugzilla->login(LOGIN_REQUIRED); - $dbh->bz_commit_transaction(); + # Check parameters + defined $params->{id} || ThrowCodeError('param_required', {param => 'id'}); + my $comment = $params->{comment}; + (defined $comment && trim($comment) ne '') + || ThrowCodeError('param_required', {param => 'comment'}); - # Email users about the change - foreach my $bug (values %bugs) { - $bug->update(); - $bug->send_changes(); - } + my $bug = Bugzilla::Bug->check_for_edit($params->{id}); - # Return the information to the user - return { attachments => \@result }; -} + # Backwards-compatibility for versions before 3.6 + if (defined $params->{private}) { + $params->{is_private} = delete $params->{private}; + } -sub add_comment { - my ($self, $params) = @_; - - # The user must login in order add a comment - my $user = Bugzilla->login(LOGIN_REQUIRED); - - # Check parameters - defined $params->{id} - || ThrowCodeError('param_required', { param => 'id' }); - my $comment = $params->{comment}; - (defined $comment && trim($comment) ne '') - || ThrowCodeError('param_required', { param => 'comment' }); - - my $bug = Bugzilla::Bug->check_for_edit($params->{id}); - - # Backwards-compatibility for versions before 3.6 - if (defined $params->{private}) { - $params->{is_private} = delete $params->{private}; - } - # Append comment - $bug->add_comment($comment, { isprivate => $params->{is_private}, - work_time => $params->{work_time} }); - $bug->update(); + # Append comment + $bug->add_comment($comment, + {isprivate => $params->{is_private}, work_time => $params->{work_time}}); + $bug->update(); - my $new_comment_id = $bug->{added_comments}[0]->id; + my $new_comment_id = $bug->{added_comments}[0]->id; - # Send mail. - Bugzilla::BugMail::Send($bug->bug_id, { changer => $user }); + # Send mail. + Bugzilla::BugMail::Send($bug->bug_id, {changer => $user}); - return { id => $self->type('int', $new_comment_id) }; + return {id => $self->type('int', $new_comment_id)}; } sub update_see_also { - my ($self, $params) = @_; - - my $user = Bugzilla->login(LOGIN_REQUIRED); - - # Check parameters - $params->{ids} - || ThrowCodeError('param_required', { param => 'id' }); - my ($add, $remove) = @$params{qw(add remove)}; - ($add || $remove) - or ThrowCodeError('params_required', { params => ['add', 'remove'] }); - - my @bugs; - foreach my $id (@{ $params->{ids} }) { - my $bug = Bugzilla::Bug->check_for_edit($id); - push(@bugs, $bug); - if ($remove) { - $bug->remove_see_also($_) foreach @$remove; - } - if ($add) { - $bug->add_see_also($_) foreach @$add; - } + my ($self, $params) = @_; + + my $user = Bugzilla->login(LOGIN_REQUIRED); + + # Check parameters + $params->{ids} || ThrowCodeError('param_required', {param => 'id'}); + my ($add, $remove) = @$params{qw(add remove)}; + ($add || $remove) + or ThrowCodeError('params_required', {params => ['add', 'remove']}); + + my @bugs; + foreach my $id (@{$params->{ids}}) { + my $bug = Bugzilla::Bug->check_for_edit($id); + push(@bugs, $bug); + if ($remove) { + $bug->remove_see_also($_) foreach @$remove; } - - my %changes; - foreach my $bug (@bugs) { - my $change = $bug->update(); - if (my $see_also = $change->{see_also}) { - $changes{$bug->id}->{see_also} = { - removed => [split(', ', $see_also->[0])], - added => [split(', ', $see_also->[1])], - }; - } - else { - # We still want a changes entry, for API consistency. - $changes{$bug->id}->{see_also} = { added => [], removed => [] }; - } - - Bugzilla::BugMail::Send($bug->id, { changer => $user }); + if ($add) { + $bug->add_see_also($_) foreach @$add; + } + } + + my %changes; + foreach my $bug (@bugs) { + my $change = $bug->update(); + if (my $see_also = $change->{see_also}) { + $changes{$bug->id}->{see_also} = { + removed => [split(', ', $see_also->[0])], + added => [split(', ', $see_also->[1])], + }; } + else { + # We still want a changes entry, for API consistency. + $changes{$bug->id}->{see_also} = {added => [], removed => []}; + } + + Bugzilla::BugMail::Send($bug->id, {changer => $user}); + } - return { changes => \%changes }; + return {changes => \%changes}; } sub attachments { - my ($self, $params) = validate(@_, 'ids', 'attachment_ids'); + my ($self, $params) = validate(@_, 'ids', 'attachment_ids'); - Bugzilla->switch_to_shadow_db() unless Bugzilla->user->id; + Bugzilla->switch_to_shadow_db() unless Bugzilla->user->id; - if (!(defined $params->{ids} - or defined $params->{attachment_ids})) - { - ThrowCodeError('param_required', - { function => 'Bug.attachments', - params => ['ids', 'attachment_ids'] }); - } - - 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 =~ /^[0-9]+$/; - - my $comment = Bugzilla::Comment->new($comment_id) - || return []; - $comment->bug->check_is_visible(); - if ($comment->is_private && !$user->is_insider) { - ThrowUserError('comment_is_private', { id => $comment_id }); - } + my ($self, $params) = @_; - my $dbh = Bugzilla->dbh; - $dbh->bz_start_transaction(); - foreach my $tag (@{ $params->{add} || [] }) { - $comment->add_tag($tag) if defined $tag; - } - foreach my $tag (@{ $params->{remove} || [] }) { - $comment->remove_tag($tag) if defined $tag; + my $user = Bugzilla->login(LOGIN_REQUIRED); + Bugzilla->params->{'comment_taggers_group'} + || ThrowUserError("comment_tag_disabled"); + $user->can_tag_comments || ThrowUserError( + "auth_failure", + { + group => Bugzilla->params->{'comment_taggers_group'}, + action => "update", + object => "comment_tags" } - $comment->update(); - $dbh->bz_commit_transaction(); - - return $comment->tags; + ); + + my $comment_id = $params->{comment_id} // ThrowCodeError('param_required', + {function => 'Bug.update_comment_tags', param => 'comment_id'}); + + ThrowCodeError('param_integer_required', + {function => 'Bug.update_comment_tags', param => 'comment_id'}) + unless $comment_id =~ /^[0-9]+$/; + + my $comment = Bugzilla::Comment->new($comment_id) || return []; + $comment->bug->check_is_visible(); + if ($comment->is_private && !$user->is_insider) { + ThrowUserError('comment_is_private', {id => $comment_id}); + } + + my $dbh = Bugzilla->dbh; + $dbh->bz_start_transaction(); + foreach my $tag (@{$params->{add} || []}) { + $comment->add_tag($tag) if defined $tag; + } + foreach my $tag (@{$params->{remove} || []}) { + $comment->remove_tag($tag) if defined $tag; + } + $comment->update(); + $dbh->bz_commit_transaction(); + + return $comment->tags; } sub search_comment_tags { - my ($self, $params) = @_; - - Bugzilla->login(LOGIN_REQUIRED); - Bugzilla->params->{'comment_taggers_group'} - || ThrowUserError("comment_tag_disabled"); - Bugzilla->user->can_tag_comments - || ThrowUserError("auth_failure", { group => Bugzilla->params->{'comment_taggers_group'}, - action => "search", - object => "comment_tags"}); - - my $query = $params->{query}; - $query - // ThrowCodeError('param_required', { param => 'query' }); - my $limit = $params->{limit} || 7; - detaint_natural($limit) - || ThrowCodeError('param_must_be_numeric', { param => 'limit', - function => 'Bug.search_comment_tags' }); - - - my $tags = Bugzilla::Comment::TagWeights->match({ - WHERE => { - 'tag LIKE ?' => "\%$query\%", - }, - LIMIT => $limit, - }); - return [ map { $_->tag } @$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,}); + return [map { $_->tag } @$tags]; } ############################## @@ -1197,232 +1206,238 @@ sub search_comment_tags { # return them directly. sub _bug_to_hash { - my ($self, $bug, $params) = @_; - - # All the basic bug attributes are here, in alphabetical order. - # A bug attribute is "basic" if it doesn't require an additional - # database call to get the info. - my %item = %{ filter $params, { - # No need to format $bug->deadline specially, because Bugzilla::Bug - # already does it for us. - deadline => $self->type('string', $bug->deadline), - id => $self->type('int', $bug->bug_id), - is_confirmed => $self->type('boolean', $bug->everconfirmed), - op_sys => $self->type('string', $bug->op_sys), - platform => $self->type('string', $bug->rep_platform), - priority => $self->type('string', $bug->priority), - resolution => $self->type('string', $bug->resolution), - severity => $self->type('string', $bug->bug_severity), - status => $self->type('string', $bug->bug_status), - summary => $self->type('string', $bug->short_desc), - target_milestone => $self->type('string', $bug->target_milestone), - url => $self->type('string', $bug->bug_file_loc), - version => $self->type('string', $bug->version), - whiteboard => $self->type('string', $bug->status_whiteboard), - } }; - - # First we handle any fields that require extra work (such as date parsing - # or SQL calls). - if (filter_wants $params, 'alias') { - $item{alias} = [ map { $self->type('string', $_) } @{ $bug->alias } ]; - } - if (filter_wants $params, 'assigned_to') { - $item{'assigned_to'} = $self->type('email', $bug->assigned_to->login); - $item{'assigned_to_detail'} = $self->_user_to_hash($bug->assigned_to, $params, undef, 'assigned_to'); - } - if (filter_wants $params, 'blocks') { - my @blocks = map { $self->type('int', $_) } @{ $bug->blocked }; - $item{'blocks'} = \@blocks; - } - if (filter_wants $params, 'classification') { - $item{classification} = $self->type('string', $bug->classification); - } - if (filter_wants $params, 'component') { - $item{component} = $self->type('string', $bug->component); - } - if (filter_wants $params, 'cc') { - my @cc = map { $self->type('email', $_) } @{ $bug->cc }; - $item{'cc'} = \@cc; - $item{'cc_detail'} = [ map { $self->_user_to_hash($_, $params, undef, 'cc') } @{ $bug->cc_users } ]; - } - if (filter_wants $params, 'creation_time') { - $item{'creation_time'} = $self->type('dateTime', $bug->creation_ts); - } - if (filter_wants $params, 'creator') { - $item{'creator'} = $self->type('email', $bug->reporter->login); - $item{'creator_detail'} = $self->_user_to_hash($bug->reporter, $params, undef, 'creator'); - } - if (filter_wants $params, 'depends_on') { - my @depends_on = map { $self->type('int', $_) } @{ $bug->dependson }; - $item{'depends_on'} = \@depends_on; - } - if (filter_wants $params, 'dupe_of') { - $item{'dupe_of'} = $self->type('int', $bug->dup_id); - } - if (filter_wants $params, 'groups') { - my @groups = map { $self->type('string', $_->name) } - @{ $bug->groups_in }; - $item{'groups'} = \@groups; - } - if (filter_wants $params, 'is_open') { - $item{'is_open'} = $self->type('boolean', $bug->status->is_open); - } - if (filter_wants $params, 'keywords') { - my @keywords = map { $self->type('string', $_->name) } - @{ $bug->keyword_objects }; - $item{'keywords'} = \@keywords; - } - if (filter_wants $params, 'last_change_time') { - $item{'last_change_time'} = $self->type('dateTime', $bug->delta_ts); - } - if (filter_wants $params, 'product') { - $item{product} = $self->type('string', $bug->product); + my ($self, $bug, $params) = @_; + + # All the basic bug attributes are here, in alphabetical order. + # A bug attribute is "basic" if it doesn't require an additional + # database call to get the info. + my %item = %{filter $params, + { + # No need to format $bug->deadline specially, because Bugzilla::Bug + # already does it for us. + deadline => $self->type('string', $bug->deadline), + id => $self->type('int', $bug->bug_id), + is_confirmed => $self->type('boolean', $bug->everconfirmed), + op_sys => $self->type('string', $bug->op_sys), + platform => $self->type('string', $bug->rep_platform), + priority => $self->type('string', $bug->priority), + resolution => $self->type('string', $bug->resolution), + severity => $self->type('string', $bug->bug_severity), + status => $self->type('string', $bug->bug_status), + summary => $self->type('string', $bug->short_desc), + target_milestone => $self->type('string', $bug->target_milestone), + url => $self->type('string', $bug->bug_file_loc), + version => $self->type('string', $bug->version), + whiteboard => $self->type('string', $bug->status_whiteboard), } - if (filter_wants $params, 'qa_contact') { - my $qa_login = $bug->qa_contact ? $bug->qa_contact->login : ''; - $item{'qa_contact'} = $self->type('email', $qa_login); - if ($bug->qa_contact) { - $item{'qa_contact_detail'} = $self->_user_to_hash($bug->qa_contact, $params, undef, 'qa_contact'); - } + }; + + # First we handle any fields that require extra work (such as date parsing + # or SQL calls). + if (filter_wants $params, 'alias') { + $item{alias} = [map { $self->type('string', $_) } @{$bug->alias}]; + } + if (filter_wants $params, 'assigned_to') { + $item{'assigned_to'} = $self->type('email', $bug->assigned_to->login); + $item{'assigned_to_detail'} + = $self->_user_to_hash($bug->assigned_to, $params, undef, 'assigned_to'); + } + if (filter_wants $params, 'blocks') { + my @blocks = map { $self->type('int', $_) } @{$bug->blocked}; + $item{'blocks'} = \@blocks; + } + if (filter_wants $params, 'classification') { + $item{classification} = $self->type('string', $bug->classification); + } + if (filter_wants $params, 'component') { + $item{component} = $self->type('string', $bug->component); + } + if (filter_wants $params, 'cc') { + my @cc = map { $self->type('email', $_) } @{$bug->cc}; + $item{'cc'} = \@cc; + $item{'cc_detail'} + = [map { $self->_user_to_hash($_, $params, undef, 'cc') } @{$bug->cc_users}]; + } + if (filter_wants $params, 'creation_time') { + $item{'creation_time'} = $self->type('dateTime', $bug->creation_ts); + } + if (filter_wants $params, 'creator') { + $item{'creator'} = $self->type('email', $bug->reporter->login); + $item{'creator_detail'} + = $self->_user_to_hash($bug->reporter, $params, undef, 'creator'); + } + if (filter_wants $params, 'depends_on') { + my @depends_on = map { $self->type('int', $_) } @{$bug->dependson}; + $item{'depends_on'} = \@depends_on; + } + if (filter_wants $params, 'dupe_of') { + $item{'dupe_of'} = $self->type('int', $bug->dup_id); + } + if (filter_wants $params, 'groups') { + my @groups = map { $self->type('string', $_->name) } @{$bug->groups_in}; + $item{'groups'} = \@groups; + } + if (filter_wants $params, 'is_open') { + $item{'is_open'} = $self->type('boolean', $bug->status->is_open); + } + if (filter_wants $params, 'keywords') { + my @keywords = map { $self->type('string', $_->name) } @{$bug->keyword_objects}; + $item{'keywords'} = \@keywords; + } + if (filter_wants $params, 'last_change_time') { + $item{'last_change_time'} = $self->type('dateTime', $bug->delta_ts); + } + if (filter_wants $params, 'product') { + $item{product} = $self->type('string', $bug->product); + } + if (filter_wants $params, 'qa_contact') { + my $qa_login = $bug->qa_contact ? $bug->qa_contact->login : ''; + $item{'qa_contact'} = $self->type('email', $qa_login); + if ($bug->qa_contact) { + $item{'qa_contact_detail'} + = $self->_user_to_hash($bug->qa_contact, $params, undef, 'qa_contact'); } - if (filter_wants $params, 'see_also') { - my @see_also = map { $self->type('string', $_->name) } - @{ $bug->see_also }; - $item{'see_also'} = \@see_also; + } + if (filter_wants $params, 'see_also') { + my @see_also = map { $self->type('string', $_->name) } @{$bug->see_also}; + $item{'see_also'} = \@see_also; + } + if (filter_wants $params, 'flags') { + $item{'flags'} = [map { $self->_flag_to_hash($_) } @{$bug->flags}]; + } + if (filter_wants $params, 'tags', 'extra') { + $item{'tags'} = $bug->tags; + } + + # And now custom fields + my @custom_fields = Bugzilla->active_custom_fields; + foreach my $field (@custom_fields) { + my $name = $field->name; + next if !filter_wants($params, $name, ['default', 'custom']); + if ($field->type == FIELD_TYPE_BUG_ID) { + $item{$name} = $self->type('int', $bug->$name); } - if (filter_wants $params, 'flags') { - $item{'flags'} = [ map { $self->_flag_to_hash($_) } @{$bug->flags} ]; + elsif ($field->type == FIELD_TYPE_DATETIME || $field->type == FIELD_TYPE_DATE) { + $item{$name} = $self->type('dateTime', $bug->$name); } - if (filter_wants $params, 'tags', 'extra') { - $item{'tags'} = $bug->tags; + elsif ($field->type == FIELD_TYPE_MULTI_SELECT) { + my @values = map { $self->type('string', $_) } @{$bug->$name}; + $item{$name} = \@values; } - - # And now custom fields - my @custom_fields = Bugzilla->active_custom_fields; - foreach my $field (@custom_fields) { - my $name = $field->name; - next if !filter_wants($params, $name, ['default', 'custom']); - if ($field->type == FIELD_TYPE_BUG_ID) { - $item{$name} = $self->type('int', $bug->$name); - } - elsif ($field->type == FIELD_TYPE_DATETIME - || $field->type == FIELD_TYPE_DATE) - { - $item{$name} = $self->type('dateTime', $bug->$name); - } - elsif ($field->type == FIELD_TYPE_MULTI_SELECT) { - my @values = map { $self->type('string', $_) } @{ $bug->$name }; - $item{$name} = \@values; - } - else { - $item{$name} = $self->type('string', $bug->$name); - } + else { + $item{$name} = $self->type('string', $bug->$name); } + } - # Timetracking fields are only sent if the user can see them. - if (Bugzilla->user->is_timetracker) { - if (filter_wants $params, 'estimated_time') { - $item{'estimated_time'} = $self->type('double', $bug->estimated_time); - } - if (filter_wants $params, 'remaining_time') { - $item{'remaining_time'} = $self->type('double', $bug->remaining_time); - } - if (filter_wants $params, 'actual_time') { - $item{'actual_time'} = $self->type('double', $bug->actual_time); - } + # Timetracking fields are only sent if the user can see them. + if (Bugzilla->user->is_timetracker) { + if (filter_wants $params, 'estimated_time') { + $item{'estimated_time'} = $self->type('double', $bug->estimated_time); } - - # The "accessible" bits go here because they have long names and it - # makes the code look nicer to separate them out. - if (filter_wants $params, 'is_cc_accessible') { - $item{'is_cc_accessible'} = $self->type('boolean', $bug->cclist_accessible); + if (filter_wants $params, 'remaining_time') { + $item{'remaining_time'} = $self->type('double', $bug->remaining_time); } - if (filter_wants $params, 'is_creator_accessible') { - $item{'is_creator_accessible'} = $self->type('boolean', $bug->reporter_accessible); + if (filter_wants $params, 'actual_time') { + $item{'actual_time'} = $self->type('double', $bug->actual_time); } - - return \%item; + } + + # The "accessible" bits go here because they have long names and it + # makes the code look nicer to separate them out. + if (filter_wants $params, 'is_cc_accessible') { + $item{'is_cc_accessible'} = $self->type('boolean', $bug->cclist_accessible); + } + if (filter_wants $params, 'is_creator_accessible') { + $item{'is_creator_accessible'} + = $self->type('boolean', $bug->reporter_accessible); + } + + return \%item; } sub _user_to_hash { - my ($self, $user, $filters, $types, $prefix) = @_; - my $item = filter $filters, { - id => $self->type('int', $user->id), - real_name => $self->type('string', $user->name), - name => $self->type('email', $user->login), - email => $self->type('email', $user->email), - }, $types, $prefix; - return $item; + my ($self, $user, $filters, $types, $prefix) = @_; + my $item = filter $filters, + { + id => $self->type('int', $user->id), + real_name => $self->type('string', $user->name), + name => $self->type('email', $user->login), + email => $self->type('email', $user->email), + }, + $types, $prefix; + return $item; } sub _attachment_to_hash { - my ($self, $attach, $filters, $types, $prefix) = @_; - - my $item = filter $filters, { - creation_time => $self->type('dateTime', $attach->attached), - last_change_time => $self->type('dateTime', $attach->modification_time), - id => $self->type('int', $attach->id), - bug_id => $self->type('int', $attach->bug_id), - file_name => $self->type('string', $attach->filename), - summary => $self->type('string', $attach->description), - content_type => $self->type('string', $attach->contenttype), - is_private => $self->type('int', $attach->isprivate), - is_obsolete => $self->type('int', $attach->isobsolete), - is_patch => $self->type('int', $attach->ispatch), - }, $types, $prefix; - - # creator requires an extra lookup, so we only send them if - # the filter wants them. - if (filter_wants $filters, 'creator', $types, $prefix) { - $item->{'creator'} = $self->type('email', $attach->attacher->login); - } - - if (filter_wants $filters, 'data', $types, $prefix) { - $item->{'data'} = $self->type('base64', $attach->data); - } - - if (filter_wants $filters, 'size', $types, $prefix) { - $item->{'size'} = $self->type('int', $attach->datasize); - } - - if (filter_wants $filters, 'flags', $types, $prefix) { - $item->{'flags'} = [ map { $self->_flag_to_hash($_) } @{$attach->flags} ]; - } + my ($self, $attach, $filters, $types, $prefix) = @_; - return $item; + my $item = filter $filters, + { + creation_time => $self->type('dateTime', $attach->attached), + last_change_time => $self->type('dateTime', $attach->modification_time), + id => $self->type('int', $attach->id), + bug_id => $self->type('int', $attach->bug_id), + file_name => $self->type('string', $attach->filename), + summary => $self->type('string', $attach->description), + content_type => $self->type('string', $attach->contenttype), + is_private => $self->type('int', $attach->isprivate), + is_obsolete => $self->type('int', $attach->isobsolete), + is_patch => $self->type('int', $attach->ispatch), + }, + $types, $prefix; + + # creator requires an extra lookup, so we only send them if + # the filter wants them. + if (filter_wants $filters, 'creator', $types, $prefix) { + $item->{'creator'} = $self->type('email', $attach->attacher->login); + } + + if (filter_wants $filters, 'data', $types, $prefix) { + $item->{'data'} = $self->type('base64', $attach->data); + } + + if (filter_wants $filters, 'size', $types, $prefix) { + $item->{'size'} = $self->type('int', $attach->datasize); + } + + if (filter_wants $filters, 'flags', $types, $prefix) { + $item->{'flags'} = [map { $self->_flag_to_hash($_) } @{$attach->flags}]; + } + + return $item; } sub _flag_to_hash { - my ($self, $flag) = @_; - - my $item = { - id => $self->type('int', $flag->id), - name => $self->type('string', $flag->name), - type_id => $self->type('int', $flag->type_id), - creation_date => $self->type('dateTime', $flag->creation_date), - modification_date => $self->type('dateTime', $flag->modification_date), - status => $self->type('string', $flag->status) - }; - - foreach my $field (qw(setter requestee)) { - my $field_id = $field . "_id"; - $item->{$field} = $self->type('email', $flag->$field->login) - if $flag->$field_id; - } - - return $item; + my ($self, $flag) = @_; + + my $item = { + id => $self->type('int', $flag->id), + name => $self->type('string', $flag->name), + type_id => $self->type('int', $flag->type_id), + creation_date => $self->type('dateTime', $flag->creation_date), + modification_date => $self->type('dateTime', $flag->modification_date), + status => $self->type('string', $flag->status) + }; + + foreach my $field (qw(setter requestee)) { + my $field_id = $field . "_id"; + $item->{$field} = $self->type('email', $flag->$field->login) + if $flag->$field_id; + } + + return $item; } sub _add_update_tokens { - my ($self, $params, $bugs, $hashes) = @_; + my ($self, $params, $bugs, $hashes) = @_; - return if !Bugzilla->user->id; - return if !filter_wants($params, 'update_token'); + return if !Bugzilla->user->id; + return if !filter_wants($params, 'update_token'); - for(my $i = 0; $i < @$bugs; $i++) { - my $token = issue_hash_token([$bugs->[$i]->id, $bugs->[$i]->delta_ts]); - $hashes->[$i]->{'update_token'} = $self->type('string', $token); - } + for (my $i = 0; $i < @$bugs; $i++) { + my $token = issue_hash_token([$bugs->[$i]->id, $bugs->[$i]->delta_ts]); + $hashes->[$i]->{'update_token'} = $self->type('string', $token); + } } 1; diff --git a/Bugzilla/WebService/BugUserLastVisit.pm b/Bugzilla/WebService/BugUserLastVisit.pm index 56e91ec31..128507376 100644 --- a/Bugzilla/WebService/BugUserLastVisit.pm +++ b/Bugzilla/WebService/BugUserLastVisit.pm @@ -19,80 +19,83 @@ use Bugzilla::WebService::Util qw( validate filter ); use Bugzilla::Constants; use constant PUBLIC_METHODS => qw( - get - update + get + update ); sub update { - my ($self, $params) = validate(@_, 'ids'); - my $user = Bugzilla->user; - my $dbh = Bugzilla->dbh; + my ($self, $params) = validate(@_, 'ids'); + my $user = Bugzilla->user; + my $dbh = Bugzilla->dbh; - $user->login(LOGIN_REQUIRED); + $user->login(LOGIN_REQUIRED); - my $ids = $params->{ids} // []; - ThrowCodeError('param_required', { param => 'ids' }) unless @$ids; + my $ids = $params->{ids} // []; + ThrowCodeError('param_required', {param => 'ids'}) unless @$ids; - # Cache permissions for bugs. This highly reduces the number of calls to the - # DB. visible_bugs() is only able to handle bug IDs, so we have to skip - # aliases. - $user->visible_bugs([grep /^[0-9]+$/, @$ids]); + # Cache permissions for bugs. This highly reduces the number of calls to the + # DB. visible_bugs() is only able to handle bug IDs, so we have to skip + # aliases. + $user->visible_bugs([grep /^[0-9]+$/, @$ids]); - $dbh->bz_start_transaction(); - my @results; - my $last_visit_ts = $dbh->selectrow_array('SELECT NOW()'); - foreach my $bug_id (@$ids) { - my $bug = Bugzilla::Bug->check({ id => $bug_id, cache => 1 }); + $dbh->bz_start_transaction(); + my @results; + my $last_visit_ts = $dbh->selectrow_array('SELECT NOW()'); + foreach my $bug_id (@$ids) { + my $bug = Bugzilla::Bug->check({id => $bug_id, cache => 1}); - ThrowUserError('user_not_involved', { bug_id => $bug->id }) - unless $user->is_involved_in_bug($bug); + ThrowUserError('user_not_involved', {bug_id => $bug->id}) + unless $user->is_involved_in_bug($bug); - $bug->update_user_last_visit($user, $last_visit_ts); + $bug->update_user_last_visit($user, $last_visit_ts); - push( - @results, - $self->_bug_user_last_visit_to_hash( - $bug->id, $last_visit_ts, $params - )); - } - $dbh->bz_commit_transaction(); + push(@results, + $self->_bug_user_last_visit_to_hash($bug->id, $last_visit_ts, $params)); + } + $dbh->bz_commit_transaction(); - return \@results; + return \@results; } sub get { - my ($self, $params) = validate(@_, 'ids'); - my $user = Bugzilla->user; - my $ids = $params->{ids}; - - $user->login(LOGIN_REQUIRED); - - my @last_visits; - if ($ids) { - # Cache permissions for bugs. This highly reduces the number of calls to - # the DB. visible_bugs() is only able to handle bug IDs, so we have to - # skip aliases. - $user->visible_bugs([grep /^[0-9]+$/, @$ids]); - - my %last_visit = map { $_->bug_id => $_->last_visit_ts } @{ $user->last_visited($ids) }; - @last_visits = map { $self->_bug_user_last_visit_to_hash($_->id, $last_visit{$_}, $params) } @$ids; - } - else { - @last_visits = map { - $self->_bug_user_last_visit_to_hash($_->bug_id, $_->last_visit_ts, $params) - } @{ $user->last_visited }; - } - - return \@last_visits; + my ($self, $params) = validate(@_, 'ids'); + my $user = Bugzilla->user; + my $ids = $params->{ids}; + + $user->login(LOGIN_REQUIRED); + + my @last_visits; + if ($ids) { + + # Cache permissions for bugs. This highly reduces the number of calls to + # the DB. visible_bugs() is only able to handle bug IDs, so we have to + # skip aliases. + $user->visible_bugs([grep /^[0-9]+$/, @$ids]); + + my %last_visit + = map { $_->bug_id => $_->last_visit_ts } @{$user->last_visited($ids)}; + @last_visits + = map { $self->_bug_user_last_visit_to_hash($_->id, $last_visit{$_}, $params) } + @$ids; + } + else { + @last_visits = map { + $self->_bug_user_last_visit_to_hash($_->bug_id, $_->last_visit_ts, $params) + } @{$user->last_visited}; + } + + return \@last_visits; } sub _bug_user_last_visit_to_hash { - my ($self, $bug_id, $last_visit_ts, $params) = @_; + my ($self, $bug_id, $last_visit_ts, $params) = @_; - my %result = (id => $self->type('int', $bug_id), - last_visit_ts => $self->type('dateTime', $last_visit_ts)); + my %result = ( + id => $self->type('int', $bug_id), + last_visit_ts => $self->type('dateTime', $last_visit_ts) + ); - return filter($params, \%result); + return filter($params, \%result); } 1; diff --git a/Bugzilla/WebService/Bugzilla.pm b/Bugzilla/WebService/Bugzilla.pm index 848cffd30..6d9563d61 100644 --- a/Bugzilla/WebService/Bugzilla.pm +++ b/Bugzilla/WebService/Bugzilla.pm @@ -20,158 +20,155 @@ use Bugzilla::Util qw(trick_taint); use DateTime; # Basic info that is needed before logins -use constant LOGIN_EXEMPT => { - parameters => 1, - timezone => 1, - version => 1, -}; +use constant LOGIN_EXEMPT => {parameters => 1, timezone => 1, version => 1,}; use constant READ_ONLY => qw( - extensions - parameters - timezone - time - version + extensions + parameters + timezone + time + version ); use constant PUBLIC_METHODS => qw( - extensions - last_audit_time - parameters - time - timezone - version + extensions + last_audit_time + parameters + time + timezone + version ); # Logged-out users do not need to know more than that. use constant PARAMETERS_LOGGED_OUT => qw( - maintainer - requirelogin + maintainer + requirelogin ); # These parameters are guessable from the web UI when the user # is logged in. So it's safe to access them. use constant PARAMETERS_LOGGED_IN => qw( - allowemailchange - attachment_base - commentonchange_resolution - commentonduplicate - cookiepath - defaultopsys - defaultplatform - defaultpriority - defaultseverity - duplicate_or_move_bug_status - emailregexpdesc - emailsuffix - letsubmitterchoosemilestone - letsubmitterchoosepriority - mailfrom - maintainer - maxattachmentsize - maxlocalattachment - musthavemilestoneonaccept - noresolveonopenblockers - password_complexity - rememberlogin - requirelogin - search_allow_no_criteria - urlbase - use_see_also - useclassification - usemenuforusers - useqacontact - usestatuswhiteboard - usetargetmilestone + allowemailchange + attachment_base + commentonchange_resolution + commentonduplicate + cookiepath + defaultopsys + defaultplatform + defaultpriority + defaultseverity + duplicate_or_move_bug_status + emailregexpdesc + emailsuffix + letsubmitterchoosemilestone + letsubmitterchoosepriority + mailfrom + maintainer + maxattachmentsize + maxlocalattachment + musthavemilestoneonaccept + noresolveonopenblockers + password_complexity + rememberlogin + requirelogin + search_allow_no_criteria + urlbase + use_see_also + useclassification + usemenuforusers + useqacontact + usestatuswhiteboard + usetargetmilestone ); sub version { - my $self = shift; - return { version => $self->type('string', BUGZILLA_VERSION) }; + my $self = shift; + return {version => $self->type('string', BUGZILLA_VERSION)}; } sub extensions { - my $self = shift; - - my %retval; - foreach my $extension (@{ Bugzilla->extensions }) { - my $version = $extension->VERSION || 0; - my $name = $extension->NAME; - $retval{$name}->{version} = $self->type('string', $version); - } - return { extensions => \%retval }; + my $self = shift; + + my %retval; + foreach my $extension (@{Bugzilla->extensions}) { + my $version = $extension->VERSION || 0; + my $name = $extension->NAME; + $retval{$name}->{version} = $self->type('string', $version); + } + return {extensions => \%retval}; } sub timezone { - my $self = shift; - # All Webservices return times in UTC; Use UTC here for backwards compat. - return { timezone => $self->type('string', "+0000") }; + my $self = shift; + + # All Webservices return times in UTC; Use UTC here for backwards compat. + return {timezone => $self->type('string', "+0000")}; } sub time { - my ($self) = @_; - # All Webservices return times in UTC; Use UTC here for backwards compat. - # Hardcode values where appropriate - my $dbh = Bugzilla->dbh; - - my $db_time = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); - $db_time = datetime_from($db_time, 'UTC'); - my $now_utc = DateTime->now(); - - return { - db_time => $self->type('dateTime', $db_time), - web_time => $self->type('dateTime', $now_utc), - web_time_utc => $self->type('dateTime', $now_utc), - tz_name => $self->type('string', 'UTC'), - tz_offset => $self->type('string', '+0000'), - tz_short_name => $self->type('string', 'UTC'), - }; + my ($self) = @_; + + # All Webservices return times in UTC; Use UTC here for backwards compat. + # Hardcode values where appropriate + my $dbh = Bugzilla->dbh; + + my $db_time = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); + $db_time = datetime_from($db_time, 'UTC'); + my $now_utc = DateTime->now(); + + return { + db_time => $self->type('dateTime', $db_time), + web_time => $self->type('dateTime', $now_utc), + web_time_utc => $self->type('dateTime', $now_utc), + tz_name => $self->type('string', 'UTC'), + tz_offset => $self->type('string', '+0000'), + tz_short_name => $self->type('string', 'UTC'), + }; } sub last_audit_time { - my ($self, $params) = validate(@_, 'class'); - my $dbh = Bugzilla->dbh; - - my $sql_statement = "SELECT MAX(at_time) FROM audit_log"; - my $class_values = $params->{class}; - my @class_values_quoted; - foreach my $class_value (@$class_values) { - push (@class_values_quoted, $dbh->quote($class_value)) - if $class_value =~ /^Bugzilla(::[a-zA-Z0-9_]+)*$/; - } - - if (@class_values_quoted) { - $sql_statement .= " WHERE " . $dbh->sql_in('class', \@class_values_quoted); - } - - my $last_audit_time = $dbh->selectrow_array("$sql_statement"); - - # All Webservices return times in UTC; Use UTC here for backwards compat. - # Hardcode values where appropriate - $last_audit_time = datetime_from($last_audit_time, 'UTC'); - - return { - last_audit_time => $self->type('dateTime', $last_audit_time) - }; + my ($self, $params) = validate(@_, 'class'); + my $dbh = Bugzilla->dbh; + + my $sql_statement = "SELECT MAX(at_time) FROM audit_log"; + my $class_values = $params->{class}; + my @class_values_quoted; + foreach my $class_value (@$class_values) { + push(@class_values_quoted, $dbh->quote($class_value)) + if $class_value =~ /^Bugzilla(::[a-zA-Z0-9_]+)*$/; + } + + if (@class_values_quoted) { + $sql_statement .= " WHERE " . $dbh->sql_in('class', \@class_values_quoted); + } + + my $last_audit_time = $dbh->selectrow_array("$sql_statement"); + + # All Webservices return times in UTC; Use UTC here for backwards compat. + # Hardcode values where appropriate + $last_audit_time = datetime_from($last_audit_time, 'UTC'); + + return {last_audit_time => $self->type('dateTime', $last_audit_time)}; } sub parameters { - my ($self, $args) = @_; - my $user = Bugzilla->login(LOGIN_OPTIONAL); - my $params = Bugzilla->params; - $args ||= {}; - - my @params_list = $user->in_group('tweakparams') - ? keys(%$params) - : $user->id ? PARAMETERS_LOGGED_IN : PARAMETERS_LOGGED_OUT; - - my %parameters; - foreach my $param (@params_list) { - next unless filter_wants($args, $param); - $parameters{$param} = $self->type('string', $params->{$param}); - } - - return { parameters => \%parameters }; + my ($self, $args) = @_; + my $user = Bugzilla->login(LOGIN_OPTIONAL); + my $params = Bugzilla->params; + $args ||= {}; + + my @params_list + = $user->in_group('tweakparams') ? keys(%$params) + : $user->id ? PARAMETERS_LOGGED_IN + : PARAMETERS_LOGGED_OUT; + + my %parameters; + foreach my $param (@params_list) { + next unless filter_wants($args, $param); + $parameters{$param} = $self->type('string', $params->{$param}); + } + + return {parameters => \%parameters}; } 1; diff --git a/Bugzilla/WebService/Classification.pm b/Bugzilla/WebService/Classification.pm index cee597b68..ab539b339 100644 --- a/Bugzilla/WebService/Classification.pm +++ b/Bugzilla/WebService/Classification.pm @@ -18,65 +18,76 @@ use Bugzilla::Error; use Bugzilla::WebService::Util qw(filter validate params_to_objects); use constant READ_ONLY => qw( - get + get ); use constant PUBLIC_METHODS => qw( - get + get ); sub get { - my ($self, $params) = validate(@_, 'names', 'ids'); + my ($self, $params) = validate(@_, 'names', 'ids'); - defined $params->{names} || defined $params->{ids} - || ThrowCodeError('params_required', { function => 'Classification.get', - params => ['names', 'ids'] }); + defined $params->{names} + || defined $params->{ids} + || ThrowCodeError('params_required', + {function => 'Classification.get', params => ['names', 'ids']}); - my $user = Bugzilla->user; + my $user = Bugzilla->user; - Bugzilla->params->{'useclassification'} - || $user->in_group('editclassifications') - || ThrowUserError('auth_classification_not_enabled'); + Bugzilla->params->{'useclassification'} + || $user->in_group('editclassifications') + || ThrowUserError('auth_classification_not_enabled'); - Bugzilla->switch_to_shadow_db; + Bugzilla->switch_to_shadow_db; - my @classification_objs = @{ params_to_objects($params, 'Bugzilla::Classification') }; - unless ($user->in_group('editclassifications')) { - my %selectable_class = map { $_->id => 1 } @{$user->get_selectable_classifications}; - @classification_objs = grep { $selectable_class{$_->id} } @classification_objs; - } + my @classification_objs + = @{params_to_objects($params, 'Bugzilla::Classification')}; + unless ($user->in_group('editclassifications')) { + my %selectable_class + = map { $_->id => 1 } @{$user->get_selectable_classifications}; + @classification_objs = grep { $selectable_class{$_->id} } @classification_objs; + } - my @classifications = map { $self->_classification_to_hash($_, $params) } @classification_objs; + my @classifications + = map { $self->_classification_to_hash($_, $params) } @classification_objs; - return { classifications => \@classifications }; + return {classifications => \@classifications}; } sub _classification_to_hash { - my ($self, $classification, $params) = @_; - - my $user = Bugzilla->user; - return unless (Bugzilla->params->{'useclassification'} || $user->in_group('editclassifications')); - - my $products = $user->in_group('editclassifications') ? - $classification->products : $user->get_selectable_products($classification->id); - - return filter $params, { - id => $self->type('int', $classification->id), - name => $self->type('string', $classification->name), - description => $self->type('string', $classification->description), - sort_key => $self->type('int', $classification->sortkey), - products => [ map { $self->_product_to_hash($_, $params) } @$products ], + my ($self, $classification, $params) = @_; + + my $user = Bugzilla->user; + return + unless (Bugzilla->params->{'useclassification'} + || $user->in_group('editclassifications')); + + my $products + = $user->in_group('editclassifications') + ? $classification->products + : $user->get_selectable_products($classification->id); + + return filter $params, + { + id => $self->type('int', $classification->id), + name => $self->type('string', $classification->name), + description => $self->type('string', $classification->description), + sort_key => $self->type('int', $classification->sortkey), + products => [map { $self->_product_to_hash($_, $params) } @$products], }; } sub _product_to_hash { - my ($self, $product, $params) = @_; - - return filter $params, { - id => $self->type('int', $product->id), - name => $self->type('string', $product->name), - description => $self->type('string', $product->description), - }, undef, 'products'; + my ($self, $product, $params) = @_; + + return filter $params, + { + id => $self->type('int', $product->id), + name => $self->type('string', $product->name), + description => $self->type('string', $product->description), + }, + undef, 'products'; } 1; diff --git a/Bugzilla/WebService/Component.pm b/Bugzilla/WebService/Component.pm index 4d6723d8b..802f40c73 100644 --- a/Bugzilla/WebService/Component.pm +++ b/Bugzilla/WebService/Component.pm @@ -19,37 +19,36 @@ use Bugzilla::Error; use Bugzilla::WebService::Constants; use Bugzilla::WebService::Util qw(translate params_to_objects validate); -use constant PUBLIC_METHODS => qw( - create +use constant PUBLIC_METHODS => qw( + create ); use constant MAPPED_FIELDS => { - default_assignee => 'initialowner', - default_qa_contact => 'initialqacontact', - default_cc => 'initial_cc', - is_open => 'isactive', + default_assignee => 'initialowner', + default_qa_contact => 'initialqacontact', + default_cc => 'initial_cc', + is_open => 'isactive', }; sub create { - my ($self, $params) = @_; + my ($self, $params) = @_; - my $user = Bugzilla->login(LOGIN_REQUIRED); + my $user = Bugzilla->login(LOGIN_REQUIRED); - $user->in_group('editcomponents') - || scalar @{ $user->get_products_by_permission('editcomponents') } - || ThrowUserError('auth_failure', { group => 'editcomponents', - action => 'edit', - object => 'components' }); + $user->in_group('editcomponents') + || scalar @{$user->get_products_by_permission('editcomponents')} + || ThrowUserError('auth_failure', + {group => 'editcomponents', action => 'edit', object => 'components'}); - my $product = $user->check_can_admin_product($params->{product}); + my $product = $user->check_can_admin_product($params->{product}); - # Translate the fields - my $values = translate($params, MAPPED_FIELDS); - $values->{product} = $product; + # Translate the fields + my $values = translate($params, MAPPED_FIELDS); + $values->{product} = $product; - # Create the component and return the newly created id. - my $component = Bugzilla::Component->create($values); - return { id => $self->type('int', $component->id) }; + # Create the component and return the newly created id. + my $component = Bugzilla::Component->create($values); + return {id => $self->type('int', $component->id)}; } 1; diff --git a/Bugzilla/WebService/Constants.pm b/Bugzilla/WebService/Constants.pm index 557a996f8..a2f83f528 100644 --- a/Bugzilla/WebService/Constants.pm +++ b/Bugzilla/WebService/Constants.pm @@ -14,25 +14,25 @@ use warnings; use parent qw(Exporter); our @EXPORT = qw( - WS_ERROR_CODE + WS_ERROR_CODE - STATUS_OK - STATUS_CREATED - STATUS_ACCEPTED - STATUS_NO_CONTENT - STATUS_MULTIPLE_CHOICES - STATUS_BAD_REQUEST - STATUS_NOT_FOUND - STATUS_GONE - REST_STATUS_CODE_MAP + STATUS_OK + STATUS_CREATED + STATUS_ACCEPTED + STATUS_NO_CONTENT + STATUS_MULTIPLE_CHOICES + STATUS_BAD_REQUEST + STATUS_NOT_FOUND + STATUS_GONE + REST_STATUS_CODE_MAP - ERROR_UNKNOWN_FATAL - ERROR_UNKNOWN_TRANSIENT + ERROR_UNKNOWN_FATAL + ERROR_UNKNOWN_TRANSIENT - XMLRPC_CONTENT_TYPE_WHITELIST - REST_CONTENT_TYPE_WHITELIST + XMLRPC_CONTENT_TYPE_WHITELIST + REST_CONTENT_TYPE_WHITELIST - WS_DISPATCH + WS_DISPATCH ); # This maps the error names in global/*-error.html.tmpl to numbers. @@ -54,173 +54,196 @@ our @EXPORT = qw( # comment that it was retired. Also, if an error changes its name, you'll # have to fix it here. use constant WS_ERROR_CODE => { - # Generic errors (Bugzilla::Object and others) are 50-99. - object_not_specified => 50, - reassign_to_empty => 50, - param_required => 50, - params_required => 50, - undefined_field => 50, - object_does_not_exist => 51, - param_must_be_numeric => 52, - number_not_numeric => 52, - param_invalid => 53, - number_too_large => 54, - number_too_small => 55, - illegal_date => 56, - param_integer_required => 57, - param_scalar_array_required => 58, - # Bug errors usually occupy the 100-200 range. - improper_bug_id_field_value => 100, - bug_id_does_not_exist => 101, - bug_access_denied => 102, - bug_access_query => 102, - # These all mean "invalid alias" - alias_too_long => 103, - alias_in_use => 103, - alias_is_numeric => 103, - alias_has_comma_or_space => 103, - multiple_alias_not_allowed => 103, - # Misc. bug field errors - illegal_field => 104, - freetext_too_long => 104, - # Component errors - require_component => 105, - component_name_too_long => 105, - product_unknown_component => 105, - # Invalid Product - no_products => 106, - entry_access_denied => 106, - product_access_denied => 106, - product_disabled => 106, - # Invalid Summary - require_summary => 107, - # Invalid field name - invalid_field_name => 108, - # Not authorized to edit the bug - product_edit_denied => 109, - # Comment-related errors - comment_is_private => 110, - comment_id_invalid => 111, - comment_too_long => 114, - comment_invalid_isprivate => 117, - # Comment tagging - comment_tag_disabled => 125, - comment_tag_invalid => 126, - comment_tag_too_long => 127, - comment_tag_too_short => 128, - # See Also errors - bug_url_invalid => 112, - bug_url_too_long => 112, - # Insidergroup Errors - user_not_insider => 113, - # Note: 114 is above in the Comment-related section. - # Bug update errors - illegal_change => 115, - # Dependency errors - dependency_loop_single => 116, - dependency_loop_multi => 116, - # Note: 117 is above in the Comment-related section. - # Dup errors - dupe_loop_detected => 118, - dupe_id_required => 119, - # Bug-related group errors - group_invalid_removal => 120, - group_restriction_not_allowed => 120, - # Status/Resolution errors - missing_resolution => 121, - resolution_not_allowed => 122, - illegal_bug_status_transition => 123, - # Flag errors - flag_status_invalid => 129, - flag_update_denied => 130, - flag_type_requestee_disabled => 131, - flag_not_unique => 132, - flag_type_not_unique => 133, - flag_type_inactive => 134, - - # Authentication errors are usually 300-400. - invalid_login_or_password => 300, - account_disabled => 301, - auth_invalid_email => 302, - extern_id_conflict => -303, - auth_failure => 304, - password_too_short => 305, - password_not_complex => 305, - api_key_not_valid => 306, - api_key_revoked => 306, - auth_invalid_token => 307, - - # Except, historically, AUTH_NODATA, which is 410. - login_required => 410, - - # User errors are 500-600. - account_exists => 500, - illegal_email_address => 501, - auth_cant_create_account => 501, - account_creation_disabled => 501, - account_creation_restricted => 501, - password_too_short => 502, - # Error 503 password_too_long no longer exists. - invalid_username => 504, - # This is from strict_isolation, but it also basically means - # "invalid user." - invalid_user_group => 504, - user_access_by_id_denied => 505, - user_access_by_match_denied => 505, - - # Attachment errors are 600-700. - file_too_large => 600, - invalid_content_type => 601, - # Error 602 attachment_illegal_url no longer exists. - file_not_specified => 603, - missing_attachment_description => 604, - # Error 605 attachment_url_disabled no longer exists. - zero_length_file => 606, - - # Product erros are 700-800 - product_blank_name => 700, - product_name_too_long => 701, - product_name_already_in_use => 702, - product_name_diff_in_case => 702, - product_must_have_description => 703, - product_must_have_version => 704, - product_must_define_defaultmilestone => 705, - - # Group errors are 800-900 - empty_group_name => 800, - group_exists => 801, - empty_group_description => 802, - invalid_regexp => 803, - invalid_group_name => 804, - group_cannot_view => 805, - - # Classification errors are 900-1000 - auth_classification_not_enabled => 900, - - # Search errors are 1000-1100 - buglist_parameters_required => 1000, - - # Flag type errors are 1100-1200 - flag_type_name_invalid => 1101, - flag_type_description_invalid => 1102, - flag_type_cc_list_invalid => 1103, - flag_type_sortkey_invalid => 1104, - flag_type_not_editable => 1105, - - # Component errors are 1200-1300 - component_already_exists => 1200, - component_is_last => 1201, - component_has_bugs => 1202, - - # Errors thrown by the WebService itself. The ones that are negative - # conform to http://xmlrpc-epi.sourceforge.net/specs/rfc.fault_codes.php - xmlrpc_invalid_value => -32600, - unknown_method => -32601, - json_rpc_post_only => 32610, - json_rpc_invalid_callback => 32611, - xmlrpc_illegal_content_type => 32612, - json_rpc_illegal_content_type => 32613, - rest_invalid_resource => 32614, + + # Generic errors (Bugzilla::Object and others) are 50-99. + object_not_specified => 50, + reassign_to_empty => 50, + param_required => 50, + params_required => 50, + undefined_field => 50, + object_does_not_exist => 51, + param_must_be_numeric => 52, + number_not_numeric => 52, + param_invalid => 53, + number_too_large => 54, + number_too_small => 55, + illegal_date => 56, + param_integer_required => 57, + param_scalar_array_required => 58, + + # Bug errors usually occupy the 100-200 range. + improper_bug_id_field_value => 100, + bug_id_does_not_exist => 101, + bug_access_denied => 102, + bug_access_query => 102, + + # These all mean "invalid alias" + alias_too_long => 103, + alias_in_use => 103, + alias_is_numeric => 103, + alias_has_comma_or_space => 103, + multiple_alias_not_allowed => 103, + + # Misc. bug field errors + illegal_field => 104, + freetext_too_long => 104, + + # Component errors + require_component => 105, + component_name_too_long => 105, + product_unknown_component => 105, + + # Invalid Product + no_products => 106, + entry_access_denied => 106, + product_access_denied => 106, + product_disabled => 106, + + # Invalid Summary + require_summary => 107, + + # Invalid field name + invalid_field_name => 108, + + # Not authorized to edit the bug + product_edit_denied => 109, + + # Comment-related errors + comment_is_private => 110, + comment_id_invalid => 111, + comment_too_long => 114, + comment_invalid_isprivate => 117, + + # Comment tagging + comment_tag_disabled => 125, + comment_tag_invalid => 126, + comment_tag_too_long => 127, + comment_tag_too_short => 128, + + # See Also errors + bug_url_invalid => 112, + bug_url_too_long => 112, + + # Insidergroup Errors + user_not_insider => 113, + + # Note: 114 is above in the Comment-related section. + # Bug update errors + illegal_change => 115, + + # Dependency errors + dependency_loop_single => 116, + dependency_loop_multi => 116, + + # Note: 117 is above in the Comment-related section. + # Dup errors + dupe_loop_detected => 118, + dupe_id_required => 119, + + # Bug-related group errors + group_invalid_removal => 120, + group_restriction_not_allowed => 120, + + # Status/Resolution errors + missing_resolution => 121, + resolution_not_allowed => 122, + illegal_bug_status_transition => 123, + + # Flag errors + flag_status_invalid => 129, + flag_update_denied => 130, + flag_type_requestee_disabled => 131, + flag_not_unique => 132, + flag_type_not_unique => 133, + flag_type_inactive => 134, + + # Authentication errors are usually 300-400. + invalid_login_or_password => 300, + account_disabled => 301, + auth_invalid_email => 302, + extern_id_conflict => -303, + auth_failure => 304, + password_too_short => 305, + password_not_complex => 305, + api_key_not_valid => 306, + api_key_revoked => 306, + auth_invalid_token => 307, + + # Except, historically, AUTH_NODATA, which is 410. + login_required => 410, + + # User errors are 500-600. + account_exists => 500, + illegal_email_address => 501, + auth_cant_create_account => 501, + account_creation_disabled => 501, + account_creation_restricted => 501, + password_too_short => 502, + + # Error 503 password_too_long no longer exists. + invalid_username => 504, + + # This is from strict_isolation, but it also basically means + # "invalid user." + invalid_user_group => 504, + user_access_by_id_denied => 505, + user_access_by_match_denied => 505, + + # Attachment errors are 600-700. + file_too_large => 600, + invalid_content_type => 601, + + # Error 602 attachment_illegal_url no longer exists. + file_not_specified => 603, + missing_attachment_description => 604, + + # Error 605 attachment_url_disabled no longer exists. + zero_length_file => 606, + + # Product erros are 700-800 + product_blank_name => 700, + product_name_too_long => 701, + product_name_already_in_use => 702, + product_name_diff_in_case => 702, + product_must_have_description => 703, + product_must_have_version => 704, + product_must_define_defaultmilestone => 705, + + # Group errors are 800-900 + empty_group_name => 800, + group_exists => 801, + empty_group_description => 802, + invalid_regexp => 803, + invalid_group_name => 804, + group_cannot_view => 805, + + # Classification errors are 900-1000 + auth_classification_not_enabled => 900, + + # Search errors are 1000-1100 + buglist_parameters_required => 1000, + + # Flag type errors are 1100-1200 + flag_type_name_invalid => 1101, + flag_type_description_invalid => 1102, + flag_type_cc_list_invalid => 1103, + flag_type_sortkey_invalid => 1104, + flag_type_not_editable => 1105, + + # Component errors are 1200-1300 + component_already_exists => 1200, + component_is_last => 1201, + component_has_bugs => 1202, + + # Errors thrown by the WebService itself. The ones that are negative + # conform to http://xmlrpc-epi.sourceforge.net/specs/rfc.fault_codes.php + xmlrpc_invalid_value => -32600, + unknown_method => -32601, + json_rpc_post_only => 32610, + json_rpc_invalid_callback => 32611, + xmlrpc_illegal_content_type => 32612, + json_rpc_illegal_content_type => 32613, + rest_invalid_resource => 32614, }; # RESTful webservices use the http status code @@ -241,73 +264,74 @@ use constant STATUS_GONE => 410; # http status code based on the error code or use the # default STATUS_BAD_REQUEST. sub REST_STATUS_CODE_MAP { - my $status_code_map = { - 51 => STATUS_NOT_FOUND, - 101 => STATUS_NOT_FOUND, - 102 => STATUS_NOT_AUTHORIZED, - 106 => STATUS_NOT_AUTHORIZED, - 109 => STATUS_NOT_AUTHORIZED, - 110 => STATUS_NOT_AUTHORIZED, - 113 => STATUS_NOT_AUTHORIZED, - 115 => STATUS_NOT_AUTHORIZED, - 120 => STATUS_NOT_AUTHORIZED, - 300 => STATUS_NOT_AUTHORIZED, - 301 => STATUS_NOT_AUTHORIZED, - 302 => STATUS_NOT_AUTHORIZED, - 303 => STATUS_NOT_AUTHORIZED, - 304 => STATUS_NOT_AUTHORIZED, - 410 => STATUS_NOT_AUTHORIZED, - 504 => STATUS_NOT_AUTHORIZED, - 505 => STATUS_NOT_AUTHORIZED, - 32614 => STATUS_NOT_FOUND, - _default => STATUS_BAD_REQUEST - }; - - Bugzilla::Hook::process('webservice_status_code_map', - { status_code_map => $status_code_map }); - - return $status_code_map; -}; + my $status_code_map = { + 51 => STATUS_NOT_FOUND, + 101 => STATUS_NOT_FOUND, + 102 => STATUS_NOT_AUTHORIZED, + 106 => STATUS_NOT_AUTHORIZED, + 109 => STATUS_NOT_AUTHORIZED, + 110 => STATUS_NOT_AUTHORIZED, + 113 => STATUS_NOT_AUTHORIZED, + 115 => STATUS_NOT_AUTHORIZED, + 120 => STATUS_NOT_AUTHORIZED, + 300 => STATUS_NOT_AUTHORIZED, + 301 => STATUS_NOT_AUTHORIZED, + 302 => STATUS_NOT_AUTHORIZED, + 303 => STATUS_NOT_AUTHORIZED, + 304 => STATUS_NOT_AUTHORIZED, + 410 => STATUS_NOT_AUTHORIZED, + 504 => STATUS_NOT_AUTHORIZED, + 505 => STATUS_NOT_AUTHORIZED, + 32614 => STATUS_NOT_FOUND, + _default => STATUS_BAD_REQUEST + }; + + Bugzilla::Hook::process('webservice_status_code_map', + {status_code_map => $status_code_map}); + + return $status_code_map; +} # These are the fallback defaults for errors not in ERROR_CODE. use constant ERROR_UNKNOWN_FATAL => -32000; use constant ERROR_UNKNOWN_TRANSIENT => 32000; -use constant ERROR_GENERAL => 999; +use constant ERROR_GENERAL => 999; use constant XMLRPC_CONTENT_TYPE_WHITELIST => qw( - text/xml - application/xml + text/xml + application/xml ); # The first content type specified is used as the default. use constant REST_CONTENT_TYPE_WHITELIST => qw( - application/json - application/javascript - text/javascript - text/html + application/json + application/javascript + text/javascript + text/html ); sub WS_DISPATCH { - # We "require" here instead of "use" above to avoid a dependency loop. - require Bugzilla::Hook; - my %hook_dispatch; - Bugzilla::Hook::process('webservice', { dispatch => \%hook_dispatch }); - - my $dispatch = { - 'Bugzilla' => 'Bugzilla::WebService::Bugzilla', - 'Bug' => 'Bugzilla::WebService::Bug', - 'Classification' => 'Bugzilla::WebService::Classification', - 'Component' => 'Bugzilla::WebService::Component', - 'FlagType' => 'Bugzilla::WebService::FlagType', - 'Group' => 'Bugzilla::WebService::Group', - 'Product' => 'Bugzilla::WebService::Product', - 'User' => 'Bugzilla::WebService::User', - 'BugUserLastVisit' => 'Bugzilla::WebService::BugUserLastVisit', - %hook_dispatch - }; - return $dispatch; -}; + + # We "require" here instead of "use" above to avoid a dependency loop. + require Bugzilla::Hook; + my %hook_dispatch; + Bugzilla::Hook::process('webservice', {dispatch => \%hook_dispatch}); + + my $dispatch = { + 'Bugzilla' => 'Bugzilla::WebService::Bugzilla', + 'Bug' => 'Bugzilla::WebService::Bug', + 'Classification' => 'Bugzilla::WebService::Classification', + 'Component' => 'Bugzilla::WebService::Component', + 'FlagType' => 'Bugzilla::WebService::FlagType', + 'Group' => 'Bugzilla::WebService::Group', + 'Product' => 'Bugzilla::WebService::Product', + 'User' => 'Bugzilla::WebService::User', + 'BugUserLastVisit' => 'Bugzilla::WebService::BugUserLastVisit', + %hook_dispatch + }; + return $dispatch; +} 1; diff --git a/Bugzilla/WebService/FlagType.pm b/Bugzilla/WebService/FlagType.pm index 9d7cce037..9dc240c7f 100644 --- a/Bugzilla/WebService/FlagType.pm +++ b/Bugzilla/WebService/FlagType.pm @@ -22,292 +22,308 @@ use Bugzilla::Util qw(trim); use List::MoreUtils qw(uniq); use constant PUBLIC_METHODS => qw( - create - get - update + create + get + update ); sub get { - my ($self, $params) = @_; - my $dbh = Bugzilla->switch_to_shadow_db(); - my $user = Bugzilla->user; - - defined $params->{product} - || ThrowCodeError('param_required', - { function => 'Bug.flag_types', - param => 'product' }); - - my $product = delete $params->{product}; - my $component = delete $params->{component}; - - $product = Bugzilla::Product->check({ name => $product, cache => 1 }); - $component = Bugzilla::Component->check( - { name => $component, product => $product, cache => 1 }) if $component; - - my $flag_params = { product_id => $product->id }; - $flag_params->{component_id} = $component->id if $component; - my $matched_flag_types = Bugzilla::FlagType::match($flag_params); - - my $flag_types = { bug => [], attachment => [] }; - foreach my $flag_type (@$matched_flag_types) { - push(@{ $flag_types->{bug} }, $self->_flagtype_to_hash($flag_type, $product)) - if $flag_type->target_type eq 'bug'; - push(@{ $flag_types->{attachment} }, $self->_flagtype_to_hash($flag_type, $product)) - if $flag_type->target_type eq 'attachment'; - } - - return $flag_types; + my ($self, $params) = @_; + my $dbh = Bugzilla->switch_to_shadow_db(); + my $user = Bugzilla->user; + + defined $params->{product} + || ThrowCodeError('param_required', + {function => 'Bug.flag_types', param => 'product'}); + + my $product = delete $params->{product}; + my $component = delete $params->{component}; + + $product = Bugzilla::Product->check({name => $product, cache => 1}); + $component + = Bugzilla::Component->check( + {name => $component, product => $product, cache => 1}) + if $component; + + my $flag_params = {product_id => $product->id}; + $flag_params->{component_id} = $component->id if $component; + my $matched_flag_types = Bugzilla::FlagType::match($flag_params); + + my $flag_types = {bug => [], attachment => []}; + foreach my $flag_type (@$matched_flag_types) { + push(@{$flag_types->{bug}}, $self->_flagtype_to_hash($flag_type, $product)) + if $flag_type->target_type eq 'bug'; + push( + @{$flag_types->{attachment}}, + $self->_flagtype_to_hash($flag_type, $product) + ) if $flag_type->target_type eq 'attachment'; + } + + return $flag_types; } sub create { - my ($self, $params) = @_; - my $user = Bugzilla->login(LOGIN_REQUIRED); - - $user->in_group('editcomponents') - || scalar(@{$user->get_products_by_permission('editcomponents')}) - || ThrowUserError("auth_failure", { group => "editcomponents", - action => "add", - object => "flagtypes" }); - - $params->{name} || ThrowCodeError('param_required', { param => 'name' }); - $params->{description} || ThrowCodeError('param_required', { param => 'description' }); - - my %args = ( - sortkey => 1, - name => undef, - inclusions => ['0:0'], # Default to __ALL__:__ALL__ - cc_list => '', - description => undef, - is_requestable => 'on', - exclusions => [], - is_multiplicable => 'on', - request_group => '', - is_active => 'on', - is_specifically_requestable => 'on', - target_type => 'bug', - grant_group => '', - ); - - foreach my $key (keys %args) { - $args{$key} = $params->{$key} if defined($params->{$key}); - } - - $args{name} = trim($params->{name}); - $args{description} = trim($params->{description}); - - # Is specifically requestable is actually is_requesteeable - if (exists $args{is_specifically_requestable}) { - $args{is_requesteeble} = delete $args{is_specifically_requestable}; - } - - # Default is on for the tickbox flags. - # If the user has set them to 'off' then undefine them so the flags are not ticked - foreach my $arg_name (qw(is_requestable is_multiplicable is_active is_requesteeble)) { - if (defined($args{$arg_name}) && ($args{$arg_name} eq '0')) { - $args{$arg_name} = undef; - } + my ($self, $params) = @_; + my $user = Bugzilla->login(LOGIN_REQUIRED); + + $user->in_group('editcomponents') + || scalar(@{$user->get_products_by_permission('editcomponents')}) + || ThrowUserError("auth_failure", + {group => "editcomponents", action => "add", object => "flagtypes"}); + + $params->{name} || ThrowCodeError('param_required', {param => 'name'}); + $params->{description} + || ThrowCodeError('param_required', {param => 'description'}); + + my %args = ( + sortkey => 1, + name => undef, + inclusions => ['0:0'], # Default to __ALL__:__ALL__ + cc_list => '', + description => undef, + is_requestable => 'on', + exclusions => [], + is_multiplicable => 'on', + request_group => '', + is_active => 'on', + is_specifically_requestable => 'on', + target_type => 'bug', + grant_group => '', + ); + + foreach my $key (keys %args) { + $args{$key} = $params->{$key} if defined($params->{$key}); + } + + $args{name} = trim($params->{name}); + $args{description} = trim($params->{description}); + + # Is specifically requestable is actually is_requesteeable + if (exists $args{is_specifically_requestable}) { + $args{is_requesteeble} = delete $args{is_specifically_requestable}; + } + +# Default is on for the tickbox flags. +# If the user has set them to 'off' then undefine them so the flags are not ticked + foreach + my $arg_name (qw(is_requestable is_multiplicable is_active is_requesteeble)) + { + if (defined($args{$arg_name}) && ($args{$arg_name} eq '0')) { + $args{$arg_name} = undef; } + } - # Process group inclusions and exclusions - $args{inclusions} = _process_lists($params->{inclusions}) if defined $params->{inclusions}; - $args{exclusions} = _process_lists($params->{exclusions}) if defined $params->{exclusions}; + # Process group inclusions and exclusions + $args{inclusions} = _process_lists($params->{inclusions}) + if defined $params->{inclusions}; + $args{exclusions} = _process_lists($params->{exclusions}) + if defined $params->{exclusions}; - my $flagtype = Bugzilla::FlagType->create(\%args); + my $flagtype = Bugzilla::FlagType->create(\%args); - return { id => $self->type('int', $flagtype->id) }; + return {id => $self->type('int', $flagtype->id)}; } sub update { - my ($self, $params) = @_; - my $dbh = Bugzilla->dbh; - my $user = Bugzilla->login(LOGIN_REQUIRED); - - $user->in_group('editcomponents') - || scalar(@{$user->get_products_by_permission('editcomponents')}) - || ThrowUserError("auth_failure", { group => "editcomponents", - action => "edit", - object => "flagtypes" }); - - defined($params->{names}) || defined($params->{ids}) - || ThrowCodeError('params_required', - { function => 'FlagType.update', params => ['ids', 'names'] }); - - # Get the list of unique flag type ids we are updating - my @flag_type_ids = defined($params->{ids}) ? @{$params->{ids}} : (); - if (defined $params->{names}) { - push @flag_type_ids, map { $_->id } - @{ Bugzilla::FlagType::match({ name => $params->{names} }) }; + my ($self, $params) = @_; + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->login(LOGIN_REQUIRED); + + $user->in_group('editcomponents') + || scalar(@{$user->get_products_by_permission('editcomponents')}) + || ThrowUserError("auth_failure", + {group => "editcomponents", action => "edit", object => "flagtypes"}); + + defined($params->{names}) + || defined($params->{ids}) + || ThrowCodeError('params_required', + {function => 'FlagType.update', params => ['ids', 'names']}); + + # Get the list of unique flag type ids we are updating + my @flag_type_ids = defined($params->{ids}) ? @{$params->{ids}} : (); + if (defined $params->{names}) { + push @flag_type_ids, + map { $_->id } @{Bugzilla::FlagType::match({name => $params->{names}})}; + } + @flag_type_ids = uniq @flag_type_ids; + + # We delete names and ids to keep only new values to set. + delete $params->{names}; + delete $params->{ids}; + + # Process group inclusions and exclusions + # We removed them from $params because these are handled differently + my $inclusions = _process_lists(delete $params->{inclusions}) + if defined $params->{inclusions}; + my $exclusions = _process_lists(delete $params->{exclusions}) + if defined $params->{exclusions}; + + $dbh->bz_start_transaction(); + my %changes = (); + + foreach my $flag_type_id (@flag_type_ids) { + my ($flagtype, $can_fully_edit) + = $user->check_can_admin_flagtype($flag_type_id); + + if ($can_fully_edit) { + $flagtype->set_all($params); + } + elsif (scalar keys %$params) { + ThrowUserError('flag_type_not_editable', {flagtype => $flagtype}); } - @flag_type_ids = uniq @flag_type_ids; - - # We delete names and ids to keep only new values to set. - delete $params->{names}; - delete $params->{ids}; - - # Process group inclusions and exclusions - # We removed them from $params because these are handled differently - my $inclusions = _process_lists(delete $params->{inclusions}) if defined $params->{inclusions}; - my $exclusions = _process_lists(delete $params->{exclusions}) if defined $params->{exclusions}; - - $dbh->bz_start_transaction(); - my %changes = (); - foreach my $flag_type_id (@flag_type_ids) { - my ($flagtype, $can_fully_edit) = $user->check_can_admin_flagtype($flag_type_id); + # Process the clusions + foreach my $type ('inclusions', 'exclusions') { + my $clusions = $type eq 'inclusions' ? $inclusions : $exclusions; + next if not defined $clusions; - if ($can_fully_edit) { - $flagtype->set_all($params); - } - elsif (scalar keys %$params) { - ThrowUserError('flag_type_not_editable', { flagtype => $flagtype }); - } + my @extra_clusions = (); + if (!$user->in_group('editcomponents')) { + my $products = $user->get_products_by_permission('editcomponents'); - # Process the clusions - foreach my $type ('inclusions', 'exclusions') { - my $clusions = $type eq 'inclusions' ? $inclusions : $exclusions; - next if not defined $clusions; - - my @extra_clusions = (); - if (!$user->in_group('editcomponents')) { - my $products = $user->get_products_by_permission('editcomponents'); - # Bring back the products the user cannot edit. - foreach my $item (values %{$flagtype->$type}) { - my ($prod_id, $comp_id) = split(':', $item); - push(@extra_clusions, $item) unless grep { $_->id == $prod_id } @$products; - } - } - - $flagtype->set_clusions({ - $type => [@$clusions, @extra_clusions], - }); + # Bring back the products the user cannot edit. + foreach my $item (values %{$flagtype->$type}) { + my ($prod_id, $comp_id) = split(':', $item); + push(@extra_clusions, $item) unless grep { $_->id == $prod_id } @$products; } + } - my $returned_changes = $flagtype->update(); - $changes{$flagtype->id} = { - name => $flagtype->name, - changes => $returned_changes, - }; + $flagtype->set_clusions({$type => [@$clusions, @extra_clusions],}); } - $dbh->bz_commit_transaction(); - - my @result; - foreach my $flag_type_id (keys %changes) { - my %hash = ( - id => $self->type('int', $flag_type_id), - name => $self->type('string', $changes{$flag_type_id}{name}), - changes => {}, - ); - - foreach my $field (keys %{ $changes{$flag_type_id}{changes} }) { - my $change = $changes{$flag_type_id}{changes}{$field}; - $hash{changes}{$field} = { - removed => $self->type('string', $change->[0]), - added => $self->type('string', $change->[1]) - }; - } - push(@result, \%hash); + my $returned_changes = $flagtype->update(); + $changes{$flagtype->id} + = {name => $flagtype->name, changes => $returned_changes,}; + } + $dbh->bz_commit_transaction(); + + my @result; + foreach my $flag_type_id (keys %changes) { + my %hash = ( + id => $self->type('int', $flag_type_id), + name => $self->type('string', $changes{$flag_type_id}{name}), + changes => {}, + ); + + foreach my $field (keys %{$changes{$flag_type_id}{changes}}) { + my $change = $changes{$flag_type_id}{changes}{$field}; + $hash{changes}{$field} = { + removed => $self->type('string', $change->[0]), + added => $self->type('string', $change->[1]) + }; } - return { flagtypes => \@result }; + push(@result, \%hash); + } + + return {flagtypes => \@result}; } sub _flagtype_to_hash { - my ($self, $flagtype, $product) = @_; - my $user = Bugzilla->user; - - my @values = ('X'); - push(@values, '?') if ($flagtype->is_requestable && $user->can_request_flag($flagtype)); - push(@values, '+', '-') if $user->can_set_flag($flagtype); - - my $item = { - id => $self->type('int' , $flagtype->id), - name => $self->type('string' , $flagtype->name), - description => $self->type('string' , $flagtype->description), - type => $self->type('string' , $flagtype->target_type), - values => \@values, - is_active => $self->type('boolean', $flagtype->is_active), - is_requesteeble => $self->type('boolean', $flagtype->is_requesteeble), - is_multiplicable => $self->type('boolean', $flagtype->is_multiplicable) - }; - - if ($product) { - my $inclusions = $self->_flagtype_clusions_to_hash($flagtype->inclusions, $product->id); - my $exclusions = $self->_flagtype_clusions_to_hash($flagtype->exclusions, $product->id); - # if we have both inclusions and exclusions, the exclusions are redundant - $exclusions = [] if @$inclusions && @$exclusions; - # no need to return anything if there's just "any component" - $item->{inclusions} = $inclusions if @$inclusions && $inclusions->[0] ne ''; - $item->{exclusions} = $exclusions if @$exclusions && $exclusions->[0] ne ''; - } - - return $item; + my ($self, $flagtype, $product) = @_; + my $user = Bugzilla->user; + + my @values = ('X'); + push(@values, '?') + if ($flagtype->is_requestable && $user->can_request_flag($flagtype)); + push(@values, '+', '-') if $user->can_set_flag($flagtype); + + my $item = { + id => $self->type('int', $flagtype->id), + name => $self->type('string', $flagtype->name), + description => $self->type('string', $flagtype->description), + type => $self->type('string', $flagtype->target_type), + values => \@values, + is_active => $self->type('boolean', $flagtype->is_active), + is_requesteeble => $self->type('boolean', $flagtype->is_requesteeble), + is_multiplicable => $self->type('boolean', $flagtype->is_multiplicable) + }; + + if ($product) { + my $inclusions + = $self->_flagtype_clusions_to_hash($flagtype->inclusions, $product->id); + my $exclusions + = $self->_flagtype_clusions_to_hash($flagtype->exclusions, $product->id); + + # if we have both inclusions and exclusions, the exclusions are redundant + $exclusions = [] if @$inclusions && @$exclusions; + + # no need to return anything if there's just "any component" + $item->{inclusions} = $inclusions if @$inclusions && $inclusions->[0] ne ''; + $item->{exclusions} = $exclusions if @$exclusions && $exclusions->[0] ne ''; + } + + return $item; } sub _flagtype_clusions_to_hash { - my ($self, $clusions, $product_id) = @_; - my $result = []; - foreach my $key (keys %$clusions) { - my ($prod_id, $comp_id) = split(/:/, $clusions->{$key}, 2); - if ($prod_id == 0 || $prod_id == $product_id) { - if ($comp_id) { - my $component = Bugzilla::Component->new({ id => $comp_id, cache => 1 }); - push @$result, $component->name; - } - else { - return [ '' ]; - } - } + my ($self, $clusions, $product_id) = @_; + my $result = []; + foreach my $key (keys %$clusions) { + my ($prod_id, $comp_id) = split(/:/, $clusions->{$key}, 2); + if ($prod_id == 0 || $prod_id == $product_id) { + if ($comp_id) { + my $component = Bugzilla::Component->new({id => $comp_id, cache => 1}); + push @$result, $component->name; + } + else { + return ['']; + } } - return $result; + } + return $result; } sub _process_lists { - my $list = shift; - my $user = Bugzilla->user; - - my @products; - if ($user->in_group('editcomponents')) { - @products = Bugzilla::Product->get_all; - } - else { - @products = @{$user->get_products_by_permission('editcomponents')}; - } - - my @component_list; - - foreach my $item (@$list) { - # A hash with products as the key and component names as the values - if(ref($item) eq 'HASH') { - while (my ($product_name, $component_names) = each %$item) { - my $product = Bugzilla::Product->check({name => $product_name}); - unless (grep { $product->name eq $_->name } @products) { - ThrowUserError('product_access_denied', { name => $product_name }); - } - my @component_ids; - - foreach my $comp_name (@$component_names) { - my $component = Bugzilla::Component->check({product => $product, name => $comp_name}); - ThrowCodeError('param_invalid', { param => $comp_name}) unless defined $component; - push @component_list, $product->id . ':' . $component->id; - } - } + my $list = shift; + my $user = Bugzilla->user; + + my @products; + if ($user->in_group('editcomponents')) { + @products = Bugzilla::Product->get_all; + } + else { + @products = @{$user->get_products_by_permission('editcomponents')}; + } + + my @component_list; + + foreach my $item (@$list) { + + # A hash with products as the key and component names as the values + if (ref($item) eq 'HASH') { + while (my ($product_name, $component_names) = each %$item) { + my $product = Bugzilla::Product->check({name => $product_name}); + unless (grep { $product->name eq $_->name } @products) { + ThrowUserError('product_access_denied', {name => $product_name}); } - elsif(!ref($item)) { - # These are whole products - my $product = Bugzilla::Product->check({name => $item}); - unless (grep { $product->name eq $_->name } @products) { - ThrowUserError('product_access_denied', { name => $item }); - } - push @component_list, $product->id . ':0'; - } - else { - # The user has passed something invalid - ThrowCodeError('param_invalid', { param => $item }); + my @component_ids; + + foreach my $comp_name (@$component_names) { + my $component + = Bugzilla::Component->check({product => $product, name => $comp_name}); + ThrowCodeError('param_invalid', {param => $comp_name}) + unless defined $component; + push @component_list, $product->id . ':' . $component->id; } + } + } + elsif (!ref($item)) { + + # These are whole products + my $product = Bugzilla::Product->check({name => $item}); + unless (grep { $product->name eq $_->name } @products) { + ThrowUserError('product_access_denied', {name => $item}); + } + push @component_list, $product->id . ':0'; + } + else { + # The user has passed something invalid + ThrowCodeError('param_invalid', {param => $item}); } + } - return \@component_list; + return \@component_list; } 1; diff --git a/Bugzilla/WebService/Group.pm b/Bugzilla/WebService/Group.pm index 468575a35..c92583d0b 100644 --- a/Bugzilla/WebService/Group.pm +++ b/Bugzilla/WebService/Group.pm @@ -17,207 +17,210 @@ use Bugzilla::Error; use Bugzilla::WebService::Util qw(validate translate params_to_objects); use constant PUBLIC_METHODS => qw( - create - get - update + create + get + update ); -use constant MAPPED_RETURNS => { - userregexp => 'user_regexp', - isactive => 'is_active' -}; +use constant MAPPED_RETURNS => + {userregexp => 'user_regexp', isactive => 'is_active'}; sub create { - my ($self, $params) = @_; - - Bugzilla->login(LOGIN_REQUIRED); - Bugzilla->user->in_group('creategroups') - || ThrowUserError("auth_failure", { group => "creategroups", - action => "add", - object => "group"}); - # Create group - my $group = Bugzilla::Group->create({ - name => $params->{name}, - description => $params->{description}, - userregexp => $params->{user_regexp}, - isactive => $params->{is_active}, - isbuggroup => 1, - icon_url => $params->{icon_url} - }); - return { id => $self->type('int', $group->id) }; + my ($self, $params) = @_; + + Bugzilla->login(LOGIN_REQUIRED); + Bugzilla->user->in_group('creategroups') + || ThrowUserError("auth_failure", + {group => "creategroups", action => "add", object => "group"}); + + # Create group + my $group = Bugzilla::Group->create({ + name => $params->{name}, + description => $params->{description}, + userregexp => $params->{user_regexp}, + isactive => $params->{is_active}, + isbuggroup => 1, + icon_url => $params->{icon_url} + }); + return {id => $self->type('int', $group->id)}; } sub update { - my ($self, $params) = @_; + my ($self, $params) = @_; + + my $dbh = Bugzilla->dbh; + + Bugzilla->login(LOGIN_REQUIRED); + Bugzilla->user->in_group('creategroups') + || ThrowUserError("auth_failure", + {group => "creategroups", action => "edit", object => "group"}); + + defined($params->{names}) + || defined($params->{ids}) + || ThrowCodeError('params_required', + {function => 'Group.update', params => ['ids', 'names']}); + + my $group_objects = params_to_objects($params, 'Bugzilla::Group'); + + my %values = %$params; + + # We delete names and ids to keep only new values to set. + delete $values{names}; + delete $values{ids}; + + $dbh->bz_start_transaction(); + foreach my $group (@$group_objects) { + $group->set_all(\%values); + } + + my %changes; + foreach my $group (@$group_objects) { + my $returned_changes = $group->update(); + $changes{$group->id} = translate($returned_changes, MAPPED_RETURNS); + } + $dbh->bz_commit_transaction(); + + my @result; + foreach my $group (@$group_objects) { + my %hash = (id => $group->id, changes => {},); + foreach my $field (keys %{$changes{$group->id}}) { + my $change = $changes{$group->id}->{$field}; + $hash{changes}{$field} = { + removed => $self->type('string', $change->[0]), + added => $self->type('string', $change->[1]) + }; + } + push(@result, \%hash); + } - my $dbh = Bugzilla->dbh; + return {groups => \@result}; +} - Bugzilla->login(LOGIN_REQUIRED); - Bugzilla->user->in_group('creategroups') - || ThrowUserError("auth_failure", { group => "creategroups", - action => "edit", - object => "group" }); +sub get { + my ($self, $params) = validate(@_, 'ids', 'names', 'type'); - defined($params->{names}) || defined($params->{ids}) - || ThrowCodeError('params_required', - { function => 'Group.update', params => ['ids', 'names'] }); + Bugzilla->login(LOGIN_REQUIRED); - my $group_objects = params_to_objects($params, 'Bugzilla::Group'); + # Reject access if there is no sense in continuing. + my $user = Bugzilla->user; + my $all_groups + = $user->in_group('editusers') || $user->in_group('creategroups'); + if (!$all_groups && !$user->can_bless) { + ThrowUserError('group_cannot_view'); + } - my %values = %$params; - - # We delete names and ids to keep only new values to set. - delete $values{names}; - delete $values{ids}; + Bugzilla->switch_to_shadow_db(); - $dbh->bz_start_transaction(); - foreach my $group (@$group_objects) { - $group->set_all(\%values); - } + my $groups = []; - my %changes; - foreach my $group (@$group_objects) { - my $returned_changes = $group->update(); - $changes{$group->id} = translate($returned_changes, MAPPED_RETURNS); - } - $dbh->bz_commit_transaction(); - - my @result; - foreach my $group (@$group_objects) { - my %hash = ( - id => $group->id, - changes => {}, - ); - foreach my $field (keys %{ $changes{$group->id} }) { - my $change = $changes{$group->id}->{$field}; - $hash{changes}{$field} = { - removed => $self->type('string', $change->[0]), - added => $self->type('string', $change->[1]) - }; - } - push(@result, \%hash); - } + if (defined $params->{ids}) { - return { groups => \@result }; -} - -sub get { - my ($self, $params) = validate(@_, 'ids', 'names', 'type'); + # Get the groups by id + $groups = Bugzilla::Group->new_from_list($params->{ids}); + } - Bugzilla->login(LOGIN_REQUIRED); - - # Reject access if there is no sense in continuing. - my $user = Bugzilla->user; - my $all_groups = $user->in_group('editusers') || $user->in_group('creategroups'); - if (!$all_groups && !$user->can_bless) { - ThrowUserError('group_cannot_view'); - } + if (defined $params->{names}) { - Bugzilla->switch_to_shadow_db(); + # Get the groups by name. Check will throw an error if a bad name is given + foreach my $name (@{$params->{names}}) { - my $groups = []; + # Skip if we got this from params->{id} + next if grep { $_->name eq $name } @$groups; - if (defined $params->{ids}) { - # Get the groups by id - $groups = Bugzilla::Group->new_from_list($params->{ids}); + push @$groups, Bugzilla::Group->check({name => $name}); } + } - if (defined $params->{names}) { - # Get the groups by name. Check will throw an error if a bad name is given - foreach my $name (@{$params->{names}}) { - # Skip if we got this from params->{id} - next if grep { $_->name eq $name } @$groups; - - push @$groups, Bugzilla::Group->check({ name => $name }); - } + if (!defined $params->{ids} && !defined $params->{names}) { + if ($all_groups) { + @$groups = Bugzilla::Group->get_all; } - - if (!defined $params->{ids} && !defined $params->{names}) { - if ($all_groups) { - @$groups = Bugzilla::Group->get_all; - } - else { - # Get only groups the user has bless groups too - $groups = $user->bless_groups; - } + else { + # Get only groups the user has bless groups too + $groups = $user->bless_groups; } + } - # Now create a result entry for each. - my @groups = map { $self->_group_to_hash($params, $_) } @$groups; - return { groups => \@groups }; + # Now create a result entry for each. + my @groups = map { $self->_group_to_hash($params, $_) } @$groups; + return {groups => \@groups}; } sub _group_to_hash { - my ($self, $params, $group) = @_; - my $user = Bugzilla->user; - - my $field_data = { - id => $self->type('int', $group->id), - name => $self->type('string', $group->name), - description => $self->type('string', $group->description), - }; - - if ($user->in_group('creategroups')) { - $field_data->{is_active} = $self->type('boolean', $group->is_active); - $field_data->{is_bug_group} = $self->type('boolean', $group->is_bug_group); - $field_data->{user_regexp} = $self->type('string', $group->user_regexp); - } - - if ($params->{membership}) { - $field_data->{membership} = $self->_get_group_membership($group, $params); - } - return $field_data; + my ($self, $params, $group) = @_; + my $user = Bugzilla->user; + + my $field_data = { + id => $self->type('int', $group->id), + name => $self->type('string', $group->name), + description => $self->type('string', $group->description), + }; + + if ($user->in_group('creategroups')) { + $field_data->{is_active} = $self->type('boolean', $group->is_active); + $field_data->{is_bug_group} = $self->type('boolean', $group->is_bug_group); + $field_data->{user_regexp} = $self->type('string', $group->user_regexp); + } + + if ($params->{membership}) { + $field_data->{membership} = $self->_get_group_membership($group, $params); + } + return $field_data; } sub _get_group_membership { - my ($self, $group, $params) = @_; - my $user = Bugzilla->user; + my ($self, $group, $params) = @_; + my $user = Bugzilla->user; - my %users_only; - my $dbh = Bugzilla->dbh; - my $editusers = $user->in_group('editusers'); + my %users_only; + my $dbh = Bugzilla->dbh; + my $editusers = $user->in_group('editusers'); - my $query = 'SELECT userid FROM profiles'; - my $visibleGroups; + my $query = 'SELECT userid FROM profiles'; + my $visibleGroups; - if (!$editusers && Bugzilla->params->{'usevisibilitygroups'}) { - # Show only users in visible groups. - $visibleGroups = $user->visible_groups_inherited; + if (!$editusers && Bugzilla->params->{'usevisibilitygroups'}) { - if (scalar @$visibleGroups) { - $query .= qq{, user_group_map AS ugm + # Show only users in visible groups. + $visibleGroups = $user->visible_groups_inherited; + + if (scalar @$visibleGroups) { + $query .= qq{, user_group_map AS ugm WHERE ugm.user_id = profiles.userid AND ugm.isbless = 0 AND } . $dbh->sql_in('ugm.group_id', $visibleGroups); - } - } elsif ($editusers || $user->can_bless($group->id) || $user->in_group('creategroups')) { - $visibleGroups = 1; - $query .= qq{, user_group_map AS ugm + } + } + elsif ($editusers + || $user->can_bless($group->id) + || $user->in_group('creategroups')) + { + $visibleGroups = 1; + $query .= qq{, user_group_map AS ugm WHERE ugm.user_id = profiles.userid AND ugm.isbless = 0 }; - } - if (!$visibleGroups) { - ThrowUserError('group_not_visible', { group => $group }); - } - - my $grouplist = Bugzilla::Group->flatten_group_membership($group->id); - $query .= ' AND ' . $dbh->sql_in('ugm.group_id', $grouplist); - - my $userids = $dbh->selectcol_arrayref($query); - my $user_objects = Bugzilla::User->new_from_list($userids); - my @users = - map {{ - id => $self->type('int', $_->id), - real_name => $self->type('string', $_->name), - name => $self->type('string', $_->login), - email => $self->type('string', $_->email), - can_login => $self->type('boolean', $_->is_enabled), - email_enabled => $self->type('boolean', $_->email_enabled), - login_denied_text => $self->type('string', $_->disabledtext), - }} @$user_objects; - - return \@users; + } + if (!$visibleGroups) { + ThrowUserError('group_not_visible', {group => $group}); + } + + my $grouplist = Bugzilla::Group->flatten_group_membership($group->id); + $query .= ' AND ' . $dbh->sql_in('ugm.group_id', $grouplist); + + my $userids = $dbh->selectcol_arrayref($query); + my $user_objects = Bugzilla::User->new_from_list($userids); + my @users = map { { + id => $self->type('int', $_->id), + real_name => $self->type('string', $_->name), + name => $self->type('string', $_->login), + email => $self->type('string', $_->email), + can_login => $self->type('boolean', $_->is_enabled), + email_enabled => $self->type('boolean', $_->email_enabled), + login_denied_text => $self->type('string', $_->disabledtext), + } } @$user_objects; + + return \@users; } 1; diff --git a/Bugzilla/WebService/Product.pm b/Bugzilla/WebService/Product.pm index 7d9e7f181..e0f357e61 100644 --- a/Bugzilla/WebService/Product.pm +++ b/Bugzilla/WebService/Product.pm @@ -17,40 +17,37 @@ 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_products - get_selectable_products - update + create + get + get_accessible_products + get_enterable_products + get_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 # @@ -58,300 +55,277 @@ use constant FIELD_MAP => { # Get the ids of the products the user can search sub get_selectable_products { - Bugzilla->switch_to_shadow_db(); - return {ids => [map {$_->id} @{Bugzilla->user->get_selectable_products}]}; + Bugzilla->switch_to_shadow_db(); + return {ids => [map { $_->id } @{Bugzilla->user->get_selectable_products}]}; } # Get the ids of the products the user can enter bugs against sub get_enterable_products { - Bugzilla->switch_to_shadow_db(); - return {ids => [map {$_->id} @{Bugzilla->user->get_enterable_products}]}; + Bugzilla->switch_to_shadow_db(); + return {ids => [map { $_->id } @{Bugzilla->user->get_enterable_products}]}; } # Get the union of the products the user can search and enter bugs against. sub get_accessible_products { - Bugzilla->switch_to_shadow_db(); - return {ids => [map {$_->id} @{Bugzilla->user->get_accessible_products}]}; + Bugzilla->switch_to_shadow_db(); + return {ids => [map { $_->id } @{Bugzilla->user->get_accessible_products}]}; } # Get a list of actual products, based on list of ids or names sub get { - my ($self, $params) = validate(@_, 'ids', 'names', 'type'); - my $user = Bugzilla->user; - - defined $params->{ids} || defined $params->{names} || defined $params->{type} - || ThrowCodeError("params_required", { function => "Product.get", - params => ['ids', 'names', 'type'] }); - Bugzilla->switch_to_shadow_db(); - - my $products = []; - if (defined $params->{type}) { - my %product_hash; - foreach my $type (@{ $params->{type} }) { - my $result = []; - if ($type eq 'accessible') { - $result = $user->get_accessible_products(); - } - elsif ($type eq 'enterable') { - $result = $user->get_enterable_products(); - } - elsif ($type eq 'selectable') { - $result = $user->get_selectable_products(); - } - else { - ThrowUserError('get_products_invalid_type', - { type => $type }); - } - map { $product_hash{$_->id} = $_ } @$result; - } - $products = [ values %product_hash ]; - } - else { - $products = $user->get_accessible_products; + my ($self, $params) = validate(@_, 'ids', 'names', 'type'); + my $user = Bugzilla->user; + + defined $params->{ids} + || defined $params->{names} + || defined $params->{type} + || ThrowCodeError("params_required", + {function => "Product.get", params => ['ids', 'names', 'type']}); + Bugzilla->switch_to_shadow_db(); + + my $products = []; + if (defined $params->{type}) { + my %product_hash; + foreach my $type (@{$params->{type}}) { + my $result = []; + if ($type eq 'accessible') { + $result = $user->get_accessible_products(); + } + elsif ($type eq 'enterable') { + $result = $user->get_enterable_products(); + } + elsif ($type eq 'selectable') { + $result = $user->get_selectable_products(); + } + else { + ThrowUserError('get_products_invalid_type', {type => $type}); + } + map { $product_hash{$_->id} = $_ } @$result; } + $products = [values %product_hash]; + } + else { + $products = $user->get_accessible_products; + } - my @requested_products; + my @requested_products; - if (defined $params->{ids}) { - # Create a hash with the ids the user wants - my %ids = map { $_ => 1 } @{$params->{ids}}; + if (defined $params->{ids}) { - # Return the intersection of this, by grepping the ids from $products. - push(@requested_products, - grep { $ids{$_->id} } @$products); - } + # Create a hash with the ids the user wants + my %ids = map { $_ => 1 } @{$params->{ids}}; - if (defined $params->{names}) { - # Create a hash with the names the user wants - my %names = map { lc($_) => 1 } @{$params->{names}}; - - # Return the intersection of this, by grepping the names - # from $products, union'ed with products found by ID to - # avoid duplicates - foreach my $product (grep { $names{lc $_->name} } - @$products) { - next if grep { $_->id == $product->id } - @requested_products; - push @requested_products, $product; - } - } + # Return the intersection of this, by grepping the ids from $products. + push(@requested_products, grep { $ids{$_->id} } @$products); + } + + if (defined $params->{names}) { - # If we just requested a specific type of products without - # specifying ids or names, then return the entire list. - if (!defined $params->{ids} && !defined $params->{names}) { - @requested_products = @$products; + # Create a hash with the names the user wants + my %names = map { lc($_) => 1 } @{$params->{names}}; + + # Return the intersection of this, by grepping the names + # from $products, union'ed with products found by ID to + # avoid duplicates + foreach my $product (grep { $names{lc $_->name} } @$products) { + next if grep { $_->id == $product->id } @requested_products; + push @requested_products, $product; } + } + + # If we just requested a specific type of products without + # specifying ids or names, then return the entire list. + if (!defined $params->{ids} && !defined $params->{names}) { + @requested_products = @$products; + } - # Now create a result entry for each. - my @products = map { $self->_product_to_hash($params, $_) } - @requested_products; - return { products => \@products }; + # Now create a result entry for each. + my @products = map { $self->_product_to_hash($params, $_) } @requested_products; + return {products => \@products}; } sub create { - my ($self, $params) = @_; - - Bugzilla->login(LOGIN_REQUIRED); - Bugzilla->user->in_group('editcomponents') - || ThrowUserError("auth_failure", { group => "editcomponents", - action => "add", - object => "products"}); - # Create product - my $args = { - name => $params->{name}, - description => $params->{description}, - version => $params->{version}, - defaultmilestone => $params->{default_milestone}, - # create_series has no default value. - create_series => defined $params->{create_series} ? - $params->{create_series} : 1 - }; - foreach my $field (qw(has_unconfirmed is_open classification)) { - if (defined $params->{$field}) { - my $name = FIELD_MAP->{$field} || $field; - $args->{$name} = $params->{$field}; - } + my ($self, $params) = @_; + + Bugzilla->login(LOGIN_REQUIRED); + Bugzilla->user->in_group('editcomponents') + || ThrowUserError("auth_failure", + {group => "editcomponents", action => "add", object => "products"}); + + # Create product + my $args = { + name => $params->{name}, + description => $params->{description}, + version => $params->{version}, + defaultmilestone => $params->{default_milestone}, + + # create_series has no default value. + create_series => defined $params->{create_series} + ? $params->{create_series} + : 1 + }; + foreach my $field (qw(has_unconfirmed is_open classification)) { + if (defined $params->{$field}) { + my $name = FIELD_MAP->{$field} || $field; + $args->{$name} = $params->{$field}; } - my $product = Bugzilla::Product->create($args); - return { id => $self->type('int', $product->id) }; + } + my $product = Bugzilla::Product->create($args); + return {id => $self->type('int', $product->id)}; } sub update { - my ($self, $params) = @_; - - my $dbh = Bugzilla->dbh; - - Bugzilla->login(LOGIN_REQUIRED); - Bugzilla->user->in_group('editcomponents') - || ThrowUserError("auth_failure", { group => "editcomponents", - action => "edit", - object => "products" }); - - defined($params->{names}) || defined($params->{ids}) - || ThrowCodeError('params_required', - { function => 'Product.update', params => ['ids', 'names'] }); - - my $product_objects = params_to_objects($params, 'Bugzilla::Product'); - - my $values = translate($params, MAPPED_FIELDS); - - # We delete names and ids to keep only new values to set. - delete $values->{names}; - delete $values->{ids}; - - $dbh->bz_start_transaction(); - foreach my $product (@$product_objects) { - $product->set_all($values); + my ($self, $params) = @_; + + my $dbh = Bugzilla->dbh; + + Bugzilla->login(LOGIN_REQUIRED); + Bugzilla->user->in_group('editcomponents') + || ThrowUserError("auth_failure", + {group => "editcomponents", action => "edit", object => "products"}); + + defined($params->{names}) + || defined($params->{ids}) + || ThrowCodeError('params_required', + {function => 'Product.update', params => ['ids', 'names']}); + + my $product_objects = params_to_objects($params, 'Bugzilla::Product'); + + my $values = translate($params, MAPPED_FIELDS); + + # We delete names and ids to keep only new values to set. + delete $values->{names}; + delete $values->{ids}; + + $dbh->bz_start_transaction(); + foreach my $product (@$product_objects) { + $product->set_all($values); + } + + my %changes; + foreach my $product (@$product_objects) { + my $returned_changes = $product->update(); + $changes{$product->id} = translate($returned_changes, MAPPED_RETURNS); + } + $dbh->bz_commit_transaction(); + + my @result; + foreach my $product (@$product_objects) { + my %hash = (id => $product->id, changes => {},); + + foreach my $field (keys %{$changes{$product->id}}) { + my $change = $changes{$product->id}->{$field}; + $hash{changes}{$field} = { + removed => $self->type('string', $change->[0]), + added => $self->type('string', $change->[1]) + }; } - my %changes; - foreach my $product (@$product_objects) { - my $returned_changes = $product->update(); - $changes{$product->id} = translate($returned_changes, MAPPED_RETURNS); - } - $dbh->bz_commit_transaction(); - - my @result; - foreach my $product (@$product_objects) { - my %hash = ( - id => $product->id, - changes => {}, - ); - - foreach my $field (keys %{ $changes{$product->id} }) { - my $change = $changes{$product->id}->{$field}; - $hash{changes}{$field} = { - removed => $self->type('string', $change->[0]), - added => $self->type('string', $change->[1]) - }; - } - - push(@result, \%hash); - } + push(@result, \%hash); + } - return { products => \@result }; + return {products => \@result}; } sub _product_to_hash { - my ($self, $params, $product) = @_; - - my $field_data = { - id => $self->type('int', $product->id), - name => $self->type('string', $product->name), - description => $self->type('string', $product->description), - is_active => $self->type('boolean', $product->is_active), - default_milestone => $self->type('string', $product->default_milestone), - has_unconfirmed => $self->type('boolean', $product->allows_unconfirmed), - classification => $self->type('string', $product->classification->name), - }; - if (filter_wants($params, 'components')) { - $field_data->{components} = [map { - $self->_component_to_hash($_, $params) - } @{$product->components}]; - } - if (filter_wants($params, 'versions')) { - $field_data->{versions} = [map { - $self->_version_to_hash($_, $params) - } @{$product->versions}]; - } - if (filter_wants($params, 'milestones')) { - $field_data->{milestones} = [map { - $self->_milestone_to_hash($_, $params) - } @{$product->milestones}]; - } - return filter($params, $field_data); + my ($self, $params, $product) = @_; + + my $field_data = { + id => $self->type('int', $product->id), + name => $self->type('string', $product->name), + description => $self->type('string', $product->description), + is_active => $self->type('boolean', $product->is_active), + default_milestone => $self->type('string', $product->default_milestone), + has_unconfirmed => $self->type('boolean', $product->allows_unconfirmed), + classification => $self->type('string', $product->classification->name), + }; + if (filter_wants($params, 'components')) { + $field_data->{components} + = [map { $self->_component_to_hash($_, $params) } @{$product->components}]; + } + if (filter_wants($params, 'versions')) { + $field_data->{versions} + = [map { $self->_version_to_hash($_, $params) } @{$product->versions}]; + } + if (filter_wants($params, 'milestones')) { + $field_data->{milestones} + = [map { $self->_milestone_to_hash($_, $params) } @{$product->milestones}]; + } + return filter($params, $field_data); } sub _component_to_hash { - my ($self, $component, $params) = @_; - my $field_data = filter $params, { - id => - $self->type('int', $component->id), - name => - $self->type('string', $component->name), - description => - $self->type('string' , $component->description), - default_assigned_to => - $self->type('email', $component->default_assignee->login), - default_qa_contact => - $self->type('email', $component->default_qa_contact ? - $component->default_qa_contact->login : ""), - sort_key => # sort_key is returned to match Bug.fields - 0, - is_active => - $self->type('boolean', $component->is_active), - }, undef, 'components'; - - if (filter_wants($params, 'flag_types', undef, 'components')) { - $field_data->{flag_types} = { - bug => - [map { - $self->_flag_type_to_hash($_) - } @{$component->flag_types->{'bug'}}], - attachment => - [map { - $self->_flag_type_to_hash($_) - } @{$component->flag_types->{'attachment'}}], - }; - } + my ($self, $component, $params) = @_; + my $field_data = filter $params, { + id => $self->type('int', $component->id), + name => $self->type('string', $component->name), + description => $self->type('string', $component->description), + default_assigned_to => + $self->type('email', $component->default_assignee->login), + default_qa_contact => $self->type( + 'email', + $component->default_qa_contact ? $component->default_qa_contact->login : "" + ), + sort_key => # sort_key is returned to match Bug.fields + 0, + is_active => $self->type('boolean', $component->is_active), + }, + undef, 'components'; + + if (filter_wants($params, 'flag_types', undef, 'components')) { + $field_data->{flag_types} = { + bug => + [map { $self->_flag_type_to_hash($_) } @{$component->flag_types->{'bug'}}], + attachment => [ + map { $self->_flag_type_to_hash($_) } @{$component->flag_types->{'attachment'}} + ], + }; + } - return $field_data; + return $field_data; } sub _flag_type_to_hash { - my ($self, $flag_type, $params) = @_; - return filter $params, { - id => - $self->type('int', $flag_type->id), - name => - $self->type('string', $flag_type->name), - description => - $self->type('string', $flag_type->description), - cc_list => - $self->type('string', $flag_type->cc_list), - sort_key => - $self->type('int', $flag_type->sortkey), - is_active => - $self->type('boolean', $flag_type->is_active), - is_requestable => - $self->type('boolean', $flag_type->is_requestable), - is_requesteeble => - $self->type('boolean', $flag_type->is_requesteeble), - is_multiplicable => - $self->type('boolean', $flag_type->is_multiplicable), - grant_group => - $self->type('int', $flag_type->grant_group_id), - request_group => - $self->type('int', $flag_type->request_group_id), - }, undef, 'flag_types'; + my ($self, $flag_type, $params) = @_; + return filter $params, + { + id => $self->type('int', $flag_type->id), + name => $self->type('string', $flag_type->name), + description => $self->type('string', $flag_type->description), + cc_list => $self->type('string', $flag_type->cc_list), + sort_key => $self->type('int', $flag_type->sortkey), + is_active => $self->type('boolean', $flag_type->is_active), + is_requestable => $self->type('boolean', $flag_type->is_requestable), + is_requesteeble => $self->type('boolean', $flag_type->is_requesteeble), + is_multiplicable => $self->type('boolean', $flag_type->is_multiplicable), + grant_group => $self->type('int', $flag_type->grant_group_id), + request_group => $self->type('int', $flag_type->request_group_id), + }, + undef, 'flag_types'; } sub _version_to_hash { - my ($self, $version, $params) = @_; - return filter $params, { - id => - $self->type('int', $version->id), - name => - $self->type('string', $version->name), - sort_key => # sort_key is returened to match Bug.fields - 0, - is_active => - $self->type('boolean', $version->is_active), - }, undef, 'versions'; + my ($self, $version, $params) = @_; + return filter $params, { + id => $self->type('int', $version->id), + name => $self->type('string', $version->name), + sort_key => # sort_key is returened to match Bug.fields + 0, + is_active => $self->type('boolean', $version->is_active), + }, + undef, 'versions'; } sub _milestone_to_hash { - my ($self, $milestone, $params) = @_; - return filter $params, { - id => - $self->type('int', $milestone->id), - name => - $self->type('string', $milestone->name), - sort_key => - $self->type('int', $milestone->sortkey), - is_active => - $self->type('boolean', $milestone->is_active), - }, undef, 'milestones'; + my ($self, $milestone, $params) = @_; + return filter $params, + { + id => $self->type('int', $milestone->id), + name => $self->type('string', $milestone->name), + sort_key => $self->type('int', $milestone->sortkey), + is_active => $self->type('boolean', $milestone->is_active), + }, + undef, 'milestones'; } 1; diff --git a/Bugzilla/WebService/Server.pm b/Bugzilla/WebService/Server.pm index 7950c7a3b..f2844cdcb 100644 --- a/Bugzilla/WebService/Server.pm +++ b/Bugzilla/WebService/Server.pm @@ -20,72 +20,77 @@ use Digest::MD5 qw(md5_base64); use Storable qw(freeze); sub handle_login { - my ($self, $class, $method, $full_method) = @_; - # Throw error if the supplied class does not exist or the method is private - ThrowCodeError('unknown_method', {method => $full_method}) if (!$class or $method =~ /^_/); - - eval "require $class"; - ThrowCodeError('unknown_method', {method => $full_method}) if $@; - return if ($class->login_exempt($method) - and !defined Bugzilla->input_params->{Bugzilla_login}); - Bugzilla->login(); - - Bugzilla::Hook::process( - 'webservice_before_call', - { 'method' => $method, full_method => $full_method }); + my ($self, $class, $method, $full_method) = @_; + + # Throw error if the supplied class does not exist or the method is private + ThrowCodeError('unknown_method', {method => $full_method}) + if (!$class or $method =~ /^_/); + + eval "require $class"; + ThrowCodeError('unknown_method', {method => $full_method}) if $@; + return + if ($class->login_exempt($method) + and !defined Bugzilla->input_params->{Bugzilla_login}); + Bugzilla->login(); + + Bugzilla::Hook::process('webservice_before_call', + {'method' => $method, full_method => $full_method}); } sub datetime_format_inbound { - my ($self, $time) = @_; - - my $converted = datetime_from($time, Bugzilla->local_timezone); - if (!defined $converted) { - ThrowUserError('illegal_date', { date => $time }); - } - $time = $converted->ymd() . ' ' . $converted->hms(); - return $time + my ($self, $time) = @_; + + my $converted = datetime_from($time, Bugzilla->local_timezone); + if (!defined $converted) { + ThrowUserError('illegal_date', {date => $time}); + } + $time = $converted->ymd() . ' ' . $converted->hms(); + return $time; } sub datetime_format_outbound { - my ($self, $date) = @_; - - return undef if (!defined $date or $date eq ''); - - my $time = $date; - if (blessed($date)) { - # We expect this to mean we were sent a datetime object - $time->set_time_zone('UTC'); - } else { - # We always send our time in UTC, for consistency. - # passed in value is likely a string, create a datetime object - $time = datetime_from($date, 'UTC'); - } - return $time->iso8601(); + my ($self, $date) = @_; + + return undef if (!defined $date or $date eq ''); + + my $time = $date; + if (blessed($date)) { + + # We expect this to mean we were sent a datetime object + $time->set_time_zone('UTC'); + } + else { + # We always send our time in UTC, for consistency. + # passed in value is likely a string, create a datetime object + $time = datetime_from($date, 'UTC'); + } + return $time->iso8601(); } # ETag support sub bz_etag { - my ($self, $data) = @_; - my $cache = Bugzilla->request_cache; - if (defined $data) { - # Serialize the data if passed a reference - local $Storable::canonical = 1; - $data = freeze($data) if ref $data; - - # Wide characters cause md5_base64() to die. - utf8::encode($data) if utf8::is_utf8($data); - - # Append content_type to the end of the data - # string as we want the etag to be unique to - # the content_type. We do not need this for - # XMLRPC as text/xml is always returned. - if (blessed($self) && $self->can('content_type')) { - $data .= $self->content_type if $self->content_type; - } - - $cache->{'bz_etag'} = md5_base64($data); + my ($self, $data) = @_; + my $cache = Bugzilla->request_cache; + if (defined $data) { + + # Serialize the data if passed a reference + local $Storable::canonical = 1; + $data = freeze($data) if ref $data; + + # Wide characters cause md5_base64() to die. + utf8::encode($data) if utf8::is_utf8($data); + + # Append content_type to the end of the data + # string as we want the etag to be unique to + # the content_type. We do not need this for + # XMLRPC as text/xml is always returned. + if (blessed($self) && $self->can('content_type')) { + $data .= $self->content_type if $self->content_type; } - return $cache->{'bz_etag'}; + + $cache->{'bz_etag'} = md5_base64($data); + } + return $cache->{'bz_etag'}; } 1; diff --git a/Bugzilla/WebService/Server/JSONRPC.pm b/Bugzilla/WebService/Server/JSONRPC.pm index 70b8fd96c..66640beb7 100644 --- a/Bugzilla/WebService/Server/JSONRPC.pm +++ b/Bugzilla/WebService/Server/JSONRPC.pm @@ -12,16 +12,17 @@ use strict; use warnings; use Bugzilla::WebService::Server; -BEGIN { - our @ISA = qw(Bugzilla::WebService::Server); - if (eval { require JSON::RPC::Server::CGI }) { - unshift(@ISA, 'JSON::RPC::Server::CGI'); - } - else { - require JSON::RPC::Legacy::Server::CGI; - unshift(@ISA, 'JSON::RPC::Legacy::Server::CGI'); - } +BEGIN { + our @ISA = qw(Bugzilla::WebService::Server); + + if (eval { require JSON::RPC::Server::CGI }) { + unshift(@ISA, 'JSON::RPC::Server::CGI'); + } + else { + require JSON::RPC::Legacy::Server::CGI; + unshift(@ISA, 'JSON::RPC::Legacy::Server::CGI'); + } } use Bugzilla::Error; @@ -38,79 +39,83 @@ use List::MoreUtils qw(none); ##################################### sub new { - my $class = shift; - my $self = $class->SUPER::new(@_); - Bugzilla->_json_server($self); - $self->dispatch(WS_DISPATCH); - $self->return_die_message(1); - return $self; + my $class = shift; + my $self = $class->SUPER::new(@_); + Bugzilla->_json_server($self); + $self->dispatch(WS_DISPATCH); + $self->return_die_message(1); + return $self; } sub create_json_coder { - my $self = shift; - my $json = $self->SUPER::create_json_coder(@_); - $json->allow_blessed(1); - $json->convert_blessed(1); - # This may seem a little backwards, but what this really means is - # "don't convert our utf8 into byte strings, just leave it as a - # utf8 string." - $json->utf8(0) if Bugzilla->params->{'utf8'}; - return $json; + my $self = shift; + my $json = $self->SUPER::create_json_coder(@_); + $json->allow_blessed(1); + $json->convert_blessed(1); + + # This may seem a little backwards, but what this really means is + # "don't convert our utf8 into byte strings, just leave it as a + # utf8 string." + $json->utf8(0) if Bugzilla->params->{'utf8'}; + return $json; } # Override the JSON::RPC method to return our CGI object instead of theirs. sub cgi { return Bugzilla->cgi; } sub response_header { - my $self = shift; - # The HTTP body needs to be bytes (not a utf8 string) for recent - # versions of HTTP::Message, but JSON::RPC::Server doesn't handle this - # properly. $_[1] is the HTTP body content we're going to be sending. - if (utf8::is_utf8($_[1])) { - utf8::encode($_[1]); - # Since we're going to just be sending raw bytes, we need to - # set STDOUT to not expect utf8. - disable_utf8(); - } - return $self->SUPER::response_header(@_); + my $self = shift; + + # The HTTP body needs to be bytes (not a utf8 string) for recent + # versions of HTTP::Message, but JSON::RPC::Server doesn't handle this + # properly. $_[1] is the HTTP body content we're going to be sending. + if (utf8::is_utf8($_[1])) { + utf8::encode($_[1]); + + # Since we're going to just be sending raw bytes, we need to + # set STDOUT to not expect utf8. + disable_utf8(); + } + return $self->SUPER::response_header(@_); } sub response { - my ($self, $response) = @_; - my $cgi = $self->cgi; - - # Implement JSONP. - if (my $callback = $self->_bz_callback) { - my $content = $response->content; - # Prepend the JSONP response with /**/ in order to protect - # against possible encoding attacks (e.g., affecting Flash). - $response->content("/**/$callback($content)"); - } - - # Use $cgi->header properly instead of just printing text directly. - # This fixes various problems, including sending Bugzilla's cookies - # properly. - my $headers = $response->headers; - my @header_args; - foreach my $name ($headers->header_field_names) { - my @values = $headers->header($name); - $name =~ s/-/_/g; - foreach my $value (@values) { - push(@header_args, "-$name", $value); - } - } - - # ETag support - my $etag = $self->bz_etag; - if ($etag && $cgi->check_etag($etag)) { - push(@header_args, "-ETag", $etag); - print $cgi->header(-status => '304 Not Modified', @header_args); - } - else { - push(@header_args, "-ETag", $etag) if $etag; - print $cgi->header(-status => $response->code, @header_args); - print $response->content; - } + my ($self, $response) = @_; + my $cgi = $self->cgi; + + # Implement JSONP. + if (my $callback = $self->_bz_callback) { + my $content = $response->content; + + # Prepend the JSONP response with /**/ in order to protect + # against possible encoding attacks (e.g., affecting Flash). + $response->content("/**/$callback($content)"); + } + + # Use $cgi->header properly instead of just printing text directly. + # This fixes various problems, including sending Bugzilla's cookies + # properly. + my $headers = $response->headers; + my @header_args; + foreach my $name ($headers->header_field_names) { + my @values = $headers->header($name); + $name =~ s/-/_/g; + foreach my $value (@values) { + push(@header_args, "-$name", $value); + } + } + + # ETag support + my $etag = $self->bz_etag; + if ($etag && $cgi->check_etag($etag)) { + push(@header_args, "-ETag", $etag); + print $cgi->header(-status => '304 Not Modified', @header_args); + } + else { + push(@header_args, "-ETag", $etag) if $etag; + print $cgi->header(-status => $response->code, @header_args); + print $response->content; + } } # The JSON-RPC 1.1 GET specification is not so great--you can't specify @@ -122,70 +127,69 @@ sub response { # Base64 encoded, because that is ridiculous and obnoxious for JavaScript # clients. sub retrieve_json_from_get { - my $self = shift; - my $cgi = $self->cgi; - - my %input; - - # Both version and id must be set before any errors are thrown. - if ($cgi->param('version')) { - $self->version(scalar $cgi->param('version')); - $input{version} = $cgi->param('version'); - } - else { - $self->version('1.0'); - } - - # The JSON-RPC 2.0 spec says that any request that omits an id doesn't - # want a response. However, in an HTTP GET situation, it's stupid to - # expect all clients to specify some id parameter just to get a response, - # so we don't require it. - my $id; - if (defined $cgi->param('id')) { - $id = $cgi->param('id'); - } - # However, JSON::RPC does require that an id exist in most cases, in - # order to throw proper errors. We use the installation's urlbase as - # the id, in this case. - else { - $id = correct_urlbase(); - } - # Setting _bz_request_id here is required in case we throw errors early, - # before _handle. - $self->{_bz_request_id} = $input{id} = $id; - - # _bz_callback can throw an error, so we have to set it here, after we're - # ready to throw errors. - $self->_bz_callback(scalar $cgi->param('callback')); - - if (!$cgi->param('method')) { - ThrowUserError('json_rpc_get_method_required'); - } - $input{method} = $cgi->param('method'); - - my $params; - if (defined $cgi->param('params')) { - local $@; - $params = eval { - $self->json->decode(scalar $cgi->param('params')) - }; - if ($@) { - ThrowUserError('json_rpc_invalid_params', - { params => scalar $cgi->param('params'), - err_msg => $@ }); - } - } - elsif (!$self->version or $self->version ne '1.1') { - $params = []; - } - else { - $params = {}; - } - - $input{params} = $params; - - my $json = $self->json->encode(\%input); - return $json; + my $self = shift; + my $cgi = $self->cgi; + + my %input; + + # Both version and id must be set before any errors are thrown. + if ($cgi->param('version')) { + $self->version(scalar $cgi->param('version')); + $input{version} = $cgi->param('version'); + } + else { + $self->version('1.0'); + } + + # The JSON-RPC 2.0 spec says that any request that omits an id doesn't + # want a response. However, in an HTTP GET situation, it's stupid to + # expect all clients to specify some id parameter just to get a response, + # so we don't require it. + my $id; + if (defined $cgi->param('id')) { + $id = $cgi->param('id'); + } + + # However, JSON::RPC does require that an id exist in most cases, in + # order to throw proper errors. We use the installation's urlbase as + # the id, in this case. + else { + $id = correct_urlbase(); + } + + # Setting _bz_request_id here is required in case we throw errors early, + # before _handle. + $self->{_bz_request_id} = $input{id} = $id; + + # _bz_callback can throw an error, so we have to set it here, after we're + # ready to throw errors. + $self->_bz_callback(scalar $cgi->param('callback')); + + if (!$cgi->param('method')) { + ThrowUserError('json_rpc_get_method_required'); + } + $input{method} = $cgi->param('method'); + + my $params; + if (defined $cgi->param('params')) { + local $@; + $params = eval { $self->json->decode(scalar $cgi->param('params')) }; + if ($@) { + ThrowUserError('json_rpc_invalid_params', + {params => scalar $cgi->param('params'), err_msg => $@}); + } + } + elsif (!$self->version or $self->version ne '1.1') { + $params = []; + } + else { + $params = {}; + } + + $input{params} = $params; + + my $json = $self->json->encode(\%input); + return $json; } ####################################### @@ -193,72 +197,76 @@ sub retrieve_json_from_get { ####################################### sub type { - my ($self, $type, $value) = @_; - - # This is the only type that does something special with undef. - if ($type eq 'boolean') { - return $value ? JSON::true : JSON::false; - } - - return JSON::null if !defined $value; - - my $retval = $value; - - if ($type eq 'int') { - $retval = int($value); - } - if ($type eq 'double') { - $retval = 0.0 + $value; - } - elsif ($type eq 'string') { - # Forces string context, so that JSON will make it a string. - $retval = "$value"; - } - elsif ($type eq 'dateTime') { - # ISO-8601 "YYYYMMDDTHH:MM:SS" with a literal T - $retval = $self->datetime_format_outbound($value); - } - elsif ($type eq 'base64') { - utf8::encode($value) if utf8::is_utf8($value); - $retval = encode_base64($value, ''); - } - elsif ($type eq 'email' && Bugzilla->params->{'webservice_email_filter'}) { - $retval = email_filter($value); - } - - return $retval; + my ($self, $type, $value) = @_; + + # This is the only type that does something special with undef. + if ($type eq 'boolean') { + return $value ? JSON::true : JSON::false; + } + + return JSON::null if !defined $value; + + my $retval = $value; + + if ($type eq 'int') { + $retval = int($value); + } + if ($type eq 'double') { + $retval = 0.0 + $value; + } + elsif ($type eq 'string') { + + # Forces string context, so that JSON will make it a string. + $retval = "$value"; + } + elsif ($type eq 'dateTime') { + + # ISO-8601 "YYYYMMDDTHH:MM:SS" with a literal T + $retval = $self->datetime_format_outbound($value); + } + elsif ($type eq 'base64') { + utf8::encode($value) if utf8::is_utf8($value); + $retval = encode_base64($value, ''); + } + elsif ($type eq 'email' && Bugzilla->params->{'webservice_email_filter'}) { + $retval = email_filter($value); + } + + return $retval; } sub datetime_format_outbound { - my $self = shift; - # YUI expects ISO8601 in UTC time; including TZ specifier - return $self->SUPER::datetime_format_outbound(@_) . 'Z'; + my $self = shift; + + # YUI expects ISO8601 in UTC time; including TZ specifier + return $self->SUPER::datetime_format_outbound(@_) . 'Z'; } sub handle_login { - my $self = shift; - - # If we're being called using GET, we don't allow cookie-based or Env - # login, because GET requests can be done cross-domain, and we don't - # want private data showing up on another site unless the user - # explicitly gives that site their username and password. (This is - # particularly important for JSONP, which would allow a remote site - # to use private data without the user's knowledge, unless we had this - # protection in place.) - if ($self->request->method ne 'POST') { - # XXX There's no particularly good way for us to get a parameter - # to Bugzilla->login at this point, so we pass this information - # around using request_cache, which is a bit of a hack. The - # implementation of it is in Bugzilla::Auth::Login::Stack. - Bugzilla->request_cache->{auth_no_automatic_login} = 1; - } - - my $path = $self->path_info; - my $class = $self->{dispatch_path}->{$path}; - my $full_method = $self->_bz_method_name; - $full_method =~ /^\S+\.(\S+)/; - my $method = $1; - $self->SUPER::handle_login($class, $method, $full_method); + my $self = shift; + + # If we're being called using GET, we don't allow cookie-based or Env + # login, because GET requests can be done cross-domain, and we don't + # want private data showing up on another site unless the user + # explicitly gives that site their username and password. (This is + # particularly important for JSONP, which would allow a remote site + # to use private data without the user's knowledge, unless we had this + # protection in place.) + if ($self->request->method ne 'POST') { + + # XXX There's no particularly good way for us to get a parameter + # to Bugzilla->login at this point, so we pass this information + # around using request_cache, which is a bit of a hack. The + # implementation of it is in Bugzilla::Auth::Login::Stack. + Bugzilla->request_cache->{auth_no_automatic_login} = 1; + } + + my $path = $self->path_info; + my $class = $self->{dispatch_path}->{$path}; + my $full_method = $self->_bz_method_name; + $full_method =~ /^\S+\.(\S+)/; + my $method = $1; + $self->SUPER::handle_login($class, $method, $full_method); } ###################################### @@ -267,165 +275,165 @@ sub handle_login { # Store the ID of the current call, because Bugzilla::Error will need it. sub _handle { - my $self = shift; - my ($obj) = @_; - $self->{_bz_request_id} = $obj->{id}; + my $self = shift; + my ($obj) = @_; + $self->{_bz_request_id} = $obj->{id}; - my $result = $self->SUPER::_handle(@_); + my $result = $self->SUPER::_handle(@_); - # Set the ETag if not already set in the webservice methods. - my $etag = $self->bz_etag; - if (!$etag && ref $result) { - my $data = $self->json->decode($result)->{'result'}; - $self->bz_etag($data); - } + # Set the ETag if not already set in the webservice methods. + my $etag = $self->bz_etag; + if (!$etag && ref $result) { + my $data = $self->json->decode($result)->{'result'}; + $self->bz_etag($data); + } - return $result; + return $result; } # Make all error messages returned by JSON::RPC go into the 100000 # range, and bring down all our errors into the normal range. sub _error { - my ($self, $id, $code) = (shift, shift, shift); - # All JSON::RPC errors are less than 1000. - if ($code < 1000) { - $code += 100000; - } - # Bugzilla::Error adds 100,000 to all *our* errors, so - # we know they came from us. - elsif ($code > 100000) { - $code -= 100000; - } - - # We can't just set $_[1] because it's not always settable, - # in JSON::RPC::Server. - unshift(@_, $id, $code); - my $json = $self->SUPER::_error(@_); - - # We want to always send the JSON-RPC 1.1 error format, although - # If we're not in JSON-RPC 1.1, we don't need the silly "name" parameter. - if (!$self->version or $self->version ne '1.1') { - my $object = $self->json->decode($json); - my $message = $object->{error}; - # Just assure that future versions of JSON::RPC don't change the - # JSON-RPC 1.0 error format. - if (!ref $message) { - $object->{error} = { - code => $code, - message => $message, - }; - $json = $self->json->encode($object); - } - } - return $json; + my ($self, $id, $code) = (shift, shift, shift); + + # All JSON::RPC errors are less than 1000. + if ($code < 1000) { + $code += 100000; + } + + # Bugzilla::Error adds 100,000 to all *our* errors, so + # we know they came from us. + elsif ($code > 100000) { + $code -= 100000; + } + + # We can't just set $_[1] because it's not always settable, + # in JSON::RPC::Server. + unshift(@_, $id, $code); + my $json = $self->SUPER::_error(@_); + + # We want to always send the JSON-RPC 1.1 error format, although + # If we're not in JSON-RPC 1.1, we don't need the silly "name" parameter. + if (!$self->version or $self->version ne '1.1') { + my $object = $self->json->decode($json); + my $message = $object->{error}; + + # Just assure that future versions of JSON::RPC don't change the + # JSON-RPC 1.0 error format. + if (!ref $message) { + $object->{error} = {code => $code, message => $message,}; + $json = $self->json->encode($object); + } + } + return $json; } # This handles dispatching our calls to the appropriate class based on # the name of the method. sub _find_procedure { - my $self = shift; + my $self = shift; - my $method = shift; - $self->{_bz_method_name} = $method; + my $method = shift; + $self->{_bz_method_name} = $method; - # This tricks SUPER::_find_procedure into finding the right class. - $method =~ /^(\S+)\.(\S+)$/; - $self->path_info($1); - unshift(@_, $2); + # This tricks SUPER::_find_procedure into finding the right class. + $method =~ /^(\S+)\.(\S+)$/; + $self->path_info($1); + unshift(@_, $2); - return $self->SUPER::_find_procedure(@_); + return $self->SUPER::_find_procedure(@_); } # This is a hacky way to do something right before methods are called. # This is the last thing that JSON::RPC::Server::_handle calls right before # the method is actually called. sub _argument_type_check { - my $self = shift; - my $params = $self->SUPER::_argument_type_check(@_); - - # JSON-RPC 1.0 requires all parameters to be passed as an array, so - # we just pull out the first item and assume it's an object. - my $params_is_array; - if (ref $params eq 'ARRAY') { - $params = $params->[0]; - $params_is_array = 1; - } - - taint_data($params); - - # Now, convert dateTime fields on input. - $self->_bz_method_name =~ /^(\S+)\.(\S+)$/; - my ($class, $method) = ($1, $2); - my $pkg = $self->{dispatch_path}->{$class}; - my @date_fields = @{ $pkg->DATE_FIELDS->{$method} || [] }; - foreach my $field (@date_fields) { - if (defined $params->{$field}) { - my $value = $params->{$field}; - if (ref $value eq 'ARRAY') { - $params->{$field} = - [ map { $self->datetime_format_inbound($_) } @$value ]; - } - else { - $params->{$field} = $self->datetime_format_inbound($value); - } - } - } - my @base64_fields = @{ $pkg->BASE64_FIELDS->{$method} || [] }; - foreach my $field (@base64_fields) { - if (defined $params->{$field}) { - $params->{$field} = decode_base64($params->{$field}); - } - } - - # Update the params to allow for several convenience key/values - # use for authentication - fix_credentials($params); - - Bugzilla->input_params($params); - - if ($self->request->method eq 'POST') { - # CSRF is possible via XMLHttpRequest when the Content-Type header - # is not application/json (for example: text/plain or - # application/x-www-form-urlencoded). - # application/json is the single official MIME type, per RFC 4627. - my $content_type = $self->cgi->content_type; - # The charset can be appended to the content type, so we use a regexp. - if ($content_type !~ m{^application/json(-rpc)?(;.*)?$}i) { - ThrowUserError('json_rpc_illegal_content_type', - { content_type => $content_type }); - } - } - else { - # When being called using GET, we don't allow calling - # methods that can change data. This protects us against cross-site - # request forgeries. - if (!grep($_ eq $method, $pkg->READ_ONLY)) { - ThrowUserError('json_rpc_post_only', - { method => $self->_bz_method_name }); - } - } - - # Only allowed methods to be used from our whitelist - if (none { $_ eq $method} $pkg->PUBLIC_METHODS) { - ThrowCodeError('unknown_method', { method => $self->_bz_method_name }); - } - - # This is the best time to do login checks. - $self->handle_login(); - - # Bugzilla::WebService packages call internal methods like - # $self->_some_private_method. So we have to inherit from - # that class as well as this Server class. - my $new_class = ref($self) . '::' . $pkg; - my $isa_string = 'our @ISA = qw(' . ref($self) . " $pkg)"; - eval "package $new_class;$isa_string;"; - bless $self, $new_class; - - if ($params_is_array) { - $params = [$params]; - } - - return $params; + my $self = shift; + my $params = $self->SUPER::_argument_type_check(@_); + + # JSON-RPC 1.0 requires all parameters to be passed as an array, so + # we just pull out the first item and assume it's an object. + my $params_is_array; + if (ref $params eq 'ARRAY') { + $params = $params->[0]; + $params_is_array = 1; + } + + taint_data($params); + + # Now, convert dateTime fields on input. + $self->_bz_method_name =~ /^(\S+)\.(\S+)$/; + my ($class, $method) = ($1, $2); + my $pkg = $self->{dispatch_path}->{$class}; + my @date_fields = @{$pkg->DATE_FIELDS->{$method} || []}; + foreach my $field (@date_fields) { + if (defined $params->{$field}) { + my $value = $params->{$field}; + if (ref $value eq 'ARRAY') { + $params->{$field} = [map { $self->datetime_format_inbound($_) } @$value]; + } + else { + $params->{$field} = $self->datetime_format_inbound($value); + } + } + } + my @base64_fields = @{$pkg->BASE64_FIELDS->{$method} || []}; + foreach my $field (@base64_fields) { + if (defined $params->{$field}) { + $params->{$field} = decode_base64($params->{$field}); + } + } + + # Update the params to allow for several convenience key/values + # use for authentication + fix_credentials($params); + + Bugzilla->input_params($params); + + if ($self->request->method eq 'POST') { + + # CSRF is possible via XMLHttpRequest when the Content-Type header + # is not application/json (for example: text/plain or + # application/x-www-form-urlencoded). + # application/json is the single official MIME type, per RFC 4627. + my $content_type = $self->cgi->content_type; + + # The charset can be appended to the content type, so we use a regexp. + if ($content_type !~ m{^application/json(-rpc)?(;.*)?$}i) { + ThrowUserError('json_rpc_illegal_content_type', + {content_type => $content_type}); + } + } + else { + # When being called using GET, we don't allow calling + # methods that can change data. This protects us against cross-site + # request forgeries. + if (!grep($_ eq $method, $pkg->READ_ONLY)) { + ThrowUserError('json_rpc_post_only', {method => $self->_bz_method_name}); + } + } + + # Only allowed methods to be used from our whitelist + if (none { $_ eq $method } $pkg->PUBLIC_METHODS) { + ThrowCodeError('unknown_method', {method => $self->_bz_method_name}); + } + + # This is the best time to do login checks. + $self->handle_login(); + + # Bugzilla::WebService packages call internal methods like + # $self->_some_private_method. So we have to inherit from + # that class as well as this Server class. + my $new_class = ref($self) . '::' . $pkg; + my $isa_string = 'our @ISA = qw(' . ref($self) . " $pkg)"; + eval "package $new_class;$isa_string;"; + bless $self, $new_class; + + if ($params_is_array) { + $params = [$params]; + } + + return $params; } ########################## @@ -434,22 +442,24 @@ sub _argument_type_check { # _bz_method_name is stored by _find_procedure for later use. sub _bz_method_name { - return $_[0]->{_bz_method_name}; + return $_[0]->{_bz_method_name}; } sub _bz_callback { - my ($self, $value) = @_; - if (defined $value) { - $value = trim($value); - # We don't use \w because we don't want to allow Unicode here. - if ($value !~ /^[A-Za-z0-9_\.\[\]]+$/) { - ThrowUserError('json_rpc_invalid_callback', { callback => $value }); - } - $self->{_bz_callback} = $value; - # JSONP needs to be parsed by a JS parser, not by a JSON parser. - $self->content_type('text/javascript'); + my ($self, $value) = @_; + if (defined $value) { + $value = trim($value); + + # We don't use \w because we don't want to allow Unicode here. + if ($value !~ /^[A-Za-z0-9_\.\[\]]+$/) { + ThrowUserError('json_rpc_invalid_callback', {callback => $value}); } - return $self->{_bz_callback}; + $self->{_bz_callback} = $value; + + # JSONP needs to be parsed by a JS parser, not by a JSON parser. + $self->content_type('text/javascript'); + } + return $self->{_bz_callback}; } 1; diff --git a/Bugzilla/WebService/Server/REST.pm b/Bugzilla/WebService/Server/REST.pm index 8450a7a28..8108e1d4f 100644 --- a/Bugzilla/WebService/Server/REST.pm +++ b/Bugzilla/WebService/Server/REST.pm @@ -40,134 +40,134 @@ use MIME::Base64 qw(decode_base64); ########################### sub handle { - my ($self) = @_; - - # Determine how the data should be represented. We do this early so - # errors will also be returned with the proper content type. - # If no accept header was sent or the content types specified were not - # matched, we default to the first type in the whitelist. - $self->content_type($self->_best_content_type(REST_CONTENT_TYPE_WHITELIST())); - - # Using current path information, decide which class/method to - # use to serve the request. Throw error if no resource was found - # unless we were looking for OPTIONS - if (!$self->_find_resource($self->cgi->path_info)) { - if ($self->request->method eq 'OPTIONS' - && $self->bz_rest_options) - { - my $response = $self->response_header(STATUS_OK, ""); - my $options_string = join(', ', @{ $self->bz_rest_options }); - $response->header('Allow' => $options_string, - 'Access-Control-Allow-Methods' => $options_string); - return $self->response($response); - } - - ThrowUserError("rest_invalid_resource", - { path => $self->cgi->path_info, - method => $self->request->method }); + my ($self) = @_; + + # Determine how the data should be represented. We do this early so + # errors will also be returned with the proper content type. + # If no accept header was sent or the content types specified were not + # matched, we default to the first type in the whitelist. + $self->content_type($self->_best_content_type(REST_CONTENT_TYPE_WHITELIST())); + + # Using current path information, decide which class/method to + # use to serve the request. Throw error if no resource was found + # unless we were looking for OPTIONS + if (!$self->_find_resource($self->cgi->path_info)) { + if ($self->request->method eq 'OPTIONS' && $self->bz_rest_options) { + my $response = $self->response_header(STATUS_OK, ""); + my $options_string = join(', ', @{$self->bz_rest_options}); + $response->header( + 'Allow' => $options_string, + 'Access-Control-Allow-Methods' => $options_string + ); + return $self->response($response); } - # Dispatch to the proper module - my $class = $self->bz_class_name; - my ($path) = $class =~ /::([^:]+)$/; - $self->path_info($path); - delete $self->{dispatch_path}; - $self->dispatch({ $path => $class }); + ThrowUserError("rest_invalid_resource", + {path => $self->cgi->path_info, method => $self->request->method}); + } - my $params = $self->_retrieve_json_params; + # Dispatch to the proper module + my $class = $self->bz_class_name; + my ($path) = $class =~ /::([^:]+)$/; + $self->path_info($path); + delete $self->{dispatch_path}; + $self->dispatch({$path => $class}); - fix_credentials($params); + my $params = $self->_retrieve_json_params; - # Fix includes/excludes for each call - rest_include_exclude($params); + fix_credentials($params); - # Set callback name if exists - $self->_bz_callback($params->{'callback'}) if $params->{'callback'}; + # Fix includes/excludes for each call + rest_include_exclude($params); - Bugzilla->input_params($params); + # Set callback name if exists + $self->_bz_callback($params->{'callback'}) if $params->{'callback'}; - # Set the JSON version to 1.1 and the id to the current urlbase - # also set up the correct handler method - my $obj = { - version => '1.1', - id => correct_urlbase(), - method => $self->bz_method_name, - params => $params - }; + Bugzilla->input_params($params); - # Execute the handler - my $result = $self->_handle($obj); + # Set the JSON version to 1.1 and the id to the current urlbase + # also set up the correct handler method + my $obj = { + version => '1.1', + id => correct_urlbase(), + method => $self->bz_method_name, + params => $params + }; - if (!$self->error_response_header) { - return $self->response( - $self->response_header($self->bz_success_code || STATUS_OK, $result)); - } + # Execute the handler + my $result = $self->_handle($obj); + + if (!$self->error_response_header) { + return $self->response( + $self->response_header($self->bz_success_code || STATUS_OK, $result)); + } - $self->response($self->error_response_header); + $self->response($self->error_response_header); } sub response { - my ($self, $response) = @_; - - # If we have thrown an error, the 'error' key will exist - # otherwise we use 'result'. JSONRPC returns other data - # along with the result/error such as version and id which - # we will strip off for REST calls. - my $content = $response->content; - my $json_data = {}; - if ($content) { - $json_data = $self->json->decode($content); - } - - my $result = {}; - if (exists $json_data->{error}) { - $result = $json_data->{error}; - $result->{error} = $self->type('boolean', 1); - $result->{documentation} = REST_DOC; - delete $result->{'name'}; # Remove JSONRPCError - } - elsif (exists $json_data->{result}) { - $result = $json_data->{result}; - } - - # The result needs to be a valid JSON data structure - # and not a undefined or scalar value. - if (!ref $result - || blessed($result) - || (ref $result ne 'HASH' && ref $result ne 'ARRAY')) - { - $result = { result => $result }; - } - - Bugzilla::Hook::process('webservice_rest_response', - { rpc => $self, result => \$result, response => $response }); - - # Access Control - $response->header("Access-Control-Allow-Origin", "*"); - $response->header("Access-Control-Allow-Headers", "origin, content-type, accept, x-requested-with"); - - # ETag support - my $etag = $self->bz_etag; - $self->bz_etag($result) if !$etag; - - # If accessing through web browser, then display in readable format - if ($self->content_type eq 'text/html') { - $result = $self->json->pretty->canonical->allow_nonref->encode($result); - - my $template = Bugzilla->template; - $content = ""; - $template->process("rest.html.tmpl", { result => $result }, \$content) - || ThrowTemplateError($template->error()); - - $response->content_type('text/html'); - } - else { - $content = $self->json->encode($result); - } - - $response->content($content); - - $self->SUPER::response($response); + my ($self, $response) = @_; + + # If we have thrown an error, the 'error' key will exist + # otherwise we use 'result'. JSONRPC returns other data + # along with the result/error such as version and id which + # we will strip off for REST calls. + my $content = $response->content; + my $json_data = {}; + if ($content) { + $json_data = $self->json->decode($content); + } + + my $result = {}; + if (exists $json_data->{error}) { + $result = $json_data->{error}; + $result->{error} = $self->type('boolean', 1); + $result->{documentation} = REST_DOC; + delete $result->{'name'}; # Remove JSONRPCError + } + elsif (exists $json_data->{result}) { + $result = $json_data->{result}; + } + + # The result needs to be a valid JSON data structure + # and not a undefined or scalar value. + if ( !ref $result + || blessed($result) + || (ref $result ne 'HASH' && ref $result ne 'ARRAY')) + { + $result = {result => $result}; + } + + Bugzilla::Hook::process('webservice_rest_response', + {rpc => $self, result => \$result, response => $response}); + + # Access Control + $response->header("Access-Control-Allow-Origin", "*"); + $response->header("Access-Control-Allow-Headers", + "origin, content-type, accept, x-requested-with"); + + # ETag support + my $etag = $self->bz_etag; + $self->bz_etag($result) if !$etag; + + # If accessing through web browser, then display in readable format + if ($self->content_type eq 'text/html') { + $result = $self->json->pretty->canonical->allow_nonref->encode($result); + + my $template = Bugzilla->template; + $content = ""; + $template->process("rest.html.tmpl", {result => $result}, \$content) + || ThrowTemplateError($template->error()); + + $response->content_type('text/html'); + } + else { + $content = $self->json->encode($result); + } + + $response->content($content); + + $self->SUPER::response($response); } ####################################### @@ -175,36 +175,40 @@ sub response { ####################################### sub handle_login { - my $self = shift; - - # If we're being called using GET, we don't allow cookie-based or Env - # login, because GET requests can be done cross-domain, and we don't - # want private data showing up on another site unless the user - # explicitly gives that site their username and password. (This is - # particularly important for JSONP, which would allow a remote site - # to use private data without the user's knowledge, unless we had this - # protection in place.) We do allow this for GET /login as we need to - # for Bugzilla::Auth::Persist::Cookie to create a login cookie that we - # can also use for Bugzilla_token support. This is OK as it requires - # a login and password to be supplied and will fail if they are not - # valid for the user. - if (!grep($_ eq $self->request->method, ('POST', 'PUT')) - && !($self->bz_class_name eq 'Bugzilla::WebService::User' - && $self->bz_method_name eq 'login')) - { - # XXX There's no particularly good way for us to get a parameter - # to Bugzilla->login at this point, so we pass this information - # around using request_cache, which is a bit of a hack. The - # implementation of it is in Bugzilla::Auth::Login::Stack. - Bugzilla->request_cache->{'auth_no_automatic_login'} = 1; - } - - my $class = $self->bz_class_name; - my $method = $self->bz_method_name; - my $full_method = $class . "." . $method; - - # Bypass JSONRPC::handle_login - Bugzilla::WebService::Server->handle_login($class, $method, $full_method); + my $self = shift; + + # If we're being called using GET, we don't allow cookie-based or Env + # login, because GET requests can be done cross-domain, and we don't + # want private data showing up on another site unless the user + # explicitly gives that site their username and password. (This is + # particularly important for JSONP, which would allow a remote site + # to use private data without the user's knowledge, unless we had this + # protection in place.) We do allow this for GET /login as we need to + # for Bugzilla::Auth::Persist::Cookie to create a login cookie that we + # can also use for Bugzilla_token support. This is OK as it requires + # a login and password to be supplied and will fail if they are not + # valid for the user. + if ( + !grep($_ eq $self->request->method, ('POST', 'PUT')) + && !( + $self->bz_class_name eq 'Bugzilla::WebService::User' + && $self->bz_method_name eq 'login' + ) + ) + { + # XXX There's no particularly good way for us to get a parameter + # to Bugzilla->login at this point, so we pass this information + # around using request_cache, which is a bit of a hack. The + # implementation of it is in Bugzilla::Auth::Login::Stack. + Bugzilla->request_cache->{'auth_no_automatic_login'} = 1; + } + + my $class = $self->bz_class_name; + my $method = $self->bz_method_name; + my $full_method = $class . "." . $method; + + # Bypass JSONRPC::handle_login + Bugzilla::WebService::Server->handle_login($class, $method, $full_method); } ############################ @@ -214,79 +218,78 @@ sub handle_login { # We do not want to run Bugzilla::WebService::Server::JSONRPC->_find_prodedure # as it determines the method name differently. sub _find_procedure { - my $self = shift; - if ($self->isa('JSON::RPC::Server::CGI')) { - return JSON::RPC::Server::_find_procedure($self, @_); - } - else { - return JSON::RPC::Legacy::Server::_find_procedure($self, @_); - } + my $self = shift; + if ($self->isa('JSON::RPC::Server::CGI')) { + return JSON::RPC::Server::_find_procedure($self, @_); + } + else { + return JSON::RPC::Legacy::Server::_find_procedure($self, @_); + } } sub _argument_type_check { - my $self = shift; - my $params; - - if ($self->isa('JSON::RPC::Server::CGI')) { - $params = JSON::RPC::Server::_argument_type_check($self, @_); + my $self = shift; + my $params; + + if ($self->isa('JSON::RPC::Server::CGI')) { + $params = JSON::RPC::Server::_argument_type_check($self, @_); + } + else { + $params = JSON::RPC::Legacy::Server::_argument_type_check($self, @_); + } + + # JSON-RPC 1.0 requires all parameters to be passed as an array, so + # we just pull out the first item and assume it's an object. + my $params_is_array; + if (ref $params eq 'ARRAY') { + $params = $params->[0]; + $params_is_array = 1; + } + + taint_data($params); + + # Now, convert dateTime fields on input. + my $method = $self->bz_method_name; + my $pkg = $self->{dispatch_path}->{$self->path_info}; + my @date_fields = @{$pkg->DATE_FIELDS->{$method} || []}; + foreach my $field (@date_fields) { + if (defined $params->{$field}) { + my $value = $params->{$field}; + if (ref $value eq 'ARRAY') { + $params->{$field} = [map { $self->datetime_format_inbound($_) } @$value]; + } + else { + $params->{$field} = $self->datetime_format_inbound($value); + } } - else { - $params = JSON::RPC::Legacy::Server::_argument_type_check($self, @_); + } + my @base64_fields = @{$pkg->BASE64_FIELDS->{$method} || []}; + foreach my $field (@base64_fields) { + if (defined $params->{$field}) { + $params->{$field} = decode_base64($params->{$field}); } + } - # JSON-RPC 1.0 requires all parameters to be passed as an array, so - # we just pull out the first item and assume it's an object. - my $params_is_array; - if (ref $params eq 'ARRAY') { - $params = $params->[0]; - $params_is_array = 1; - } + # This is the best time to do login checks. + $self->handle_login(); - taint_data($params); - - # Now, convert dateTime fields on input. - my $method = $self->bz_method_name; - my $pkg = $self->{dispatch_path}->{$self->path_info}; - my @date_fields = @{ $pkg->DATE_FIELDS->{$method} || [] }; - foreach my $field (@date_fields) { - if (defined $params->{$field}) { - my $value = $params->{$field}; - if (ref $value eq 'ARRAY') { - $params->{$field} = - [ map { $self->datetime_format_inbound($_) } @$value ]; - } - else { - $params->{$field} = $self->datetime_format_inbound($value); - } - } - } - my @base64_fields = @{ $pkg->BASE64_FIELDS->{$method} || [] }; - foreach my $field (@base64_fields) { - if (defined $params->{$field}) { - $params->{$field} = decode_base64($params->{$field}); - } - } + # Bugzilla::WebService packages call internal methods like + # $self->_some_private_method. So we have to inherit from + # that class as well as this Server class. + my $new_class = ref($self) . '::' . $pkg; + my $isa_string = 'our @ISA = qw(' . ref($self) . " $pkg)"; + eval "package $new_class;$isa_string;"; + bless $self, $new_class; - # This is the best time to do login checks. - $self->handle_login(); + # Allow extensions to modify the params post login + Bugzilla::Hook::process('webservice_rest_request', + {rpc => $self, params => $params}); - # Bugzilla::WebService packages call internal methods like - # $self->_some_private_method. So we have to inherit from - # that class as well as this Server class. - my $new_class = ref($self) . '::' . $pkg; - my $isa_string = 'our @ISA = qw(' . ref($self) . " $pkg)"; - eval "package $new_class;$isa_string;"; - bless $self, $new_class; + if ($params_is_array) { + $params = [$params]; + } - # Allow extensions to modify the params post login - Bugzilla::Hook::process('webservice_rest_request', - { rpc => $self, params => $params }); - - if ($params_is_array) { - $params = [$params]; - } - - return $params; + return $params; } ################### @@ -294,46 +297,46 @@ sub _argument_type_check { ################### sub bz_method_name { - my ($self, $method) = @_; - $self->{_bz_method_name} = $method if $method; - return $self->{_bz_method_name}; + my ($self, $method) = @_; + $self->{_bz_method_name} = $method if $method; + return $self->{_bz_method_name}; } sub bz_class_name { - my ($self, $class) = @_; - $self->{_bz_class_name} = $class if $class; - return $self->{_bz_class_name}; + my ($self, $class) = @_; + $self->{_bz_class_name} = $class if $class; + return $self->{_bz_class_name}; } sub bz_success_code { - my ($self, $value) = @_; - $self->{_bz_success_code} = $value if $value; - return $self->{_bz_success_code}; + my ($self, $value) = @_; + $self->{_bz_success_code} = $value if $value; + return $self->{_bz_success_code}; } sub bz_rest_params { - my ($self, $params) = @_; - $self->{_bz_rest_params} = $params if $params; - return $self->{_bz_rest_params}; + my ($self, $params) = @_; + $self->{_bz_rest_params} = $params if $params; + return $self->{_bz_rest_params}; } sub bz_rest_options { - my ($self, $options) = @_; - $self->{_bz_rest_options} = $options if $options; - return $self->{_bz_rest_options}; + my ($self, $options) = @_; + $self->{_bz_rest_options} = $options if $options; + return $self->{_bz_rest_options}; } sub rest_include_exclude { - my ($params) = @_; + my ($params) = @_; - if ($params->{'include_fields'} && !ref $params->{'include_fields'}) { - $params->{'include_fields'} = [ split(/[\s+,]/, $params->{'include_fields'}) ]; - } - if ($params->{'exclude_fields'} && !ref $params->{'exclude_fields'}) { - $params->{'exclude_fields'} = [ split(/[\s+,]/, $params->{'exclude_fields'}) ]; - } + if ($params->{'include_fields'} && !ref $params->{'include_fields'}) { + $params->{'include_fields'} = [split(/[\s+,]/, $params->{'include_fields'})]; + } + if ($params->{'exclude_fields'} && !ref $params->{'exclude_fields'}) { + $params->{'exclude_fields'} = [split(/[\s+,]/, $params->{'exclude_fields'})]; + } - return $params; + return $params; } ########################## @@ -341,184 +344,191 @@ sub rest_include_exclude { ########################## sub _retrieve_json_params { - my $self = shift; - - # Make a copy of the current input_params rather than edit directly - my $params = {}; - %{$params} = %{ Bugzilla->input_params }; - - # First add any parameters we were able to pull out of the path - # based on the resource regexp and combine with the normal URL - # parameters. - if (my $rest_params = $self->bz_rest_params) { - foreach my $param (keys %$rest_params) { - # If the param does not already exist or if the - # rest param is a single value, add it to the - # global params. - if (!exists $params->{$param} || !ref $rest_params->{$param}) { - $params->{$param} = $rest_params->{$param}; - } - # If rest_param is a list then add any extra values to the list - elsif (ref $rest_params->{$param}) { - my @extra_values = ref $params->{$param} - ? @{ $params->{$param} } - : ($params->{$param}); - $params->{$param} - = [ uniq (@{ $rest_params->{$param} }, @extra_values) ]; - } - } + my $self = shift; + + # Make a copy of the current input_params rather than edit directly + my $params = {}; + %{$params} = %{Bugzilla->input_params}; + + # First add any parameters we were able to pull out of the path + # based on the resource regexp and combine with the normal URL + # parameters. + if (my $rest_params = $self->bz_rest_params) { + foreach my $param (keys %$rest_params) { + + # If the param does not already exist or if the + # rest param is a single value, add it to the + # global params. + if (!exists $params->{$param} || !ref $rest_params->{$param}) { + $params->{$param} = $rest_params->{$param}; + } + + # If rest_param is a list then add any extra values to the list + elsif (ref $rest_params->{$param}) { + my @extra_values + = ref $params->{$param} ? @{$params->{$param}} : ($params->{$param}); + $params->{$param} = [uniq(@{$rest_params->{$param}}, @extra_values)]; + } + } + } + + # Any parameters passed in in the body of a non-GET request will override + # any parameters pull from the url path. Otherwise non-unique keys are + # combined. + if ($self->request->method ne 'GET') { + my $extra_params = {}; + + # We do this manually because CGI.pm doesn't understand JSON strings. + my $json = delete $params->{'POSTDATA'} || delete $params->{'PUTDATA'}; + if ($json) { + eval { $extra_params = $self->json->decode($json); }; + if ($@) { + ThrowUserError('json_rpc_invalid_params', {err_msg => $@}); + } } - # Any parameters passed in in the body of a non-GET request will override - # any parameters pull from the url path. Otherwise non-unique keys are - # combined. - if ($self->request->method ne 'GET') { - my $extra_params = {}; - # We do this manually because CGI.pm doesn't understand JSON strings. - my $json = delete $params->{'POSTDATA'} || delete $params->{'PUTDATA'}; - if ($json) { - eval { $extra_params = $self->json->decode($json); }; - if ($@) { - ThrowUserError('json_rpc_invalid_params', { err_msg => $@ }); - } - } - - # Allow parameters in the query string if request was non-GET. - # Note: parameters in query string body override any matching - # parameters in the request body. - foreach my $param ($self->cgi->url_param()) { - $extra_params->{$param} = $self->cgi->url_param($param); - } - - %{$params} = (%{$params}, %{$extra_params}) if %{$extra_params}; + # Allow parameters in the query string if request was non-GET. + # Note: parameters in query string body override any matching + # parameters in the request body. + foreach my $param ($self->cgi->url_param()) { + $extra_params->{$param} = $self->cgi->url_param($param); } - return $params; + %{$params} = (%{$params}, %{$extra_params}) if %{$extra_params}; + } + + return $params; } sub _find_resource { - my ($self, $path) = @_; - - # Load in the WebService module from the dispatch map and then call - # $module->rest_resources to get the resources array ref. - my $resources = {}; - foreach my $module (values %{ $self->{dispatch_path} }) { - eval("require $module") || die $@; - next if !$module->can('rest_resources'); - $resources->{$module} = $module->rest_resources; - } - - Bugzilla::Hook::process('webservice_rest_resources', - { rpc => $self, resources => $resources }); - - # Use the resources hash from each module loaded earlier to determine - # which handler to use based on a regex match of the CGI path. - # Also any matches found in the regex will be passed in later to the - # handler for possible use. - my $request_method = $self->request->method; - - my (@matches, $handler_found, $handler_method, $handler_class); - foreach my $class (keys %{ $resources }) { - # The resource data for each module needs to be - # an array ref with an even number of elements - # to work correctly. - next if (ref $resources->{$class} ne 'ARRAY' - || scalar @{ $resources->{$class} } % 2 != 0); - - while (my $regex = shift @{ $resources->{$class} }) { - my $options_data = shift @{ $resources->{$class} }; - next if ref $options_data ne 'HASH'; - - if (@matches = ($path =~ $regex)) { - # If a specific path is accompanied by a OPTIONS request - # method, the user is asking for a list of possible request - # methods for a specific path. - $self->bz_rest_options([ keys %{ $options_data } ]); - - if ($options_data->{$request_method}) { - my $resource_data = $options_data->{$request_method}; - $self->bz_class_name($class); - - # The method key/value can be a simple scalar method name - # or a anonymous subroutine so we execute it here. - my $method = ref $resource_data->{method} eq 'CODE' - ? $resource_data->{method}->($self) - : $resource_data->{method}; - $self->bz_method_name($method); - - # Pull out any parameters parsed from the URL path - # and store them for use by the method. - if ($resource_data->{params}) { - $self->bz_rest_params($resource_data->{params}->(@matches)); - } - - # If a special success code is needed for this particular - # method, then store it for later when generating response. - if ($resource_data->{success_code}) { - $self->bz_success_code($resource_data->{success_code}); - } - $handler_found = 1; - } - } - last if $handler_found; + my ($self, $path) = @_; + + # Load in the WebService module from the dispatch map and then call + # $module->rest_resources to get the resources array ref. + my $resources = {}; + foreach my $module (values %{$self->{dispatch_path}}) { + eval("require $module") || die $@; + next if !$module->can('rest_resources'); + $resources->{$module} = $module->rest_resources; + } + + Bugzilla::Hook::process('webservice_rest_resources', + {rpc => $self, resources => $resources}); + + # Use the resources hash from each module loaded earlier to determine + # which handler to use based on a regex match of the CGI path. + # Also any matches found in the regex will be passed in later to the + # handler for possible use. + my $request_method = $self->request->method; + + my (@matches, $handler_found, $handler_method, $handler_class); + foreach my $class (keys %{$resources}) { + + # The resource data for each module needs to be + # an array ref with an even number of elements + # to work correctly. + next + if (ref $resources->{$class} ne 'ARRAY' + || scalar @{$resources->{$class}} % 2 != 0); + + while (my $regex = shift @{$resources->{$class}}) { + my $options_data = shift @{$resources->{$class}}; + next if ref $options_data ne 'HASH'; + + if (@matches = ($path =~ $regex)) { + + # If a specific path is accompanied by a OPTIONS request + # method, the user is asking for a list of possible request + # methods for a specific path. + $self->bz_rest_options([keys %{$options_data}]); + + if ($options_data->{$request_method}) { + my $resource_data = $options_data->{$request_method}; + $self->bz_class_name($class); + + # The method key/value can be a simple scalar method name + # or a anonymous subroutine so we execute it here. + my $method + = ref $resource_data->{method} eq 'CODE' + ? $resource_data->{method}->($self) + : $resource_data->{method}; + $self->bz_method_name($method); + + # Pull out any parameters parsed from the URL path + # and store them for use by the method. + if ($resource_data->{params}) { + $self->bz_rest_params($resource_data->{params}->(@matches)); + } + + # If a special success code is needed for this particular + # method, then store it for later when generating response. + if ($resource_data->{success_code}) { + $self->bz_success_code($resource_data->{success_code}); + } + $handler_found = 1; } - last if $handler_found; + } + last if $handler_found; } + last if $handler_found; + } - return $handler_found; + return $handler_found; } sub _best_content_type { - my ($self, @types) = @_; - return ($self->_simple_content_negotiation(@types))[0] || '*/*'; + my ($self, @types) = @_; + return ($self->_simple_content_negotiation(@types))[0] || '*/*'; } sub _simple_content_negotiation { - my ($self, @types) = @_; - my @accept_types = $self->_get_content_prefs(); - # Return the types as-is if no accept header sent, since sorting will be a no-op. - if (!@accept_types) { - return @types; - } - my $score = sub { $self->_score_type(shift, @accept_types) }; - return sort {$score->($b) <=> $score->($a)} @types; + my ($self, @types) = @_; + my @accept_types = $self->_get_content_prefs(); + + # Return the types as-is if no accept header sent, since sorting will be a no-op. + if (!@accept_types) { + return @types; + } + my $score = sub { $self->_score_type(shift, @accept_types) }; + return sort { $score->($b) <=> $score->($a) } @types; } sub _score_type { - my ($self, $type, @accept_types) = @_; - my $score = scalar(@accept_types); - for my $accept_type (@accept_types) { - return $score if $type eq $accept_type; - $score--; - } - return 0; + my ($self, $type, @accept_types) = @_; + my $score = scalar(@accept_types); + for my $accept_type (@accept_types) { + return $score if $type eq $accept_type; + $score--; + } + return 0; } sub _get_content_prefs { - my $self = shift; - my $default_weight = 1; - my @prefs; - - # Parse the Accept header, and save type name, score, and position. - my @accept_types = split /,/, $self->cgi->http('accept') || ''; - my $order = 0; - for my $accept_type (@accept_types) { - my ($weight) = ($accept_type =~ /q=(\d\.\d+|\d+)/); - my ($name) = ($accept_type =~ m#(\S+/[^;]+)#); - next unless $name; - push @prefs, { name => $name, order => $order++}; - if (defined $weight) { - $prefs[-1]->{score} = $weight; - } else { - $prefs[-1]->{score} = $default_weight; - $default_weight -= 0.001; - } + my $self = shift; + my $default_weight = 1; + my @prefs; + + # Parse the Accept header, and save type name, score, and position. + my @accept_types = split /,/, $self->cgi->http('accept') || ''; + my $order = 0; + for my $accept_type (@accept_types) { + my ($weight) = ($accept_type =~ /q=(\d\.\d+|\d+)/); + my ($name) = ($accept_type =~ m#(\S+/[^;]+)#); + next unless $name; + push @prefs, {name => $name, order => $order++}; + if (defined $weight) { + $prefs[-1]->{score} = $weight; + } + else { + $prefs[-1]->{score} = $default_weight; + $default_weight -= 0.001; } + } - # Sort the types by score, subscore by order, and pull out just the name - @prefs = map {$_->{name}} sort {$b->{score} <=> $a->{score} || - $a->{order} <=> $b->{order}} @prefs; - return @prefs; + # Sort the types by score, subscore by order, and pull out just the name + @prefs = map { $_->{name} } + sort { $b->{score} <=> $a->{score} || $a->{order} <=> $b->{order} } @prefs; + return @prefs; } 1; diff --git a/Bugzilla/WebService/Server/REST/Resources/Bug.pm b/Bugzilla/WebService/Server/REST/Resources/Bug.pm index 3fa8b65cf..5cc25f432 100644 --- a/Bugzilla/WebService/Server/REST/Resources/Bug.pm +++ b/Bugzilla/WebService/Server/REST/Resources/Bug.pm @@ -15,150 +15,150 @@ use Bugzilla::WebService::Constants; use Bugzilla::WebService::Bug; BEGIN { - *Bugzilla::WebService::Bug::rest_resources = \&_rest_resources; -}; + *Bugzilla::WebService::Bug::rest_resources = \&_rest_resources; +} sub _rest_resources { - my $rest_resources = [ - qr{^/bug$}, { - GET => { - method => 'search', - }, - POST => { - method => 'create', - status_code => STATUS_CREATED - } - }, - qr{^/bug/$}, { - GET => { - method => 'get' - } - }, - qr{^/bug/([^/]+)$}, { - GET => { - method => 'get', - params => sub { - return { ids => [ $_[0] ] }; - } - }, - PUT => { - method => 'update', - params => sub { - return { ids => [ $_[0] ] }; - } - } - }, - qr{^/bug/([^/]+)/comment$}, { - GET => { - method => 'comments', - params => sub { - return { ids => [ $_[0] ] }; - } - }, - POST => { - method => 'add_comment', - params => sub { - return { id => $_[0] }; - }, - success_code => STATUS_CREATED - } - }, - qr{^/bug/comment/([^/]+)$}, { - GET => { - method => 'comments', - params => sub { - return { comment_ids => [ $_[0] ] }; - } - } - }, - qr{^/bug/comment/tags/([^/]+)$}, { - GET => { - method => 'search_comment_tags', - params => sub { - return { query => $_[0] }; - }, - }, - }, - qr{^/bug/comment/([^/]+)/tags$}, { - PUT => { - method => 'update_comment_tags', - params => sub { - return { comment_id => $_[0] }; - }, - }, - }, - qr{^/bug/([^/]+)/history$}, { - GET => { - method => 'history', - params => sub { - return { ids => [ $_[0] ] }; - }, - } - }, - qr{^/bug/([^/]+)/attachment$}, { - GET => { - method => 'attachments', - params => sub { - return { ids => [ $_[0] ] }; - } - }, - POST => { - method => 'add_attachment', - params => sub { - return { ids => [ $_[0] ] }; - }, - success_code => STATUS_CREATED - } - }, - qr{^/bug/attachment/([^/]+)$}, { - GET => { - method => 'attachments', - params => sub { - return { attachment_ids => [ $_[0] ] }; - } - }, - PUT => { - method => 'update_attachment', - params => sub { - return { ids => [ $_[0] ] }; - } - } + my $rest_resources = [ + qr{^/bug$}, + { + GET => {method => 'search',}, + POST => {method => 'create', status_code => STATUS_CREATED} + }, + qr{^/bug/$}, + {GET => {method => 'get'}}, + qr{^/bug/([^/]+)$}, + { + GET => { + method => 'get', + params => sub { + return {ids => [$_[0]]}; + } + }, + PUT => { + method => 'update', + params => sub { + return {ids => [$_[0]]}; + } + } + }, + qr{^/bug/([^/]+)/comment$}, + { + GET => { + method => 'comments', + params => sub { + return {ids => [$_[0]]}; + } + }, + POST => { + method => 'add_comment', + params => sub { + return {id => $_[0]}; }, - qr{^/field/bug$}, { - GET => { - method => 'fields', - } + success_code => STATUS_CREATED + } + }, + qr{^/bug/comment/([^/]+)$}, + { + GET => { + method => 'comments', + params => sub { + return {comment_ids => [$_[0]]}; + } + } + }, + qr{^/bug/comment/tags/([^/]+)$}, + { + GET => { + method => 'search_comment_tags', + params => sub { + return {query => $_[0]}; }, - qr{^/field/bug/([^/]+)$}, { - GET => { - method => 'fields', - params => sub { - my $value = $_[0]; - my $param = 'names'; - $param = 'ids' if $value =~ /^\d+$/; - return { $param => [ $_[0] ] }; - } - } + }, + }, + qr{^/bug/comment/([^/]+)/tags$}, + { + PUT => { + method => 'update_comment_tags', + params => sub { + return {comment_id => $_[0]}; }, - qr{^/field/bug/([^/]+)/values$}, { - GET => { - method => 'legal_values', - params => sub { - return { field => $_[0] }; - } - } + }, + }, + qr{^/bug/([^/]+)/history$}, + { + GET => { + method => 'history', + params => sub { + return {ids => [$_[0]]}; }, - qr{^/field/bug/([^/]+)/([^/]+)/values$}, { - GET => { - method => 'legal_values', - params => sub { - return { field => $_[0], - product_id => $_[1] }; - } - } + } + }, + qr{^/bug/([^/]+)/attachment$}, + { + GET => { + method => 'attachments', + params => sub { + return {ids => [$_[0]]}; + } + }, + POST => { + method => 'add_attachment', + params => sub { + return {ids => [$_[0]]}; }, - ]; - return $rest_resources; + success_code => STATUS_CREATED + } + }, + qr{^/bug/attachment/([^/]+)$}, + { + GET => { + method => 'attachments', + params => sub { + return {attachment_ids => [$_[0]]}; + } + }, + PUT => { + method => 'update_attachment', + params => sub { + return {ids => [$_[0]]}; + } + } + }, + qr{^/field/bug$}, + {GET => {method => 'fields',}}, + qr{^/field/bug/([^/]+)$}, + { + GET => { + method => 'fields', + params => sub { + my $value = $_[0]; + my $param = 'names'; + $param = 'ids' if $value =~ /^\d+$/; + return {$param => [$_[0]]}; + } + } + }, + qr{^/field/bug/([^/]+)/values$}, + { + GET => { + method => 'legal_values', + params => sub { + return {field => $_[0]}; + } + } + }, + qr{^/field/bug/([^/]+)/([^/]+)/values$}, + { + GET => { + method => 'legal_values', + params => sub { + return {field => $_[0], product_id => $_[1]}; + } + } + }, + ]; + return $rest_resources; } 1; diff --git a/Bugzilla/WebService/Server/REST/Resources/BugUserLastVisit.pm b/Bugzilla/WebService/Server/REST/Resources/BugUserLastVisit.pm index 8502d6b3b..806c3f9c7 100644 --- a/Bugzilla/WebService/Server/REST/Resources/BugUserLastVisit.pm +++ b/Bugzilla/WebService/Server/REST/Resources/BugUserLastVisit.pm @@ -12,27 +12,28 @@ use strict; use warnings; BEGIN { - *Bugzilla::WebService::BugUserLastVisit::rest_resources = \&_rest_resources; + *Bugzilla::WebService::BugUserLastVisit::rest_resources = \&_rest_resources; } sub _rest_resources { - return [ - # bug-id - qr{^/bug_user_last_visit/(\d+)$}, { - GET => { - method => 'get', - params => sub { - return { ids => [$_[0]] }; - }, - }, - POST => { - method => 'update', - params => sub { - return { ids => [$_[0]] }; - }, - }, + return [ + # bug-id + qr{^/bug_user_last_visit/(\d+)$}, + { + GET => { + method => 'get', + params => sub { + return {ids => [$_[0]]}; }, - ]; + }, + POST => { + method => 'update', + params => sub { + return {ids => [$_[0]]}; + }, + }, + }, + ]; } 1; diff --git a/Bugzilla/WebService/Server/REST/Resources/Bugzilla.pm b/Bugzilla/WebService/Server/REST/Resources/Bugzilla.pm index a8f3f9330..072cfe2f6 100644 --- a/Bugzilla/WebService/Server/REST/Resources/Bugzilla.pm +++ b/Bugzilla/WebService/Server/REST/Resources/Bugzilla.pm @@ -15,43 +15,19 @@ use Bugzilla::WebService::Constants; use Bugzilla::WebService::Bugzilla; BEGIN { - *Bugzilla::WebService::Bugzilla::rest_resources = \&_rest_resources; -}; + *Bugzilla::WebService::Bugzilla::rest_resources = \&_rest_resources; +} sub _rest_resources { - my $rest_resources = [ - qr{^/version$}, { - GET => { - method => 'version' - } - }, - qr{^/extensions$}, { - GET => { - method => 'extensions' - } - }, - qr{^/timezone$}, { - GET => { - method => 'timezone' - } - }, - qr{^/time$}, { - GET => { - method => 'time' - } - }, - qr{^/last_audit_time$}, { - GET => { - method => 'last_audit_time' - } - }, - qr{^/parameters$}, { - GET => { - method => 'parameters' - } - } - ]; - return $rest_resources; + my $rest_resources = [ + qr{^/version$}, {GET => {method => 'version'}}, + qr{^/extensions$}, {GET => {method => 'extensions'}}, + qr{^/timezone$}, {GET => {method => 'timezone'}}, + qr{^/time$}, {GET => {method => 'time'}}, + qr{^/last_audit_time$}, {GET => {method => 'last_audit_time'}}, + qr{^/parameters$}, {GET => {method => 'parameters'}} + ]; + return $rest_resources; } 1; diff --git a/Bugzilla/WebService/Server/REST/Resources/Classification.pm b/Bugzilla/WebService/Server/REST/Resources/Classification.pm index 3f8d32a03..ed65aea5c 100644 --- a/Bugzilla/WebService/Server/REST/Resources/Classification.pm +++ b/Bugzilla/WebService/Server/REST/Resources/Classification.pm @@ -15,22 +15,23 @@ use Bugzilla::WebService::Constants; use Bugzilla::WebService::Classification; BEGIN { - *Bugzilla::WebService::Classification::rest_resources = \&_rest_resources; -}; + *Bugzilla::WebService::Classification::rest_resources = \&_rest_resources; +} sub _rest_resources { - my $rest_resources = [ - qr{^/classification/([^/]+)$}, { - GET => { - method => 'get', - params => sub { - my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names'; - return { $param => [ $_[0] ] }; - } - } + my $rest_resources = [ + qr{^/classification/([^/]+)$}, + { + GET => { + method => 'get', + params => sub { + my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names'; + return {$param => [$_[0]]}; } - ]; - return $rest_resources; + } + } + ]; + return $rest_resources; } 1; diff --git a/Bugzilla/WebService/Server/REST/Resources/Component.pm b/Bugzilla/WebService/Server/REST/Resources/Component.pm index 198c09332..8870a0f04 100644 --- a/Bugzilla/WebService/Server/REST/Resources/Component.pm +++ b/Bugzilla/WebService/Server/REST/Resources/Component.pm @@ -17,19 +17,15 @@ use Bugzilla::WebService::Component; use Bugzilla::Error; BEGIN { - *Bugzilla::WebService::Component::rest_resources = \&_rest_resources; -}; + *Bugzilla::WebService::Component::rest_resources = \&_rest_resources; +} sub _rest_resources { - my $rest_resources = [ - qr{^/component$}, { - POST => { - method => 'create', - success_code => STATUS_CREATED - } - }, - ]; - return $rest_resources; + my $rest_resources = [ + qr{^/component$}, + {POST => {method => 'create', success_code => STATUS_CREATED}}, + ]; + return $rest_resources; } 1; diff --git a/Bugzilla/WebService/Server/REST/Resources/FlagType.pm b/Bugzilla/WebService/Server/REST/Resources/FlagType.pm index 21dad0f73..438c8fb30 100644 --- a/Bugzilla/WebService/Server/REST/Resources/FlagType.pm +++ b/Bugzilla/WebService/Server/REST/Resources/FlagType.pm @@ -17,43 +17,40 @@ use Bugzilla::WebService::FlagType; use Bugzilla::Error; BEGIN { - *Bugzilla::WebService::FlagType::rest_resources = \&_rest_resources; -}; + *Bugzilla::WebService::FlagType::rest_resources = \&_rest_resources; +} sub _rest_resources { - my $rest_resources = [ - qr{^/flag_type$}, { - POST => { - method => 'create', - success_code => STATUS_CREATED - } - }, - qr{^/flag_type/([^/]+)/([^/]+)$}, { - GET => { - method => 'get', - params => sub { - return { product => $_[0], - component => $_[1] }; - } - } - }, - qr{^/flag_type/([^/]+)$}, { - GET => { - method => 'get', - params => sub { - return { product => $_[0] }; - } - }, - PUT => { - method => 'update', - params => sub { - my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names'; - return { $param => [ $_[0] ] }; - } - } - }, - ]; - return $rest_resources; + my $rest_resources = [ + qr{^/flag_type$}, + {POST => {method => 'create', success_code => STATUS_CREATED}}, + qr{^/flag_type/([^/]+)/([^/]+)$}, + { + GET => { + method => 'get', + params => sub { + return {product => $_[0], component => $_[1]}; + } + } + }, + qr{^/flag_type/([^/]+)$}, + { + GET => { + method => 'get', + params => sub { + return {product => $_[0]}; + } + }, + PUT => { + method => 'update', + params => sub { + my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names'; + return {$param => [$_[0]]}; + } + } + }, + ]; + return $rest_resources; } 1; diff --git a/Bugzilla/WebService/Server/REST/Resources/Group.pm b/Bugzilla/WebService/Server/REST/Resources/Group.pm index b052e384b..7f607b7d1 100644 --- a/Bugzilla/WebService/Server/REST/Resources/Group.pm +++ b/Bugzilla/WebService/Server/REST/Resources/Group.pm @@ -15,31 +15,28 @@ use Bugzilla::WebService::Constants; use Bugzilla::WebService::Group; BEGIN { - *Bugzilla::WebService::Group::rest_resources = \&_rest_resources; -}; + *Bugzilla::WebService::Group::rest_resources = \&_rest_resources; +} sub _rest_resources { - my $rest_resources = [ - qr{^/group$}, { - GET => { - method => 'get' - }, - POST => { - method => 'create', - success_code => STATUS_CREATED - } - }, - qr{^/group/([^/]+)$}, { - PUT => { - method => 'update', - params => sub { - my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names'; - return { $param => [ $_[0] ] }; - } - } + my $rest_resources = [ + qr{^/group$}, + { + GET => {method => 'get'}, + POST => {method => 'create', success_code => STATUS_CREATED} + }, + qr{^/group/([^/]+)$}, + { + PUT => { + method => 'update', + params => sub { + my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names'; + return {$param => [$_[0]]}; } - ]; - return $rest_resources; + } + } + ]; + return $rest_resources; } 1; diff --git a/Bugzilla/WebService/Server/REST/Resources/Product.pm b/Bugzilla/WebService/Server/REST/Resources/Product.pm index 607b94b53..eabe19681 100644 --- a/Bugzilla/WebService/Server/REST/Resources/Product.pm +++ b/Bugzilla/WebService/Server/REST/Resources/Product.pm @@ -17,53 +17,41 @@ use Bugzilla::WebService::Product; use Bugzilla::Error; BEGIN { - *Bugzilla::WebService::Product::rest_resources = \&_rest_resources; -}; + *Bugzilla::WebService::Product::rest_resources = \&_rest_resources; +} sub _rest_resources { - my $rest_resources = [ - qr{^/product_accessible$}, { - GET => { - method => 'get_accessible_products' - } - }, - qr{^/product_enterable$}, { - GET => { - method => 'get_enterable_products' - } - }, - qr{^/product_selectable$}, { - GET => { - method => 'get_selectable_products' - } - }, - qr{^/product$}, { - GET => { - method => 'get' - }, - POST => { - method => 'create', - success_code => STATUS_CREATED - } - }, - qr{^/product/([^/]+)$}, { - GET => { - method => 'get', - params => sub { - my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names'; - return { $param => [ $_[0] ] }; - } - }, - PUT => { - method => 'update', - params => sub { - my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names'; - return { $param => [ $_[0] ] }; - } - } - }, - ]; - return $rest_resources; + my $rest_resources = [ + qr{^/product_accessible$}, + {GET => {method => 'get_accessible_products'}}, + qr{^/product_enterable$}, + {GET => {method => 'get_enterable_products'}}, + qr{^/product_selectable$}, + {GET => {method => 'get_selectable_products'}}, + qr{^/product$}, + { + GET => {method => 'get'}, + POST => {method => 'create', success_code => STATUS_CREATED} + }, + qr{^/product/([^/]+)$}, + { + GET => { + method => 'get', + params => sub { + my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names'; + return {$param => [$_[0]]}; + } + }, + PUT => { + method => 'update', + params => sub { + my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names'; + return {$param => [$_[0]]}; + } + } + }, + ]; + return $rest_resources; } 1; diff --git a/Bugzilla/WebService/Server/REST/Resources/User.pm b/Bugzilla/WebService/Server/REST/Resources/User.pm index a83109e73..4555b4dbc 100644 --- a/Bugzilla/WebService/Server/REST/Resources/User.pm +++ b/Bugzilla/WebService/Server/REST/Resources/User.pm @@ -15,53 +15,41 @@ use Bugzilla::WebService::Constants; use Bugzilla::WebService::User; BEGIN { - *Bugzilla::WebService::User::rest_resources = \&_rest_resources; -}; + *Bugzilla::WebService::User::rest_resources = \&_rest_resources; +} sub _rest_resources { - my $rest_resources = [ - qr{^/login$}, { - GET => { - method => 'login' - } - }, - qr{^/logout$}, { - GET => { - method => 'logout' - } - }, - qr{^/valid_login$}, { - GET => { - method => 'valid_login' - } - }, - qr{^/user$}, { - GET => { - method => 'get' - }, - POST => { - method => 'create', - success_code => STATUS_CREATED - } - }, - qr{^/user/([^/]+)$}, { - GET => { - method => 'get', - params => sub { - my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names'; - return { $param => [ $_[0] ] }; - } - }, - PUT => { - method => 'update', - params => sub { - my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names'; - return { $param => [ $_[0] ] }; - } - } + my $rest_resources = [ + qr{^/login$}, + {GET => {method => 'login'}}, + qr{^/logout$}, + {GET => {method => 'logout'}}, + qr{^/valid_login$}, + {GET => {method => 'valid_login'}}, + qr{^/user$}, + { + GET => {method => 'get'}, + POST => {method => 'create', success_code => STATUS_CREATED} + }, + qr{^/user/([^/]+)$}, + { + GET => { + method => 'get', + params => sub { + my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names'; + return {$param => [$_[0]]}; + } + }, + PUT => { + method => 'update', + params => sub { + my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names'; + return {$param => [$_[0]]}; } - ]; - return $rest_resources; + } + } + ]; + return $rest_resources; } 1; diff --git a/Bugzilla/WebService/Server/XMLRPC.pm b/Bugzilla/WebService/Server/XMLRPC.pm index 8deb253ad..b0eae8e19 100644 --- a/Bugzilla/WebService/Server/XMLRPC.pm +++ b/Bugzilla/WebService/Server/XMLRPC.pm @@ -14,9 +14,10 @@ use warnings; use XMLRPC::Transport::HTTP; use Bugzilla::WebService::Server; if ($ENV{MOD_PERL}) { - our @ISA = qw(XMLRPC::Transport::HTTP::Apache Bugzilla::WebService::Server); -} else { - our @ISA = qw(XMLRPC::Transport::HTTP::CGI Bugzilla::WebService::Server); + our @ISA = qw(XMLRPC::Transport::HTTP::Apache Bugzilla::WebService::Server); +} +else { + our @ISA = qw(XMLRPC::Transport::HTTP::CGI Bugzilla::WebService::Server); } use Bugzilla::WebService::Constants; @@ -26,97 +27,99 @@ use Bugzilla::Util; use List::MoreUtils qw(none); BEGIN { - # Allow WebService methods to call XMLRPC::Lite's type method directly - *Bugzilla::WebService::type = sub { - my ($self, $type, $value) = @_; - if ($type eq 'dateTime') { - # This is the XML-RPC implementation, see the README in Bugzilla/WebService/. - # Our "base" implementation is in Bugzilla::WebService::Server. - $value = Bugzilla::WebService::Server->datetime_format_outbound($value); - $value =~ s/-//g; - } - elsif ($type eq 'email') { - $type = 'string'; - if (Bugzilla->params->{'webservice_email_filter'}) { - $value = email_filter($value); - } - } - return XMLRPC::Data->type($type)->value($value); - }; - - # Add support for ETags into XMLRPC WebServices - *Bugzilla::WebService::bz_etag = sub { - return Bugzilla::WebService::Server->bz_etag($_[1]); - }; + # Allow WebService methods to call XMLRPC::Lite's type method directly + *Bugzilla::WebService::type = sub { + my ($self, $type, $value) = @_; + if ($type eq 'dateTime') { + + # This is the XML-RPC implementation, see the README in Bugzilla/WebService/. + # Our "base" implementation is in Bugzilla::WebService::Server. + $value = Bugzilla::WebService::Server->datetime_format_outbound($value); + $value =~ s/-//g; + } + elsif ($type eq 'email') { + $type = 'string'; + if (Bugzilla->params->{'webservice_email_filter'}) { + $value = email_filter($value); + } + } + return XMLRPC::Data->type($type)->value($value); + }; + + # Add support for ETags into XMLRPC WebServices + *Bugzilla::WebService::bz_etag = sub { + return Bugzilla::WebService::Server->bz_etag($_[1]); + }; } sub initialize { - my $self = shift; - my %retval = $self->SUPER::initialize(@_); - $retval{'serializer'} = Bugzilla::XMLRPC::Serializer->new; - $retval{'deserializer'} = Bugzilla::XMLRPC::Deserializer->new; - $retval{'dispatch_with'} = WS_DISPATCH; - return %retval; + my $self = shift; + my %retval = $self->SUPER::initialize(@_); + $retval{'serializer'} = Bugzilla::XMLRPC::Serializer->new; + $retval{'deserializer'} = Bugzilla::XMLRPC::Deserializer->new; + $retval{'dispatch_with'} = WS_DISPATCH; + return %retval; } sub make_response { - my $self = shift; - my $cgi = Bugzilla->cgi; - - # Fix various problems with IIS. - if ($ENV{'SERVER_SOFTWARE'} =~ /IIS/) { - $ENV{CONTENT_LENGTH} = 0; - binmode(STDOUT, ':bytes'); - } - - $self->SUPER::make_response(@_); - - # XMLRPC::Transport::HTTP::CGI doesn't know about Bugzilla carrying around - # its cookies in Bugzilla::CGI, so we need to copy them over. - foreach my $cookie (@{$cgi->{'Bugzilla_cookie_list'}}) { - $self->response->headers->push_header('Set-Cookie', $cookie); - } - - # Copy across security related headers from Bugzilla::CGI - foreach my $header (split(/[\r\n]+/, $cgi->header)) { - my ($name, $value) = $header =~ /^([^:]+): (.*)/; - if (!$self->response->headers->header($name)) { - $self->response->headers->header($name => $value); - } - } - - # ETag support - my $etag = $self->bz_etag; - if (!$etag) { - my $data = $self->response->as_string; - $etag = $self->bz_etag($data); - } - - if ($etag && $cgi->check_etag($etag)) { - $self->response->headers->push_header('ETag', $etag); - $self->response->headers->push_header('status', '304 Not Modified'); - } - elsif ($etag) { - $self->response->headers->push_header('ETag', $etag); + my $self = shift; + my $cgi = Bugzilla->cgi; + + # Fix various problems with IIS. + if ($ENV{'SERVER_SOFTWARE'} =~ /IIS/) { + $ENV{CONTENT_LENGTH} = 0; + binmode(STDOUT, ':bytes'); + } + + $self->SUPER::make_response(@_); + + # XMLRPC::Transport::HTTP::CGI doesn't know about Bugzilla carrying around + # its cookies in Bugzilla::CGI, so we need to copy them over. + foreach my $cookie (@{$cgi->{'Bugzilla_cookie_list'}}) { + $self->response->headers->push_header('Set-Cookie', $cookie); + } + + # Copy across security related headers from Bugzilla::CGI + foreach my $header (split(/[\r\n]+/, $cgi->header)) { + my ($name, $value) = $header =~ /^([^:]+): (.*)/; + if (!$self->response->headers->header($name)) { + $self->response->headers->header($name => $value); } + } + + # ETag support + my $etag = $self->bz_etag; + if (!$etag) { + my $data = $self->response->as_string; + $etag = $self->bz_etag($data); + } + + if ($etag && $cgi->check_etag($etag)) { + $self->response->headers->push_header('ETag', $etag); + $self->response->headers->push_header('status', '304 Not Modified'); + } + elsif ($etag) { + $self->response->headers->push_header('ETag', $etag); + } } sub handle_login { - my ($self, $classes, $action, $uri, $method) = @_; - my $class = $classes->{$uri}; - my $full_method = $uri . "." . $method; - # Only allowed methods to be used from the module's whitelist - my $file = $class; - $file =~ s{::}{/}g; - $file .= ".pm"; - require $file; - if (none { $_ eq $method } $class->PUBLIC_METHODS) { - ThrowCodeError('unknown_method', { method => $full_method }); - } - - $ENV{CONTENT_LENGTH} = 0 if $ENV{'SERVER_SOFTWARE'} =~ /IIS/; - $self->SUPER::handle_login($class, $method, $full_method); - return; + my ($self, $classes, $action, $uri, $method) = @_; + my $class = $classes->{$uri}; + my $full_method = $uri . "." . $method; + + # Only allowed methods to be used from the module's whitelist + my $file = $class; + $file =~ s{::}{/}g; + $file .= ".pm"; + require $file; + if (none { $_ eq $method } $class->PUBLIC_METHODS) { + ThrowCodeError('unknown_method', {method => $full_method}); + } + + $ENV{CONTENT_LENGTH} = 0 if $ENV{'SERVER_SOFTWARE'} =~ /IIS/; + $self->SUPER::handle_login($class, $method, $full_method); + return; } 1; @@ -140,100 +143,111 @@ use Bugzilla::WebService::Util qw(fix_credentials); use Scalar::Util qw(tainted); sub new { - my $self = shift->SUPER::new(@_); - # Initialise XML::Parser to not expand references to entities, to prevent DoS - require XML::Parser; - my $parser = XML::Parser->new( NoExpand => 1, Handlers => { Default => sub {} } ); - $self->{_parser}->parser($parser, $parser); - return $self; + my $self = shift->SUPER::new(@_); + + # Initialise XML::Parser to not expand references to entities, to prevent DoS + require XML::Parser; + my $parser = XML::Parser->new( + NoExpand => 1, + Handlers => { + Default => sub { } + } + ); + $self->{_parser}->parser($parser, $parser); + return $self; } sub deserialize { - my $self = shift; - - # Only allow certain content types to protect against CSRF attacks - my $content_type = lc($ENV{'CONTENT_TYPE'}); - # Remove charset, etc, if provided - $content_type =~ s/^([^;]+);.*/$1/; - if (!grep($_ eq $content_type, XMLRPC_CONTENT_TYPE_WHITELIST)) { - ThrowUserError('xmlrpc_illegal_content_type', - { content_type => $ENV{'CONTENT_TYPE'} }); - } + my $self = shift; - my ($xml) = @_; - my $som = $self->SUPER::deserialize(@_); - if (tainted($xml)) { - $som->{_bz_do_taint} = 1; - } - bless $som, 'Bugzilla::XMLRPC::SOM'; - my $params = $som->paramsin; - # This allows positional parameters for Testopia. - $params = {} if ref $params ne 'HASH'; + # Only allow certain content types to protect against CSRF attacks + my $content_type = lc($ENV{'CONTENT_TYPE'}); + + # Remove charset, etc, if provided + $content_type =~ s/^([^;]+);.*/$1/; + if (!grep($_ eq $content_type, XMLRPC_CONTENT_TYPE_WHITELIST)) { + ThrowUserError('xmlrpc_illegal_content_type', + {content_type => $ENV{'CONTENT_TYPE'}}); + } - # Update the params to allow for several convenience key/values - # use for authentication - fix_credentials($params); + my ($xml) = @_; + my $som = $self->SUPER::deserialize(@_); + if (tainted($xml)) { + $som->{_bz_do_taint} = 1; + } + bless $som, 'Bugzilla::XMLRPC::SOM'; + my $params = $som->paramsin; - Bugzilla->input_params($params); + # This allows positional parameters for Testopia. + $params = {} if ref $params ne 'HASH'; - return $som; + # Update the params to allow for several convenience key/values + # use for authentication + fix_credentials($params); + + Bugzilla->input_params($params); + + return $som; } # Some method arguments need to be converted in some way, when they are input. sub decode_value { - my $self = shift; - my ($type) = @{ $_[0] }; - my $value = $self->SUPER::decode_value(@_); - - # We only validate/convert certain types here. - return $value if $type !~ /^(?:int|i4|boolean|double|dateTime\.iso8601)$/; - - # Though the XML-RPC standard doesn't allow an empty, - # ,or , we do, and we just say - # "that's undef". - if (grep($type eq $_, qw(int double dateTime))) { - return undef if $value eq ''; - } - - my $validator = $self->_validation_subs->{$type}; - if (!$validator->($value)) { - ThrowUserError('xmlrpc_invalid_value', - { type => $type, value => $value }); - } - - # We convert dateTimes to a DB-friendly date format. - if ($type eq 'dateTime.iso8601') { - if ($value !~ /T.*[\-+Z]/i) { - # The caller did not specify a timezone, so we assume UTC. - # pass 'Z' specifier to datetime_from to force it - $value = $value . 'Z'; - } - $value = Bugzilla::WebService::Server::XMLRPC->datetime_format_inbound($value); + my $self = shift; + my ($type) = @{$_[0]}; + my $value = $self->SUPER::decode_value(@_); + + # We only validate/convert certain types here. + return $value if $type !~ /^(?:int|i4|boolean|double|dateTime\.iso8601)$/; + + # Though the XML-RPC standard doesn't allow an empty , + # ,or , we do, and we just say + # "that's undef". + if (grep($type eq $_, qw(int double dateTime))) { + return undef if $value eq ''; + } + + my $validator = $self->_validation_subs->{$type}; + if (!$validator->($value)) { + ThrowUserError('xmlrpc_invalid_value', {type => $type, value => $value}); + } + + # We convert dateTimes to a DB-friendly date format. + if ($type eq 'dateTime.iso8601') { + if ($value !~ /T.*[\-+Z]/i) { + + # The caller did not specify a timezone, so we assume UTC. + # pass 'Z' specifier to datetime_from to force it + $value = $value . 'Z'; } + $value = Bugzilla::WebService::Server::XMLRPC->datetime_format_inbound($value); + } - return $value; + return $value; } sub _validation_subs { - my $self = shift; - return $self->{_validation_subs} if $self->{_validation_subs}; - # The only place that XMLRPC::Lite stores any sort of validation - # regex is in XMLRPC::Serializer. We want to re-use those regexes here. - my $lookup = Bugzilla::XMLRPC::Serializer->new->typelookup; - - # $lookup is a hash whose values are arrayrefs, and whose keys are the - # names of types. The second item of each arrayref is a subroutine - # that will do our validation for us. - my %validators = map { $_ => $lookup->{$_}->[1] } (keys %$lookup); - # Add a boolean validator - $validators{'boolean'} = sub {$_[0] =~ /^[01]$/}; - # Some types have multiple names, or have a different name in - # XMLRPC::Serializer than their standard XML-RPC name. - $validators{'dateTime.iso8601'} = $validators{'dateTime'}; - $validators{'i4'} = $validators{'int'}; - - $self->{_validation_subs} = \%validators; - return \%validators; + my $self = shift; + return $self->{_validation_subs} if $self->{_validation_subs}; + + # The only place that XMLRPC::Lite stores any sort of validation + # regex is in XMLRPC::Serializer. We want to re-use those regexes here. + my $lookup = Bugzilla::XMLRPC::Serializer->new->typelookup; + + # $lookup is a hash whose values are arrayrefs, and whose keys are the + # names of types. The second item of each arrayref is a subroutine + # that will do our validation for us. + my %validators = map { $_ => $lookup->{$_}->[1] } (keys %$lookup); + + # Add a boolean validator + $validators{'boolean'} = sub { $_[0] =~ /^[01]$/ }; + + # Some types have multiple names, or have a different name in + # XMLRPC::Serializer than their standard XML-RPC name. + $validators{'dateTime.iso8601'} = $validators{'dateTime'}; + $validators{'i4'} = $validators{'int'}; + + $self->{_validation_subs} = \%validators; + return \%validators; } 1; @@ -249,16 +263,16 @@ our @ISA = qw(XMLRPC::SOM); use Bugzilla::WebService::Util qw(taint_data); sub paramsin { - my $self = shift; - if (!$self->{bz_params_in}) { - my @params = $self->SUPER::paramsin(@_); - if ($self->{_bz_do_taint}) { - taint_data(@params); - } - $self->{bz_params_in} = \@params; + my $self = shift; + if (!$self->{bz_params_in}) { + my @params = $self->SUPER::paramsin(@_); + if ($self->{_bz_do_taint}) { + taint_data(@params); } - my $params = $self->{bz_params_in}; - return wantarray ? @$params : $params->[0]; + $self->{bz_params_in} = \@params; + } + my $params = $self->{bz_params_in}; + return wantarray ? @$params : $params->[0]; } 1; @@ -272,43 +286,46 @@ use strict; use warnings; use Scalar::Util qw(blessed reftype); + # We can't use "use parent" because XMLRPC::Serializer doesn't return # a true value. use XMLRPC::Lite; our @ISA = qw(XMLRPC::Serializer); sub new { - my $class = shift; - my $self = $class->SUPER::new(@_); - # This fixes UTF-8. - $self->{'_typelookup'}->{'base64'} = - [10, sub { !utf8::is_utf8($_[0]) && $_[0] =~ /[^\x09\x0a\x0d\x20-\x7f]/}, - 'as_base64']; - # This makes arrays work right even though we're a subclass. - # (See http://rt.cpan.org//Ticket/Display.html?id=34514) - $self->{'_encodingStyle'} = ''; - return $self; + my $class = shift; + my $self = $class->SUPER::new(@_); + + # This fixes UTF-8. + $self->{'_typelookup'}->{'base64'} = [ + 10, sub { !utf8::is_utf8($_[0]) && $_[0] =~ /[^\x09\x0a\x0d\x20-\x7f]/ }, + 'as_base64' + ]; + + # This makes arrays work right even though we're a subclass. + # (See http://rt.cpan.org//Ticket/Display.html?id=34514) + $self->{'_encodingStyle'} = ''; + return $self; } # Here the XMLRPC::Serializer is extended to use the XMLRPC nil extension. sub encode_object { - my $self = shift; - my @encoded = $self->SUPER::encode_object(@_); + my $self = shift; + my @encoded = $self->SUPER::encode_object(@_); - return $encoded[0]->[0] eq 'nil' - ? ['value', {}, [@encoded]] - : @encoded; + return $encoded[0]->[0] eq 'nil' ? ['value', {}, [@encoded]] : @encoded; } # Removes undefined values so they do not produce invalid XMLRPC. sub envelope { - my $self = shift; - my ($type, $method, $data) = @_; - # If the type isn't a successful response we don't want to change the values. - if ($type eq 'response') { - _strip_undefs($data); - } - return $self->SUPER::envelope($type, $method, $data); + my $self = shift; + my ($type, $method, $data) = @_; + + # If the type isn't a successful response we don't want to change the values. + if ($type eq 'response') { + _strip_undefs($data); + } + return $self->SUPER::envelope($type, $method, $data); } # In an XMLRPC response we have to handle hashes of arrays, hashes, scalars, @@ -316,58 +333,58 @@ sub envelope { # The whole XMLRPC::Data object must be removed if its value key is undefined # so it cannot be recursed like the other hash type objects. sub _strip_undefs { - my ($initial) = @_; - my $type = reftype($initial) or return; - - if ($type eq "HASH") { - while (my ($key, $value) = each(%$initial)) { - if ( !defined $value - || (blessed $value && $value->isa('XMLRPC::Data') && !defined $value->value) ) - { - # If the value is undefined remove it from the hash. - delete $initial->{$key}; - } - else { - _strip_undefs($value); - } - } + my ($initial) = @_; + my $type = reftype($initial) or return; + + if ($type eq "HASH") { + while (my ($key, $value) = each(%$initial)) { + if (!defined $value + || (blessed $value && $value->isa('XMLRPC::Data') && !defined $value->value)) + { + # If the value is undefined remove it from the hash. + delete $initial->{$key}; + } + else { + _strip_undefs($value); + } } - elsif ($type eq "ARRAY") { - for (my $count = 0; $count < scalar @{$initial}; $count++) { - my $value = $initial->[$count]; - if ( !defined $value - || (blessed $value && $value->isa('XMLRPC::Data') && !defined $value->value) ) - { - # If the value is undefined remove it from the array. - splice(@$initial, $count, 1); - $count--; - } - else { - _strip_undefs($value); - } - } + } + elsif ($type eq "ARRAY") { + for (my $count = 0; $count < scalar @{$initial}; $count++) { + my $value = $initial->[$count]; + if (!defined $value + || (blessed $value && $value->isa('XMLRPC::Data') && !defined $value->value)) + { + # If the value is undefined remove it from the array. + splice(@$initial, $count, 1); + $count--; + } + else { + _strip_undefs($value); + } } + } } sub BEGIN { - no strict 'refs'; - for my $type (qw(double i4 int dateTime)) { - my $method = 'as_' . $type; - *$method = sub { - my ($self, $value) = @_; - if (!defined($value)) { - return as_nil(); - } - else { - my $super_method = "SUPER::$method"; - return $self->$super_method($value); - } - } - } + no strict 'refs'; + for my $type (qw(double i4 int dateTime)) { + my $method = 'as_' . $type; + *$method = sub { + my ($self, $value) = @_; + if (!defined($value)) { + return as_nil(); + } + else { + my $super_method = "SUPER::$method"; + return $self->$super_method($value); + } + } + } } sub as_nil { - return ['nil', {}]; + return ['nil', {}]; } 1; diff --git a/Bugzilla/WebService/User.pm b/Bugzilla/WebService/User.pm index 0ae76d70f..3fcc929ce 100644 --- a/Bugzilla/WebService/User.pm +++ b/Bugzilla/WebService/User.pm @@ -18,40 +18,35 @@ use Bugzilla::Error; use Bugzilla::Group; use Bugzilla::User; use Bugzilla::Util qw(trim detaint_natural); -use Bugzilla::WebService::Util qw(filter filter_wants validate translate params_to_objects); +use Bugzilla::WebService::Util + qw(filter filter_wants validate translate params_to_objects); use List::Util qw(first min); # Don't need auth to login -use constant LOGIN_EXEMPT => { - login => 1, - offer_account_by_email => 1, -}; +use constant LOGIN_EXEMPT => {login => 1, offer_account_by_email => 1,}; use constant READ_ONLY => qw( - get + get ); use constant PUBLIC_METHODS => qw( - create - get - login - logout - offer_account_by_email - update - valid_login + create + get + login + logout + offer_account_by_email + update + valid_login ); -use constant MAPPED_FIELDS => { - email => 'login', - full_name => 'name', - login_denied_text => 'disabledtext', -}; +use constant MAPPED_FIELDS => + {email => 'login', full_name => 'name', login_denied_text => 'disabledtext',}; use constant MAPPED_RETURNS => { - login_name => 'email', - realname => 'full_name', - disabledtext => 'login_denied_text', + login_name => 'email', + realname => 'full_name', + disabledtext => 'login_denied_text', }; ############## @@ -59,38 +54,38 @@ use constant MAPPED_RETURNS => { ############## sub login { - my ($self, $params) = @_; + my ($self, $params) = @_; - # Check to see if we are already logged in - my $user = Bugzilla->user; - if ($user->id) { - return $self->_login_to_hash($user); - } + # Check to see if we are already logged in + my $user = Bugzilla->user; + if ($user->id) { + return $self->_login_to_hash($user); + } - # Username and password params are required - foreach my $param ("login", "password") { - (defined $params->{$param} || defined $params->{'Bugzilla_' . $param}) - || ThrowCodeError('param_required', { param => $param }); - } + # Username and password params are required + foreach my $param ("login", "password") { + (defined $params->{$param} || defined $params->{'Bugzilla_' . $param}) + || ThrowCodeError('param_required', {param => $param}); + } - $user = Bugzilla->login(); - return $self->_login_to_hash($user); + $user = Bugzilla->login(); + return $self->_login_to_hash($user); } sub logout { - my $self = shift; - Bugzilla->logout; + my $self = shift; + Bugzilla->logout; } sub valid_login { - my ($self, $params) = @_; - defined $params->{login} - || ThrowCodeError('param_required', { param => 'login' }); - Bugzilla->login(); - if (Bugzilla->user->id && Bugzilla->user->login eq $params->{login}) { - return $self->type('boolean', 1); - } - return $self->type('boolean', 0); + my ($self, $params) = @_; + defined $params->{login} + || ThrowCodeError('param_required', {param => 'login'}); + Bugzilla->login(); + if (Bugzilla->user->id && Bugzilla->user->login eq $params->{login}) { + return $self->type('boolean', 1); + } + return $self->type('boolean', 0); } ################# @@ -98,168 +93,169 @@ sub valid_login { ################# sub offer_account_by_email { - my $self = shift; - my ($params) = @_; - my $email = trim($params->{email}) - || ThrowCodeError('param_required', { param => 'email' }); - - Bugzilla->user->check_account_creation_enabled; - Bugzilla->user->check_and_send_account_creation_confirmation($email); - return undef; + my $self = shift; + my ($params) = @_; + my $email = trim($params->{email}) + || ThrowCodeError('param_required', {param => 'email'}); + + Bugzilla->user->check_account_creation_enabled; + Bugzilla->user->check_and_send_account_creation_confirmation($email); + return undef; } sub create { - my $self = shift; - my ($params) = @_; - - Bugzilla->user->in_group('editusers') - || ThrowUserError("auth_failure", { group => "editusers", - action => "add", - object => "users"}); - - my $email = trim($params->{email}) - || ThrowCodeError('param_required', { param => 'email' }); - my $realname = trim($params->{full_name}); - my $password = trim($params->{password}) || '*'; - - my $user = Bugzilla::User->create({ - login_name => $email, - realname => $realname, - cryptpassword => $password - }); - - return { id => $self->type('int', $user->id) }; + my $self = shift; + my ($params) = @_; + + Bugzilla->user->in_group('editusers') + || ThrowUserError("auth_failure", + {group => "editusers", action => "add", object => "users"}); + + my $email = trim($params->{email}) + || ThrowCodeError('param_required', {param => 'email'}); + my $realname = trim($params->{full_name}); + my $password = trim($params->{password}) || '*'; + + my $user = Bugzilla::User->create( + {login_name => $email, realname => $realname, cryptpassword => $password}); + + return {id => $self->type('int', $user->id)}; } -# function to return user information by passing either user ids or +# function to return user information by passing either user ids or # login names or both together: -# $call = $rpc->call( 'User.get', { ids => [1,2,3], +# $call = $rpc->call( 'User.get', { ids => [1,2,3], # names => ['testusera@redhat.com', 'testuserb@redhat.com'] }); sub get { - my ($self, $params) = validate(@_, 'names', 'ids', 'match', 'group_ids', 'groups'); - - Bugzilla->switch_to_shadow_db(); - - defined($params->{names}) || defined($params->{ids}) - || defined($params->{match}) - || ThrowCodeError('params_required', - { function => 'User.get', params => ['ids', 'names', 'match'] }); - - my @user_objects; - @user_objects = map { Bugzilla::User->check($_) } @{ $params->{names} } - if $params->{names}; - - # start filtering to remove duplicate user ids - my %unique_users = map { $_->id => $_ } @user_objects; - @user_objects = values %unique_users; - - my @users; - - # If the user is not logged in: Return an error if they passed any user ids. - # Otherwise, return a limited amount of information based on login names. - if (!Bugzilla->user->id){ - if ($params->{ids}){ - ThrowUserError("user_access_by_id_denied"); - } - if ($params->{match}) { - ThrowUserError('user_access_by_match_denied'); - } - my $in_group = $self->_filter_users_by_group( - \@user_objects, $params); - @users = map { filter $params, { - id => $self->type('int', $_->id), - real_name => $self->type('string', $_->name), - name => $self->type('email', $_->login), - } } @$in_group; - - return { users => \@users }; - } + my ($self, $params) + = validate(@_, 'names', 'ids', 'match', 'group_ids', 'groups'); - my $obj_by_ids; - $obj_by_ids = Bugzilla::User->new_from_list($params->{ids}) if $params->{ids}; - - # obj_by_ids are only visible to the user if they can see - # the otheruser, for non visible otheruser throw an error - foreach my $obj (@$obj_by_ids) { - if (Bugzilla->user->can_see_user($obj)){ - if (!$unique_users{$obj->id}) { - push (@user_objects, $obj); - $unique_users{$obj->id} = $obj; - } - } - else { - ThrowUserError('auth_failure', {reason => "not_visible", - action => "access", - object => "user", - userid => $obj->id}); - } - } + Bugzilla->switch_to_shadow_db(); - # User Matching - my $limit = Bugzilla->params->{maxusermatches}; - if ($params->{limit}) { - detaint_natural($params->{limit}) - || ThrowCodeError('param_must_be_numeric', - { function => 'Bugzilla::WebService::User::match', - param => 'limit' }); - $limit = $limit ? min($params->{limit}, $limit) : $params->{limit}; - } + defined($params->{names}) + || defined($params->{ids}) + || defined($params->{match}) + || ThrowCodeError('params_required', + {function => 'User.get', params => ['ids', 'names', 'match']}); - my $exclude_disabled = $params->{'include_disabled'} ? 0 : 1; - foreach my $match_string (@{ $params->{'match'} || [] }) { - my $matched = Bugzilla::User::match($match_string, $limit, $exclude_disabled); - foreach my $user (@$matched) { - if (!$unique_users{$user->id}) { - push(@user_objects, $user); - $unique_users{$user->id} = $user; - } - } - } + my @user_objects; + @user_objects = map { Bugzilla::User->check($_) } @{$params->{names}} + if $params->{names}; + + # start filtering to remove duplicate user ids + my %unique_users = map { $_->id => $_ } @user_objects; + @user_objects = values %unique_users; + my @users; + + # If the user is not logged in: Return an error if they passed any user ids. + # Otherwise, return a limited amount of information based on login names. + if (!Bugzilla->user->id) { + if ($params->{ids}) { + ThrowUserError("user_access_by_id_denied"); + } + if ($params->{match}) { + ThrowUserError('user_access_by_match_denied'); + } my $in_group = $self->_filter_users_by_group(\@user_objects, $params); - foreach my $user (@$in_group) { - my $user_info = filter $params, { - id => $self->type('int', $user->id), - real_name => $self->type('string', $user->name), - name => $self->type('email', $user->login), - email => $self->type('email', $user->email), - can_login => $self->type('boolean', $user->is_enabled ? 1 : 0), - }; - - if (Bugzilla->user->in_group('editusers')) { - $user_info->{email_enabled} = $self->type('boolean', $user->email_enabled); - $user_info->{login_denied_text} = $self->type('string', $user->disabledtext); + @users = map { + filter $params, + { + id => $self->type('int', $_->id), + real_name => $self->type('string', $_->name), + name => $self->type('email', $_->login), } - - if (Bugzilla->user->id == $user->id) { - if (filter_wants($params, 'saved_searches')) { - $user_info->{saved_searches} = [ - map { $self->_query_to_hash($_) } @{ $user->queries } - ]; - } - if (filter_wants($params, 'saved_reports')) { - $user_info->{saved_reports} = [ - map { $self->_report_to_hash($_) } @{ $user->reports } - ]; - } + } @$in_group; + + return {users => \@users}; + } + + my $obj_by_ids; + $obj_by_ids = Bugzilla::User->new_from_list($params->{ids}) if $params->{ids}; + + # obj_by_ids are only visible to the user if they can see + # the otheruser, for non visible otheruser throw an error + foreach my $obj (@$obj_by_ids) { + if (Bugzilla->user->can_see_user($obj)) { + if (!$unique_users{$obj->id}) { + push(@user_objects, $obj); + $unique_users{$obj->id} = $obj; + } + } + else { + ThrowUserError( + 'auth_failure', + { + reason => "not_visible", + action => "access", + object => "user", + userid => $obj->id } + ); + } + } + + # User Matching + my $limit = Bugzilla->params->{maxusermatches}; + if ($params->{limit}) { + detaint_natural($params->{limit}) + || ThrowCodeError('param_must_be_numeric', + {function => 'Bugzilla::WebService::User::match', param => 'limit'}); + $limit = $limit ? min($params->{limit}, $limit) : $params->{limit}; + } + + my $exclude_disabled = $params->{'include_disabled'} ? 0 : 1; + foreach my $match_string (@{$params->{'match'} || []}) { + my $matched = Bugzilla::User::match($match_string, $limit, $exclude_disabled); + foreach my $user (@$matched) { + if (!$unique_users{$user->id}) { + push(@user_objects, $user); + $unique_users{$user->id} = $user; + } + } + } + + my $in_group = $self->_filter_users_by_group(\@user_objects, $params); + foreach my $user (@$in_group) { + my $user_info = filter $params, + { + id => $self->type('int', $user->id), + real_name => $self->type('string', $user->name), + name => $self->type('email', $user->login), + email => $self->type('email', $user->email), + can_login => $self->type('boolean', $user->is_enabled ? 1 : 0), + }; + + if (Bugzilla->user->in_group('editusers')) { + $user_info->{email_enabled} = $self->type('boolean', $user->email_enabled); + $user_info->{login_denied_text} = $self->type('string', $user->disabledtext); + } - if (filter_wants($params, 'groups')) { - if (Bugzilla->user->id == $user->id || Bugzilla->user->in_group('editusers')) { - $user_info->{groups} = [ - map { $self->_group_to_hash($_) } @{ $user->groups } - ]; - } - else { - $user_info->{groups} = $self->_filter_bless_groups($user->groups); - } - } + if (Bugzilla->user->id == $user->id) { + if (filter_wants($params, 'saved_searches')) { + $user_info->{saved_searches} + = [map { $self->_query_to_hash($_) } @{$user->queries}]; + } + if (filter_wants($params, 'saved_reports')) { + $user_info->{saved_reports} + = [map { $self->_report_to_hash($_) } @{$user->reports}]; + } + } - push(@users, $user_info); + if (filter_wants($params, 'groups')) { + if (Bugzilla->user->id == $user->id || Bugzilla->user->in_group('editusers')) { + $user_info->{groups} = [map { $self->_group_to_hash($_) } @{$user->groups}]; + } + else { + $user_info->{groups} = $self->_filter_bless_groups($user->groups); + } } - return { users => \@users }; + push(@users, $user_info); + } + + return {users => \@users}; } ############### @@ -267,156 +263,157 @@ 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') - || 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') + || 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(); - foreach my $user (@$user_objects){ - $user->set_all($values); - } + # We delete names and ids to keep only new values to set. + delete $values->{names}; + delete $values->{ids}; - my %changes; - foreach my $user (@$user_objects){ - my $returned_changes = $user->update(); - $changes{$user->id} = translate($returned_changes, MAPPED_RETURNS); - } - $dbh->bz_commit_transaction(); - - 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]) - }; - } + $dbh->bz_start_transaction(); + 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; } 1; diff --git a/Bugzilla/WebService/Util.pm b/Bugzilla/WebService/Util.pm index a879c0e0d..3e70921b3 100644 --- a/Bugzilla/WebService/Util.pm +++ b/Bugzilla/WebService/Util.pm @@ -25,269 +25,277 @@ use parent qw(Exporter); require Test::Taint; our @EXPORT_OK = qw( - extract_flags - filter - filter_wants - taint_data - validate - translate - params_to_objects - fix_credentials + extract_flags + filter + filter_wants + taint_data + validate + translate + params_to_objects + fix_credentials ); sub extract_flags { - my ($flags, $bug, $attachment) = @_; - my (@new_flags, @old_flags); + my ($flags, $bug, $attachment) = @_; + my (@new_flags, @old_flags); - my $flag_types = $attachment ? $attachment->flag_types : $bug->flag_types; - my $current_flags = $attachment ? $attachment->flags : $bug->flags; + my $flag_types = $attachment ? $attachment->flag_types : $bug->flag_types; + my $current_flags = $attachment ? $attachment->flags : $bug->flags; - # Copy the user provided $flags as we may call extract_flags more than - # once when editing multiple bugs or attachments. - my $flags_copy = dclone($flags); + # Copy the user provided $flags as we may call extract_flags more than + # once when editing multiple bugs or attachments. + my $flags_copy = dclone($flags); - foreach my $flag (@$flags_copy) { - my $id = $flag->{id}; - my $type_id = $flag->{type_id}; + foreach my $flag (@$flags_copy) { + my $id = $flag->{id}; + my $type_id = $flag->{type_id}; - my $new = delete $flag->{new}; - my $name = delete $flag->{name}; + my $new = delete $flag->{new}; + my $name = delete $flag->{name}; - if ($id) { - my $flag_obj = grep($id == $_->id, @$current_flags); - $flag_obj || ThrowUserError('object_does_not_exist', - { class => 'Bugzilla::Flag', id => $id }); - } - elsif ($type_id) { - my $type_obj = grep($type_id == $_->id, @$flag_types); - $type_obj || ThrowUserError('object_does_not_exist', - { class => 'Bugzilla::FlagType', id => $type_id }); - if (!$new) { - my @flag_matches = grep($type_id == $_->type->id, @$current_flags); - @flag_matches > 1 && ThrowUserError('flag_not_unique', - { value => $type_id }); - if (!@flag_matches) { - delete $flag->{id}; - } - else { - delete $flag->{type_id}; - $flag->{id} = $flag_matches[0]->id; - } - } + if ($id) { + my $flag_obj = grep($id == $_->id, @$current_flags); + $flag_obj + || ThrowUserError('object_does_not_exist', + {class => 'Bugzilla::Flag', id => $id}); + } + elsif ($type_id) { + my $type_obj = grep($type_id == $_->id, @$flag_types); + $type_obj + || ThrowUserError('object_does_not_exist', + {class => 'Bugzilla::FlagType', id => $type_id}); + if (!$new) { + my @flag_matches = grep($type_id == $_->type->id, @$current_flags); + @flag_matches > 1 && ThrowUserError('flag_not_unique', {value => $type_id}); + if (!@flag_matches) { + delete $flag->{id}; } - elsif ($name) { - my @type_matches = grep($name eq $_->name, @$flag_types); - @type_matches > 1 && ThrowUserError('flag_type_not_unique', - { value => $name }); - @type_matches || ThrowUserError('object_does_not_exist', - { class => 'Bugzilla::FlagType', name => $name }); - if ($new) { - delete $flag->{id}; - $flag->{type_id} = $type_matches[0]->id; - } - else { - my @flag_matches = grep($name eq $_->type->name, @$current_flags); - @flag_matches > 1 && ThrowUserError('flag_not_unique', { value => $name }); - if (@flag_matches) { - $flag->{id} = $flag_matches[0]->id; - } - else { - delete $flag->{id}; - $flag->{type_id} = $type_matches[0]->id; - } - } + else { + delete $flag->{type_id}; + $flag->{id} = $flag_matches[0]->id; } - - if ($flag->{id}) { - push(@old_flags, $flag); + } + } + elsif ($name) { + my @type_matches = grep($name eq $_->name, @$flag_types); + @type_matches > 1 && ThrowUserError('flag_type_not_unique', {value => $name}); + @type_matches + || ThrowUserError('object_does_not_exist', + {class => 'Bugzilla::FlagType', name => $name}); + if ($new) { + delete $flag->{id}; + $flag->{type_id} = $type_matches[0]->id; + } + else { + my @flag_matches = grep($name eq $_->type->name, @$current_flags); + @flag_matches > 1 && ThrowUserError('flag_not_unique', {value => $name}); + if (@flag_matches) { + $flag->{id} = $flag_matches[0]->id; } else { - push(@new_flags, $flag); + delete $flag->{id}; + $flag->{type_id} = $type_matches[0]->id; } + } } - return (\@old_flags, \@new_flags); + if ($flag->{id}) { + push(@old_flags, $flag); + } + else { + push(@new_flags, $flag); + } + } + + return (\@old_flags, \@new_flags); } sub filter($$;$$) { - my ($params, $hash, $types, $prefix) = @_; - my %newhash = %$hash; + my ($params, $hash, $types, $prefix) = @_; + my %newhash = %$hash; - foreach my $key (keys %$hash) { - delete $newhash{$key} if !filter_wants($params, $key, $types, $prefix); - } + foreach my $key (keys %$hash) { + delete $newhash{$key} if !filter_wants($params, $key, $types, $prefix); + } - return \%newhash; + return \%newhash; } sub filter_wants($$;$$) { - my ($params, $field, $types, $prefix) = @_; - - # Since this is operation is resource intensive, we will cache the results - # This assumes that $params->{*_fields} doesn't change between calls - my $cache = Bugzilla->request_cache->{filter_wants} ||= {}; - $field = "${prefix}.${field}" if $prefix; - - if (exists $cache->{$field}) { - return $cache->{$field}; - } - - # Mimic old behavior if no types provided - my %field_types = map { $_ => 1 } (ref $types ? @$types : ($types || 'default')); - - my %include = map { $_ => 1 } @{ $params->{'include_fields'} || [] }; - my %exclude = map { $_ => 1 } @{ $params->{'exclude_fields'} || [] }; - - my %include_types; - my %exclude_types; - - # Only return default fields if nothing is specified - $include_types{default} = 1 if !%include; - - # Look for any field types requested - foreach my $key (keys %include) { - next if $key !~ /^_(.*)$/; - $include_types{$1} = 1; - delete $include{$key}; - } - foreach my $key (keys %exclude) { - next if $key !~ /^_(.*)$/; - $exclude_types{$1} = 1; - delete $exclude{$key}; - } - - # Explicit inclusion/exclusion - return $cache->{$field} = 0 if $exclude{$field}; - return $cache->{$field} = 1 if $include{$field}; - - # If the user has asked to include all or exclude all - return $cache->{$field} = 0 if $exclude_types{'all'}; - return $cache->{$field} = 1 if $include_types{'all'}; - - # If the user has not asked for any fields specifically or if the user has asked - # for one or more of the field's types (and not excluded them) - foreach my $type (keys %field_types) { - return $cache->{$field} = 0 if $exclude_types{$type}; - return $cache->{$field} = 1 if $include_types{$type}; - } - - my $wants = 0; - if ($prefix) { - # Include the field if the parent is include (and this one is not excluded) - $wants = 1 if $include{$prefix}; - } - else { - # We want to include this if one of the sub keys is included - my $key = $field . '.'; - my $len = length($key); - $wants = 1 if grep { substr($_, 0, $len) eq $key } keys %include; - } - - return $cache->{$field} = $wants; + my ($params, $field, $types, $prefix) = @_; + + # Since this is operation is resource intensive, we will cache the results + # This assumes that $params->{*_fields} doesn't change between calls + my $cache = Bugzilla->request_cache->{filter_wants} ||= {}; + $field = "${prefix}.${field}" if $prefix; + + if (exists $cache->{$field}) { + return $cache->{$field}; + } + + # Mimic old behavior if no types provided + my %field_types + = map { $_ => 1 } (ref $types ? @$types : ($types || 'default')); + + my %include = map { $_ => 1 } @{$params->{'include_fields'} || []}; + my %exclude = map { $_ => 1 } @{$params->{'exclude_fields'} || []}; + + my %include_types; + my %exclude_types; + + # Only return default fields if nothing is specified + $include_types{default} = 1 if !%include; + + # Look for any field types requested + foreach my $key (keys %include) { + next if $key !~ /^_(.*)$/; + $include_types{$1} = 1; + delete $include{$key}; + } + foreach my $key (keys %exclude) { + next if $key !~ /^_(.*)$/; + $exclude_types{$1} = 1; + delete $exclude{$key}; + } + + # Explicit inclusion/exclusion + return $cache->{$field} = 0 if $exclude{$field}; + return $cache->{$field} = 1 if $include{$field}; + + # If the user has asked to include all or exclude all + return $cache->{$field} = 0 if $exclude_types{'all'}; + return $cache->{$field} = 1 if $include_types{'all'}; + + # If the user has not asked for any fields specifically or if the user has asked + # for one or more of the field's types (and not excluded them) + foreach my $type (keys %field_types) { + return $cache->{$field} = 0 if $exclude_types{$type}; + return $cache->{$field} = 1 if $include_types{$type}; + } + + my $wants = 0; + if ($prefix) { + + # Include the field if the parent is include (and this one is not excluded) + $wants = 1 if $include{$prefix}; + } + else { + # We want to include this if one of the sub keys is included + my $key = $field . '.'; + my $len = length($key); + $wants = 1 if grep { substr($_, 0, $len) eq $key } keys %include; + } + + return $cache->{$field} = $wants; } sub taint_data { - my @params = @_; - return if !@params; - # Though this is a private function, it hasn't changed since 2004 and - # should be safe to use, and prevents us from having to write it ourselves - # or require another module to do it. - Test::Taint::_deeply_traverse(\&_delete_bad_keys, \@params); - Test::Taint::taint_deeply(\@params); + my @params = @_; + return if !@params; + + # Though this is a private function, it hasn't changed since 2004 and + # should be safe to use, and prevents us from having to write it ourselves + # or require another module to do it. + Test::Taint::_deeply_traverse(\&_delete_bad_keys, \@params); + Test::Taint::taint_deeply(\@params); } sub _delete_bad_keys { - foreach my $item (@_) { - next if ref $item ne 'HASH'; - foreach my $key (keys %$item) { - # Making something a hash key always untaints it, in Perl. - # However, we need to validate our argument names in some way. - # We know that all hash keys passed in to the WebService will - # match \w+, contain '.' or '-', so we delete any key that - # doesn't match that. - if ($key !~ /^[\w\.\-]+$/) { - delete $item->{$key}; - } - } + foreach my $item (@_) { + next if ref $item ne 'HASH'; + foreach my $key (keys %$item) { + + # Making something a hash key always untaints it, in Perl. + # However, we need to validate our argument names in some way. + # We know that all hash keys passed in to the WebService will + # match \w+, contain '.' or '-', so we delete any key that + # doesn't match that. + if ($key !~ /^[\w\.\-]+$/) { + delete $item->{$key}; + } } - return @_; + } + return @_; } -sub validate { - my ($self, $params, @keys) = @_; - - # If $params is defined but not a reference, then we weren't - # sent any parameters at all, and we're getting @keys where - # $params should be. - return ($self, undef) if (defined $params and !ref $params); - - my @id_params = qw(ids comment_ids); - # If @keys is not empty then we convert any named - # parameters that have scalar values to arrayrefs - # that match. - foreach my $key (@keys) { - if (exists $params->{$key}) { - $params->{$key} = [ $params->{$key} ] unless ref $params->{$key}; - - if (any { $key eq $_ } @id_params) { - my $ids = $params->{$key}; - ThrowCodeError('param_scalar_array_required', { param => $key }) - unless ref($ids) eq 'ARRAY' && none { ref $_ } @$ids; - } - } +sub validate { + my ($self, $params, @keys) = @_; + + # If $params is defined but not a reference, then we weren't + # sent any parameters at all, and we're getting @keys where + # $params should be. + return ($self, undef) if (defined $params and !ref $params); + + my @id_params = qw(ids comment_ids); + + # If @keys is not empty then we convert any named + # parameters that have scalar values to arrayrefs + # that match. + foreach my $key (@keys) { + if (exists $params->{$key}) { + $params->{$key} = [$params->{$key}] unless ref $params->{$key}; + + if (any { $key eq $_ } @id_params) { + my $ids = $params->{$key}; + ThrowCodeError('param_scalar_array_required', {param => $key}) + unless ref($ids) eq 'ARRAY' && none { ref $_ } @$ids; + } } + } - return ($self, $params); + return ($self, $params); } sub translate { - my ($params, $mapped) = @_; - my %changes; - while (my ($key,$value) = each (%$params)) { - my $new_field = $mapped->{$key} || $key; - $changes{$new_field} = $value; - } - return \%changes; + my ($params, $mapped) = @_; + my %changes; + while (my ($key, $value) = each(%$params)) { + my $new_field = $mapped->{$key} || $key; + $changes{$new_field} = $value; + } + return \%changes; } sub params_to_objects { - my ($params, $class) = @_; - my (@objects, @objects_by_ids); + my ($params, $class) = @_; + my (@objects, @objects_by_ids); - @objects = map { $class->check($_) } - @{ $params->{names} } if $params->{names}; + @objects = map { $class->check($_) } @{$params->{names}} if $params->{names}; - @objects_by_ids = map { $class->check({ id => $_ }) } - @{ $params->{ids} } if $params->{ids}; + @objects_by_ids = map { $class->check({id => $_}) } @{$params->{ids}} + if $params->{ids}; - push(@objects, @objects_by_ids); - my %seen; - @objects = grep { !$seen{$_->id}++ } @objects; - return \@objects; + push(@objects, @objects_by_ids); + my %seen; + @objects = grep { !$seen{$_->id}++ } @objects; + return \@objects; } sub fix_credentials { - my ($params) = @_; - # Allow user to pass in login=foo&password=bar as a convenience - # even if not calling GET /login. We also do not delete them as - # GET /login requires "login" and "password". - if (exists $params->{'login'} && exists $params->{'password'}) { - $params->{'Bugzilla_login'} = delete $params->{'login'}; - $params->{'Bugzilla_password'} = delete $params->{'password'}; - } - # Allow user to pass api_key=12345678 as a convenience which becomes - # "Bugzilla_api_key" which is what the auth code looks for. - if (exists $params->{api_key}) { - $params->{Bugzilla_api_key} = delete $params->{api_key}; - } - # Allow user to pass token=12345678 as a convenience which becomes - # "Bugzilla_token" which is what the auth code looks for. - if (exists $params->{'token'}) { - $params->{'Bugzilla_token'} = delete $params->{'token'}; - } - - # Allow extensions to modify the credential data before login - Bugzilla::Hook::process('webservice_fix_credentials', { params => $params }); + my ($params) = @_; + + # Allow user to pass in login=foo&password=bar as a convenience + # even if not calling GET /login. We also do not delete them as + # GET /login requires "login" and "password". + if (exists $params->{'login'} && exists $params->{'password'}) { + $params->{'Bugzilla_login'} = delete $params->{'login'}; + $params->{'Bugzilla_password'} = delete $params->{'password'}; + } + + # Allow user to pass api_key=12345678 as a convenience which becomes + # "Bugzilla_api_key" which is what the auth code looks for. + if (exists $params->{api_key}) { + $params->{Bugzilla_api_key} = delete $params->{api_key}; + } + + # Allow user to pass token=12345678 as a convenience which becomes + # "Bugzilla_token" which is what the auth code looks for. + if (exists $params->{'token'}) { + $params->{'Bugzilla_token'} = delete $params->{'token'}; + } + + # Allow extensions to modify the credential data before login + Bugzilla::Hook::process('webservice_fix_credentials', {params => $params}); } __END__ diff --git a/Bugzilla/Whine.pm b/Bugzilla/Whine.pm index eeaea6da4..081933cba 100644 --- a/Bugzilla/Whine.pm +++ b/Bugzilla/Whine.pm @@ -27,11 +27,11 @@ use Bugzilla::Whine::Query; use constant DB_TABLE => 'whine_events'; use constant DB_COLUMNS => qw( - id - owner_userid - subject - body - mailifnobugs + id + owner_userid + subject + body + mailifnobugs ); use constant LIST_ORDER => 'id'; @@ -39,15 +39,15 @@ use constant LIST_ORDER => 'id'; #################### # Simple Accessors # #################### -sub subject { return $_[0]->{'subject'}; } -sub body { return $_[0]->{'body'}; } +sub subject { return $_[0]->{'subject'}; } +sub body { return $_[0]->{'body'}; } sub mail_if_no_bugs { return $_[0]->{'mailifnobugs'}; } sub user { - my ($self) = @_; - return $self->{user} if defined $self->{user}; - $self->{user} = new Bugzilla::User($self->{'owner_userid'}); - return $self->{user}; + my ($self) = @_; + return $self->{user} if defined $self->{user}; + $self->{user} = new Bugzilla::User($self->{'owner_userid'}); + return $self->{user}; } 1; diff --git a/Bugzilla/Whine/Query.pm b/Bugzilla/Whine/Query.pm index b2a2c9e07..6648eab66 100644 --- a/Bugzilla/Whine/Query.pm +++ b/Bugzilla/Whine/Query.pm @@ -23,12 +23,12 @@ use Bugzilla::Search::Saved; use constant DB_TABLE => 'whine_queries'; use constant DB_COLUMNS => qw( - id - eventid - query_name - sortkey - onemailperbug - title + id + eventid + query_name + sortkey + onemailperbug + title ); use constant NAME_FIELD => 'id'; @@ -37,11 +37,11 @@ use constant LIST_ORDER => 'sortkey'; #################### # Simple Accessors # #################### -sub eventid { return $_[0]->{'eventid'}; } -sub sortkey { return $_[0]->{'sortkey'}; } +sub eventid { return $_[0]->{'eventid'}; } +sub sortkey { return $_[0]->{'sortkey'}; } sub one_email_per_bug { return $_[0]->{'onemailperbug'}; } -sub title { return $_[0]->{'title'}; } -sub name { return $_[0]->{'query_name'}; } +sub title { return $_[0]->{'title'}; } +sub name { return $_[0]->{'query_name'}; } 1; diff --git a/Bugzilla/Whine/Schedule.pm b/Bugzilla/Whine/Schedule.pm index 11f0bf16f..7517a3f26 100644 --- a/Bugzilla/Whine/Schedule.pm +++ b/Bugzilla/Whine/Schedule.pm @@ -22,22 +22,22 @@ use Bugzilla::Constants; use constant DB_TABLE => 'whine_schedules'; use constant DB_COLUMNS => qw( - id - eventid - run_day - run_time - run_next - mailto - mailto_type + id + eventid + run_day + run_time + run_next + mailto + mailto_type ); use constant UPDATE_COLUMNS => qw( - eventid - run_day - run_time - run_next - mailto - mailto_type + eventid + run_day + run_time + run_next + mailto + mailto_type ); use constant NAME_FIELD => 'id'; use constant LIST_ORDER => 'id'; @@ -45,36 +45,38 @@ use constant LIST_ORDER => 'id'; #################### # Simple Accessors # #################### -sub eventid { return $_[0]->{'eventid'}; } -sub run_day { return $_[0]->{'run_day'}; } -sub run_time { return $_[0]->{'run_time'}; } +sub eventid { return $_[0]->{'eventid'}; } +sub run_day { return $_[0]->{'run_day'}; } +sub run_time { return $_[0]->{'run_time'}; } sub mailto_is_group { return $_[0]->{'mailto_type'}; } sub mailto { - my $self = shift; - - return $self->{mailto_object} if exists $self->{mailto_object}; - my $id = $self->{'mailto'}; - - if ($self->mailto_is_group) { - $self->{mailto_object} = Bugzilla::Group->new($id); - } else { - $self->{mailto_object} = Bugzilla::User->new($id); - } - return $self->{mailto_object}; + my $self = shift; + + return $self->{mailto_object} if exists $self->{mailto_object}; + my $id = $self->{'mailto'}; + + if ($self->mailto_is_group) { + $self->{mailto_object} = Bugzilla::Group->new($id); + } + else { + $self->{mailto_object} = Bugzilla::User->new($id); + } + return $self->{mailto_object}; } -sub mailto_users { - my $self = shift; - return $self->{mailto_users} if exists $self->{mailto_users}; - my $object = $self->mailto; - - if ($self->mailto_is_group) { - $self->{mailto_users} = $object->members_non_inherited if $object->is_active; - } else { - $self->{mailto_users} = $object; - } - return $self->{mailto_users}; +sub mailto_users { + my $self = shift; + return $self->{mailto_users} if exists $self->{mailto_users}; + my $object = $self->mailto; + + if ($self->mailto_is_group) { + $self->{mailto_users} = $object->members_non_inherited if $object->is_active; + } + else { + $self->{mailto_users} = $object; + } + return $self->{mailto_users}; } 1; diff --git a/Build.PL b/Build.PL index 024a56024..37072fc18 100644 --- a/Build.PL +++ b/Build.PL @@ -19,43 +19,43 @@ use Bugzilla::Install::Requirements qw(REQUIRED_MODULES OPTIONAL_MODULES); use Bugzilla::Constants qw(BUGZILLA_VERSION); sub requires { - my $requirements = REQUIRED_MODULES(); - my $hrequires = {}; - foreach my $module (@$requirements) { - $hrequires->{$module->{module}} = $module->{version}; - } - return $hrequires; -}; + my $requirements = REQUIRED_MODULES(); + my $hrequires = {}; + foreach my $module (@$requirements) { + $hrequires->{$module->{module}} = $module->{version}; + } + return $hrequires; +} sub build_requires { - return requires(); + return requires(); } sub recommends { - my $recommends = OPTIONAL_MODULES(); - my @blacklist = ('Apache-SizeLimit', 'mod_perl'); # Does not compile properly on Travis - my $hrecommends = {}; - foreach my $module (@$recommends) { - next if grep($_ eq $module->{package}, @blacklist); - $hrecommends->{$module->{module}} = $module->{version}; - } - return $hrecommends; + my $recommends = OPTIONAL_MODULES(); + my @blacklist = ('Apache-SizeLimit', 'mod_perl'); # Does not compile properly on Travis + my $hrecommends = {}; + foreach my $module (@$recommends) { + next if grep($_ eq $module->{package}, @blacklist); + $hrecommends->{$module->{module}} = $module->{version}; + } + return $hrecommends; } my $build = Module::Build->new( - module_name => 'Bugzilla', - dist_abstract => < 'Bugzilla', + dist_abstract => < 'Bugzilla/Constants.pm', - dist_version => BUGZILLA_VERSION, - requires => requires(), - recommends => recommends(), - license => 'Mozilla_2_0', - create_readme => 0, - create_makefile_pl => 0 + dist_version_from => 'Bugzilla/Constants.pm', + dist_version => BUGZILLA_VERSION, + requires => requires(), + recommends => recommends(), + license => 'Mozilla_2_0', + create_readme => 0, + create_makefile_pl => 0 ); $build->create_build_script; diff --git a/admin.cgi b/admin.cgi index 1dc9b2c1b..2ba0cdc7f 100755 --- a/admin.cgi +++ b/admin.cgi @@ -16,14 +16,15 @@ use Bugzilla; use Bugzilla::Constants; use Bugzilla::Error; -my $cgi = Bugzilla->cgi; +my $cgi = Bugzilla->cgi; my $template = Bugzilla->template; -my $user = Bugzilla->login(LOGIN_REQUIRED); +my $user = Bugzilla->login(LOGIN_REQUIRED); print $cgi->header(); $user->can_administer - || ThrowUserError('auth_failure', {action => 'access', object => 'administrative_pages'}); + || ThrowUserError('auth_failure', + {action => 'access', object => 'administrative_pages'}); $template->process('admin/admin.html.tmpl') || ThrowTemplateError($template->error()); diff --git a/attachment.cgi b/attachment.cgi index 3f0ff22ba..e437fe8e5 100755 --- a/attachment.cgi +++ b/attachment.cgi @@ -16,8 +16,8 @@ use Bugzilla; use Bugzilla::BugMail; use Bugzilla::Constants; use Bugzilla::Error; -use Bugzilla::Flag; -use Bugzilla::FlagType; +use Bugzilla::Flag; +use Bugzilla::FlagType; use Bugzilla::User; use Bugzilla::Util; use Bugzilla::Bug; @@ -26,15 +26,15 @@ use Bugzilla::Attachment::PatchReader; use Bugzilla::Token; use Encode qw(encode find_encoding); -use Encode::MIME::Header; # Required to alter Encode::Encoding{'MIME-Q'}. +use Encode::MIME::Header; # Required to alter Encode::Encoding{'MIME-Q'}. # For most scripts we don't make $cgi and $template global variables. But # when preparing Bugzilla for mod_perl, this script used these # variables in so many subroutines that it was easier to just # make them globals. -local our $cgi = Bugzilla->cgi; -local our $template = Bugzilla->template; -local our $vars = {}; +local our $cgi = Bugzilla->cgi; +local our $template = Bugzilla->template; +local our $vars = {}; local $Bugzilla::CGI::ALLOW_UNSAFE_RESPONSE = 1; # All calls to this script should contain an "action" variable whose @@ -49,57 +49,48 @@ my $format = $cgi->param('format') || ''; # You must use the appropriate urlbase/sslbase param when doing anything # but viewing an attachment, or a raw diff. if ($action ne 'view' - && (($action !~ /^(?:interdiff|diff)$/) || $format ne 'raw')) + && (($action !~ /^(?:interdiff|diff)$/) || $format ne 'raw')) { - do_ssl_redirect_if_required(); - if ($cgi->url_is_attachment_base) { - $cgi->redirect_to_urlbase; - } - Bugzilla->login(); + do_ssl_redirect_if_required(); + if ($cgi->url_is_attachment_base) { + $cgi->redirect_to_urlbase; + } + Bugzilla->login(); } # When viewing an attachment, do not request credentials if we are on # the alternate host. Let view() decide when to call Bugzilla->login. -if ($action eq "view") -{ - view(); +if ($action eq "view") { + view(); } -elsif ($action eq "interdiff") -{ - interdiff(); +elsif ($action eq "interdiff") { + interdiff(); } -elsif ($action eq "diff") -{ - diff(); +elsif ($action eq "diff") { + diff(); } -elsif ($action eq "viewall") -{ - viewall(); +elsif ($action eq "viewall") { + viewall(); } -elsif ($action eq "enter") -{ - Bugzilla->login(LOGIN_REQUIRED); - enter(); +elsif ($action eq "enter") { + Bugzilla->login(LOGIN_REQUIRED); + enter(); } -elsif ($action eq "insert") -{ - Bugzilla->login(LOGIN_REQUIRED); - insert(); +elsif ($action eq "insert") { + Bugzilla->login(LOGIN_REQUIRED); + insert(); } -elsif ($action eq "edit") -{ - edit(); +elsif ($action eq "edit") { + edit(); } -elsif ($action eq "update") -{ - Bugzilla->login(LOGIN_REQUIRED); - update(); +elsif ($action eq "update") { + Bugzilla->login(LOGIN_REQUIRED); + update(); } elsif ($action eq "delete") { - delete_attachment(); + delete_attachment(); } -else -{ +else { ThrowUserError('unknown_action', {action => $action}); } @@ -121,72 +112,73 @@ exit; # Returns an attachment object. sub validateID { - my($param, $dont_validate_access) = @_; - $param ||= 'id'; + my ($param, $dont_validate_access) = @_; + $param ||= 'id'; - # If we're not doing interdiffs, check if id wasn't specified and - # prompt them with a page that allows them to choose an attachment. - # Happens when calling plain attachment.cgi from the urlbar directly - if ($param eq 'id' && !$cgi->param('id')) { - print $cgi->header(); - $template->process("attachment/choose.html.tmpl", $vars) || - ThrowTemplateError($template->error()); - exit; - } - - my $attach_id = $cgi->param($param); - - # Validate the specified attachment id. detaint kills $attach_id if - # non-natural, so use the original value from $cgi in our exception - # message here. - detaint_natural($attach_id) - || ThrowUserError("invalid_attach_id", - { attach_id => scalar $cgi->param($param) }); - - # Make sure the attachment exists in the database. - my $attachment = new Bugzilla::Attachment({ id => $attach_id, cache => 1 }) - || ThrowUserError("invalid_attach_id", { attach_id => $attach_id }); - - return $attachment if ($dont_validate_access || check_can_access($attachment)); + # If we're not doing interdiffs, check if id wasn't specified and + # prompt them with a page that allows them to choose an attachment. + # Happens when calling plain attachment.cgi from the urlbar directly + if ($param eq 'id' && !$cgi->param('id')) { + print $cgi->header(); + $template->process("attachment/choose.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + exit; + } + + my $attach_id = $cgi->param($param); + + # Validate the specified attachment id. detaint kills $attach_id if + # non-natural, so use the original value from $cgi in our exception + # message here. + detaint_natural($attach_id) + || ThrowUserError("invalid_attach_id", + {attach_id => scalar $cgi->param($param)}); + + # Make sure the attachment exists in the database. + my $attachment = new Bugzilla::Attachment({id => $attach_id, cache => 1}) + || ThrowUserError("invalid_attach_id", {attach_id => $attach_id}); + + return $attachment if ($dont_validate_access || check_can_access($attachment)); } # Make sure the current user has access to the specified attachment. sub check_can_access { - my $attachment = shift; - my $user = Bugzilla->user; - - # Make sure the user is authorized to access this attachment's bug. - Bugzilla::Bug->check({ id => $attachment->bug_id, cache => 1 }); - if ($attachment->isprivate && $user->id != $attachment->attacher->id - && !$user->is_insider) - { - ThrowUserError('auth_failure', {action => 'access', - object => 'attachment', - attach_id => $attachment->id}); - } - return 1; + my $attachment = shift; + my $user = Bugzilla->user; + + # Make sure the user is authorized to access this attachment's bug. + Bugzilla::Bug->check({id => $attachment->bug_id, cache => 1}); + if ( $attachment->isprivate + && $user->id != $attachment->attacher->id + && !$user->is_insider) + { + ThrowUserError('auth_failure', + {action => 'access', object => 'attachment', attach_id => $attachment->id}); + } + return 1; } # Determines if the attachment is public -- that is, if users who are # not logged in have access to the attachment sub attachmentIsPublic { - my $attachment = shift; + my $attachment = shift; - return 0 if Bugzilla->params->{'requirelogin'}; - return 0 if $attachment->isprivate; + return 0 if Bugzilla->params->{'requirelogin'}; + return 0 if $attachment->isprivate; - my $anon_user = new Bugzilla::User; - return $anon_user->can_see_bug($attachment->bug_id); + my $anon_user = new Bugzilla::User; + return $anon_user->can_see_bug($attachment->bug_id); } # Validates format of a diff/interdiff. Takes a list as an parameter, which # defines the valid format values. Will throw an error if the format is not # in the list. Returns either the user selected or default format. sub validateFormat { + # receives a list of legal formats; first item is a default my $format = $cgi->param('format') || $_[0]; if (not grep($_ eq $format, @_)) { - ThrowUserError("invalid_format", { format => $format, formats => \@_ }); + ThrowUserError("invalid_format", {format => $format, formats => \@_}); } return $format; @@ -195,125 +187,139 @@ sub validateFormat { # Gets the attachment object(s) generated by validateID, while ensuring # attachbase and token authentication is used when required. sub get_attachment { - my @field_names = @_ ? @_ : qw(id); - - my %attachments; - - if (use_attachbase()) { - # Load each attachment, and ensure they are all from the same bug - my $bug_id = 0; + my @field_names = @_ ? @_ : qw(id); + + my %attachments; + + if (use_attachbase()) { + + # Load each attachment, and ensure they are all from the same bug + my $bug_id = 0; + foreach my $field_name (@field_names) { + my $attachment = validateID($field_name, 1); + if (!$bug_id) { + $bug_id = $attachment->bug_id; + } + elsif ($attachment->bug_id != $bug_id) { + ThrowUserError('attachment_bug_id_mismatch'); + } + $attachments{$field_name} = $attachment; + } + my @args = map { $_ . '=' . $attachments{$_}->id } @field_names; + my $cgi_params = $cgi->canonicalise_query(@field_names, 't', 'Bugzilla_login', + 'Bugzilla_password'); + push(@args, $cgi_params) if $cgi_params; + my $path = 'attachment.cgi?' . join('&', @args); + + # Make sure the attachment is served from the correct server. + if ($cgi->url_is_attachment_base($bug_id)) { + + # No need to validate the token for public attachments. We cannot request + # credentials as we are on the alternate host. + if (!all_attachments_are_public(\%attachments)) { + my $token = $cgi->param('t'); + my ($userid, undef, $token_data) = Bugzilla::Token::GetTokenData($token); + my %token_data = unpack_token_data($token_data); + my $valid_token = 1; foreach my $field_name (@field_names) { - my $attachment = validateID($field_name, 1); - if (!$bug_id) { - $bug_id = $attachment->bug_id; - } elsif ($attachment->bug_id != $bug_id) { - ThrowUserError('attachment_bug_id_mismatch'); - } - $attachments{$field_name} = $attachment; - } - my @args = map { $_ . '=' . $attachments{$_}->id } @field_names; - my $cgi_params = $cgi->canonicalise_query(@field_names, 't', - 'Bugzilla_login', 'Bugzilla_password'); - push(@args, $cgi_params) if $cgi_params; - my $path = 'attachment.cgi?' . join('&', @args); - - # Make sure the attachment is served from the correct server. - if ($cgi->url_is_attachment_base($bug_id)) { - # No need to validate the token for public attachments. We cannot request - # credentials as we are on the alternate host. - if (!all_attachments_are_public(\%attachments)) { - my $token = $cgi->param('t'); - my ($userid, undef, $token_data) = Bugzilla::Token::GetTokenData($token); - my %token_data = unpack_token_data($token_data); - my $valid_token = 1; - foreach my $field_name (@field_names) { - my $token_id = $token_data{$field_name}; - if (!$token_id - || !detaint_natural($token_id) - || $attachments{$field_name}->id != $token_id) - { - $valid_token = 0; - last; - } - } - unless ($userid && $valid_token) { - # Not a valid token. - print $cgi->redirect('-location' => correct_urlbase() . $path); - exit; - } - # Change current user without creating cookies. - Bugzilla->set_user(new Bugzilla::User($userid)); - # Tokens are single use only, delete it. - delete_token($token); - } - } - 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; + my $token_id = $token_data{$field_name}; + if ( !$token_id + || !detaint_natural($token_id) + || $attachments{$field_name}->id != $token_id) + { + $valid_token = 0; + last; + } } - else { - # We couldn't call Bugzilla->login earlier as we first had to - # make sure we were not going to request credentials on the - # alternate host. - Bugzilla->login(); - my $attachbase = Bugzilla->params->{'attachment_base'}; - # Replace %bugid% by the ID of the bug the attachment - # belongs to, if present. - $attachbase =~ s/\%bugid\%/$bug_id/; - if (all_attachments_are_public(\%attachments)) { - # No need for a token; redirect to attachment base. - print $cgi->redirect(-location => $attachbase . $path); - exit; - } else { - # Make sure the user can view the attachment. - foreach my $field_name (@field_names) { - check_can_access($attachments{$field_name}); - } - # Create a token and redirect. - my $token = url_quote(issue_session_token(pack_token_data(\%attachments))); - print $cgi->redirect(-location => $attachbase . "$path&t=$token"); - exit; - } + unless ($userid && $valid_token) { + + # Not a valid token. + print $cgi->redirect('-location' => correct_urlbase() . $path); + exit; } - } else { - do_ssl_redirect_if_required(); - # No alternate host is used. Request credentials if required. - Bugzilla->login(); + + # Change current user without creating cookies. + Bugzilla->set_user(new Bugzilla::User($userid)); + + # Tokens are single use only, delete it. + delete_token($token); + } + } + elsif ($cgi->url_is_attachment_base) { + + # If we come here, this means that each bug has its own host + # for attachments, and that we are trying to view one attachment + # using another bug's host. That's not desired. + $cgi->redirect_to_urlbase; + } + else { + # We couldn't call Bugzilla->login earlier as we first had to + # make sure we were not going to request credentials on the + # alternate host. + Bugzilla->login(); + my $attachbase = Bugzilla->params->{'attachment_base'}; + + # Replace %bugid% by the ID of the bug the attachment + # belongs to, if present. + $attachbase =~ s/\%bugid\%/$bug_id/; + if (all_attachments_are_public(\%attachments)) { + + # No need for a token; redirect to attachment base. + print $cgi->redirect(-location => $attachbase . $path); + exit; + } + else { + # Make sure the user can view the attachment. foreach my $field_name (@field_names) { - $attachments{$field_name} = validateID($field_name); + check_can_access($attachments{$field_name}); } + + # Create a token and redirect. + my $token = url_quote(issue_session_token(pack_token_data(\%attachments))); + print $cgi->redirect(-location => $attachbase . "$path&t=$token"); + exit; + } + } + } + else { + do_ssl_redirect_if_required(); + + # No alternate host is used. Request credentials if required. + Bugzilla->login(); + foreach my $field_name (@field_names) { + $attachments{$field_name} = validateID($field_name); } + } - return wantarray - ? map { $attachments{$_} } @field_names - : $attachments{$field_names[0]}; + return + wantarray + ? map { $attachments{$_} } @field_names + : $attachments{$field_names[0]}; } sub all_attachments_are_public { - my $attachments = shift; - foreach my $field_name (keys %$attachments) { - if (!attachmentIsPublic($attachments->{$field_name})) { - return 0; - } + my $attachments = shift; + foreach my $field_name (keys %$attachments) { + if (!attachmentIsPublic($attachments->{$field_name})) { + return 0; } - return 1; + } + return 1; } sub pack_token_data { - my $attachments = shift; - return join(' ', map { $_ . '=' . $attachments->{$_}->id } keys %$attachments); + my $attachments = shift; + return join(' ', map { $_ . '=' . $attachments->{$_}->id } keys %$attachments); } sub unpack_token_data { - my @token_data = split(/ /, shift || ''); - my %data; - foreach my $token (@token_data) { - my ($field_name, $attach_id) = split('=', $token); - $data{$field_name} = $attach_id; - } - return %data; + my @token_data = split(/ /, shift || ''); + my %data; + foreach my $token (@token_data) { + my ($field_name, $attach_id) = split('=', $token); + $data{$field_name} = $attach_id; + } + return %data; } ################################################################################ @@ -322,256 +328,278 @@ sub unpack_token_data { # Display an attachment. sub view { - my $attachment = get_attachment(); + my $attachment = get_attachment(); - # At this point, Bugzilla->login has been called if it had to. - my $contenttype = $attachment->contenttype; - my $filename = $attachment->filename; + # At this point, Bugzilla->login has been called if it had to. + my $contenttype = $attachment->contenttype; + my $filename = $attachment->filename; - # Bug 111522: allow overriding content-type manually in the posted form - # params. - if (defined $cgi->param('content_type')) { - $contenttype = $attachment->_check_content_type($cgi->param('content_type')); - } + # Bug 111522: allow overriding content-type manually in the posted form + # params. + if (defined $cgi->param('content_type')) { + $contenttype = $attachment->_check_content_type($cgi->param('content_type')); + } - # Return the appropriate HTTP response headers. - $attachment->datasize || ThrowUserError("attachment_removed"); + # 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 + $filename =~ s/^.*[\/\\]//; - # Avoid line wrapping done by Encode, which we don't need for HTTP - # headers. See discussion in bug 328628 for details. - local $Encode::Encoding{'MIME-Q'}->{'bpl'} = 10000; - $filename = encode('MIME-Q', $filename); + # escape quotes and backslashes in the filename, per RFCs 2045/822 + $filename =~ s/\\/\\\\/g; # escape backslashes + $filename =~ s/"/\\"/g; # escape quotes - my $disposition = (Bugzilla->params->{'allow_attachment_display'} || $contenttype eq "text/plain") ? 'inline' : 'attachment'; + # Avoid line wrapping done by Encode, which we don't need for HTTP + # headers. See discussion in bug 328628 for details. + local $Encode::Encoding{'MIME-Q'}->{'bpl'} = 10000; + $filename = encode('MIME-Q', $filename); - # 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('UTF-8'); - } - print $cgi->header(-type=>"$contenttype; name=\"$filename\"", - -content_disposition=> "$disposition; filename=\"$filename\"", - -content_length => $attachment->datasize); - disable_utf8(); - print $attachment->data; + my $disposition = (Bugzilla->params->{'allow_attachment_display'} + || $contenttype eq "text/plain") ? '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('UTF-8'); + } + print $cgi->header( + -type => "$contenttype; name=\"$filename\"", + -content_disposition => "$disposition; filename=\"$filename\"", + -content_length => $attachment->datasize + ); + disable_utf8(); + print $attachment->data; } sub interdiff { - # Retrieve and validate parameters - my $format = validateFormat('html', 'raw'); - my($old_attachment, $new_attachment); - if ($format eq 'raw') { - ($old_attachment, $new_attachment) = get_attachment('oldid', 'newid'); - } else { - $old_attachment = validateID('oldid'); - $new_attachment = validateID('newid'); - } - Bugzilla::Attachment::PatchReader::process_interdiff( - $old_attachment, $new_attachment, $format); + # Retrieve and validate parameters + my $format = validateFormat('html', 'raw'); + my ($old_attachment, $new_attachment); + if ($format eq 'raw') { + ($old_attachment, $new_attachment) = get_attachment('oldid', 'newid'); + } + else { + $old_attachment = validateID('oldid'); + $new_attachment = validateID('newid'); + } + + Bugzilla::Attachment::PatchReader::process_interdiff($old_attachment, + $new_attachment, $format); } sub diff { - # Retrieve and validate parameters - my $format = validateFormat('html', 'raw'); - my $attachment = $format eq 'raw' ? get_attachment() : validateID(); - - # If it is not a patch, view normally. - if (!$attachment->ispatch) { - view(); - return; - } - Bugzilla::Attachment::PatchReader::process_diff($attachment, $format); + # Retrieve and validate parameters + my $format = validateFormat('html', 'raw'); + my $attachment = $format eq 'raw' ? get_attachment() : validateID(); + + # If it is not a patch, view normally. + if (!$attachment->ispatch) { + view(); + return; + } + + Bugzilla::Attachment::PatchReader::process_diff($attachment, $format); } # Display all attachments for a given bug in a series of IFRAMEs within one # HTML page. sub viewall { - # Retrieve and validate parameters - my $bug = Bugzilla::Bug->check({ id => scalar $cgi->param('bugid'), cache => 1 }); - my $attachments = Bugzilla::Attachment->get_attachments_by_bug($bug); - # Ignore deleted attachments. - @$attachments = grep { $_->datasize } @$attachments; + # Retrieve and validate parameters + my $bug = Bugzilla::Bug->check({id => scalar $cgi->param('bugid'), cache => 1}); - if ($cgi->param('hide_obsolete')) { - @$attachments = grep { !$_->isobsolete } @$attachments; - $vars->{'hide_obsolete'} = 1; - } + my $attachments = Bugzilla::Attachment->get_attachments_by_bug($bug); - # Define the variables and functions that will be passed to the UI template. - $vars->{'bug'} = $bug; - $vars->{'attachments'} = $attachments; + # Ignore deleted attachments. + @$attachments = grep { $_->datasize } @$attachments; - print $cgi->header(); + if ($cgi->param('hide_obsolete')) { + @$attachments = grep { !$_->isobsolete } @$attachments; + $vars->{'hide_obsolete'} = 1; + } - # Generate and return the UI (HTML page) from the appropriate template. - $template->process("attachment/show-multiple.html.tmpl", $vars) - || ThrowTemplateError($template->error()); + # Define the variables and functions that will be passed to the UI template. + $vars->{'bug'} = $bug; + $vars->{'attachments'} = $attachments; + + print $cgi->header(); + + # Generate and return the UI (HTML page) from the appropriate template. + $template->process("attachment/show-multiple.html.tmpl", $vars) + || ThrowTemplateError($template->error()); } # Display a form for entering a new attachment. sub enter { - # Retrieve and validate parameters - my $bug = Bugzilla::Bug->check(scalar $cgi->param('bugid')); - my $bugid = $bug->id; - Bugzilla::Attachment->_check_bug($bug); - my $dbh = Bugzilla->dbh; - my $user = Bugzilla->user; - - # Retrieve the attachments the user can edit from the database and write - # them into an array of hashes where each hash represents one attachment. - - my ($can_edit, $not_private) = ('', ''); - if (!$user->in_group('editbugs', $bug->product_id)) { - $can_edit = "AND submitter_id = " . $user->id; - } - if (!$user->is_insider) { - $not_private = "AND isprivate = 0"; - } - my $attach_ids = $dbh->selectcol_arrayref( - "SELECT attach_id + + # Retrieve and validate parameters + my $bug = Bugzilla::Bug->check(scalar $cgi->param('bugid')); + my $bugid = $bug->id; + Bugzilla::Attachment->_check_bug($bug); + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->user; + + # Retrieve the attachments the user can edit from the database and write + # them into an array of hashes where each hash represents one attachment. + + my ($can_edit, $not_private) = ('', ''); + if (!$user->in_group('editbugs', $bug->product_id)) { + $can_edit = "AND submitter_id = " . $user->id; + } + if (!$user->is_insider) { + $not_private = "AND isprivate = 0"; + } + my $attach_ids = $dbh->selectcol_arrayref( + "SELECT attach_id FROM attachments WHERE bug_id = ? AND isobsolete = 0 $can_edit $not_private - ORDER BY attach_id", - undef, $bugid); - - # Define the variables and functions that will be passed to the UI template. - $vars->{'bug'} = $bug; - $vars->{'attachments'} = Bugzilla::Attachment->new_from_list($attach_ids); - - my $flag_types = Bugzilla::FlagType::match({ - 'target_type' => 'attachment', - 'product_id' => $bug->product_id, - 'component_id' => $bug->component_id - }); - $vars->{'flag_types'} = $flag_types; - $vars->{'any_flags_requesteeble'} = - grep { $_->is_requestable && $_->is_requesteeble } @$flag_types; - $vars->{'token'} = issue_session_token('create_attachment'); - - print $cgi->header(); - - # Generate and return the UI (HTML page) from the appropriate template. - $template->process("attachment/create.html.tmpl", $vars) - || ThrowTemplateError($template->error()); + ORDER BY attach_id", undef, $bugid + ); + + # Define the variables and functions that will be passed to the UI template. + $vars->{'bug'} = $bug; + $vars->{'attachments'} = Bugzilla::Attachment->new_from_list($attach_ids); + + my $flag_types = Bugzilla::FlagType::match({ + 'target_type' => 'attachment', + 'product_id' => $bug->product_id, + 'component_id' => $bug->component_id + }); + $vars->{'flag_types'} = $flag_types; + $vars->{'any_flags_requesteeble'} + = grep { $_->is_requestable && $_->is_requesteeble } @$flag_types; + $vars->{'token'} = issue_session_token('create_attachment'); + + print $cgi->header(); + + # Generate and return the UI (HTML page) from the appropriate template. + $template->process("attachment/create.html.tmpl", $vars) + || ThrowTemplateError($template->error()); } # Insert a new attachment into the database. sub insert { - my $dbh = Bugzilla->dbh; - my $user = Bugzilla->user; - - $dbh->bz_start_transaction; + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->user; + + $dbh->bz_start_transaction; + + # Retrieve and validate parameters + my $bug = Bugzilla::Bug->check(scalar $cgi->param('bugid')); + my $bugid = $bug->id; + my ($timestamp) = $dbh->selectrow_array("SELECT NOW()"); + + # Detect if the user already used the same form to submit an attachment + my $token = trim($cgi->param('token')); + check_token_data($token, 'create_attachment', 'index.cgi'); + + # Check attachments the user tries to mark as obsolete. + my @obsolete_attachments; + if ($cgi->param('obsolete')) { + my @obsolete = $cgi->param('obsolete'); + @obsolete_attachments + = Bugzilla::Attachment->validate_obsolete($bug, \@obsolete); + } - # Retrieve and validate parameters - my $bug = Bugzilla::Bug->check(scalar $cgi->param('bugid')); - my $bugid = $bug->id; - my ($timestamp) = $dbh->selectrow_array("SELECT NOW()"); + # Must be called before create() as it may alter $cgi->param('ispatch'). + my $content_type = Bugzilla::Attachment::get_content_type(); + + # Get the filehandle of the attachment. + my $data_fh = $cgi->upload('data'); + my $attach_text = $cgi->param('attach_text'); + + my $attachment = Bugzilla::Attachment->create({ + bug => $bug, + creation_ts => $timestamp, + data => $attach_text || $data_fh, + description => scalar $cgi->param('description'), + filename => $attach_text ? "file_$bugid.txt" : $data_fh, + ispatch => scalar $cgi->param('ispatch'), + isprivate => scalar $cgi->param('isprivate'), + mimetype => $content_type, + }); + + # Delete the token used to create this attachment. + delete_token($token); + + foreach my $obsolete_attachment (@obsolete_attachments) { + $obsolete_attachment->set_is_obsolete(1); + $obsolete_attachment->update($timestamp); + } - # Detect if the user already used the same form to submit an attachment - my $token = trim($cgi->param('token')); - check_token_data($token, 'create_attachment', 'index.cgi'); + my ($flags, $new_flags) + = Bugzilla::Flag->extract_flags_from_cgi($bug, $attachment, $vars, + SKIP_REQUESTEE_ON_ERROR); + $attachment->set_flags($flags, $new_flags); - # Check attachments the user tries to mark as obsolete. - my @obsolete_attachments; - if ($cgi->param('obsolete')) { - my @obsolete = $cgi->param('obsolete'); - @obsolete_attachments = Bugzilla::Attachment->validate_obsolete($bug, \@obsolete); + # Insert a comment about the new attachment into the database. + my $comment = $cgi->param('comment'); + $comment = '' unless defined $comment; + $bug->add_comment( + $comment, + { + isprivate => $attachment->isprivate, + type => CMT_ATTACHMENT_CREATED, + extra_data => $attachment->id } + ); - # Must be called before create() as it may alter $cgi->param('ispatch'). - my $content_type = Bugzilla::Attachment::get_content_type(); - - # Get the filehandle of the attachment. - my $data_fh = $cgi->upload('data'); - my $attach_text = $cgi->param('attach_text'); - - my $attachment = Bugzilla::Attachment->create( - {bug => $bug, - creation_ts => $timestamp, - data => $attach_text || $data_fh, - description => scalar $cgi->param('description'), - filename => $attach_text ? "file_$bugid.txt" : $data_fh, - ispatch => scalar $cgi->param('ispatch'), - isprivate => scalar $cgi->param('isprivate'), - mimetype => $content_type, - }); - - # Delete the token used to create this attachment. - delete_token($token); + # Assign the bug to the user, if they are allowed to take it + my $owner = ""; + if ($cgi->param('takebug') && $user->in_group('editbugs', $bug->product_id)) { + + # 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( - $bug, $attachment, $vars, SKIP_REQUESTEE_ON_ERROR); - $attachment->set_flags($flags, $new_flags); + # Make sure the person we are taking the bug from gets mail. + $owner = $bug->assigned_to->login; + $bug->set_assigned_to($user); + } - # Insert a comment about the new attachment into the database. - my $comment = $cgi->param('comment'); - $comment = '' unless defined $comment; - $bug->add_comment($comment, { isprivate => $attachment->isprivate, - type => CMT_ATTACHMENT_CREATED, - extra_data => $attachment->id }); - - # Assign the bug to the user, if they are allowed to take it - my $owner = ""; - if ($cgi->param('takebug') && $user->in_group('editbugs', $bug->product_id)) { - # When taking a bug, we have to follow the workflow. - my $bug_status = $cgi->param('bug_status') || ''; - ($bug_status) = grep { $_->name eq $bug_status } - @{ $bug->status->can_change_to }; - - if ($bug_status && $bug_status->is_open - && ($bug_status->name ne 'UNCONFIRMED' - || $bug->product_obj->allows_unconfirmed)) - { - $bug->set_bug_status($bug_status->name); - $bug->clear_resolution(); - } - # Make sure the person we are taking the bug from gets mail. - $owner = $bug->assigned_to->login; - $bug->set_assigned_to($user); - } + $bug->add_cc($user) if $cgi->param('addselfcc'); + $bug->update($timestamp); - $bug->add_cc($user) if $cgi->param('addselfcc'); - $bug->update($timestamp); + # We have to update the attachment after updating the bug, to ensure new + # comments are available. + $attachment->update($timestamp); - # We have to update the attachment after updating the bug, to ensure new - # comments are available. - $attachment->update($timestamp); + $dbh->bz_commit_transaction; - $dbh->bz_commit_transaction; + # Define the variables and functions that will be passed to the UI template. + $vars->{'attachment'} = $attachment; - # Define the variables and functions that will be passed to the UI template. - $vars->{'attachment'} = $attachment; - # We cannot reuse the $bug object as delta_ts has eventually been updated - # since the object was created. - $vars->{'bugs'} = [new Bugzilla::Bug($bugid)]; - $vars->{'header_done'} = 1; - $vars->{'contenttypemethod'} = $cgi->param('contenttypemethod'); + # We cannot reuse the $bug object as delta_ts has eventually been updated + # since the object was created. + $vars->{'bugs'} = [new Bugzilla::Bug($bugid)]; + $vars->{'header_done'} = 1; + $vars->{'contenttypemethod'} = $cgi->param('contenttypemethod'); - my $recipients = { 'changer' => $user, 'owner' => $owner }; - $vars->{'sent_bugmail'} = Bugzilla::BugMail::Send($bugid, $recipients); + my $recipients = {'changer' => $user, 'owner' => $owner}; + $vars->{'sent_bugmail'} = Bugzilla::BugMail::Send($bugid, $recipients); - print $cgi->header(); - # Generate and return the UI (HTML page) from the appropriate template. - $template->process("attachment/created.html.tmpl", $vars) - || ThrowTemplateError($template->error()); + print $cgi->header(); + + # Generate and return the UI (HTML page) from the appropriate template. + $template->process("attachment/created.html.tmpl", $vars) + || ThrowTemplateError($template->error()); } # Displays a form for editing attachment properties. @@ -579,227 +607,237 @@ sub insert { # is private and the user does not belong to the insider group. # Validations are done later when the user submits changes. sub edit { - my $attachment = validateID(); + my $attachment = validateID(); - my $bugattachments = - Bugzilla::Attachment->get_attachments_by_bug($attachment->bug); + my $bugattachments + = Bugzilla::Attachment->get_attachments_by_bug($attachment->bug); - my $any_flags_requesteeble = grep { $_->is_requestable && $_->is_requesteeble } - @{ $attachment->flag_types }; - # Useful in case a flagtype is no longer requestable but a requestee - # has been set before we turned off that bit. - $any_flags_requesteeble ||= grep { $_->requestee_id } @{ $attachment->flags }; - $vars->{'any_flags_requesteeble'} = $any_flags_requesteeble; - $vars->{'attachment'} = $attachment; - $vars->{'attachments'} = $bugattachments; + my $any_flags_requesteeble = grep { $_->is_requestable && $_->is_requesteeble } + @{$attachment->flag_types}; - print $cgi->header(); + # Useful in case a flagtype is no longer requestable but a requestee + # has been set before we turned off that bit. + $any_flags_requesteeble ||= grep { $_->requestee_id } @{$attachment->flags}; + $vars->{'any_flags_requesteeble'} = $any_flags_requesteeble; + $vars->{'attachment'} = $attachment; + $vars->{'attachments'} = $bugattachments; + + 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]); - - # 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; + } } + } - $bug->add_cc($user) if $cgi->param('addselfcc'); + # We couldn't do this check earlier as we first had to validate attachment ID + # and display the mid-air collision page if modification_time changed. + my $token = $cgi->param('token'); + check_hash_token($token, [$attachment->id, $attachment->modification_time]); + + # If the user submitted a comment while editing the attachment, + # add the comment to the bug. Do this after having validated isprivate! + my $comment = $cgi->param('comment'); + if (defined $comment && trim($comment) ne '') { + $bug->add_comment( + $comment, + { + isprivate => $attachment->isprivate, + type => CMT_ATTACHMENT_UPDATED, + extra_data => $attachment->id + } + ); + } - my ($flags, $new_flags) = - Bugzilla::Flag->extract_flags_from_cgi($bug, $attachment, $vars); + $bug->add_cc($user) if $cgi->param('addselfcc'); + + my ($flags, $new_flags) + = Bugzilla::Flag->extract_flags_from_cgi($bug, $attachment, $vars); + + if ($can_edit) { + $attachment->set_flags($flags, $new_flags); + } - if ($can_edit) { - $attachment->set_flags($flags, $new_flags); + # Requestees can set flags targetted to them, even if they cannot + # edit the attachment. Flag setters can edit their own flags too. + elsif (scalar @$flags) { + my %flag_list = map { $_->{id} => $_ } @$flags; + my $flag_objs = Bugzilla::Flag->new_from_list([keys %flag_list]); + + my @editable_flags; + foreach my $flag_obj (@$flag_objs) { + if ($flag_obj->setter_id == $user->id + || ($flag_obj->requestee_id && $flag_obj->requestee_id == $user->id)) + { + push(@editable_flags, $flag_list{$flag_obj->id}); + } } - # Requestees can set flags targetted to them, even if they cannot - # edit the attachment. Flag setters can edit their own flags too. - elsif (scalar @$flags) { - my %flag_list = map { $_->{id} => $_ } @$flags; - my $flag_objs = Bugzilla::Flag->new_from_list([keys %flag_list]); - - my @editable_flags; - foreach my $flag_obj (@$flag_objs) { - if ($flag_obj->setter_id == $user->id - || ($flag_obj->requestee_id && $flag_obj->requestee_id == $user->id)) - { - push(@editable_flags, $flag_list{$flag_obj->id}); - } - } - if (scalar @editable_flags) { - $attachment->set_flags(\@editable_flags, []); - # Flag changes must be committed. - $can_edit = 1; - } + if (scalar @editable_flags) { + $attachment->set_flags(\@editable_flags, []); + + # Flag changes must be committed. + $can_edit = 1; } + } - # Figure out when the changes were made. - my $timestamp = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); + # Figure out when the changes were made. + my $timestamp = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); - # Commit the comment, if any. - # This has to happen before updating the attachment, to ensure new comments - # are available to $attachment->update. - $bug->update($timestamp); + # Commit the comment, if any. + # This has to happen before updating the attachment, to ensure new comments + # are available to $attachment->update. + $bug->update($timestamp); - if ($can_edit) { - my $changes = $attachment->update($timestamp); - # If there are changes, we updated delta_ts in the DB. We have to - # reflect this change in the bug object. - $bug->{delta_ts} = $timestamp if scalar(keys %$changes); - } + if ($can_edit) { + my $changes = $attachment->update($timestamp); - # Commit the transaction now that we are finished updating the database. - $dbh->bz_commit_transaction(); + # If there are changes, we updated delta_ts in the DB. We have to + # reflect this change in the bug object. + $bug->{delta_ts} = $timestamp if scalar(keys %$changes); + } - # Define the variables and functions that will be passed to the UI template. - $vars->{'attachment'} = $attachment; - $vars->{'bugs'} = [$bug]; - $vars->{'header_done'} = 1; - $vars->{'sent_bugmail'} = - Bugzilla::BugMail::Send($bug->id, { 'changer' => $user }); + # Commit the transaction now that we are finished updating the database. + $dbh->bz_commit_transaction(); - print $cgi->header(); + # Define the variables and functions that will be passed to the UI template. + $vars->{'attachment'} = $attachment; + $vars->{'bugs'} = [$bug]; + $vars->{'header_done'} = 1; + $vars->{'sent_bugmail'} + = Bugzilla::BugMail::Send($bug->id, {'changer' => $user}); - # Generate and return the UI (HTML page) from the appropriate template. - $template->process("attachment/updated.html.tmpl", $vars) - || ThrowTemplateError($template->error()); + print $cgi->header(); + + # Generate and return the UI (HTML page) from the appropriate template. + $template->process("attachment/updated.html.tmpl", $vars) + || ThrowTemplateError($template->error()); } # Only administrators can delete attachments. sub delete_attachment { - my $user = Bugzilla->login(LOGIN_REQUIRED); - my $dbh = Bugzilla->dbh; + my $user = Bugzilla->login(LOGIN_REQUIRED); + my $dbh = Bugzilla->dbh; - print $cgi->header(); + print $cgi->header(); - $user->in_group('admin') - || ThrowUserError('auth_failure', {group => 'admin', - action => 'delete', - object => 'attachment'}); - - Bugzilla->params->{'allow_attachment_deletion'} - || ThrowUserError('attachment_deletion_disabled'); - - # Make sure the administrator is allowed to edit this attachment. - my $attachment = validateID(); - Bugzilla::Attachment->_check_bug($attachment->bug); - - $attachment->datasize || ThrowUserError('attachment_removed'); - - # We don't want to let a malicious URL accidentally delete an attachment. - my $token = trim($cgi->param('token')); - if ($token) { - my ($creator_id, $date, $event) = Bugzilla::Token::GetTokenData($token); - unless ($creator_id - && ($creator_id == $user->id) - && ($event eq 'delete_attachment' . $attachment->id)) - { - # The token is invalid. - ThrowUserError('token_does_not_exist'); - } + $user->in_group('admin') + || ThrowUserError('auth_failure', + {group => 'admin', action => 'delete', object => 'attachment'}); - my $bug = new Bugzilla::Bug($attachment->bug_id); + Bugzilla->params->{'allow_attachment_deletion'} + || ThrowUserError('attachment_deletion_disabled'); - # The token is valid. Delete the content of the attachment. - my $msg; - $vars->{'attachment'} = $attachment; - $vars->{'reason'} = clean_text($cgi->param('reason') || ''); + # Make sure the administrator is allowed to edit this attachment. + my $attachment = validateID(); + Bugzilla::Attachment->_check_bug($attachment->bug); - $template->process("attachment/delete_reason.txt.tmpl", $vars, \$msg) - || ThrowTemplateError($template->error()); + $attachment->datasize || ThrowUserError('attachment_removed'); - # 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($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/buglist.cgi b/buglist.cgi index 719bb9639..d8b48c048 100755 --- a/buglist.cgi +++ b/buglist.cgi @@ -28,10 +28,10 @@ use Bugzilla::Token; use Date::Parse; -my $cgi = Bugzilla->cgi; -my $dbh = Bugzilla->dbh; +my $cgi = Bugzilla->cgi; +my $dbh = Bugzilla->dbh; my $template = Bugzilla->template; -my $vars = {}; +my $vars = {}; # We have to check the login here to get the correct footer if an error is # thrown and to prevent a logged out user to use QuickSearch if 'requirelogin' @@ -42,26 +42,28 @@ $cgi->redirect_search_url(); my $buffer = $cgi->query_string(); if (length($buffer) == 0) { - ThrowUserError("buglist_parameters_required"); + ThrowUserError("buglist_parameters_required"); } # Determine whether this is a quicksearch query. my $searchstring = $cgi->param('quicksearch'); if (defined($searchstring)) { - $buffer = quicksearch($searchstring); - # Quicksearch may do a redirect, in which case it does not return. - # If it does return, it has modified $cgi->params so we can use them here - # as if this had been a normal query from the beginning. + $buffer = quicksearch($searchstring); + + # Quicksearch may do a redirect, in which case it does not return. + # If it does return, it has modified $cgi->params so we can use them here + # as if this had been a normal query from the beginning. } # If configured to not allow empty words, reject empty searches from the -# Find a Specific Bug search form, including words being a single or +# Find a Specific Bug search form, including words being a single or # several consecutive whitespaces only. -if (!Bugzilla->params->{'search_allow_no_criteria'} - && defined($cgi->param('content')) && $cgi->param('content') =~ /^\s*$/) +if ( !Bugzilla->params->{'search_allow_no_criteria'} + && defined($cgi->param('content')) + && $cgi->param('content') =~ /^\s*$/) { - ThrowUserError("buglist_parameters_required"); + ThrowUserError("buglist_parameters_required"); } ################################################################################ @@ -73,26 +75,31 @@ my $dotweak = $cgi->param('tweak') ? 1 : 0; # Log the user in if ($dotweak) { - Bugzilla->login(LOGIN_REQUIRED); + Bugzilla->login(LOGIN_REQUIRED); } # Hack to support legacy applications that think the RDF ctype is at format=rdf. -if (defined $cgi->param('format') && $cgi->param('format') eq "rdf" - && !defined $cgi->param('ctype')) { - $cgi->param('ctype', "rdf"); - $cgi->delete('format'); +if ( defined $cgi->param('format') + && $cgi->param('format') eq "rdf" + && !defined $cgi->param('ctype')) +{ + $cgi->param('ctype', "rdf"); + $cgi->delete('format'); } # Treat requests for ctype=rss as requests for ctype=atom if (defined $cgi->param('ctype') && $cgi->param('ctype') eq "rss") { - $cgi->param('ctype', "atom"); + $cgi->param('ctype', "atom"); } # Determine the format in which the user would like to receive the output. # Uses the default format if the user did not specify an output format; # otherwise validates the user's choice against the list of available formats. -my $format = $template->get_format("list/list", scalar $cgi->param('format'), - scalar $cgi->param('ctype')); +my $format = $template->get_format( + "list/list", + scalar $cgi->param('format'), + scalar $cgi->param('ctype') +); # Use server push to display a "Please wait..." message for the user while # executing their query if their browser supports it and they are viewing @@ -102,14 +109,13 @@ my $format = $template->get_format("list/list", scalar $cgi->param('format'), # Server push is compatible with Gecko-based browsers and Opera, but not with # MSIE, Lynx or Safari (bug 441496). -my $serverpush = - $format->{'extension'} eq "html" - && exists $ENV{'HTTP_USER_AGENT'} - && $ENV{'HTTP_USER_AGENT'} =~ /(Mozilla.[3-9]|Opera)/ - && $ENV{'HTTP_USER_AGENT'} !~ /compatible/i - && $ENV{'HTTP_USER_AGENT'} !~ /(?:WebKit|Trident|KHTML)/ - && !defined($cgi->param('serverpush')) - || $cgi->param('serverpush'); +my $serverpush + = $format->{'extension'} eq "html" + && exists $ENV{'HTTP_USER_AGENT'} + && $ENV{'HTTP_USER_AGENT'} =~ /(Mozilla.[3-9]|Opera)/ + && $ENV{'HTTP_USER_AGENT'} !~ /compatible/i + && $ENV{'HTTP_USER_AGENT'} !~ /(?:WebKit|Trident|KHTML)/ + && !defined($cgi->param('serverpush')) || $cgi->param('serverpush'); my $order = $cgi->param('order') || ""; @@ -119,25 +125,26 @@ my $params; # If the user is retrieving the last bug list they looked at, hack the buffer # storing the query string so that it looks like a query retrieving those bugs. if (my $last_list = $cgi->param('regetlastlist')) { - my $bug_ids; - - # Logged-out users use the old cookie method for storing the last search. - if (!$user->id or $last_list eq 'cookie') { - $bug_ids = $cgi->cookie('BUGLIST') or ThrowUserError("missing_cookie"); - $bug_ids =~ s/[:-]/,/g; - $order ||= "reuse last sort"; - } - # But logged in users store the last X searches in the DB so they can - # have multiple bug lists available. - else { - my $last_search = Bugzilla::Search::Recent->check( - { id => $last_list }); - $bug_ids = join(',', @{ $last_search->bug_list }); - $order ||= $last_search->list_order; - } - # set up the params for this new query - $params = new Bugzilla::CGI({ bug_id => $bug_ids, order => $order }); - $params->param('list_id', $last_list); + my $bug_ids; + + # Logged-out users use the old cookie method for storing the last search. + if (!$user->id or $last_list eq 'cookie') { + $bug_ids = $cgi->cookie('BUGLIST') or ThrowUserError("missing_cookie"); + $bug_ids =~ s/[:-]/,/g; + $order ||= "reuse last sort"; + } + + # But logged in users store the last X searches in the DB so they can + # have multiple bug lists available. + else { + my $last_search = Bugzilla::Search::Recent->check({id => $last_list}); + $bug_ids = join(',', @{$last_search->bug_list}); + $order ||= $last_search->list_order; + } + + # set up the params for this new query + $params = new Bugzilla::CGI({bug_id => $bug_ids, order => $order}); + $params->param('list_id', $last_list); } # Figure out whether or not the user is doing a fulltext search. If not, @@ -147,10 +154,10 @@ my $fulltext = 0; if ($cgi->param('content')) { $fulltext = 1 } my @charts = map(/^field(\d-\d-\d)$/ ? $1 : (), $cgi->param()); foreach my $chart (@charts) { - if ($cgi->param("field$chart") eq 'content' && $cgi->param("value$chart")) { - $fulltext = 1; - last; - } + if ($cgi->param("field$chart") eq 'content' && $cgi->param("value$chart")) { + $fulltext = 1; + last; + } } ################################################################################ @@ -158,34 +165,35 @@ foreach my $chart (@charts) { ################################################################################ sub DiffDate { - my ($datestr) = @_; - my $date = str2time($datestr); - my $age = time() - $date; - - if( $age < 18*60*60 ) { - $date = format_time($datestr, '%H:%M:%S'); - } elsif( $age < 6*24*60*60 ) { - $date = format_time($datestr, '%a %H:%M'); - } else { - $date = format_time($datestr, '%Y-%m-%d'); - } - return $date; + my ($datestr) = @_; + my $date = str2time($datestr); + my $age = time() - $date; + + if ($age < 18 * 60 * 60) { + $date = format_time($datestr, '%H:%M:%S'); + } + elsif ($age < 6 * 24 * 60 * 60) { + $date = format_time($datestr, '%a %H:%M'); + } + else { + $date = format_time($datestr, '%Y-%m-%d'); + } + return $date; } sub LookupNamedQuery { - my ($name, $sharer_id) = @_; + my ($name, $sharer_id) = @_; - Bugzilla->login(LOGIN_REQUIRED); + Bugzilla->login(LOGIN_REQUIRED); - my $query = Bugzilla::Search::Saved->check( - { user => $sharer_id, name => $name, _error => 'missing_query' }); + my $query = Bugzilla::Search::Saved->check( + {user => $sharer_id, name => $name, _error => 'missing_query'}); - $query->url - || ThrowUserError("buglist_parameters_required"); + $query->url || ThrowUserError("buglist_parameters_required"); - # Detaint $sharer_id. - $sharer_id = $query->user->id if $sharer_id; - return wantarray ? ($query->url, $query->id, $sharer_id) : $query->url; + # Detaint $sharer_id. + $sharer_id = $query->user->id if $sharer_id; + return wantarray ? ($query->url, $query->id, $sharer_id) : $query->url; } # Inserts a Named Query (a "Saved Search") into the database, or @@ -197,119 +205,120 @@ sub LookupNamedQuery { # will throw a UserError. Leading and trailing whitespace # will be stripped from this value before it is inserted # into the DB. -# query - The query part of the buglist.cgi URL, unencoded. Must not be +# query - The query part of the buglist.cgi URL, unencoded. Must not be # empty, or we will throw a UserError. -# link_in_footer (optional) - 1 if the Named Query should be +# link_in_footer (optional) - 1 if the Named Query should be # displayed in the user's footer, 0 otherwise. # # All parameters are validated before passing them into the database. # -# Returns: A boolean true value if the query existed in the database +# Returns: A boolean true value if the query existed in the database # before, and we updated it. A boolean false value otherwise. sub InsertNamedQuery { - my ($query_name, $query, $link_in_footer) = @_; - my $dbh = Bugzilla->dbh; - - $query_name = trim($query_name); - my ($query_obj) = grep {lc($_->name) eq lc($query_name)} @{Bugzilla->user->queries}; - - if ($query_obj) { - $query_obj->set_name($query_name); - $query_obj->set_url($query); - $query_obj->update(); - } else { - Bugzilla::Search::Saved->create({ - name => $query_name, - query => $query, - link_in_footer => $link_in_footer - }); - } - - return $query_obj ? 1 : 0; + my ($query_name, $query, $link_in_footer) = @_; + my $dbh = Bugzilla->dbh; + + $query_name = trim($query_name); + my ($query_obj) + = grep { lc($_->name) eq lc($query_name) } @{Bugzilla->user->queries}; + + if ($query_obj) { + $query_obj->set_name($query_name); + $query_obj->set_url($query); + $query_obj->update(); + } + else { + Bugzilla::Search::Saved->create( + {name => $query_name, query => $query, link_in_footer => $link_in_footer}); + } + + return $query_obj ? 1 : 0; } sub LookupSeries { - my ($series_id) = @_; - detaint_natural($series_id) || ThrowCodeError("invalid_series_id"); - - my $dbh = Bugzilla->dbh; - my $result = $dbh->selectrow_array("SELECT query FROM series " . - "WHERE series_id = ?" - , undef, ($series_id)); - $result - || ThrowCodeError("invalid_series_id", {'series_id' => $series_id}); - return $result; + my ($series_id) = @_; + detaint_natural($series_id) || ThrowCodeError("invalid_series_id"); + + my $dbh = Bugzilla->dbh; + my $result + = $dbh->selectrow_array("SELECT query FROM series " . "WHERE series_id = ?", + undef, ($series_id)); + $result || ThrowCodeError("invalid_series_id", {'series_id' => $series_id}); + return $result; } sub GetQuip { - my $dbh = Bugzilla->dbh; - # COUNT is quick because it is cached for MySQL. We may want to revisit - # this when we support other databases. - my $count = $dbh->selectrow_array("SELECT COUNT(quip)" - . " FROM quips WHERE approved = 1"); - my $random = int(rand($count)); - my $quip = - $dbh->selectrow_array("SELECT quip FROM quips WHERE approved = 1 " . - $dbh->sql_limit(1, $random)); - return $quip; + my $dbh = Bugzilla->dbh; + + # COUNT is quick because it is cached for MySQL. We may want to revisit + # this when we support other databases. + my $count = $dbh->selectrow_array( + "SELECT COUNT(quip)" . " FROM quips WHERE approved = 1"); + my $random = int(rand($count)); + my $quip = $dbh->selectrow_array( + "SELECT quip FROM quips WHERE approved = 1 " . $dbh->sql_limit(1, $random)); + return $quip; } # Return groups available for at least one product of the buglist. sub GetGroups { - my $product_names = shift; - my $user = Bugzilla->user; - my %legal_groups; + my $product_names = shift; + my $user = Bugzilla->user; + my %legal_groups; - foreach my $product_name (@$product_names) { - my $product = Bugzilla::Product->new({name => $product_name, cache => 1}); + foreach my $product_name (@$product_names) { + my $product = Bugzilla::Product->new({name => $product_name, cache => 1}); - foreach my $gid (keys %{$product->group_controls}) { - # The user can only edit groups they belong to. - next unless $user->in_group_id($gid); + foreach my $gid (keys %{$product->group_controls}) { - # The user has no control on groups marked as NA or MANDATORY. - my $group = $product->group_controls->{$gid}; - next if ($group->{membercontrol} == CONTROLMAPMANDATORY - || $group->{membercontrol} == CONTROLMAPNA); + # The user can only edit groups they belong to. + next unless $user->in_group_id($gid); - # It's fine to include inactive groups. Those will be marked - # as "remove only" when editing several bugs at once. - $legal_groups{$gid} ||= $group->{group}; - } + # The user has no control on groups marked as NA or MANDATORY. + my $group = $product->group_controls->{$gid}; + next + if ($group->{membercontrol} == CONTROLMAPMANDATORY + || $group->{membercontrol} == CONTROLMAPNA); + + # It's fine to include inactive groups. Those will be marked + # as "remove only" when editing several bugs at once. + $legal_groups{$gid} ||= $group->{group}; } - # Return a list of group objects. - return [values %legal_groups]; + } + + # Return a list of group objects. + return [values %legal_groups]; } sub _get_common_flag_types { - my $component_ids = shift; - my $user = Bugzilla->user; - - # Get all the different components in the bug list - my $components = Bugzilla::Component->new_from_list($component_ids); - my %flag_types; - my @flag_types_ids; - foreach my $component (@$components) { - foreach my $flag_type (@{$component->flag_types->{'bug'}}) { - push @flag_types_ids, $flag_type->id; - $flag_types{$flag_type->id} = $flag_type; - } - } - - # We only want flags that appear in all components - my %common_flag_types; - foreach my $id (keys %flag_types) { - my $flag_type_count = scalar grep { $_ == $id } @flag_types_ids; - $common_flag_types{$id} = $flag_types{$id} - if $flag_type_count == scalar @$components; + my $component_ids = shift; + my $user = Bugzilla->user; + + # Get all the different components in the bug list + my $components = Bugzilla::Component->new_from_list($component_ids); + my %flag_types; + my @flag_types_ids; + foreach my $component (@$components) { + foreach my $flag_type (@{$component->flag_types->{'bug'}}) { + push @flag_types_ids, $flag_type->id; + $flag_types{$flag_type->id} = $flag_type; } - - # We only show flags that a user can request. - my @show_flag_types - = grep { $user->can_request_flag($_) } values %common_flag_types; - my $any_flags_requesteeble = grep { $_->is_requesteeble } @show_flag_types; - - return(\@show_flag_types, $any_flags_requesteeble); + } + + # We only want flags that appear in all components + my %common_flag_types; + foreach my $id (keys %flag_types) { + my $flag_type_count = scalar grep { $_ == $id } @flag_types_ids; + $common_flag_types{$id} = $flag_types{$id} + if $flag_type_count == scalar @$components; + } + + # We only show flags that a user can request. + my @show_flag_types + = grep { $user->can_request_flag($_) } values %common_flag_types; + my $any_flags_requesteeble = grep { $_->is_requesteeble } @show_flag_types; + + return (\@show_flag_types, $any_flags_requesteeble); } ################################################################################ @@ -322,13 +331,13 @@ my $sharer_id; # Backwards-compatibility - the old interface had cmdtype="runnamed" to run # a named command, and we can't break this because it's in bookmarks. -if ($cmdtype eq "runnamed") { - $cmdtype = "dorem"; - $remaction = "run"; +if ($cmdtype eq "runnamed") { + $cmdtype = "dorem"; + $remaction = "run"; } # Now we're going to be running, so ensure that the params object is set up, -# using ||= so that we only do so if someone hasn't overridden this +# using ||= so that we only do so if someone hasn't overridden this # earlier, for example by setting up a named query search. # This will be modified, so make a copy. @@ -336,51 +345,52 @@ $params ||= new Bugzilla::CGI($cgi); # Generate a reasonable filename for the user agent to suggest to the user # when the user saves the bug list. Uses the name of the remembered query -# if available. We have to do this now, even though we return HTTP headers -# at the end, because the fact that there is a remembered query gets +# if available. We have to do this now, even though we return HTTP headers +# at the end, because the fact that there is a remembered query gets # forgotten in the process of retrieving it. my $disp_prefix = "bugs"; if ($cmdtype eq "dorem" && $remaction =~ /^run/) { - $disp_prefix = $cgi->param('namedcmd'); + $disp_prefix = $cgi->param('namedcmd'); } # Take appropriate action based on user's request. -if ($cmdtype eq "dorem") { - if ($remaction eq "run") { - my $query_id; - ($buffer, $query_id, $sharer_id) = - LookupNamedQuery(scalar $cgi->param("namedcmd"), - scalar $cgi->param('sharer_id')); - # If this is the user's own query, remember information about it - # so that it can be modified easily. - $vars->{'searchname'} = $cgi->param('namedcmd'); - if (!$cgi->param('sharer_id') || - $cgi->param('sharer_id') == $user->id) { - $vars->{'searchtype'} = "saved"; - $vars->{'search_id'} = $query_id; - } - $params = new Bugzilla::CGI($buffer); - $order = $params->param('order') || $order; - - } - elsif ($remaction eq "runseries") { - $buffer = LookupSeries(scalar $cgi->param("series_id")); - $vars->{'searchname'} = $cgi->param('namedcmd'); - $vars->{'searchtype'} = "series"; - $params = new Bugzilla::CGI($buffer); - $order = $params->param('order') || $order; +if ($cmdtype eq "dorem") { + if ($remaction eq "run") { + my $query_id; + ($buffer, $query_id, $sharer_id) + = LookupNamedQuery(scalar $cgi->param("namedcmd"), + scalar $cgi->param('sharer_id')); + + # If this is the user's own query, remember information about it + # so that it can be modified easily. + $vars->{'searchname'} = $cgi->param('namedcmd'); + if (!$cgi->param('sharer_id') || $cgi->param('sharer_id') == $user->id) { + $vars->{'searchtype'} = "saved"; + $vars->{'search_id'} = $query_id; } - elsif ($remaction eq "forget") { - $user = Bugzilla->login(LOGIN_REQUIRED); - # Copy the name into a variable, so that we can trick_taint it for - # the DB. We know it's safe, because we're using placeholders in - # the SQL, and the SQL is only a DELETE. - my $qname = $cgi->param('namedcmd'); - trick_taint($qname); - - # Do not forget the saved search if it is being used in a whine - my $whines_in_use = - $dbh->selectcol_arrayref('SELECT DISTINCT whine_events.subject + $params = new Bugzilla::CGI($buffer); + $order = $params->param('order') || $order; + + } + elsif ($remaction eq "runseries") { + $buffer = LookupSeries(scalar $cgi->param("series_id")); + $vars->{'searchname'} = $cgi->param('namedcmd'); + $vars->{'searchtype'} = "series"; + $params = new Bugzilla::CGI($buffer); + $order = $params->param('order') || $order; + } + elsif ($remaction eq "forget") { + $user = Bugzilla->login(LOGIN_REQUIRED); + + # Copy the name into a variable, so that we can trick_taint it for + # the DB. We know it's safe, because we're using placeholders in + # the SQL, and the SQL is only a DELETE. + my $qname = $cgi->param('namedcmd'); + trick_taint($qname); + + # Do not forget the saved search if it is being used in a whine + my $whines_in_use = $dbh->selectcol_arrayref( + 'SELECT DISTINCT whine_events.subject FROM whine_events INNER JOIN whine_queries ON whine_queries.eventid @@ -389,92 +399,100 @@ if ($cmdtype eq "dorem") { = ? AND whine_queries.query_name = ? - ', undef, $user->id, $qname); - if (scalar(@$whines_in_use)) { - ThrowUserError('saved_search_used_by_whines', - { subjects => join(',', @$whines_in_use), - search_name => $qname } - ); - } - - # If we are here, then we can safely remove the saved search - my $query_id; - ($buffer, $query_id) = LookupNamedQuery(scalar $cgi->param("namedcmd"), - $user->id); - if ($query_id) { - # Make sure the user really wants to delete their saved search. - my $token = $cgi->param('token'); - check_hash_token($token, [$query_id, $qname]); - - $dbh->do('DELETE FROM namedqueries - WHERE id = ?', - undef, $query_id); - $dbh->do('DELETE FROM namedqueries_link_in_footer - WHERE namedquery_id = ?', - undef, $query_id); - $dbh->do('DELETE FROM namedquery_group_map - WHERE namedquery_id = ?', - undef, $query_id); - Bugzilla->memcached->clear({ table => 'namedqueries', id => $query_id }); - } + ', undef, $user->id, $qname + ); + if (scalar(@$whines_in_use)) { + ThrowUserError('saved_search_used_by_whines', + {subjects => join(',', @$whines_in_use), search_name => $qname}); + } - # Now reset the cached queries - $user->flush_queries_cache(); - - print $cgi->header(); - # Generate and return the UI (HTML page) from the appropriate template. - $vars->{'message'} = "buglist_query_gone"; - $vars->{'namedcmd'} = $qname; - $vars->{'url'} = "buglist.cgi?newquery=" . url_quote($buffer) - . "&cmdtype=doit&remtype=asnamed&newqueryname=" . url_quote($qname) - . "&token=" . url_quote(issue_hash_token(['savedsearch'])); - $template->process("global/message.html.tmpl", $vars) - || ThrowTemplateError($template->error()); - exit; + # If we are here, then we can safely remove the saved search + my $query_id; + ($buffer, $query_id) + = LookupNamedQuery(scalar $cgi->param("namedcmd"), $user->id); + if ($query_id) { + + # Make sure the user really wants to delete their saved search. + my $token = $cgi->param('token'); + check_hash_token($token, [$query_id, $qname]); + + $dbh->do( + 'DELETE FROM namedqueries + WHERE id = ?', undef, $query_id + ); + $dbh->do( + 'DELETE FROM namedqueries_link_in_footer + WHERE namedquery_id = ?', undef, $query_id + ); + $dbh->do( + 'DELETE FROM namedquery_group_map + WHERE namedquery_id = ?', undef, $query_id + ); + Bugzilla->memcached->clear({table => 'namedqueries', id => $query_id}); } + + # Now reset the cached queries + $user->flush_queries_cache(); + + print $cgi->header(); + + # Generate and return the UI (HTML page) from the appropriate template. + $vars->{'message'} = "buglist_query_gone"; + $vars->{'namedcmd'} = $qname; + $vars->{'url'} + = "buglist.cgi?newquery=" + . url_quote($buffer) + . "&cmdtype=doit&remtype=asnamed&newqueryname=" + . url_quote($qname) + . "&token=" + . url_quote(issue_hash_token(['savedsearch'])); + $template->process("global/message.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + exit; + } } elsif (($cmdtype eq "doit") && defined $cgi->param('remtype')) { - if ($cgi->param('remtype') eq "asdefault") { - $user = Bugzilla->login(LOGIN_REQUIRED); - my $token = $cgi->param('token'); - check_hash_token($token, ['searchknob']); - $buffer = $params->canonicalise_query('cmdtype', 'remtype', - 'query_based_on', 'token'); - InsertNamedQuery(DEFAULT_QUERY_NAME, $buffer); - $vars->{'message'} = "buglist_new_default_query"; + if ($cgi->param('remtype') eq "asdefault") { + $user = Bugzilla->login(LOGIN_REQUIRED); + my $token = $cgi->param('token'); + check_hash_token($token, ['searchknob']); + $buffer = $params->canonicalise_query('cmdtype', 'remtype', 'query_based_on', + 'token'); + InsertNamedQuery(DEFAULT_QUERY_NAME, $buffer); + $vars->{'message'} = "buglist_new_default_query"; + } + elsif ($cgi->param('remtype') eq "asnamed") { + $user = Bugzilla->login(LOGIN_REQUIRED); + my $query_name = $cgi->param('newqueryname'); + my $new_query = $cgi->param('newquery'); + my $token = $cgi->param('token'); + check_hash_token($token, ['savedsearch']); + my $existed_before = InsertNamedQuery($query_name, $new_query, 1); + if ($existed_before) { + $vars->{'message'} = "buglist_updated_named_query"; } - elsif ($cgi->param('remtype') eq "asnamed") { - $user = Bugzilla->login(LOGIN_REQUIRED); - my $query_name = $cgi->param('newqueryname'); - my $new_query = $cgi->param('newquery'); - my $token = $cgi->param('token'); - check_hash_token($token, ['savedsearch']); - my $existed_before = InsertNamedQuery($query_name, $new_query, 1); - if ($existed_before) { - $vars->{'message'} = "buglist_updated_named_query"; - } - else { - $vars->{'message'} = "buglist_new_named_query"; - } - $vars->{'queryname'} = $query_name; + else { + $vars->{'message'} = "buglist_new_named_query"; + } + $vars->{'queryname'} = $query_name; - # Make sure to invalidate any cached query data, so that the footer is - # correctly displayed - $user->flush_queries_cache(); + # Make sure to invalidate any cached query data, so that the footer is + # correctly displayed + $user->flush_queries_cache(); - print $cgi->header(); - $template->process("global/message.html.tmpl", $vars) - || ThrowTemplateError($template->error()); - exit; - } + print $cgi->header(); + $template->process("global/message.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + exit; + } } # backward compatibility hack: if the saved query doesn't say which # form was used to create it, assume it was on the advanced query # form - see bug 252295 if (!$params->param('query_format')) { - $params->param('query_format', 'advanced'); - $buffer = $params->query_string; + $params->param('query_format', 'advanced'); + $buffer = $params->query_string; } ################################################################################ @@ -487,40 +505,42 @@ my $columns = Bugzilla::Search::COLUMNS; # Display Column Determination ################################################################################ -# Determine the columns that will be displayed in the bug list via the +# Determine the columns that will be displayed in the bug list via the # columnlist CGI parameter, the user's preferences, or the default. my @displaycolumns = (); if (defined $params->param('columnlist')) { - if ($params->param('columnlist') eq "all") { - # If the value of the CGI parameter is "all", display all columns, - # but remove the redundant "short_desc" column. - @displaycolumns = grep($_ ne 'short_desc', keys(%$columns)); - } - else { - @displaycolumns = split(/[ ,]+/, $params->param('columnlist')); - } + if ($params->param('columnlist') eq "all") { + + # If the value of the CGI parameter is "all", display all columns, + # but remove the redundant "short_desc" column. + @displaycolumns = grep($_ ne 'short_desc', keys(%$columns)); + } + else { + @displaycolumns = split(/[ ,]+/, $params->param('columnlist')); + } } elsif (defined $cgi->cookie('COLUMNLIST')) { - # 2002-10-31 Rename column names (see bug 176461) - my $columnlist = $cgi->cookie('COLUMNLIST'); - $columnlist =~ s/\bowner\b/assigned_to/; - $columnlist =~ s/\bowner_realname\b/assigned_to_realname/; - $columnlist =~ s/\bplatform\b/rep_platform/; - $columnlist =~ s/\bseverity\b/bug_severity/; - $columnlist =~ s/\bstatus\b/bug_status/; - $columnlist =~ s/\bsummaryfull\b/short_desc/; - $columnlist =~ s/\bsummary\b/short_short_desc/; - - # Use the columns listed in the user's preferences. - @displaycolumns = split(/ /, $columnlist); + + # 2002-10-31 Rename column names (see bug 176461) + my $columnlist = $cgi->cookie('COLUMNLIST'); + $columnlist =~ s/\bowner\b/assigned_to/; + $columnlist =~ s/\bowner_realname\b/assigned_to_realname/; + $columnlist =~ s/\bplatform\b/rep_platform/; + $columnlist =~ s/\bseverity\b/bug_severity/; + $columnlist =~ s/\bstatus\b/bug_status/; + $columnlist =~ s/\bsummaryfull\b/short_desc/; + $columnlist =~ s/\bsummary\b/short_short_desc/; + + # Use the columns listed in the user's preferences. + @displaycolumns = split(/ /, $columnlist); } else { - # Use the default list of columns. - @displaycolumns = DEFAULT_COLUMN_LIST; + # Use the default list of columns. + @displaycolumns = DEFAULT_COLUMN_LIST; } -# Weed out columns that don't actually exist to prevent the user -# from hacking their column list cookie to grab data to which they +# Weed out columns that don't actually exist to prevent the user +# from hacking their column list cookie to grab data to which they # should not have access. Detaint the data along the way. @displaycolumns = grep($columns->{$_} && trick_taint($_), @displaycolumns); @@ -531,14 +551,14 @@ else { # Remove the timetracking columns if they are not a part of the group # (happens if a user had access to time tracking and it was revoked/disabled) if (!$user->is_timetracker) { - foreach my $tt_field (TIMETRACKING_FIELDS) { - @displaycolumns = grep($_ ne $tt_field, @displaycolumns); - } + foreach my $tt_field (TIMETRACKING_FIELDS) { + @displaycolumns = grep($_ ne $tt_field, @displaycolumns); + } } # Remove the relevance column if the user is not doing a fulltext search. if (grep('relevance', @displaycolumns) && !$fulltext) { - @displaycolumns = grep($_ ne 'relevance', @displaycolumns); + @displaycolumns = grep($_ ne 'relevance', @displaycolumns); } ################################################################################ @@ -550,13 +570,14 @@ if (grep('relevance', @displaycolumns) && !$fulltext) { # The bug ID is always selected because bug IDs are always displayed. # Severity, priority, resolution and status are required for buglist # CSS classes. -my @selectcolumns = ("bug_id", "bug_severity", "priority", "bug_status", - "resolution", "product"); +my @selectcolumns + = ("bug_id", "bug_severity", "priority", "bug_status", "resolution", + "product"); # remaining and actual_time are required for percentage_complete calculation: if (grep { $_ eq "percentage_complete" } @displaycolumns) { - push (@selectcolumns, "remaining_time"); - push (@selectcolumns, "actual_time"); + push(@selectcolumns, "remaining_time"); + push(@selectcolumns, "actual_time"); } # Make sure that the login_name version of a field is always also @@ -564,58 +585,51 @@ if (grep { $_ eq "percentage_complete" } @displaycolumns) { # display the login name when the realname is empty. my @realname_fields = grep(/_realname$/, @displaycolumns); foreach my $item (@realname_fields) { - my $login_field = $item; - $login_field =~ s/_realname$//; - if (!grep($_ eq $login_field, @selectcolumns)) { - push(@selectcolumns, $login_field); - } + my $login_field = $item; + $login_field =~ s/_realname$//; + if (!grep($_ eq $login_field, @selectcolumns)) { + push(@selectcolumns, $login_field); + } } # Display columns are selected because otherwise we could not display them. foreach my $col (@displaycolumns) { - push (@selectcolumns, $col) if !grep($_ eq $col, @selectcolumns); + push(@selectcolumns, $col) if !grep($_ eq $col, @selectcolumns); } -# If the user is editing multiple bugs, we also make sure to select the +# If the user is editing multiple bugs, we also make sure to select the # status, because the values of that field determines what options the user # has for modifying the bugs. if ($dotweak) { - push(@selectcolumns, "bug_status") if !grep($_ eq 'bug_status', @selectcolumns); - push(@selectcolumns, "bugs.component_id"); + push(@selectcolumns, "bug_status") if !grep($_ eq 'bug_status', @selectcolumns); + push(@selectcolumns, "bugs.component_id"); } if ($format->{'extension'} eq 'ics') { - push(@selectcolumns, "opendate") if !grep($_ eq 'opendate', @selectcolumns); - if (Bugzilla->params->{'timetrackinggroup'}) { - push(@selectcolumns, "deadline") if !grep($_ eq 'deadline', @selectcolumns); - } + push(@selectcolumns, "opendate") if !grep($_ eq 'opendate', @selectcolumns); + if (Bugzilla->params->{'timetrackinggroup'}) { + push(@selectcolumns, "deadline") if !grep($_ eq 'deadline', @selectcolumns); + } } if ($format->{'extension'} eq 'atom') { - # The title of the Atom feed will be the same one as for the bug list. - $vars->{'title'} = $cgi->param('title'); - - # This is the list of fields that are needed by the Atom filter. - my @required_atom_columns = ( - 'short_desc', - 'opendate', - 'changeddate', - 'reporter', - 'reporter_realname', - 'priority', - 'bug_severity', - 'assigned_to', - 'assigned_to_realname', - 'bug_status', - 'product', - 'component', - 'resolution' - ); - push(@required_atom_columns, 'target_milestone') if Bugzilla->params->{'usetargetmilestone'}; - foreach my $required (@required_atom_columns) { - push(@selectcolumns, $required) if !grep($_ eq $required,@selectcolumns); - } + # The title of the Atom feed will be the same one as for the bug list. + $vars->{'title'} = $cgi->param('title'); + + # This is the list of fields that are needed by the Atom filter. + my @required_atom_columns = ( + 'short_desc', 'opendate', 'changeddate', 'reporter', + 'reporter_realname', 'priority', 'bug_severity', 'assigned_to', + 'assigned_to_realname', 'bug_status', 'product', 'component', + 'resolution' + ); + push(@required_atom_columns, 'target_milestone') + if Bugzilla->params->{'usetargetmilestone'}; + + foreach my $required (@required_atom_columns) { + push(@selectcolumns, $required) if !grep($_ eq $required, @selectcolumns); + } } ################################################################################ @@ -627,76 +641,79 @@ if ($format->{'extension'} eq 'atom') { # First check if we'll want to reuse the last sorting order; that happens if # the order is not defined or its value is "reuse last sort" if (!$order || $order =~ /^reuse/i) { - if ($cgi->cookie('LASTORDER')) { - $order = $cgi->cookie('LASTORDER'); - - # Cookies from early versions of Specific Search included this text, - # which is now invalid. - $order =~ s/ LIMIT 200//; - } - else { - $order = ''; # Remove possible "reuse" identifier as unnecessary - } + if ($cgi->cookie('LASTORDER')) { + $order = $cgi->cookie('LASTORDER'); + + # Cookies from early versions of Specific Search included this text, + # which is now invalid. + $order =~ s/ LIMIT 200//; + } + else { + $order = ''; # Remove possible "reuse" identifier as unnecessary + } } my @order_columns; if ($order) { - # Convert the value of the "order" form field into a list of columns - # by which to sort the results. - ORDER: for ($order) { - /^Bug Number$/ && do { - @order_columns = ("bug_id"); - last ORDER; - }; - /^Importance$/ && do { - @order_columns = ("priority", "bug_severity"); - last ORDER; - }; - /^Assignee$/ && do { - @order_columns = ("assigned_to", "bug_status", "priority", - "bug_id"); - last ORDER; - }; - /^Last Changed$/ && do { - @order_columns = ("changeddate", "bug_status", "priority", - "assigned_to", "bug_id"); - last ORDER; - }; - do { - # A custom list of columns. Bugzilla::Search will validate items. - @order_columns = split(/\s*,\s*/, $order); - }; - } + + # Convert the value of the "order" form field into a list of columns + # by which to sort the results. +ORDER: for ($order) { + /^Bug Number$/ && do { + @order_columns = ("bug_id"); + last ORDER; + }; + /^Importance$/ && do { + @order_columns = ("priority", "bug_severity"); + last ORDER; + }; + /^Assignee$/ && do { + @order_columns = ("assigned_to", "bug_status", "priority", "bug_id"); + last ORDER; + }; + /^Last Changed$/ && do { + @order_columns + = ("changeddate", "bug_status", "priority", "assigned_to", "bug_id"); + last ORDER; + }; + do { + # A custom list of columns. Bugzilla::Search will validate items. + @order_columns = split(/\s*,\s*/, $order); + }; + } } if (!scalar @order_columns) { - # DEFAULT - @order_columns = ("bug_status", "priority", "assigned_to", "bug_id"); + + # DEFAULT + @order_columns = ("bug_status", "priority", "assigned_to", "bug_id"); } # In the HTML interface, by default, we limit the returned results, # which speeds up quite a few searches where people are really only looking # for the top results. if ($format->{'extension'} eq 'html' && !defined $params->param('limit')) { - $params->param('limit', Bugzilla->params->{'default_search_limit'}); - $vars->{'default_limited'} = 1; + $params->param('limit', Bugzilla->params->{'default_search_limit'}); + $vars->{'default_limited'} = 1; } # Generate the basic SQL query that will be used to generate the bug list. -my $search = new Bugzilla::Search('fields' => \@selectcolumns, - 'params' => scalar $params->Vars, - 'order' => \@order_columns, - 'sharer' => $sharer_id); +my $search = new Bugzilla::Search( + 'fields' => \@selectcolumns, + 'params' => scalar $params->Vars, + 'order' => \@order_columns, + 'sharer' => $sharer_id +); $order = join(',', $search->order); if (scalar @{$search->invalid_order_columns}) { - $vars->{'message'} = 'invalid_column_name'; - $vars->{'invalid_fragments'} = $search->invalid_order_columns; + $vars->{'message'} = 'invalid_column_name'; + $vars->{'invalid_fragments'} = $search->invalid_order_columns; } -if ($fulltext and grep { /^relevance/ } $search->order) { - $vars->{'message'} = 'buglist_sorted_by_relevance' +if ($fulltext and grep {/^relevance/} $search->order) { + $vars->{'message'} = 'buglist_sorted_by_relevance'; } # We don't want saved searches and other buglist things to save @@ -710,22 +727,22 @@ $params->delete('limit') if $vars->{'default_limited'}; # Time to use server push to display an interim message to the user until # the query completes and we can display the bug list. if ($serverpush) { - print $cgi->multipart_init(); - print $cgi->multipart_start(-type => 'text/html'); - - # Generate and return the UI (HTML page) from the appropriate template. - $template->process("list/server-push.html.tmpl", $vars) - || ThrowTemplateError($template->error()); - - # Under mod_perl, flush stdout so that the page actually shows up. - if ($ENV{MOD_PERL}) { - require Apache2::RequestUtil; - Apache2::RequestUtil->request->rflush(); - } - - # Don't do multipart_end() until we're ready to display the replacement - # page, otherwise any errors that happen before then (like SQL errors) - # will result in a blank page being shown to the user instead of the error. + print $cgi->multipart_init(); + print $cgi->multipart_start(-type => 'text/html'); + + # Generate and return the UI (HTML page) from the appropriate template. + $template->process("list/server-push.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + + # Under mod_perl, flush stdout so that the page actually shows up. + if ($ENV{MOD_PERL}) { + require Apache2::RequestUtil; + Apache2::RequestUtil->request->rflush(); + } + + # Don't do multipart_end() until we're ready to display the replacement + # page, otherwise any errors that happen before then (like SQL errors) + # will result in a blank page being shown to the user instead of the error. } # Connect to the shadow database if this installation is using one to improve @@ -742,24 +759,25 @@ $::SIG{PIPE} = 'DEFAULT'; my ($data, $extra_data) = $search->data; $vars->{'search_description'} = $search->search_description; -if ($cgi->param('debug') - && Bugzilla->params->{debug_group} - && $user->in_group(Bugzilla->params->{debug_group}) -) { - $vars->{'debug'} = 1; - $vars->{'queries'} = $extra_data; - my $query_time = 0; - $query_time += $_->{'time'} foreach @$extra_data; - $vars->{'query_time'} = $query_time; - # Explains are limited to admins because you could use them to figure - # out how many hidden bugs are in a particular product (by doing - # searches and looking at the number of rows the explain says it's - # examining). - if ($user->in_group('admin')) { - foreach my $query (@$extra_data) { - $query->{explain} = $dbh->bz_explain($query->{sql}); - } +if ( $cgi->param('debug') + && Bugzilla->params->{debug_group} + && $user->in_group(Bugzilla->params->{debug_group})) +{ + $vars->{'debug'} = 1; + $vars->{'queries'} = $extra_data; + my $query_time = 0; + $query_time += $_->{'time'} foreach @$extra_data; + $vars->{'query_time'} = $query_time; + + # Explains are limited to admins because you could use them to figure + # out how many hidden bugs are in a particular product (by doing + # searches and looking at the number of rows the explain says it's + # examining). + if ($user->in_group('admin')) { + foreach my $query (@$extra_data) { + $query->{explain} = $dbh->bz_explain($query->{sql}); } + } } ################################################################################ @@ -771,73 +789,75 @@ 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 - detaint_natural($bug->{'bug_id'}); - 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 + detaint_natural($bug->{'bug_id'}); + 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' @@ -845,39 +865,41 @@ foreach my $row (@$data) { # or because of human choice my %min_membercontrol; if (@bugidlist) { - my $sth = $dbh->prepare( - "SELECT DISTINCT bugs.bug_id, MIN(group_control_map.membercontrol) " . - "FROM bugs " . - "INNER JOIN bug_group_map " . - "ON bugs.bug_id = bug_group_map.bug_id " . - "LEFT JOIN group_control_map " . - "ON group_control_map.product_id = bugs.product_id " . - "AND group_control_map.group_id = bug_group_map.group_id " . - "WHERE " . $dbh->sql_in('bugs.bug_id', \@bugidlist) . - $dbh->sql_group_by('bugs.bug_id')); - $sth->execute(); - while (my ($bug_id, $min_membercontrol) = $sth->fetchrow_array()) { - $min_membercontrol{$bug_id} = $min_membercontrol || CONTROLMAPNA; + my $sth + = $dbh->prepare( + "SELECT DISTINCT bugs.bug_id, MIN(group_control_map.membercontrol) " + . "FROM bugs " + . "INNER JOIN bug_group_map " + . "ON bugs.bug_id = bug_group_map.bug_id " + . "LEFT JOIN group_control_map " + . "ON group_control_map.product_id = bugs.product_id " + . "AND group_control_map.group_id = bug_group_map.group_id " + . "WHERE " + . $dbh->sql_in('bugs.bug_id', \@bugidlist) + . $dbh->sql_group_by('bugs.bug_id')); + $sth->execute(); + while (my ($bug_id, $min_membercontrol) = $sth->fetchrow_array()) { + $min_membercontrol{$bug_id} = $min_membercontrol || CONTROLMAPNA; + } + foreach my $bug (@bugs) { + next unless defined($min_membercontrol{$bug->{'bug_id'}}); + if ($min_membercontrol{$bug->{'bug_id'}} == CONTROLMAPMANDATORY) { + $bug->{'secure_mode'} = 'implied'; } - foreach my $bug (@bugs) { - next unless defined($min_membercontrol{$bug->{'bug_id'}}); - if ($min_membercontrol{$bug->{'bug_id'}} == CONTROLMAPMANDATORY) { - $bug->{'secure_mode'} = 'implied'; - } - else { - $bug->{'secure_mode'} = 'manual'; - } + else { + $bug->{'secure_mode'} = 'manual'; } + } } # Compute percentage complete without rounding. -my $sum = $time_info->{'actual_time'}+$time_info->{'remaining_time'}; +my $sum = $time_info->{'actual_time'} + $time_info->{'remaining_time'}; if ($sum > 0) { - $time_info->{'percentage_complete'} = 100*$time_info->{'actual_time'}/$sum; + $time_info->{'percentage_complete'} = 100 * $time_info->{'actual_time'} / $sum; +} +else { # remaining_time <= 0 + $time_info->{'percentage_complete'} = 0; } -else { # remaining_time <= 0 - $time_info->{'percentage_complete'} = 0 -} ################################################################################ # Template Variable Definition @@ -885,45 +907,45 @@ else { # remaining_time <= 0 # Define the variables and functions that will be passed to the UI template. -$vars->{'bugs'} = \@bugs; -$vars->{'buglist'} = \@bugidlist; -$vars->{'columns'} = $columns; +$vars->{'bugs'} = \@bugs; +$vars->{'buglist'} = \@bugidlist; +$vars->{'columns'} = $columns; $vars->{'displaycolumns'} = \@displaycolumns; $vars->{'openstates'} = [BUG_STATE_OPEN]; -$vars->{'closedstates'} = [map {$_->name} closed_bug_statuses()]; +$vars->{'closedstates'} = [map { $_->name } closed_bug_statuses()]; # The iCal file needs priorities ordered from 1 to 9 (highest to lowest) # If there are more than 9 values, just make all the lower ones 9 if ($format->{'extension'} eq 'ics') { - my $n = 1; - $vars->{'ics_priorities'} = {}; - my $priorities = get_legal_field_values('priority'); - foreach my $p (@$priorities) { - $vars->{'ics_priorities'}->{$p} = ($n > 9) ? 9 : $n++; - } + my $n = 1; + $vars->{'ics_priorities'} = {}; + my $priorities = get_legal_field_values('priority'); + foreach my $p (@$priorities) { + $vars->{'ics_priorities'}->{$p} = ($n > 9) ? 9 : $n++; + } } -$vars->{'order'} = $order; +$vars->{'order'} = $order; $vars->{'caneditbugs'} = 1; -$vars->{'time_info'} = $time_info; +$vars->{'time_info'} = $time_info; if (!$user->in_group('editbugs')) { - foreach my $product (keys %$bugproducts) { - my $prod = Bugzilla::Product->new({name => $product, cache => 1}); - if (!$user->in_group('editbugs', $prod->id)) { - $vars->{'caneditbugs'} = 0; - last; - } + foreach my $product (keys %$bugproducts) { + my $prod = Bugzilla::Product->new({name => $product, cache => 1}); + if (!$user->in_group('editbugs', $prod->id)) { + $vars->{'caneditbugs'} = 0; + last; } + } } my @bugowners = keys %$bugowners; if (scalar(@bugowners) > 1 && $user->in_group('editbugs')) { - my $suffix = Bugzilla->params->{'emailsuffix'}; - map(s/$/$suffix/, @bugowners) if $suffix; - my $bugowners = join(",", @bugowners); - $vars->{'bugowners'} = $bugowners; + my $suffix = Bugzilla->params->{'emailsuffix'}; + map(s/$/$suffix/, @bugowners) if $suffix; + my $bugowners = join(",", @bugowners); + $vars->{'bugowners'} = $bugowners; } # Whether or not to split the column titles across two rows to make @@ -931,7 +953,7 @@ if (scalar(@bugowners) > 1 && $user->in_group('editbugs')) { $vars->{'splitheader'} = $cgi->cookie('SPLITHEADER') ? 1 : 0; if ($user->settings->{'display_quips'}->{'value'} eq 'on') { - $vars->{'quip'} = GetQuip(); + $vars->{'quip'} = GetQuip(); } $vars->{'currenttime'} = localtime(time()); @@ -941,18 +963,20 @@ $vars->{'currenttime'} = localtime(time()); my @products = keys %$bugproducts; my $one_product; if (scalar(@products) == 1) { - $one_product = Bugzilla::Product->new({ name => $products[0], cache => 1 }); + $one_product = Bugzilla::Product->new({name => $products[0], cache => 1}); } + # This is used in the "Zarroo Boogs" case. elsif (my @product_input = $cgi->param('product')) { - if (scalar(@product_input) == 1 and $product_input[0] ne '') { - $one_product = Bugzilla::Product->new({ name => $product_input[0], cache => 1 }); - } + if (scalar(@product_input) == 1 and $product_input[0] ne '') { + $one_product = Bugzilla::Product->new({name => $product_input[0], cache => 1}); + } } -# We only want the template to use it if the user can actually + +# We only want the template to use it if the user can actually # enter bugs against it. if ($one_product && $user->can_enter_product($one_product)) { - $vars->{'one_product'} = $one_product; + $vars->{'one_product'} = $one_product; } # See if there's only one component in all the results (or only one component @@ -960,50 +984,50 @@ if ($one_product && $user->can_enter_product($one_product)) { my @components = keys %$bugcomponents; my $one_component; if (scalar(@components) == 1) { - $vars->{one_component} = $components[0]; + $vars->{one_component} = $components[0]; } + # This is used in the "Zarroo Boogs" case. elsif (my @component_input = $cgi->param('component')) { - if (scalar(@component_input) == 1 and $component_input[0] ne '') { - $vars->{one_component}= $cgi->param('component'); - } + if (scalar(@component_input) == 1 and $component_input[0] ne '') { + $vars->{one_component} = $cgi->param('component'); + } } # The following variables are used when the user is making changes to multiple bugs. if ($dotweak && scalar @bugs) { - if (!$vars->{'caneditbugs'}) { - ThrowUserError('auth_failure', {group => 'editbugs', - action => 'modify', - object => 'multiple_bugs'}); - } - $vars->{'dotweak'} = 1; - - # issue_session_token needs to write to the master DB. - Bugzilla->switch_to_main_db(); - $vars->{'token'} = issue_session_token('buglist_mass_change'); - Bugzilla->switch_to_shadow_db(); - - $vars->{'products'} = $user->get_enterable_products; - $vars->{'platforms'} = get_legal_field_values('rep_platform'); - $vars->{'op_sys'} = get_legal_field_values('op_sys'); - $vars->{'priorities'} = get_legal_field_values('priority'); - $vars->{'severities'} = get_legal_field_values('bug_severity'); - $vars->{'resolutions'} = get_legal_field_values('resolution'); - - ($vars->{'flag_types'}, $vars->{any_flags_requesteeble}) - = _get_common_flag_types([keys %$bugcomponentids]); - - # Convert bug statuses to their ID. - my @bug_statuses = map {$dbh->quote($_)} keys %$bugstatuses; - my $bug_status_ids = - $dbh->selectcol_arrayref('SELECT id FROM bug_status - WHERE ' . $dbh->sql_in('value', \@bug_statuses)); - - # This query collects new statuses which are common to all current bug statuses. - # It also accepts transitions where the bug status doesn't change. - $bug_status_ids = - $dbh->selectcol_arrayref( - 'SELECT DISTINCT sw1.new_status + if (!$vars->{'caneditbugs'}) { + ThrowUserError('auth_failure', + {group => 'editbugs', action => 'modify', object => 'multiple_bugs'}); + } + $vars->{'dotweak'} = 1; + + # issue_session_token needs to write to the master DB. + Bugzilla->switch_to_main_db(); + $vars->{'token'} = issue_session_token('buglist_mass_change'); + Bugzilla->switch_to_shadow_db(); + + $vars->{'products'} = $user->get_enterable_products; + $vars->{'platforms'} = get_legal_field_values('rep_platform'); + $vars->{'op_sys'} = get_legal_field_values('op_sys'); + $vars->{'priorities'} = get_legal_field_values('priority'); + $vars->{'severities'} = get_legal_field_values('bug_severity'); + $vars->{'resolutions'} = get_legal_field_values('resolution'); + + ($vars->{'flag_types'}, $vars->{any_flags_requesteeble}) + = _get_common_flag_types([keys %$bugcomponentids]); + + # Convert bug statuses to their ID. + my @bug_statuses = map { $dbh->quote($_) } keys %$bugstatuses; + my $bug_status_ids = $dbh->selectcol_arrayref( + 'SELECT id FROM bug_status + WHERE ' . $dbh->sql_in('value', \@bug_statuses) + ); + + # This query collects new statuses which are common to all current bug statuses. + # It also accepts transitions where the bug status doesn't change. + $bug_status_ids = $dbh->selectcol_arrayref( + 'SELECT DISTINCT sw1.new_status FROM status_workflow sw1 INNER JOIN bug_status ON bug_status.id = sw1.new_status @@ -1012,74 +1036,78 @@ if ($dotweak && scalar @bugs) { (SELECT * FROM status_workflow sw2 WHERE sw2.old_status != sw1.new_status AND ' - . $dbh->sql_in('sw2.old_status', $bug_status_ids) - . ' AND NOT EXISTS + . $dbh->sql_in('sw2.old_status', $bug_status_ids) . ' AND NOT EXISTS (SELECT * FROM status_workflow sw3 WHERE sw3.new_status = sw1.new_status - AND sw3.old_status = sw2.old_status))'); - - $vars->{'current_bug_statuses'} = [keys %$bugstatuses]; - $vars->{'new_bug_statuses'} = Bugzilla::Status->new_from_list($bug_status_ids); - - # The groups the user belongs to and which are editable for the given buglist. - $vars->{'groups'} = GetGroups(\@products); - - # If all bugs being changed are in the same product, the user can change - # their version and component, so generate a list of products, a list of - # versions for the product (if there is only one product on the list of - # products), and a list of components for the product. - if ($one_product) { - $vars->{'versions'} = [map($_->name, grep($_->is_active, @{ $one_product->versions }))]; - $vars->{'components'} = [map($_->name, grep($_->is_active, @{ $one_product->components }))]; - if (Bugzilla->params->{'usetargetmilestone'}) { - $vars->{'milestones'} = [map($_->name, grep($_->is_active, - @{ $one_product->milestones }))]; - } + AND sw3.old_status = sw2.old_status))' + ); + + $vars->{'current_bug_statuses'} = [keys %$bugstatuses]; + $vars->{'new_bug_statuses'} = Bugzilla::Status->new_from_list($bug_status_ids); + + # The groups the user belongs to and which are editable for the given buglist. + $vars->{'groups'} = GetGroups(\@products); + + # If all bugs being changed are in the same product, the user can change + # their version and component, so generate a list of products, a list of + # versions for the product (if there is only one product on the list of + # products), and a list of components for the product. + if ($one_product) { + $vars->{'versions'} + = [map($_->name, grep($_->is_active, @{$one_product->versions}))]; + $vars->{'components'} + = [map($_->name, grep($_->is_active, @{$one_product->components}))]; + if (Bugzilla->params->{'usetargetmilestone'}) { + $vars->{'milestones'} + = [map($_->name, grep($_->is_active, @{$one_product->milestones}))]; + } + } + else { + # We will only show the values at are active in all products. + my %values = (); + my @fields = ('components', 'versions'); + if (Bugzilla->params->{'usetargetmilestone'}) { + push @fields, 'milestones'; } - else { - # We will only show the values at are active in all products. - my %values = (); - my @fields = ('components', 'versions'); - if (Bugzilla->params->{'usetargetmilestone'}) { - push @fields, 'milestones'; - } - # Go through each product and count the number of times each field - # is used - foreach my $product_name (@products) { - my $product = Bugzilla::Product->new({name => $product_name, cache => 1}); - foreach my $field (@fields) { - my $list = $product->$field; - foreach my $item (@$list) { - ++$values{$field}{$item->name} if $item->is_active; - } - } + # Go through each product and count the number of times each field + # is used + foreach my $product_name (@products) { + my $product = Bugzilla::Product->new({name => $product_name, cache => 1}); + foreach my $field (@fields) { + my $list = $product->$field; + foreach my $item (@$list) { + ++$values{$field}{$item->name} if $item->is_active; } + } + } - # Now we get the list of each field and see which values have - # $product_count (i.e. appears in every product) - my $product_count = scalar(@products); - foreach my $field (@fields) { - my @values = grep { $values{$field}{$_} == $product_count } keys %{$values{$field}}; - if (scalar @values) { - @{$vars->{$field}} = $field eq 'version' - ? sort { vers_cmp(lc($a), lc($b)) } @values - : sort { lc($a) cmp lc($b) } @values - } - - # Do we need to show a warning about limited visiblity? - if (@values != scalar keys %{$values{$field}}) { - $vars->{excluded_values} = 1; - } - } + # Now we get the list of each field and see which values have + # $product_count (i.e. appears in every product) + my $product_count = scalar(@products); + foreach my $field (@fields) { + my @values + = grep { $values{$field}{$_} == $product_count } keys %{$values{$field}}; + if (scalar @values) { + @{$vars->{$field}} + = $field eq 'version' + ? sort { vers_cmp(lc($a), lc($b)) } @values + : sort { lc($a) cmp lc($b) } @values; + } + + # Do we need to show a warning about limited visiblity? + if (@values != scalar keys %{$values{$field}}) { + $vars->{excluded_values} = 1; + } } + } } # If we're editing a stored query, use the existing query name as default for # the "Remember search as" field. $vars->{'defaultsavename'} = $cgi->param('query_based_on'); -# If we did a quick search then redisplay the previously entered search +# If we did a quick search then redisplay the previously entered search # string in the text field. $vars->{'quicksearch'} = $searchstring; @@ -1093,32 +1121,33 @@ my $contenttype; my $disposition = "inline"; if ($format->{'extension'} eq "html") { - my $list_id = $cgi->param('list_id') || $cgi->param('regetlastlist'); - my $search = $user->save_last_search( - { bugs => \@bugidlist, order => $order, vars => $vars, list_id => $list_id }); - $cgi->param('list_id', $search->id) if $search; - $contenttype = "text/html"; + my $list_id = $cgi->param('list_id') || $cgi->param('regetlastlist'); + my $search = $user->save_last_search( + {bugs => \@bugidlist, order => $order, vars => $vars, list_id => $list_id}); + $cgi->param('list_id', $search->id) if $search; + $contenttype = "text/html"; } else { - $contenttype = $format->{'ctype'}; + $contenttype = $format->{'ctype'}; } # Set 'urlquerypart' once the buglist ID is known. -$vars->{'urlquerypart'} = $params->canonicalise_query('order', 'cmdtype', - 'query_based_on', - 'token'); +$vars->{'urlquerypart'} + = $params->canonicalise_query('order', 'cmdtype', 'query_based_on', 'token'); if ($format->{'extension'} eq "csv") { - # We set CSV files to be downloaded, as they are designed for importing - # into other programs. - $disposition = "attachment"; - # If the user clicked the CSV link in the search results, - # They should get the Field Description, not the column name in the db - $vars->{'human'} = $cgi->param('human'); + # We set CSV files to be downloaded, as they are designed for importing + # into other programs. + $disposition = "attachment"; + + # If the user clicked the CSV link in the search results, + # They should get the Field Description, not the column name in the db + $vars->{'human'} = $cgi->param('human'); } -$cgi->close_standby_message($contenttype, $disposition, $disp_prefix, $format->{'extension'}); +$cgi->close_standby_message($contenttype, $disposition, $disp_prefix, + $format->{'extension'}); ################################################################################ # Content Generation diff --git a/chart.cgi b/chart.cgi index c1bafa117..a8c609fce 100755 --- a/chart.cgi +++ b/chart.cgi @@ -46,26 +46,27 @@ use Bugzilla::Token; # when preparing Bugzilla for mod_perl, this script used these # variables in so many subroutines that it was easier to just # make them globals. -local our $cgi = Bugzilla->cgi; +local our $cgi = Bugzilla->cgi; local our $template = Bugzilla->template; -local our $vars = {}; +local our $vars = {}; my $dbh = Bugzilla->dbh; my $user = Bugzilla->login(LOGIN_REQUIRED); if (!Bugzilla->feature('new_charts')) { - ThrowUserError('feature_disabled', { feature => 'new_charts' }); + ThrowUserError('feature_disabled', {feature => 'new_charts'}); } # Go back to query.cgi if we are adding a boolean chart parameter. if (grep(/^cmd-/, $cgi->param())) { - my $params = $cgi->canonicalise_query("format", "ctype", "action"); - print $cgi->redirect("query.cgi?format=" . $cgi->param('query_format') . - ($params ? "&$params" : "")); - exit; + my $params = $cgi->canonicalise_query("format", "ctype", "action"); + print $cgi->redirect("query.cgi?format=" + . $cgi->param('query_format') + . ($params ? "&$params" : "")); + exit; } -my $action = $cgi->param('action'); +my $action = $cgi->param('action'); my $series_id = $cgi->param('series_id'); $vars->{'doc_section'} = 'using/reports-and-charts.html#charts'; @@ -75,283 +76,296 @@ $vars->{'doc_section'} = 'using/reports-and-charts.html#charts'; # series_id they apply to (e.g. subscribe, unsubscribe). my @actions = grep(/^action-/, $cgi->param()); if ($actions[0] && $actions[0] =~ /^action-([^\d]+)(\d*)$/) { - $action = $1; - $series_id = $2 if $2; + $action = $1; + $series_id = $2 if $2; } $action ||= "assemble"; # Go to buglist.cgi if we are doing a search. if ($action eq "search") { - my $params = $cgi->canonicalise_query("format", "ctype", "action"); - print $cgi->redirect("buglist.cgi" . ($params ? "?$params" : "")); - exit; + my $params = $cgi->canonicalise_query("format", "ctype", "action"); + print $cgi->redirect("buglist.cgi" . ($params ? "?$params" : "")); + exit; } -$user->in_group(Bugzilla->params->{"chartgroup"}) - || ThrowUserError("auth_failure", {group => Bugzilla->params->{"chartgroup"}, - action => "use", - object => "charts"}); +$user->in_group(Bugzilla->params->{"chartgroup"}) || ThrowUserError( + "auth_failure", + { + group => Bugzilla->params->{"chartgroup"}, + action => "use", + object => "charts" + } +); # Only admins may create public queries $user->in_group('admin') || $cgi->delete('public'); # All these actions relate to chart construction. if ($action =~ /^(assemble|add|remove|sum|subscribe|unsubscribe)$/) { - # These two need to be done before the creation of the Chart object, so - # that the changes they make will be reflected in it. - if ($action =~ /^subscribe|unsubscribe$/) { - detaint_natural($series_id) || ThrowCodeError("invalid_series_id"); - my $series = new Bugzilla::Series($series_id); - $series->$action($user->id); - } - - my $chart = new Bugzilla::Chart($cgi); - if ($action =~ /^remove|sum$/) { - $chart->$action(getSelectedLines()); - } - elsif ($action eq "add") { - my @series_ids = getAndValidateSeriesIDs(); - $chart->add(@series_ids); - } - - view($chart); + # These two need to be done before the creation of the Chart object, so + # that the changes they make will be reflected in it. + if ($action =~ /^subscribe|unsubscribe$/) { + detaint_natural($series_id) || ThrowCodeError("invalid_series_id"); + my $series = new Bugzilla::Series($series_id); + $series->$action($user->id); + } + + my $chart = new Bugzilla::Chart($cgi); + + if ($action =~ /^remove|sum$/) { + $chart->$action(getSelectedLines()); + } + elsif ($action eq "add") { + my @series_ids = getAndValidateSeriesIDs(); + $chart->add(@series_ids); + } + + view($chart); } elsif ($action eq "plot") { - plot(); + plot(); } elsif ($action eq "wrap") { - # For CSV "wrap", we go straight to "plot". - if ($cgi->param('ctype') && $cgi->param('ctype') eq "csv") { - plot(); - } - else { - wrap(); - } + + # For CSV "wrap", we go straight to "plot". + if ($cgi->param('ctype') && $cgi->param('ctype') eq "csv") { + plot(); + } + else { + wrap(); + } } elsif ($action eq "create") { - assertCanCreate($cgi); - my $token = $cgi->param('token'); - check_hash_token($token, ['create-series']); - - my $series = new Bugzilla::Series($cgi); + assertCanCreate($cgi); + my $token = $cgi->param('token'); + check_hash_token($token, ['create-series']); - ThrowUserError("series_already_exists", {'series' => $series}) - if $series->existsInDatabase; + my $series = new Bugzilla::Series($cgi); - $series->writeToDatabase(); - $vars->{'message'} = "series_created"; - $vars->{'series'} = $series; + ThrowUserError("series_already_exists", {'series' => $series}) + if $series->existsInDatabase; - my $chart = new Bugzilla::Chart($cgi); - view($chart); + $series->writeToDatabase(); + $vars->{'message'} = "series_created"; + $vars->{'series'} = $series; + + my $chart = new Bugzilla::Chart($cgi); + view($chart); } elsif ($action eq "edit") { - my $series = assertCanEdit($series_id); - edit($series); + my $series = assertCanEdit($series_id); + edit($series); } elsif ($action eq "alter") { - my $series = assertCanEdit($series_id); - my $token = $cgi->param('token'); - check_hash_token($token, [$series->id, $series->name]); - # XXX - This should be replaced by $series->set_foo() methods. - $series = new Bugzilla::Series($cgi); - - # We need to check if there is _another_ series in the database with - # our (potentially new) name. So we call existsInDatabase() to see if - # the return value is us or some other series we need to avoid stomping - # on. - my $id_of_series_in_db = $series->existsInDatabase(); - if (defined($id_of_series_in_db) && - $id_of_series_in_db != $series->{'series_id'}) - { - ThrowUserError("series_already_exists", {'series' => $series}); - } - - $series->writeToDatabase(); - $vars->{'changes_saved'} = 1; - - edit($series); + my $series = assertCanEdit($series_id); + my $token = $cgi->param('token'); + check_hash_token($token, [$series->id, $series->name]); + + # XXX - This should be replaced by $series->set_foo() methods. + $series = new Bugzilla::Series($cgi); + + # We need to check if there is _another_ series in the database with + # our (potentially new) name. So we call existsInDatabase() to see if + # the return value is us or some other series we need to avoid stomping + # on. + my $id_of_series_in_db = $series->existsInDatabase(); + if (defined($id_of_series_in_db) + && $id_of_series_in_db != $series->{'series_id'}) + { + ThrowUserError("series_already_exists", {'series' => $series}); + } + + $series->writeToDatabase(); + $vars->{'changes_saved'} = 1; + + edit($series); } elsif ($action eq "confirm-delete") { - $vars->{'series'} = assertCanEdit($series_id); + $vars->{'series'} = assertCanEdit($series_id); - print $cgi->header(); - $template->process("reports/delete-series.html.tmpl", $vars) - || ThrowTemplateError($template->error()); + print $cgi->header(); + $template->process("reports/delete-series.html.tmpl", $vars) + || ThrowTemplateError($template->error()); } elsif ($action eq "delete") { - my $series = assertCanEdit($series_id); - my $token = $cgi->param('token'); - check_hash_token($token, [$series->id, $series->name]); - - $dbh->bz_start_transaction(); - - $series->remove_from_db(); - # Remove (sub)categories which no longer have any series. - foreach my $cat (qw(category subcategory)) { - my $is_used = $dbh->selectrow_array("SELECT COUNT(*) FROM series WHERE $cat = ?", - undef, $series->{"${cat}_id"}); - if (!$is_used) { - $dbh->do('DELETE FROM series_categories WHERE id = ?', - undef, $series->{"${cat}_id"}); - } + my $series = assertCanEdit($series_id); + my $token = $cgi->param('token'); + check_hash_token($token, [$series->id, $series->name]); + + $dbh->bz_start_transaction(); + + $series->remove_from_db(); + + # Remove (sub)categories which no longer have any series. + foreach my $cat (qw(category subcategory)) { + my $is_used + = $dbh->selectrow_array("SELECT COUNT(*) FROM series WHERE $cat = ?", + undef, $series->{"${cat}_id"}); + if (!$is_used) { + $dbh->do('DELETE FROM series_categories WHERE id = ?', + undef, $series->{"${cat}_id"}); } - $dbh->bz_commit_transaction(); + } + $dbh->bz_commit_transaction(); - $vars->{'message'} = "series_deleted"; - $vars->{'series'} = $series; - view(); + $vars->{'message'} = "series_deleted"; + $vars->{'series'} = $series; + view(); } elsif ($action eq "convert_search") { - my $saved_search = $cgi->param('series_from_search') || ''; - my ($query) = grep { $_->name eq $saved_search } @{ $user->queries }; - my $url = ''; - if ($query) { - my $params = new Bugzilla::CGI($query->edit_link); - # These two parameters conflict with the one below. - $url = $params->canonicalise_query('format', 'query_format'); - $url = '&' . html_quote($url); - } - print $cgi->redirect(-location => correct_urlbase() . "query.cgi?format=create-series$url"); + my $saved_search = $cgi->param('series_from_search') || ''; + my ($query) = grep { $_->name eq $saved_search } @{$user->queries}; + my $url = ''; + if ($query) { + my $params = new Bugzilla::CGI($query->edit_link); + + # These two parameters conflict with the one below. + $url = $params->canonicalise_query('format', 'query_format'); + $url = '&' . html_quote($url); + } + print $cgi->redirect( + -location => correct_urlbase() . "query.cgi?format=create-series$url"); } else { - ThrowUserError('unknown_action', {action => $action}); + ThrowUserError('unknown_action', {action => $action}); } exit; # Find any selected series and return either the first or all of them. sub getAndValidateSeriesIDs { - my @series_ids = grep(/^\d+$/, $cgi->param("name")); + my @series_ids = grep(/^\d+$/, $cgi->param("name")); - return wantarray ? @series_ids : $series_ids[0]; + return wantarray ? @series_ids : $series_ids[0]; } # Return a list of IDs of all the lines selected in the UI. sub getSelectedLines { - my @ids = map { /^select(\d+)$/ ? $1 : () } $cgi->param(); + my @ids = map { /^select(\d+)$/ ? $1 : () } $cgi->param(); - return @ids; + return @ids; } -# Check if the user is the owner of series_id or is an admin. +# Check if the user is the owner of series_id or is an admin. sub assertCanEdit { - my $series_id = shift; - my $user = Bugzilla->user; + my $series_id = shift; + my $user = Bugzilla->user; - my $series = new Bugzilla::Series($series_id) - || ThrowCodeError('invalid_series_id'); + my $series + = new Bugzilla::Series($series_id) || ThrowCodeError('invalid_series_id'); - if (!$user->in_group('admin') && $series->{creator_id} != $user->id) { - ThrowUserError('illegal_series_edit'); - } + if (!$user->in_group('admin') && $series->{creator_id} != $user->id) { + ThrowUserError('illegal_series_edit'); + } - return $series; + return $series; } # Check if the user is permitted to create this series with these parameters. sub assertCanCreate { - my ($cgi) = shift; - my $user = Bugzilla->user; + my ($cgi) = shift; + my $user = Bugzilla->user; - $user->in_group("editbugs") || ThrowUserError("illegal_series_creation"); + $user->in_group("editbugs") || ThrowUserError("illegal_series_creation"); - # Check permission for frequency - my $min_freq = 7; - if ($cgi->param('frequency') < $min_freq && !$user->in_group("admin")) { - ThrowUserError("illegal_frequency", { 'minimum' => $min_freq }); - } + # Check permission for frequency + my $min_freq = 7; + if ($cgi->param('frequency') < $min_freq && !$user->in_group("admin")) { + ThrowUserError("illegal_frequency", {'minimum' => $min_freq}); + } } sub validateWidthAndHeight { - $vars->{'width'} = $cgi->param('width'); - $vars->{'height'} = $cgi->param('height'); - - if (defined($vars->{'width'})) { - (detaint_natural($vars->{'width'}) && $vars->{'width'} > 0) - || ThrowUserError("invalid_dimensions"); - } - - if (defined($vars->{'height'})) { - (detaint_natural($vars->{'height'}) && $vars->{'height'} > 0) - || ThrowUserError("invalid_dimensions"); - } - - # The equivalent of 2000 square seems like a very reasonable maximum size. - # This is merely meant to prevent accidental or deliberate DOS, and should - # have no effect in practice. - if ($vars->{'width'} && $vars->{'height'}) { - (($vars->{'width'} * $vars->{'height'}) <= 4000000) - || ThrowUserError("chart_too_large"); - } + $vars->{'width'} = $cgi->param('width'); + $vars->{'height'} = $cgi->param('height'); + + if (defined($vars->{'width'})) { + (detaint_natural($vars->{'width'}) && $vars->{'width'} > 0) + || ThrowUserError("invalid_dimensions"); + } + + if (defined($vars->{'height'})) { + (detaint_natural($vars->{'height'}) && $vars->{'height'} > 0) + || ThrowUserError("invalid_dimensions"); + } + + # The equivalent of 2000 square seems like a very reasonable maximum size. + # This is merely meant to prevent accidental or deliberate DOS, and should + # have no effect in practice. + if ($vars->{'width'} && $vars->{'height'}) { + (($vars->{'width'} * $vars->{'height'}) <= 4000000) + || ThrowUserError("chart_too_large"); + } } sub edit { - my $series = shift; + my $series = shift; - $vars->{'category'} = Bugzilla::Chart::getVisibleSeries(); - $vars->{'default'} = $series; - $vars->{'message'} = 'series_updated' if $vars->{'changes_saved'}; + $vars->{'category'} = Bugzilla::Chart::getVisibleSeries(); + $vars->{'default'} = $series; + $vars->{'message'} = 'series_updated' if $vars->{'changes_saved'}; - print $cgi->header(); - $template->process("reports/edit-series.html.tmpl", $vars) - || ThrowTemplateError($template->error()); + print $cgi->header(); + $template->process("reports/edit-series.html.tmpl", $vars) + || ThrowTemplateError($template->error()); } sub plot { - validateWidthAndHeight(); - $vars->{'chart'} = new Bugzilla::Chart($cgi); + validateWidthAndHeight(); + $vars->{'chart'} = new Bugzilla::Chart($cgi); - my $format = $template->get_format("reports/chart", "", scalar($cgi->param('ctype'))); - $format->{'ctype'} = 'text/html' if $cgi->param('debug'); + my $format + = $template->get_format("reports/chart", "", scalar($cgi->param('ctype'))); + $format->{'ctype'} = 'text/html' if $cgi->param('debug'); - $cgi->set_dated_content_disp('inline', 'chart', $format->{extension}); - print $cgi->header($format->{'ctype'}); - disable_utf8() if ($format->{'ctype'} =~ /^image\//); + $cgi->set_dated_content_disp('inline', 'chart', $format->{extension}); + print $cgi->header($format->{'ctype'}); + disable_utf8() if ($format->{'ctype'} =~ /^image\//); - # Debugging PNGs is a pain; we need to be able to see the error messages - $vars->{'chart'}->dump() if $cgi->param('debug'); + # Debugging PNGs is a pain; we need to be able to see the error messages + $vars->{'chart'}->dump() if $cgi->param('debug'); - $template->process($format->{'template'}, $vars) - || ThrowTemplateError($template->error()); + $template->process($format->{'template'}, $vars) + || ThrowTemplateError($template->error()); } sub wrap { - validateWidthAndHeight(); - - # We create a Chart object so we can validate the parameters - my $chart = new Bugzilla::Chart($cgi); - - $vars->{'time'} = localtime(time()); - - $vars->{'imagebase'} = $cgi->canonicalise_query( - "action", "action-wrap", "ctype", "format", "width", "height"); - - print $cgi->header(); - $template->process("reports/chart.html.tmpl", $vars) - || ThrowTemplateError($template->error()); + validateWidthAndHeight(); + + # We create a Chart object so we can validate the parameters + my $chart = new Bugzilla::Chart($cgi); + + $vars->{'time'} = localtime(time()); + + $vars->{'imagebase'} + = $cgi->canonicalise_query("action", "action-wrap", "ctype", "format", + "width", "height"); + + print $cgi->header(); + $template->process("reports/chart.html.tmpl", $vars) + || ThrowTemplateError($template->error()); } sub view { - my $chart = shift; + my $chart = shift; - # Set defaults - foreach my $field ('category', 'subcategory', 'name', 'ctype') { - $vars->{'default'}{$field} = $cgi->param($field) || 0; - } + # Set defaults + foreach my $field ('category', 'subcategory', 'name', 'ctype') { + $vars->{'default'}{$field} = $cgi->param($field) || 0; + } - # Pass the state object to the display UI. - $vars->{'chart'} = $chart; - $vars->{'category'} = Bugzilla::Chart::getVisibleSeries(); + # Pass the state object to the display UI. + $vars->{'chart'} = $chart; + $vars->{'category'} = Bugzilla::Chart::getVisibleSeries(); - print $cgi->header(); + print $cgi->header(); - # If we have having problems with bad data, we can set debug=1 to dump - # the data structure. - $chart->dump() if $cgi->param('debug'); + # If we have having problems with bad data, we can set debug=1 to dump + # the data structure. + $chart->dump() if $cgi->param('debug'); - $template->process("reports/create-chart.html.tmpl", $vars) - || ThrowTemplateError($template->error()); + $template->process("reports/create-chart.html.tmpl", $vars) + || ThrowTemplateError($template->error()); } diff --git a/checksetup.pl b/checksetup.pl index 5dda0df6f..76564e2f0 100755 --- a/checksetup.pl +++ b/checksetup.pl @@ -26,8 +26,8 @@ use Safe; use Bugzilla::Constants; use Bugzilla::Install::Requirements; -use Bugzilla::Install::Util qw(install_string get_version_and_os - init_console success); +use Bugzilla::Install::Util qw(install_string get_version_and_os + init_console success); ###################################################################### # Live Code @@ -42,24 +42,25 @@ init_console(); my %switch; GetOptions(\%switch, 'help|h|?', 'check-modules', 'no-templates|t', - 'verbose|v|no-silent', 'make-admin=s', - 'reset-password=s', 'version|V'); + 'verbose|v|no-silent', 'make-admin=s', 'reset-password=s', 'version|V'); # Print the help message if that switch was selected. pod2usage({-verbose => 1, -exitval => 1}) if $switch{'help'}; -# Read in the "answers" file if it exists, for running in +# Read in the "answers" file if it exists, for running in # non-interactive mode. my $answers_file = $ARGV[0]; my $silent = $answers_file && !$switch{'verbose'}; print(install_string('header', get_version_and_os()) . "\n") unless $silent; exit 0 if $switch{'version'}; + # Check required --MODULES-- my $module_results = check_requirements(!$silent); -Bugzilla::Install::Requirements::print_module_instructions( - $module_results, !$silent); +Bugzilla::Install::Requirements::print_module_instructions($module_results, + !$silent); exit 1 if !$module_results->{pass}; + # Break out if checking the modules is all we have been asked to do. exit 0 if $switch{'check-modules'}; @@ -86,7 +87,7 @@ import Bugzilla::Install::Localconfig qw(update_localconfig); require Bugzilla::Install::Filesystem; import Bugzilla::Install::Filesystem qw(update_filesystem create_htaccess - fix_all_file_permissions); + fix_all_file_permissions); require Bugzilla::Install::DB; require Bugzilla::DB; require Bugzilla::Template; @@ -100,8 +101,8 @@ Bugzilla->installation_answers($answers_file); # Check and update --LOCAL-- configuration ########################################################################### -print "Reading " . bz_locations()->{'localconfig'} . "...\n" unless $silent; -update_localconfig({ output => !$silent }); +print "Reading " . bz_locations()->{'localconfig'} . "...\n" unless $silent; +update_localconfig({output => !$silent}); my $lc_hash = Bugzilla->localconfig; ########################################################################### @@ -117,8 +118,10 @@ Bugzilla::DB::bz_create_database() if $lc_hash->{'db_check'}; # now get a handle to the database: my $dbh = Bugzilla->dbh; + # Create the tables, and do any database-specific schema changes. $dbh->bz_setup_database(); + # Populate the tables that hold the values for the